10 |
--------------------------------------------------------------------------------
/bin/knit_lessons.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Only try running R to translate files if there are some files present.
4 | # The Makefile passes in the names of files.
5 |
6 | if [ $# -ne 0 ] ; then
7 | Rscript -e "source('bin/generate_md_episodes.R')"
8 | fi
9 |
--------------------------------------------------------------------------------
/_includes/dc/who.html:
--------------------------------------------------------------------------------
1 |
2 | Who:
3 | The course is aimed at graduate students and other researchers.
4 |
5 | You don't need to have any previous knowledge of the tools
6 | that will be presented at the workshop.
7 |
8 |
2 | Who:
3 | The course is aimed at graduate students and other researchers.
4 |
5 | You don't need to have any previous knowledge of the tools
6 | that will be presented at the workshop.
7 |
8 |
2 | Who:
3 | The course is for librarians, archivists, and other information workers.
4 |
5 | You don't need to have any previous knowledge of the tools that
6 | will be presented at the workshop.
7 |
8 |
23 |
--------------------------------------------------------------------------------
/files/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 |
--------------------------------------------------------------------------------
/bin/test_lesson_check.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | import lesson_check
4 | import util
5 |
6 | class TestFileList(unittest.TestCase):
7 | def setUp(self):
8 | self.reporter = util.Reporter() ## TODO: refactor reporter class.
9 |
10 | def test_file_list_has_expected_entries(self):
11 | # For first pass, simply assume that all required files are present
12 | all_filenames = [filename.replace('%', '')
13 | for filename in lesson_check.REQUIRED_FILES]
14 |
15 | lesson_check.check_fileset('', self.reporter, all_filenames)
16 | self.assertEqual(len(self.reporter.messages), 0)
17 |
18 | if __name__ == "__main__":
19 | unittest.main()
20 |
--------------------------------------------------------------------------------
/_includes/workshop_footer.html:
--------------------------------------------------------------------------------
1 | {% comment %}
2 | Footer for a standard workshop.
3 | {% endcomment %}
4 |
25 |
--------------------------------------------------------------------------------
/_includes/javascript.html:
--------------------------------------------------------------------------------
1 | {% comment %}
2 | Javascript used in lesson and workshop pages.
3 | {% endcomment %}
4 |
5 |
6 |
7 |
15 |
--------------------------------------------------------------------------------
/_includes/dc/intro.html:
--------------------------------------------------------------------------------
1 |
2 | Data Carpentry
3 | aims to help researchers get their work done
4 | in less time and with less pain
5 | by teaching them basic research computing skills.
6 | This hands-on workshop will cover basic concepts and tools,
7 | including program design, version control, data management,
8 | and task automation.
9 | Participants will be encouraged to help one another
10 | and to apply what they have learned to their own research problems.
11 |
2 | Software Carpentry
3 | aims to help researchers get their work done
4 | in less time and with less pain
5 | by teaching them basic research computing skills.
6 | This hands-on workshop will cover basic concepts and tools,
7 | including program design, version control, data management,
8 | and task automation.
9 | Participants will be encouraged to help one another
10 | and to apply what they have learned to their own research problems.
11 |
19 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | Please delete the text below before submitting your contribution.
2 |
3 | ---
4 |
5 | Thanks for contributing! If this contribution is for instructor training, please send an email to checkout@carpentries.org with a link to this contribution so we can record your progress. You’ve completed your contribution step for instructor checkout just by submitting this contribution.
6 |
7 | Please keep in mind that lesson maintainers are volunteers and it may be some time before they can respond to your contribution. Although not all contributions can be incorporated into the lesson materials, we appreciate your time and effort to improve the curriculum. If you have any questions about the lesson maintenance process or would like to volunteer your time as a contribution reviewer, please contact Kate Hertweck (k8hertweck@gmail.com).
8 |
9 | ---
10 |
--------------------------------------------------------------------------------
/_includes/lc/intro.html:
--------------------------------------------------------------------------------
1 |
2 | Library Carpentry
3 | is made by librarians, for librarians to help you:
4 |
5 |
6 |
automate repetitive, boring, error-prone tasks
7 |
create, maintain and analyse sustainable and reusable data
8 |
work effectively with IT and systems colleagues
9 |
better understand the use of software in research
10 |
and much more...
11 |
12 |
13 |
14 | Library Carpentry introduces you to the fundamentals of computing
15 | and provides you with a platform for further self-directed learning.
16 | For more information on what we teach and why, please see our paper
17 | "Library Carpentry: software skills training for library professionals".
18 |
19 |
20 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | Please delete the text below before submitting your contribution.
2 |
3 | ---
4 |
5 | Thanks for contributing! If this contribution is for instructor training, please send an email to checkout@carpentries.org with a link to this contribution so we can record your progress. You’ve completed your contribution step for instructor checkout just by submitting this contribution.
6 |
7 | Please keep in mind that lesson maintainers are volunteers and it may be some time before they can respond to your contribution. Although not all contributions can be incorporated into the lesson materials, we appreciate your time and effort to improve the curriculum. If you have any questions about the lesson maintenance process or would like to volunteer your time as a contribution reviewer, please contact Kate Hertweck (k8hertweck@gmail.com).
8 |
9 | ---
10 |
--------------------------------------------------------------------------------
/_includes/episode_overview.html:
--------------------------------------------------------------------------------
1 | {% comment %}
2 | Display an episode's timings and learning objectives.
3 | {% endcomment %}
4 |
5 |
Overview
6 |
7 |
8 |
9 | Teaching: {{ page.teaching }} min
10 |
11 | Exercises: {{ page.exercises }} min
12 |
13 |
14 | Questions
15 |
16 | {% for question in page.questions %}
17 |
{{ question|markdownify }}
18 | {% endfor %}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | Objectives
28 |
29 | {% for objective in page.objectives %}
30 |
{{ objective|markdownify }}
31 | {% endfor %}
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/_includes/workshop_ad.html:
--------------------------------------------------------------------------------
1 | {% comment %}
2 | Advertising box at the top of a workshop website home page.
3 | {% endcomment %}
4 |
5 |
6 |
7 |
{{page.venue}}
8 |
9 |
10 |
{{page.humandate}}
11 |
{% if page.humantime %}{{page.humantime}}{% endif %}
41 | {% include javascript.html %}
42 |
43 |
44 |
--------------------------------------------------------------------------------
/_includes/carpentries.html:
--------------------------------------------------------------------------------
1 | {% comment %}
2 | General description of Software and Data Carpentry.
3 | {% endcomment %}
4 |
5 |
6 |
7 |
8 |
9 | Since 1998,
10 | Software Carpentry
11 | has been teaching researchers in science, engineering, medicine, and related disciplines
12 | the computing skills they need to get more done in less time and with less pain.
13 | Its volunteer instructors have run hundreds of events
14 | for thousands of learners in the past two and a half years.
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Data Carpentry develops and teaches workshops on the fundamental data skills needed to conduct research.
24 | Its target audience is researchers who have little to no prior computational experience,
25 | and its lessons are domain specific,
26 | building on learners' existing knowledge to enable them to quickly apply skills learned to their own research.
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | Library Carpentry is made by librarians to help librarians
36 | automate repetitive, boring, error-prone tasks;
37 | create, maintain and analyse sustainable and reusable data;
38 | work effectively with IT and systems colleagues;
39 | better understand the use of software in research;
40 | and much more.
41 | Library Carpentry was the winner of the 2016
42 | British Library Labs Teaching and Learning Award.
43 |
44 |
45 |
--------------------------------------------------------------------------------
/bin/chunk-options.R:
--------------------------------------------------------------------------------
1 | # These settings control the behavior of all chunks in the novice R materials.
2 | # For example, to generate the lessons with all the output hidden, simply change
3 | # `results` from "markup" to "hide".
4 | # For more information on available chunk options, see
5 | # http://yihui.name/knitr/options#chunk_options
6 |
7 | library("knitr")
8 |
9 | fix_fig_path <- function(pth) file.path("..", pth)
10 |
11 |
12 | ## We set the path for the figures globally below, so if we want to
13 | ## customize it for individual episodes, we can append a prefix to the
14 | ## global path. For instance, if we call knitr_fig_path("01-") in the
15 | ## first episode of the lesson, it will generate the figures in
16 | ## `fig/rmd-01-`
17 | knitr_fig_path <- function(prefix) {
18 | new_path <- paste0(opts_chunk$get("fig.path"),
19 | prefix)
20 | opts_chunk$set(fig.path = new_path)
21 | }
22 |
23 | ## We use the rmd- prefix for the figures generated by the lssons so
24 | ## they can be easily identified and deleted by `make clean-rmd`. The
25 | ## working directory when the lessons are generated is the root so the
26 | ## figures need to be saved in fig/, but when the site is generated,
27 | ## the episodes will be one level down. We fix the path using the
28 | ## `fig.process` option.
29 |
30 | opts_chunk$set(tidy = FALSE, results = "markup", comment = NA,
31 | fig.align = "center", fig.path = "fig/rmd-",
32 | fig.process = fix_fig_path)
33 |
34 | # The hooks below add html tags to the code chunks and their output so that they
35 | # are properly formatted when the site is built.
36 |
37 | hook_in <- function(x, options) {
38 | stringr::str_c("\n\n~~~\n",
39 | paste0(x, collapse="\n"),
40 | "\n~~~\n{: .language-r}\n\n")
41 | }
42 |
43 | hook_out <- function(x, options) {
44 | x <- gsub("\n$", "", x)
45 | stringr::str_c("\n\n~~~\n",
46 | paste0(x, collapse="\n"),
47 | "\n~~~\n{: .output}\n\n")
48 | }
49 |
50 | hook_error <- function(x, options) {
51 | x <- gsub("\n$", "", x)
52 | stringr::str_c("\n\n~~~\n",
53 | paste0(x, collapse="\n"),
54 | "\n~~~\n{: .error}\n\n")
55 | }
56 |
57 | knit_hooks$set(source = hook_in, output = hook_out, warning = hook_error,
58 | error = hook_error, message = hook_out)
59 |
--------------------------------------------------------------------------------
/_includes/lc/syllabus.html:
--------------------------------------------------------------------------------
1 |
23 | {% for episode in site.episodes %}
24 | {% if episode.start %} {% comment %} Starting a new day? {% endcomment %}
25 | {% assign day = day | plus: 1 %}
26 | {% if day > 1 %} {% comment %} If about to start day 2 or later, show finishing time for previous day {% endcomment %}
27 | {% assign hours = current | divided_by: 60 %}
28 | {% assign minutes = current | modulo: 60 %}
29 |
30 | {% if multiday %}
{% endif %}
31 |
{% if hours < 10 %}0{% endif %}{{ hours }}:{% if minutes < 10 %}0{% endif %}{{ minutes }}
32 |
Finish
33 |
34 |
35 | {% endif %}
36 | {% assign current = site.start_time %} {% comment %}Re-set start time of this episode to general daily start time {% endcomment %}
37 | {% endif %}
38 | {% assign hours = current | divided_by: 60 %}
39 | {% assign minutes = current | modulo: 60 %}
40 |
41 | {% if multiday %}
{% if episode.start %}Day {{ day }}{% endif %}
{% endif %}
42 |
{% if hours < 10 %}0{% endif %}{{ hours }}:{% if minutes < 10 %}0{% endif %}{{ minutes }}
97 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | ## ========================================
2 | ## Commands for both workshop and lesson websites.
3 |
4 | # Settings
5 | MAKEFILES=Makefile $(wildcard *.mk)
6 | JEKYLL=jekyll
7 | PARSER=bin/markdown_ast.rb
8 | DST=_site
9 |
10 | # Controls
11 | .PHONY : commands clean files
12 | .NOTPARALLEL:
13 | all : commands
14 |
15 | ## commands : show all commands.
16 | commands :
17 | @grep -h -E '^##' ${MAKEFILES} | sed -e 's/## //g'
18 |
19 | ## serve : run a local server.
20 | serve : lesson-md
21 | ${JEKYLL} serve
22 |
23 | ## site : build files but do not run a server.
24 | site : lesson-md
25 | ${JEKYLL} build
26 |
27 | # repo-check : check repository settings.
28 | repo-check :
29 | @bin/repo_check.py -s .
30 |
31 | ## clean : clean up junk files.
32 | clean :
33 | @rm -rf ${DST}
34 | @rm -rf .sass-cache
35 | @rm -rf bin/__pycache__
36 | @find . -name .DS_Store -exec rm {} \;
37 | @find . -name '*~' -exec rm {} \;
38 | @find . -name '*.pyc' -exec rm {} \;
39 |
40 | ## clean-rmd : clean intermediate R files (that need to be committed to the repo).
41 | clear-rmd :
42 | @rm -rf ${RMD_DST}
43 | @rm -rf fig/rmd-*
44 |
45 | ## ----------------------------------------
46 | ## Commands specific to workshop websites.
47 |
48 | .PHONY : workshop-check
49 |
50 | ## workshop-check : check workshop homepage.
51 | workshop-check :
52 | @bin/workshop_check.py .
53 |
54 | ## ----------------------------------------
55 | ## Commands specific to lesson websites.
56 |
57 | .PHONY : lesson-check lesson-md lesson-files lesson-fixme
58 |
59 | # RMarkdown files
60 | RMD_SRC = $(wildcard _episodes_rmd/??-*.Rmd)
61 | RMD_DST = $(patsubst _episodes_rmd/%.Rmd,_episodes/%.md,$(RMD_SRC))
62 |
63 | # Lesson source files in the order they appear in the navigation menu.
64 | MARKDOWN_SRC = \
65 | index.md \
66 | CONDUCT.md \
67 | setup.md \
68 | $(sort $(wildcard _episodes/*.md)) \
69 | reference.md \
70 | $(sort $(wildcard _extras/*.md)) \
71 | LICENSE.md
72 |
73 | # Generated lesson files in the order they appear in the navigation menu.
74 | HTML_DST = \
75 | ${DST}/index.html \
76 | ${DST}/conduct/index.html \
77 | ${DST}/setup/index.html \
78 | $(patsubst _episodes/%.md,${DST}/%/index.html,$(sort $(wildcard _episodes/*.md))) \
79 | ${DST}/reference/index.html \
80 | $(patsubst _extras/%.md,${DST}/%/index.html,$(sort $(wildcard _extras/*.md))) \
81 | ${DST}/license/index.html
82 |
83 | ## lesson-md : convert Rmarkdown files to markdown
84 | lesson-md : ${RMD_DST}
85 |
86 | # Use of .NOTPARALLEL makes rule execute only once
87 | ${RMD_DST} : ${RMD_SRC}
88 | @bin/knit_lessons.sh ${RMD_SRC}
89 |
90 | ## lesson-check : validate lesson Markdown.
91 | lesson-check :
92 | @bin/lesson_check.py -s . -p ${PARSER} -r _includes/links.md
93 |
94 | ## lesson-check-all : validate lesson Markdown, checking line lengths and trailing whitespace.
95 | lesson-check-all :
96 | @bin/lesson_check.py -s . -p ${PARSER} -l -w
97 |
98 | ## unittest : run unit tests on checking tools.
99 | unittest :
100 | python bin/test_lesson_check.py
101 |
102 | ## lesson-files : show expected names of generated files for debugging.
103 | lesson-files :
104 | @echo 'RMD_SRC:' ${RMD_SRC}
105 | @echo 'RMD_DST:' ${RMD_DST}
106 | @echo 'MARKDOWN_SRC:' ${MARKDOWN_SRC}
107 | @echo 'HTML_DST:' ${HTML_DST}
108 |
109 | ## lesson-fixme : show FIXME markers embedded in source files.
110 | lesson-fixme :
111 | @fgrep -i -n FIXME ${MARKDOWN_SRC} || true
112 |
113 | #-------------------------------------------------------------------------------
114 | # Include extra commands if available.
115 | #-------------------------------------------------------------------------------
116 |
117 | -include commands.mk
118 |
--------------------------------------------------------------------------------
/_includes/navbar.html:
--------------------------------------------------------------------------------
1 | {% comment %}
2 | Lesson navigation bar.
3 | {% endcomment %}
4 |
87 |
--------------------------------------------------------------------------------
/assets/css/syntax.css:
--------------------------------------------------------------------------------
1 | .highlight .hll { background-color: #ffffcc }
2 | .highlight { background: #f8f8f8; }
3 | .highlight .c { color: #408080; font-style: italic } /* Comment */
4 | .highlight .err { border: 1px solid #FF0000 } /* Error */
5 | .highlight .k { color: #008000; font-weight: bold } /* Keyword */
6 | .highlight .o { color: #666666 } /* Operator */
7 | .highlight .ch { color: #408080; font-style: italic } /* Comment.Hashbang */
8 | .highlight .cm { color: #408080; font-style: italic } /* Comment.Multiline */
9 | .highlight .cp { color: #BC7A00 } /* Comment.Preproc */
10 | .highlight .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */
11 | .highlight .c1 { color: #408080; font-style: italic } /* Comment.Single */
12 | .highlight .cs { color: #408080; font-style: italic } /* Comment.Special */
13 | .highlight .gd { color: #A00000 } /* Generic.Deleted */
14 | .highlight .ge { font-style: italic } /* Generic.Emph */
15 | .highlight .gr { color: #FF0000 } /* Generic.Error */
16 | .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
17 | .highlight .gi { color: #00A000 } /* Generic.Inserted */
18 | .highlight .go { color: #888888 } /* Generic.Output */
19 | .highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
20 | .highlight .gs { font-weight: bold } /* Generic.Strong */
21 | .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
22 | .highlight .gt { color: #0044DD } /* Generic.Traceback */
23 | .highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
24 | .highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
25 | .highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
26 | .highlight .kp { color: #008000 } /* Keyword.Pseudo */
27 | .highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
28 | .highlight .kt { color: #B00040 } /* Keyword.Type */
29 | .highlight .m { color: #666666 } /* Literal.Number */
30 | .highlight .s { color: #BA2121 } /* Literal.String */
31 | .highlight .na { color: #7D9029 } /* Name.Attribute */
32 | .highlight .nb { color: #008000 } /* Name.Builtin */
33 | .highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */
34 | .highlight .no { color: #880000 } /* Name.Constant */
35 | .highlight .nd { color: #AA22FF } /* Name.Decorator */
36 | .highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */
37 | .highlight .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
38 | .highlight .nf { color: #0000FF } /* Name.Function */
39 | .highlight .nl { color: #A0A000 } /* Name.Label */
40 | .highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
41 | .highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */
42 | .highlight .nv { color: #19177C } /* Name.Variable */
43 | .highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
44 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */
45 | .highlight .mb { color: #666666 } /* Literal.Number.Bin */
46 | .highlight .mf { color: #666666 } /* Literal.Number.Float */
47 | .highlight .mh { color: #666666 } /* Literal.Number.Hex */
48 | .highlight .mi { color: #666666 } /* Literal.Number.Integer */
49 | .highlight .mo { color: #666666 } /* Literal.Number.Oct */
50 | .highlight .sa { color: #BA2121 } /* Literal.String.Affix */
51 | .highlight .sb { color: #BA2121 } /* Literal.String.Backtick */
52 | .highlight .sc { color: #BA2121 } /* Literal.String.Char */
53 | .highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */
54 | .highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
55 | .highlight .s2 { color: #BA2121 } /* Literal.String.Double */
56 | .highlight .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
57 | .highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */
58 | .highlight .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
59 | .highlight .sx { color: #008000 } /* Literal.String.Other */
60 | .highlight .sr { color: #BB6688 } /* Literal.String.Regex */
61 | .highlight .s1 { color: #BA2121 } /* Literal.String.Single */
62 | .highlight .ss { color: #19177C } /* Literal.String.Symbol */
63 | .highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */
64 | .highlight .fm { color: #0000FF } /* Name.Function.Magic */
65 | .highlight .vc { color: #19177C } /* Name.Variable.Class */
66 | .highlight .vg { color: #19177C } /* Name.Variable.Global */
67 | .highlight .vi { color: #19177C } /* Name.Variable.Instance */
68 | .highlight .vm { color: #19177C } /* Name.Variable.Magic */
69 | .highlight .il { color: #666666 } /* Literal.Number.Integer.Long */
70 |
--------------------------------------------------------------------------------
/assets/css/lesson.scss:
--------------------------------------------------------------------------------
1 | ---
2 | ---
3 |
4 | //----------------------------------------
5 | // Colors.
6 | //----------------------------------------
7 |
8 | // branding
9 | $color-brand: #2b3990 !default;
10 |
11 | // code boxes
12 | $color-error: #bd2c00 !default;
13 | $color-output: #303030 !default;
14 | $color-source: #6e5494 !default;
15 |
16 | // blockquotes
17 | $color-callout: #f4fd9c !default;
18 | $color-challenge: #eec275 !default;
19 | $color-checklist: #dfd2a0 !default;
20 | $color-discussion: #eec275 !default;
21 | $color-keypoints: #7ae78e !default;
22 | $color-objectives: #daee84 !default;
23 | $color-prereq: #9cd6dc !default;
24 | $color-solution: #ded4b9 !default;
25 | $color-testimonial: #fc8dc1 !default;
26 |
27 | //----------------------------------------
28 | // Specialized code blocks.
29 | //----------------------------------------
30 |
31 | @mixin cdSetup($color) {
32 | color: $color;
33 | border-left: solid 5px $color;
34 | margin-bottom: 0px;
35 | border-radius: 4px 0 0 4px;
36 | }
37 |
38 | .error { @include cdSetup($color-error); }
39 | .output { @include cdSetup($color-output); }
40 | .source { @include cdSetup($color-source); }
41 |
42 | .bash { @include cdSetup($color-source); }
43 | .make { @include cdSetup($color-source); }
44 | .matlab { @include cdSetup($color-source); }
45 | .python { @include cdSetup($color-source); }
46 | .r { @include cdSetup($color-source); }
47 | .sql { @include cdSetup($color-source); }
48 |
49 | .error pre,
50 | .output pre,
51 | .source pre,
52 | .bash pre,
53 | .make pre,
54 | .matlab pre,
55 | .python pre,
56 | .r pre,
57 | .sql pre {
58 | border-radius: 0 4px 4px 0;
59 | }
60 |
61 | //----------------------------------------
62 | // Specialized blockquote environments for learning objectives, callouts, etc.
63 | //----------------------------------------
64 |
65 | $codeblock-padding: 5px !default;
66 |
67 | @mixin bkSetup($color, $glyph) {
68 |
69 | $gradientcolor1: $color;
70 | $gradientcolor2: scale-color($color, $lightness: 10%);
71 |
72 | padding-left: $codeblock-padding;
73 | padding-top: 0;
74 | padding-bottom: 0;
75 | padding-right: 0;
76 | border: 1px solid;
77 | border-color: $color;
78 | padding-bottom: $codeblock-padding;
79 |
80 | h2 {
81 | padding-top: $codeblock-padding;
82 | padding-bottom: $codeblock-padding;
83 | font-size: 20px;
84 | background: linear-gradient(to bottom, $gradientcolor1, $gradientcolor2);
85 | border-color: $color;
86 | margin-top: 0px;
87 | margin-left: -$codeblock-padding; // to move back to the left margin of the enclosing blockquote
88 | }
89 | h2:before {
90 | font-family: 'Glyphicons Halflings';
91 | content: $glyph;
92 | float: left;
93 | padding-left: $codeblock-padding;
94 | padding-right: $codeblock-padding;
95 | display: inline-block;
96 | -webkit-font-smoothing: antialiased;
97 | }
98 |
99 | }
100 |
101 | .callout{ @include bkSetup($color-callout, "\e146"); }
102 | .challenge{ @include bkSetup($color-challenge, "\270f"); }
103 | .checklist{ @include bkSetup($color-checklist, "\e067"); }
104 | .discussion{ @include bkSetup($color-discussion, "\e123"); }
105 | .keypoints{ @include bkSetup($color-keypoints, "\e101"); }
106 | .objectives{ @include bkSetup($color-objectives, "\e085"); }
107 | .prereq{ @include bkSetup($color-prereq, "\e124"); }
108 | .solution{ @include bkSetup($color-solution, "\e105"); }
109 | .testimonial{ @include bkSetup($color-testimonial, "\e143"); }
110 |
111 | //----------------------------------------
112 | // Override Bootstrap settings.
113 | //----------------------------------------
114 |
115 | code {
116 | padding: 2px 5px;
117 | color: #3d90d9;
118 | background-color: #e7e7e7;
119 | }
120 |
121 | img {
122 | max-width: 100%;
123 | }
124 |
125 | //----------------------------------------
126 | // Miscellaneous.
127 | //----------------------------------------
128 |
129 | .maintitle {
130 | text-align: center;
131 | }
132 |
133 | .footertext {
134 | text-align: center;
135 | }
136 |
137 | img.navbar-logo {
138 | height: 40px; // synchronize with height of navbar
139 | padding-top: 5px;
140 | padding-right: 10px;
141 | }
142 |
143 | div.branding {
144 | color: $color-brand;
145 | }
146 |
147 | ul,
148 | ol {
149 | padding-left: 2em;
150 | }
151 |
152 | span.fold-unfold {
153 | margin-left: 1em;
154 | opacity: 0.5;
155 | }
156 |
157 |
158 | //----------------------------------------
159 | // keyboard key style, from StackExchange.
160 | //----------------------------------------
161 |
162 | kbd {
163 | display: inline-block;
164 | margin: 0 .1em;
165 | padding: .1em .6em;
166 | font-family: Arial,"Helvetica Neue",Helvetica,sans-serif;
167 | font-size: 11px;
168 | line-height: 1.4;
169 | color: #242729;
170 | text-shadow: 0 1px 0 #FFF;
171 | background-color: #e1e3e5;
172 | border: 1px solid #adb3b9;
173 | border-radius: 3px;
174 | box-shadow: 0 1px 0 rgba(12,13,14,0.2), 0 0 0 2px #FFF inset;
175 | white-space: nowrap;
176 | font-style: normal;
177 | }
178 |
--------------------------------------------------------------------------------
/bin/repo_check.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """
4 | Check repository settings.
5 | """
6 |
7 | from __future__ import print_function
8 | import sys
9 | import os
10 | from subprocess import Popen, PIPE
11 | import re
12 | from optparse import OptionParser
13 |
14 | from util import Reporter, load_yaml, require
15 |
16 | # Import this way to produce a more useful error message.
17 | try:
18 | import requests
19 | except ImportError:
20 | print('Unable to import requests module: please install requests', file=sys.stderr)
21 | sys.exit(1)
22 |
23 |
24 | # Pattern to match Git command-line output for remotes => (user name, project name).
25 | P_GIT_REMOTE = re.compile(r'upstream\s+[^:]+:([^/]+)/([^.]+)\.git\s+\(fetch\)')
26 |
27 | # Repository URL format string.
28 | F_REPO_URL = 'https://github.com/{0}/{1}/'
29 |
30 | # Pattern to match repository URLs => (user name, project name)
31 | P_REPO_URL = re.compile(r'https?://github\.com/([^.]+)/([^/]+)/?')
32 |
33 | # API URL format string.
34 | F_API_URL = 'https://api.github.com/repos/{0}/{1}/labels'
35 |
36 | # Expected labels and colors.
37 | EXPECTED = {
38 | 'bug' : 'bd2c00',
39 | 'discussion' : 'fc8dc1',
40 | 'enhancement' : '9cd6dc',
41 | 'help-wanted' : 'f4fd9c',
42 | 'instructor-training' : '6e5494',
43 | 'newcomer-friendly' : 'eec275',
44 | 'question' : '808040',
45 | 'template-and-tools' : '2b3990',
46 | 'work-in-progress' : '7ae78e'
47 | }
48 |
49 |
50 | def main():
51 | """
52 | Main driver.
53 | """
54 |
55 | args = parse_args()
56 | reporter = Reporter()
57 | repo_url = get_repo_url(args.source_dir, args.repo_url)
58 | check_labels(reporter, repo_url)
59 | reporter.report()
60 |
61 |
62 | def parse_args():
63 | """
64 | Parse command-line arguments.
65 | """
66 |
67 | parser = OptionParser()
68 | parser.add_option('-r', '--repo',
69 | default=None,
70 | dest='repo_url',
71 | help='repository URL')
72 | parser.add_option('-s', '--source',
73 | default=os.curdir,
74 | dest='source_dir',
75 | help='source directory')
76 |
77 | args, extras = parser.parse_args()
78 | require(not extras,
79 | 'Unexpected trailing command-line arguments "{0}"'.format(extras))
80 |
81 | return args
82 |
83 |
84 | def get_repo_url(source_dir, repo_url):
85 | """
86 | Figure out which repository to query.
87 | """
88 |
89 | # Explicitly specified.
90 | if repo_url is not None:
91 | return repo_url
92 |
93 | # Guess.
94 | cmd = 'git remote -v'
95 | p = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, close_fds=True, universal_newlines=True)
96 | stdout_data, stderr_data = p.communicate()
97 | stdout_data = stdout_data.split('\n')
98 | matches = [P_GIT_REMOTE.match(line) for line in stdout_data]
99 | matches = [m for m in matches if m is not None]
100 | require(len(matches) == 1,
101 | 'Unexpected output from git remote command: "{0}"'.format(matches))
102 |
103 | username = matches[0].group(1)
104 | require(username, 'empty username in git remote output {0}'.format(matches[0]))
105 |
106 | project_name = matches[0].group(2)
107 | require(username, 'empty project name in git remote output {0}'.format(matches[0]))
108 |
109 | url = F_REPO_URL.format(username, project_name)
110 | return url
111 |
112 |
113 | def check_labels(reporter, repo_url):
114 | """
115 | Check labels in repository.
116 | """
117 |
118 | actual = get_labels(repo_url)
119 | extra = set(actual.keys()) - set(EXPECTED.keys())
120 |
121 | reporter.check(not extra,
122 | None,
123 | 'Extra label(s) in repository {0}: {1}',
124 | repo_url, ', '.join(sorted(extra)))
125 |
126 | missing = set(EXPECTED.keys()) - set(actual.keys())
127 | reporter.check(not missing,
128 | None,
129 | 'Missing label(s) in repository {0}: {1}',
130 | repo_url, ', '.join(sorted(missing)))
131 |
132 | overlap = set(EXPECTED.keys()).intersection(set(actual.keys()))
133 | for name in sorted(overlap):
134 | reporter.check(EXPECTED[name] == actual[name],
135 | None,
136 | 'Color mis-match for label {0} in {1}: expected {2}, found {3}',
137 | name, repo_url, EXPECTED[name], actual[name])
138 |
139 |
140 | def get_labels(repo_url):
141 | """
142 | Get actual labels from repository.
143 | """
144 |
145 | m = P_REPO_URL.match(repo_url)
146 | require(m, 'repository URL {0} does not match expected pattern'.format(repo_url))
147 |
148 | username = m.group(1)
149 | require(username, 'empty username in repository URL {0}'.format(repo_url))
150 |
151 | project_name = m.group(2)
152 | require(username, 'empty project name in repository URL {0}'.format(repo_url))
153 |
154 | url = F_API_URL.format(username, project_name)
155 | r = requests.get(url)
156 | require(r.status_code == 200,
157 | 'Request for {0} failed with {1}'.format(url, r.status_code))
158 |
159 | result = {}
160 | for entry in r.json():
161 | result[entry['name']] = entry['color']
162 | return result
163 |
164 |
165 | if __name__ == '__main__':
166 | main()
167 |
--------------------------------------------------------------------------------
/assets/img/swc-icon-blue.svg:
--------------------------------------------------------------------------------
1 |
2 |
71 |
--------------------------------------------------------------------------------
/_extras/design.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Background and Design
4 | permalink: /design/
5 | ---
6 | There are a few things you need to know in order to understand why we
7 | do things the way we do. Some of them are specific to GitHub, rather
8 | than Git itself.
9 |
10 | 1. Git uses the term "clone" to mean "a copy of a repository".
11 | GitHub uses the term "fork" to mean, "a copy of a GitHub-hosted
12 | repo that is also hosted on GitHub", and the term "clone" to mean
13 | "a copy of a GitHub-hosted repo that's located on someone else's
14 | machine". In both cases, the duplicate has a remote called
15 | `origin` that points to the original repo; other remotes can be
16 | added manually.
17 |
18 | 2. A user on GitHub can only have one fork of a particular repo.
19 | This is a problem for us because an instructor may be involved in
20 | several workshops, each of which has its own website repo. Those
21 | website repositories ought to be forks of this one, but since
22 | GitHub doesn't allow that, we have to use `import.github.com`.
23 |
24 | 3. If a repository has a file called `README.md` in its root
25 | directory, GitHub displays the contents of that file on the
26 | repository's home page.
27 |
28 | 4. If a repository has a branch called `gh-pages` (which stands for
29 | "GitHub pages"), then GitHub uses the HTML and Markdown files in
30 | that branch to create a website for the repository. If the
31 | repository's URL is `http://github.com/darwin/finches`, the URL
32 | for the website is `http://darwin.github.io/finches`.
33 |
34 | 5. If an HTML or Markdown file has a header consisting of three
35 | dashes, some data about the page, and three more dashes:
36 |
37 | ~~~
38 | ---
39 | key: value
40 | other_key: other_value
41 | ---
42 | stuff in the page
43 | ~~~
44 |
45 | then GitHub doesn't just copy the file over verbatim. Instead, it
46 | runs the file through a translator called [Jekyll][jekyll] that
47 | looks for specially-formatted commands embedded in the file and
48 | uses them to fill in the page.
49 |
50 | 6. Commands can be embedded in the body of a page. One is
51 | `{% raw %}{% include something.html %}{% endraw %}`, which tells
52 | Jekyll to copy the contents of `something.html` into the file
53 | being translated; this is used to create standard headers and
54 | footers for pages. Another is `{{variable}}`: when Jekyll sees
55 | this, it replaces it with the value of `variable`. This is used
56 | to insert things like a contact email address and the URL for our
57 | Twitter account.
58 |
59 | 7. Jekyll gets variables from two places: a file called `_config.yml`
60 | located in the repo's root directory, and the header of each
61 | individual page. Variables from `_config.yml` are put in an
62 | object called `site`, and referred to as `site.variable`, so that
63 | (for example) `{{site.swc_site}}` in a page is replaced by the URL
64 | of the main Software Carpentry web site. Variables from the
65 | page's header are put in an object called `page`, and referred to
66 | as `page.variable`, so if a page's header defines a variable
67 | called `venue`, `{{page.venue}}` is replaced by "Euphoric State
68 | University" (or whatever value the variable has).
69 |
70 | 8. If a page uses `{% raw %}{% include something.html %}{% endraw %}`
71 | to include a snippet of HTML, Jekyll looks in a directory called
72 | `_includes` to find `something.html`. It always looks there, and
73 | nowhere else, so anything we want people to be able to include in
74 | their pages has to be stored in `_includes`.
75 |
76 | 9. A repository can have another special directory called `_layouts`.
77 | If a page like `index.html` has a variable called `layout`, and
78 | that variable's value is `standard.html`, Jekyll loads the file
79 | `_layouts/standard.html` and copies the content of `index.html`
80 | into it, then expands the result. This is used to give the pages
81 | in a site a uniform appearance.
82 | We have created two layouts for workshop pages:
83 |
84 | * `workshop.html` is used for workshops' home pages, and is the
85 | layout for the `index.html` page in your repo's root directory.
86 | That `index.html` page's header must define several variables as
87 | specified in the the customization instructions in order for
88 | your workshop to be included in our main website.
89 |
90 | * `page.html` is used for any other pages you want to create.
91 | **Note:** if you create extra pages, you *must* edit the values
92 | in the top section of `_config.yml` as described in
93 | [the lesson template documentation]({{ site.example_site }}).
94 |
95 | ## Extra Directories
96 |
97 | This workshop template shares resources with the template used for
98 | Carpentry lessons. As a result, your workshop website's repository
99 | contains directories that most workshops don't need, but which can be
100 | used to store extra material when necessary:
101 |
102 | * `_extras/`: extra pages (like this one).
103 | * `_episodes/`: lesson episodes (which workshops usually don't have).
104 | * `_episodes_rmd/`: R Markdown lesson episodes (if any).
105 | * `code/`: for code samples.
106 | * `data/`: for data files.
107 | * `fig/`: for figures and other images.
108 | * `files/`: for miscellaneous files.
109 |
110 | For more information on these, please see [the documentation for the
111 | lesson template]({{ site.example_site }}).
112 |
113 | [jekyll]: https://jekyllrb.com/
114 |
--------------------------------------------------------------------------------
/assets/img/dc-icon-black.svg:
--------------------------------------------------------------------------------
1 |
2 |
75 |
--------------------------------------------------------------------------------
/_extras/faq.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: FAQ
4 | permalink: /faq/
5 | ---
6 |
7 | ## General
8 |
9 | *Where can I get help?*
10 | : Mail us at [{{site.email}}](mailto:{{site.email}}),
11 | or join our [discussion list]({{site.swc_site}}/join/)
12 | and ask for help there.
13 |
14 | *What if I can't wait?*
15 | : Run `make workshop-check` to run the workshop homepage checking program on `index.html`.
16 |
17 | *Where can I report problems or suggest improvements?*
18 | : Please file an issue against [{{site.workshop_repo}}](this repository)
19 | or [mail us](mailto:{{site.email}}).
20 |
21 | *Why does the workshop repository have to be created by importing rather than forking?*
22 | : Because any particular user can only have one fork of a repository,
23 | but instructors frequently need to work on several workshops at once.
24 |
25 | *Why do I have to be logged in before I start the import?*
26 | : It's a known issue with GitHub's importer.
27 |
28 | *Why does the workshop repository name have to follow the `YYYY-MM-DD-site` pattern?*
29 | : This makes it easy for coordinators to track workshops.
30 | There are plans to move that coordination into [AMY][amy],
31 | but until that happens this pattern makes it easy to sort workshops
32 | by date without requiring an additional start-date column.
33 | **Note: `YYYY-MM-DD` should be the start date of the workshop.**
34 |
35 | *Why use the `gh-pages` branch instead of `master`?*
36 | : Because [GitHub automatically publishes `gh-pages`][github-pages] as a website.
37 |
38 | *Why use Jekyll? Why not some other markup language and some other converter?*
39 | : Because it's the only tool supported by GitHub Pages.
40 |
41 | *Where should pages go if multiple workshops are running at a site simultaneously?*
42 | : Use subdirectories like `2015-07-01-esu/beginners`,
43 | so that repository names always follow our four-part convention.
44 |
45 | *What if I want to add more values to `index.html`, like `address1` and `address2` for different rooms on different days?*
46 | : Go ahead,
47 | but you *must* have the variables described in the customization instructions.
48 |
49 | *What is the "Windows installer"?*
50 | : We have built a small installation helper for Windows
51 | that installs nano and SQLite, adds R to the path, and so on.
52 | It is maintained in
53 |
54 | which also has an up-to-date description of what it actually does.
55 | The latest version is always available at
56 | ,
57 | and contributions are always welcome.
58 |
59 | ## Debugging
60 |
61 | *Help, my website is not updating!*
62 | : Ensure that strings in the header of `index.html` are enclosed in quotations `"`.
63 | Special characters such as `"&"` may render correctly on your local machine
64 | but cause rendering to fail silently on GitHub.
65 |
66 | *Eventbrite registration isn't showing up on the workshop's home page.*
67 | : First check that you have something like:
68 |
69 | ~~~
70 | eventbrite: 1234567890AB
71 | ~~~
72 |
73 | in the YAML header of `index.html`.
74 | If the YAML header is set properly you may be accessing
75 | `file:///home/to/workshop/directory/_site/index.html` directly.
76 | Instead,
77 | please run
78 |
79 | ~~~
80 | $ make serve
81 | ~~~
82 |
83 | and look at `http://localhost:4000` in your browser
84 | (or push your changes to GitHub and view your page there).
85 |
86 | *What do I do if I see a `invalid byte sequence in ...` error when I run `tools/check`?*
87 | : Your computer is telling you that it doesn't understand some of the characters you're using.
88 | Declare your locale to be `en_US.UTF-8` in your shell:
89 |
90 | ~~~
91 | $ export LC_ALL=en_US.UTF-8
92 | $ export LANG=en_US.UTF-8
93 | ~~~
94 |
95 | *What do I do if I get a "can't convert nil into String" error?*
96 | : On some Linux distributions (e.g, Ubuntu 14.04), you may get this error:
97 |
98 | ~~~
99 | $ ./tools/preview
100 | /usr/lib/ruby/1.9.1/rubygems/custom_require.rb:36:in `require': iconv will be deprecated in the future, use String#encode instead.
101 | /usr/lib/ruby/1.9.1/time.rb:265:in `_parse': can't convert nil into String (TypeError)
102 | from /usr/lib/ruby/1.9.1/time.rb:265:in `parse'
103 | from /usr/bin/jekyll:95:in `block (2 levels) in '
104 | from /usr/lib/ruby/1.9.1/optparse.rb:1391:in `call'
105 | from /usr/lib/ruby/1.9.1/optparse.rb:1391:in `block in parse_in_order'
106 | from /usr/lib/ruby/1.9.1/optparse.rb:1347:in `catch'
107 | from /usr/lib/ruby/1.9.1/optparse.rb:1347:in `parse_in_order'
108 | from /usr/lib/ruby/1.9.1/optparse.rb:1341:in `order!'
109 | from /usr/lib/ruby/1.9.1/optparse.rb:1432:in `permute!'
110 | from /usr/lib/ruby/1.9.1/optparse.rb:1453:in `parse!'
111 | from /usr/bin/jekyll:137:in `'
112 | ~~~
113 |
114 | This occurs because you are using an old version of Jekyll located in `/usr/bin`.
115 | Make sure that you have installed Jekyll using:
116 |
117 | ~~~
118 | $ gem install jekyll
119 | ~~~
120 |
121 | This installs Jekyll in `/usr/local/bin`,
122 | so make sure this directory comes before `/usr/bin` in your `PATH` environment variable.
123 | When your path is set correctly,
124 | you should see:
125 |
126 | ~~~
127 | $ which jekyll
128 | /usr/local/bin/jekyll
129 | ~~~
130 |
131 | You may also have to install the `nodejs` package to disable references to JavaScript,
132 | which you can do using:
133 |
134 | ~~~
135 | $ sudo apt-get install nodejs
136 | ~~~
137 |
138 | For more information,
139 | see .
140 |
141 | [amy]: https://github.com/swcarpentry/amy
142 | [github-pages]: https://help.github.com/articles/creating-project-pages-manually/
143 |
--------------------------------------------------------------------------------
/bin/util.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | import sys
3 | import os
4 | import json
5 | from subprocess import Popen, PIPE
6 |
7 | # Import this way to produce a more useful error message.
8 | try:
9 | import yaml
10 | except ImportError:
11 | print('Unable to import YAML module: please install PyYAML', file=sys.stderr)
12 | sys.exit(1)
13 |
14 |
15 | # Things an image file's name can end with.
16 | IMAGE_FILE_SUFFIX = {
17 | '.gif',
18 | '.jpg',
19 | '.png',
20 | '.svg'
21 | }
22 |
23 | # Files that shouldn't be present.
24 | UNWANTED_FILES = [
25 | '.nojekyll'
26 | ]
27 |
28 | # Marker to show that an expected value hasn't been provided.
29 | # (Can't use 'None' because that might be a legitimate value.)
30 | REPORTER_NOT_SET = []
31 |
32 | class Reporter(object):
33 | """Collect and report errors."""
34 |
35 | def __init__(self):
36 | """Constructor."""
37 |
38 | super(Reporter, self).__init__()
39 | self.messages = []
40 |
41 |
42 | def check_field(self, filename, name, values, key, expected=REPORTER_NOT_SET):
43 | """Check that a dictionary has an expected value."""
44 |
45 | if key not in values:
46 | self.add(filename, '{0} does not contain {1}', name, key)
47 | elif expected is REPORTER_NOT_SET:
48 | pass
49 | elif type(expected) in (tuple, set, list):
50 | if values[key] not in expected:
51 | self.add(filename, '{0} {1} value {2} is not in {3}', name, key, values[key], expected)
52 | elif values[key] != expected:
53 | self.add(filename, '{0} {1} is {2} not {3}', name, key, values[key], expected)
54 |
55 |
56 | def check(self, condition, location, fmt, *args):
57 | """Append error if condition not met."""
58 |
59 | if not condition:
60 | self.add(location, fmt, *args)
61 |
62 |
63 | def add(self, location, fmt, *args):
64 | """Append error unilaterally."""
65 |
66 | self.messages.append((location, fmt.format(*args)))
67 |
68 |
69 | def report(self, stream=sys.stdout):
70 | """Report all messages in order."""
71 |
72 | if not self.messages:
73 | return
74 |
75 | def pretty(item):
76 | location, message = item
77 | if isinstance(location, type(None)):
78 | return message
79 | elif isinstance(location, str):
80 | return location + ': ' + message
81 | elif isinstance(location, tuple):
82 | return '{0}:{1}: '.format(*location) + message
83 | else:
84 | assert False, 'Unknown item "{0}"'.format(item)
85 |
86 | def key(item):
87 | location, message = item
88 | if isinstance(location, type(None)):
89 | return ('', -1, message)
90 | elif isinstance(location, str):
91 | return (location, -1, message)
92 | elif isinstance(location, tuple):
93 | return (location[0], location[1], message)
94 | else:
95 | assert False, 'Unknown item "{0}"'.format(item)
96 |
97 | for m in sorted(self.messages, key=key):
98 | print(pretty(m), file=stream)
99 |
100 |
101 | def read_markdown(parser, path):
102 | """
103 | Get YAML and AST for Markdown file, returning
104 | {'metadata':yaml, 'metadata_len':N, 'text':text, 'lines':[(i, line, len)], 'doc':doc}.
105 | """
106 |
107 | # Split and extract YAML (if present).
108 | with open(path, 'r') as reader:
109 | body = reader.read()
110 | metadata_raw, metadata_yaml, body = split_metadata(path, body)
111 |
112 | # Split into lines.
113 | metadata_len = 0 if metadata_raw is None else metadata_raw.count('\n')
114 | lines = [(metadata_len+i+1, line, len(line)) for (i, line) in enumerate(body.split('\n'))]
115 |
116 | # Parse Markdown.
117 | cmd = 'ruby {0}'.format(parser)
118 | p = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, close_fds=True, universal_newlines=True)
119 | stdout_data, stderr_data = p.communicate(body)
120 | doc = json.loads(stdout_data)
121 |
122 | return {
123 | 'metadata': metadata_yaml,
124 | 'metadata_len': metadata_len,
125 | 'text': body,
126 | 'lines': lines,
127 | 'doc': doc
128 | }
129 |
130 |
131 | def split_metadata(path, text):
132 | """
133 | Get raw (text) metadata, metadata as YAML, and rest of body.
134 | If no metadata, return (None, None, body).
135 | """
136 |
137 | metadata_raw = None
138 | metadata_yaml = None
139 | metadata_len = None
140 |
141 | pieces = text.split('---', 2)
142 | if len(pieces) == 3:
143 | metadata_raw = pieces[1]
144 | text = pieces[2]
145 | try:
146 | metadata_yaml = yaml.load(metadata_raw)
147 | except yaml.YAMLError as e:
148 | print('Unable to parse YAML header in {0}:\n{1}'.format(path, e), file=sys.stderr)
149 | sys.exit(1)
150 |
151 | return metadata_raw, metadata_yaml, text
152 |
153 |
154 | def load_yaml(filename):
155 | """
156 | Wrapper around YAML loading so that 'import yaml' is only needed
157 | in one file.
158 | """
159 |
160 | try:
161 | with open(filename, 'r') as reader:
162 | return yaml.load(reader)
163 | except (yaml.YAMLError, FileNotFoundError) as e:
164 | print('Unable to load YAML file {0}:\n{1}'.format(filename, e), file=sys.stderr)
165 | sys.exit(1)
166 |
167 |
168 | def check_unwanted_files(dir_path, reporter):
169 | """
170 | Check that unwanted files are not present.
171 | """
172 |
173 | for filename in UNWANTED_FILES:
174 | path = os.path.join(dir_path, filename)
175 | reporter.check(not os.path.exists(path),
176 | path,
177 | "Unwanted file found")
178 |
179 |
180 | def require(condition, message):
181 | """Fail if condition not met."""
182 |
183 | if not condition:
184 | print(message, file=sys.stderr)
185 | sys.exit(1)
186 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | [Software Carpentry][swc-site] and [Data Carpentry][dc-site] are open source projects,
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 welcome.
9 |
10 | ## Contributor Agreement
11 |
12 | By contributing,
13 | you agree that we may redistribute your work under [our license](LICENSE.md).
14 | In exchange,
15 | we will address your issues and/or assess your change proposal as promptly as we can,
16 | and help you become a member of our community.
17 | Everyone involved in [Software Carpentry][swc-site] and [Data Carpentry][dc-site]
18 | agrees to abide by our [code of conduct](CONDUCT.md).
19 |
20 | ## How to Contribute
21 |
22 | The easiest way to get started is to file an issue
23 | to tell us about a spelling mistake,
24 | some awkward wording,
25 | or a factual error.
26 | This is a good way to introduce yourself
27 | and to meet some of our community members.
28 |
29 | 1. If you do not have a [GitHub][github] account,
30 | you can [send us comments by email][contact].
31 | However,
32 | we will be able to respond more quickly if you use one of the other methods described below.
33 |
34 | 2. If you have a [GitHub][github] account,
35 | or are willing to [create one][github-join],
36 | but do not know how to use Git,
37 | you can report problems or suggest improvements by [creating an issue][issues].
38 | This allows us to assign the item to someone
39 | and to respond to it in a threaded discussion.
40 |
41 | 3. If you are comfortable with Git,
42 | and would like to add or change material,
43 | you can submit a pull request (PR).
44 | Instructions for doing this are [included below](#using-github).
45 |
46 | ## Where to Contribute
47 |
48 | 1. If you wish to change the template used for workshop websites,
49 | please work in .
50 | The home page of that repository explains how to set up workshop websites,
51 | while the extra pages in
52 | provide more background on our design choices.
53 |
54 | 2. If you wish to change CSS style files, tools,
55 | or HTML boilerplate for lessons or workshops stored in `_includes` or `_layouts`,
56 | please work in .
57 |
58 | ## What to Contribute
59 |
60 | There are many ways to contribute,
61 | from writing new exercises and improving existing ones
62 | to updating or filling in the documentation
63 | and and submitting [bug reports][issues]
64 | about things that don't work, aren't clear, or are missing.
65 | If you are looking for ideas,
66 | please see [the list of issues for this repository][issues],
67 | or the issues for [Data Carpentry][dc-issues]
68 | and [Software Carpentry][swc-issues] projects.
69 |
70 | Comments on issues and reviews of pull requests are just as welcome:
71 | we are smarter together than we are on our own.
72 | Reviews from novices and newcomers are particularly valuable:
73 | it's easy for people who have been using these lessons for a while
74 | to forget how impenetrable some of this material can be,
75 | so fresh eyes are always welcome.
76 |
77 | ## What *Not* to Contribute
78 |
79 | Our lessons already contain more material than we can cover in a typical workshop,
80 | so we are usually *not* looking for more concepts or tools to add to them.
81 | As a rule,
82 | if you want to introduce a new idea,
83 | you must (a) estimate how long it will take to teach
84 | and (b) explain what you would take out to make room for it.
85 | The first encourages contributors to be honest about requirements;
86 | the second, to think hard about priorities.
87 |
88 | We are also not looking for exercises or other material that only run on one platform.
89 | Our workshops typically contain a mixture of Windows, macOS, and Linux users;
90 | in order to be usable,
91 | our lessons must run equally well on all three.
92 |
93 | ## Using GitHub
94 |
95 | If you choose to contribute via GitHub,
96 | you may want to look at
97 | [How to Contribute to an Open Source Project on GitHub][how-contribute].
98 | In brief:
99 |
100 | 1. The published copy of the lesson is in the `gh-pages` branch of the repository
101 | (so that GitHub will regenerate it automatically).
102 | Please create all branches from that,
103 | and merge the [master repository][repo]'s `gh-pages` branch into your `gh-pages` branch
104 | before starting work.
105 | Please do *not* work directly in your `gh-pages` branch,
106 | since that will make it difficult for you to work on other contributions.
107 |
108 | 2. We use [GitHub flow][github-flow] to manage changes:
109 | 1. Create a new branch in your desktop copy of this repository for each significant change.
110 | 2. Commit the change in that branch.
111 | 3. Push that branch to your fork of this repository on GitHub.
112 | 4. Submit a pull request from that branch to the [master repository][repo].
113 | 5. If you receive feedback,
114 | make changes on your desktop and push to your branch on GitHub:
115 | the pull request will update automatically.
116 |
117 | Each lesson has two maintainers who review issues and pull requests
118 | or encourage others to do so.
119 | The maintainers are community volunteers,
120 | and have final say over what gets merged into the lesson.
121 |
122 | ## Other Resources
123 |
124 | General discussion of [Software Carpentry][swc-site] and [Data Carpentry][dc-site]
125 | happens on the [discussion mailing list][discuss-list],
126 | which everyone is welcome to join.
127 | You can also [reach us by email][contact].
128 |
129 | [contact]: mailto:admin@software-carpentry.org
130 | [dc-issues]: https://github.com/issues?q=user%3Adatacarpentry
131 | [dc-lessons]: http://datacarpentry.org/lessons/
132 | [dc-site]: http://datacarpentry.org/
133 | [discuss-list]: http://lists.software-carpentry.org/listinfo/discuss
134 | [github]: http://github.com
135 | [github-flow]: https://guides.github.com/introduction/flow/
136 | [github-join]: https://github.com/join
137 | [how-contribute]: https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github
138 | [issues]: https://github.com/swcarpentry/workshop-template/issues/
139 | [repo]: https://github.com/swcarpentry/workshop-template/
140 | [swc-issues]: https://github.com/issues?q=user%3Aswcarpentry
141 | [swc-lessons]: http://software-carpentry.org/lessons/
142 | [swc-site]: http://software-carpentry.org/
143 |
--------------------------------------------------------------------------------
/_extras/customization.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Customizing Your Workshop's Website
4 | permalink: /customization/
5 | ---
6 | ## Configuration File `_config.yml`
7 |
8 | You should edit the `_config.yml` configuration file in the root directory of your workshop to configure some site-wide variables and make the site function correctly:
9 |
10 | * `carpentry` - to tell us which carpentry workshop this is, possible values are ("swc", "dc" or "lc")
11 | * `title` - overall title for all pages
12 | * `repository` - as \/\ (e.g. `gvwilson/2015-07-01-miskatonic`), so that URLs resolve correctly both locally and on GitHub - see https://help.github.com/articles/repository-metadata-on-github-pages
13 | * `workshop_repo` - the URL of your workshop repository on GitHub
14 | * `workshop_site` - the repository's GitHub Pages URL
15 |
16 | For example, if the URL for the repository is `https://github.com/gvwilson/2015-07-01-miskatonic`,
17 | the URL for the website will be `http://gvwilson.github.io/2015-07-01-miskatonic`.
18 |
19 | You should not need to modify any of the other values in `_config.yml`.
20 |
21 | ## Home Page: Data
22 |
23 | Your workshop's home page lives in `index.html`,
24 | which must define the following values in its header:
25 |
26 | * `layout` must be `workshop`.
27 |
28 | * `carpentry` must be either "dc" (for Data Carpentry) or "swc" (for
29 | Software Carpentry).
30 |
31 | * `venue` is the short name of the institution or group hosting the
32 | workshop, like "Euphoric State University". It should *not*
33 | include the address or other details, since this value is
34 | displayed in a table on the main
35 | [Software Carpentry](http://software-carpentry.org) website.
36 |
37 | * `address` is the workshop's address (including details like the
38 | room number). The address should be all on one line.
39 |
40 | * `country` must be a two-letter ISO-3166 code for the country in
41 | which the workshop is going to take place, such as "fr" (for
42 | France) or "nz" (for New Zealand) - see [Wikipedia](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements)
43 | for a complete list.
44 |
45 | * `language` is the language that will be used in the workshop.
46 | It must be an [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes).
47 | Note that two-letter codes mean different things for countries
48 | and languages: "ar" is Arabic when used for a language, but
49 | Argentina when used for a country.
50 |
51 | * `latlng` is the latitude and longitude of the workshop site (so we
52 | can put a pin on our map). You can use
53 | [this site](http://itouchmap.com/latlong.html) to find these
54 | values. You can *not* put spaces around the comma separating the
55 | latitude from the longitude.
56 |
57 | * `humandate` is the human-friendly start and end date for the
58 | workshop. Please use three-letter month names and abbreviations
59 | (e.g., `Jul` instead of `July`), since these values are displayed
60 | in a table on our websites. (Strictly speaking this information
61 | is redundant, since we require a machine-readable `startdate` and
62 | `enddate`, but reliably translating those into human-readable
63 | dates is an interesting challenge...)
64 |
65 | * `humantime` is the human-friendly start and end time for each day of
66 | the workshop, e.g., "09:00 am - 4:00 pm" or "09:00-16:00". (We
67 | recognize that we ought to allow different start or end times on
68 | different days, but going down that path leads eventually to
69 | embedding iCal date/time specifications in our headers, which in
70 | turn leads to madness...)
71 |
72 | * `startdate` is the workshop's starting date in YYYY-MM-DD format,
73 | such as `2015-07-01`. You must use four digits for the year and
74 | two each for the month and day.
75 |
76 | * `enddate` is the workshop's ending date in the same format. If your
77 | workshop is only one day long, the `enddate` field should be deleted.
78 | If your workshop has a more complicated schedule (e.g., a half day a
79 | week for four weeks), please delete the `enddate` field and only tell
80 | us its start date.
81 |
82 | * `instructor` is a comma-separated list of instructor names. The
83 | list must be enclosed in square brackets, and each name must be in
84 | double quotes, as in `["Alan Turing","Grace Hopper"]`. Do not
85 | include other information (such as the word "instructor") in these
86 | values.
87 |
88 | * `helper` is a comma-separated list of helper names formatted in the
89 | same way as the instructor names. If there are no helpers, use an
90 | empty list `[]`.
91 |
92 | * `contact` is the contact email address to use for your workshop.
93 | If you do not provide a contact email address, your website will
94 | display the address for the workshop coordinators (who probably
95 | won't be able to answer questions about the specific details of
96 | your workshop).
97 |
98 | The header may optionally define the following:
99 |
100 | * `etherpad` is the URL for the Etherpad for your workshop. If you are
101 | not using an Etherpad, you can delete this line.
102 |
103 | * `eventbrite` is the multi-digit Eventbrite registration key. If you
104 | are using Eventbrite, the Software Carpentry administrators will
105 | give this to you. If you are using something else, you may delete
106 | this line. Note: this value must be given as a string in double
107 | quotes, rather than as a number.
108 |
109 | ## Home Page: Schedule and Syllabus
110 |
111 | You should edit the sections titled `Schedule` and `Syllabus`
112 | so that they show what you're actually planning to teach and when.
113 |
114 | ## Home Page: Setup
115 |
116 | You should delete the pieces of the `Setup` section
117 | related to software you will not be using in your workshop,
118 | so that learners don't spend time installing software they don't need.
119 |
120 | ## Setup: Installation tests
121 |
122 | If you intend to use the installation-test scripts,
123 | uncomment the paragraph linking to `setup/index.html` in `index.html`
124 | and edit `setup/swc-installation-test-2.py` as described below.
125 |
126 | `swc-installation-test-1.py` is pretty simple, and just checks that
127 | the students have a recent enough version of Python installed that
128 | they can run `swc-installation-test-2.py`.
129 |
130 | `swc-installation-test-2.py`
131 | checks for a list of dependencies and prints error messages if a
132 | package is not installed, or if the installed version is not current
133 | enough. By default, the script checks for pretty much anything that
134 | has ever been used at a Software Carpentry workshop, which is probably
135 | not what you want for your particular workshop.
136 |
137 | Go through `swc-installation-test-2.py` and
138 | comment any dependencies you don't need out of the `CHECKS` list. You
139 | might also want to skim through the minimum version numbers listed
140 | where particular dependencies are defined (e.g. `('git', 'Git', (1, 7,
141 | 0), None)`). For the most part, fairly conservative values have been
142 | selected, so students with modern machines should be fine. If your
143 | workshop has stricter version requirements, feel free to bump them
144 | accordingly.
145 |
146 | Similarly, the virtual dependencies can be satisfied by any of several
147 | packages. If you don't want to support a particular package (e.g. if
148 | you have no Emacs experience and don't want to be responsible for
149 | students who show up with Emacs as their only editor), you can comment
150 | out that particular `or_dependency`.
151 |
152 | ## Updating the repository
153 |
154 | If for some reason,
155 | such as the installation instructions having become disconnected
156 | with the current lesson material,
157 | you need to get changes from this repository
158 | into the clone of it with your workshop page,
159 | please follow the steps bellow:
160 |
161 | 1. Add the workshop-template repository as upstream:
162 |
163 | $ git remote add upstream https://github.com/swcarpentry/workshop-template.git
164 |
165 | 2. Fetch the data from upstream repository (also know as the workshop-template
166 | repository):
167 |
168 | $ git fetch upstream
169 |
170 | 3. Merge the new changes:
171 |
172 | $ git merge upstream/gh-pages
173 |
174 | 4. When get conflicts, solve it and
175 |
176 | $ git commit -a
177 |
178 | 5. Push the changes to GitHub:
179 |
180 | $ git push origin gh-pages
181 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # workshop-template
2 |
3 | This repository is [Software Carpentry][swc-site] and [Data Carpentry][dc-site]'s
4 | template for creating websites for workshops.
5 |
6 | 1. Please *do not fork this repository directly on GitHub.*
7 | Instead, please use GitHub's importer following [the instructions below](#creating-a-repository)
8 | to copy this `workshop-template` repository and customize it for your workshop.
9 |
10 | 2. Please *do your work in your repository's `gh-pages` branch*,
11 | since that is what is
12 | [automatically published as a website by GitHub][github-project-pages].
13 |
14 | 3. Once you are done, please also [let us know][email] the workshop URL. If this is a self-organised workshop, you should also [fill in the self-organized workshop form][self-organized-workshop-form] (if you have not already done so), so we can keep track of all workshops. We build the list of workshops on our websites from the data included in your `index.md` page. We can only do that if you [customize][customization] that page correctly *and* let us know the workshop URL.
15 |
16 | If you run into problems,
17 | or have ideas about how to make this process simpler,
18 | please [get in touch](#getting-and-giving-help).
19 | The pages on [customizing your website][customization],
20 | the [FAQ][faq],
21 | and the [design notes][design] have more detail on what we do and why.
22 | And please note:
23 | if you are teaching Git,
24 | please [create a separate repository](#setting-up-a-separate-repository-for-learners)
25 | for your learners to practice in.
26 |
27 | ## Creating a Repository
28 |
29 | 1. Log in to GitHub.
30 | (If you do not have an account, you can quickly create one for free.)
31 | You must be logged in for the remaining steps to work.
32 |
33 | 2. Go to GitHub's importer.
34 |
35 | 3. Paste the url of this repo as the old repository to clone:
36 | .
37 |
38 | 4. Select the owner for your new repository.
39 | (This will probably be you, but may instead be an organization you belong to.)
40 |
41 | 5. Choose a name for your workshop website repository.
42 | This name should have the form `YYYY-MM-DD-site`,
43 | e.g., `2016-12-01-miskatonic`,
44 | where `YYYY-MM-DD` is the start date of the workshop.
45 |
46 | 6. Make sure the repository is public.
47 |
48 | 7. At this point, you should have a page like this:
49 |
50 | 
51 |
52 | You can now click "Begin Import".
53 | When the process is done,
54 | you will receive a message like
55 | "Importing complete! Your new repository gvwilson/2016-12-01-miskatonic is ready."
56 | and you can go to the new repository by clicking on the name.
57 |
58 | **Note:**
59 | some people have had intermittent errors during the import process,
60 | possibly because of the network timing out.
61 | If you experience a problem, please re-try;
62 | if the problem persists,
63 | please [get in touch](#getting-and-giving-help).
64 |
65 | ## Customizing Your Website
66 |
67 | 1. Go into your newly-created repository,
68 | which will be at `https://github.com/your_username/YYYY-MM-DD-site`.
69 | For example,
70 | if your username is `gvwilson`,
71 | the repository's URL will be `https://github.com/gvwilson/2016-12-01-miskatonic`.
72 |
73 | 3. Ensure you are on the gh-pages branch by clicking on the branch under the drop
74 | down in the menu bar (see the note below):
75 |
76 | 
77 |
78 | 3. Edit the header of `index.md` to customize the list of instructors,
79 | workshop venue, etc.
80 | You can do this in the browser by clicking on it in the file view on GitHub
81 | and then selecting the pencil icon in the menu bar:
82 |
83 | 
84 |
85 | Editing hints are embedded in `index.md`,
86 | and full instructions are in [the customization instructions][customization].
87 |
88 | 4. Edit `_config.yml` to customize certain site-wide variables, such as: `carpentry` (to tell us which carpentry workshop this is), `title` (overall title for all pages), `repository` (so that URLs resolve correctly both locally and on GitHub), `workshop_repo` (the URL of the workshop repository on GitHub) and `workshop_site` (the repository's GitHub Pages URL).
89 |
90 | Editing hints are embedded in `_config.yml`,
91 | and full instructions are in [the customization instructions][customization].
92 |
93 | 5. Edit the `schedule.html` file to edit the schedule for your upcoming workshop. This file is located in the `_includes` directory, make sure to choose the one from the appropriate `dc` (Data Carpentry workshop), `lc` (Library Carpentry), or `sc` (Software Carpentry) subdirectory.
94 |
95 | 6. Alternatively,
96 | if you are already familiar with Git,
97 | you can clone the repository to your desktop,
98 | edit `index.md`, `_config.yml`, and `schedule.html` there,
99 | and push your changes back to the repository.
100 |
101 | ~~~
102 | git clone -b gh-pages https://github.com/your_username/YYYY-MM-DD-site
103 | ~~~
104 |
105 | You should specify `-b gh-pages` to checkout the gh-pages branch because the imported
106 | repository doesn't have a `master` branch.
107 |
108 | In order to view your changes once you are done editing,
109 | you must push to your GitHub repository:
110 |
111 | ~~~
112 | git push origin gh-pages
113 | ~~~
114 |
115 | 7. When you are done editing,
116 | go to the GitHub Pages URL for your workshop and preview your changes.
117 | In the example above, this is `https://gvwilson.github.io/2016-12-01-miskatonic`.
118 | The finished page should look [something like this](fig/completed-page.png?raw=true).
119 |
120 | 8. Optional: you can now change the README.md file in your website's repository, which contains these instructions, so that it contains a short description of your workshop and a link to the workshop website.
121 |
122 | **Note:**
123 | please do all of your work in your repository's `gh-pages` branch,
124 | since [GitHub automatically publishes that as a website][github-project-pages].
125 |
126 | **Note:**
127 | this template includes some files and directories that most workshops do not need,
128 | but which provide a standard place to put extra content if desired.
129 | See the [design notes][design] for more information about these.
130 |
131 | Further instructions are available in [the customization instructions][customization].
132 | This [FAQ][faq] includes a few extra tips (additions are always welcome)
133 | and these notes on [the background and design][design] of this template may help as well.
134 |
135 | ## Checking Your Changes
136 |
137 | If you want to preview your changes on your own machine before publishing them on GitHub,
138 | you can do so as described below.
139 |
140 | 1. Install the software [described below](#installing-software).
141 | This may require some work,
142 | so feel free to preview by pushing to the website.
143 |
144 | 2. Run the command
145 |
146 | ~~~
147 | make serve
148 | ~~~
149 |
150 | and go to to preview your site.
151 | You can also run this command by typing `make serve`
152 | (assuming you have Make installed).
153 |
154 | 3. Run the command
155 |
156 | ~~~
157 | make workshop-check
158 | ~~~
159 |
160 | to check for a few common errors in your workshop's home page.
161 | (You must have Python 3 installed to do this.)
162 |
163 | ## (Optional) Linking to Your Page
164 |
165 | At the top of your repository on GitHub you'll see
166 |
167 | ~~~
168 | No description, website, or topics provided. — Edit
169 | ~~~
170 |
171 | Click 'Edit' and add:
172 |
173 | 1. A very brief description of your workshop in the "Description" box (e.g., "Miskatonic University workshop, Dec. 2016")
174 |
175 | 2. The URL for your workshop in the "Website" box (e.g., `https://gvwilson.github.io/2016-12-01-miskatonic`)
176 |
177 | This will help people find your website if they come to your repository's home page.
178 |
179 | ## Creating Extra Pages
180 |
181 | In rare cases,
182 | you may want to add extra pages to your workshop website.
183 | You can do this by putting either Markdown or HTML pages in the website's root directory
184 | and styling them according to the instructions give in
185 | [the lesson template][lesson-example].
186 | If you do this,
187 | you *must* also edit `_config.yml` to set these three values:
188 |
189 | 1. `carpentry` is either "dc" (for Data Carpentry), "swc" (for Software Carpentry),
190 | or "lc" (for Library Carpentry). This determines which logos are loaded.
191 |
192 | 2. `title` is the title of your workshop (typically the venue and date).
193 |
194 | 3. `email` is the contact email address for your workshop,
195 | e.g., `gvwilson@miskatonic.edu`.
196 |
197 | Note: `carpentry` and `email` duplicate information that's in `index.md`,
198 | but there is no way to avoid this
199 | without requiring people to edit both files in the usual case
200 | where no extra pages are created.
201 |
202 | ## Installing Software
203 |
204 | If you want to set up Jekyll
205 | so that you can preview changes on your own machine before pushing them to GitHub,
206 | you must install the software described below.
207 | (Note: Julian Thilo has written instructions for
208 | [installing Jekyll on Windows][jekyll-windows].)
209 |
210 | 1. **Ruby**.
211 | This is included with Linux and macOS;
212 | the simplest option on Windows is to use [RubyInstaller][ruby-installer].
213 | You can test your installation by running `ruby --version`.
214 | For more information,
215 | see [the Ruby installation guidelines][ruby-install-guide].
216 |
217 | 2. **[RubyGems][rubygems]**
218 | (the package manager for Ruby).
219 | You can test your installation by running `gem --version`.
220 |
221 | 3. **[Jekyll][jekyll]**.
222 | You can install this by running `gem install jekyll`.
223 |
224 | ## Setting Up a Separate Repository for Learners
225 |
226 | If you are teaching Git,
227 | you should create a separate repository for learners to use in that lesson.
228 | You should not have them use the workshop website repository because:
229 |
230 | * your workshop website repository contains many files
231 | that most learners don't need to see during the lesson,
232 | and
233 |
234 | * you probably don't want to accidentally merge
235 | a damaging pull request from a novice Git user
236 | into your workshop's website while you are using it to teach.
237 |
238 | You can call this repository whatever you like,
239 | and add whatever content you need to it.
240 |
241 | ## Getting and Giving Help
242 |
243 | We are committed to offering a pleasant setup experience for our learners and organizers.
244 | If you find bugs in our instructions,
245 | or would like to suggest improvements,
246 | please [file an issue][issues]
247 | or [mail us][email].
248 |
249 | [email]: mailto:admin@software-carpentry.org
250 | [customization]: https://swcarpentry.github.io/workshop-template/customization/
251 | [dc-site]: http://datacarpentry.org
252 | [design]: https://swcarpentry.github.io/workshop-template/design/
253 | [faq]: https://swcarpentry.github.io/workshop-template/faq/
254 | [github-project-pages]: https://help.github.com/articles/creating-project-pages-manually/
255 | [importer]: https://github.com/new/import
256 | [issues]: https://github.com/swcarpentry/workshop-template/issues
257 | [jekyll-windows]: http://jekyll-windows.juthilo.com/
258 | [jekyll]: https://jekyllrb.com/
259 | [lesson-example]: https://swcarpentry.github.io/lesson-example/
260 | [pyyaml]: https://pypi.python.org/pypi/PyYAML
261 | [ruby-install-guide]: https://www.ruby-lang.org/en/downloads/
262 | [ruby-installer]: http://rubyinstaller.org/
263 | [rubygems]: https://rubygems.org/pages/download/
264 | [self-organized-workshop-form]: https://amy.software-carpentry.org/workshops/submit/
265 | [swc-site]: http://software-carpentry.org
266 |
--------------------------------------------------------------------------------
/bin/workshop_check.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | '''Check that a workshop's index.html metadata is valid. See the
4 | docstrings on the checking functions for a summary of the checks.
5 | '''
6 |
7 | from __future__ import print_function
8 | import sys
9 | import os
10 | import re
11 | from datetime import date
12 | from util import Reporter, split_metadata, load_yaml, check_unwanted_files
13 |
14 | # Metadata field patterns.
15 | EMAIL_PATTERN = r'[^@]+@[^@]+\.[^@]+'
16 | 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)'
17 | EVENTBRITE_PATTERN = r'\d{9,10}'
18 | URL_PATTERN = r'https?://.+'
19 |
20 | # Defaults.
21 | CARPENTRIES = ("dc", "swc")
22 | DEFAULT_CONTACT_EMAIL = 'admin@software-carpentry.org'
23 |
24 | USAGE = 'Usage: "workshop_check.py path/to/root/directory"'
25 |
26 | # Country and language codes. Note that codes mean different things: 'ar'
27 | # is 'Arabic' as a language but 'Argentina' as a country.
28 |
29 | ISO_COUNTRY = [
30 | 'ad', 'ae', 'af', 'ag', 'ai', 'al', 'am', 'an', 'ao', 'aq', 'ar', 'as',
31 | 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd', 'be', 'bf', 'bg', 'bh',
32 | 'bi', 'bj', 'bm', 'bn', 'bo', 'br', 'bs', 'bt', 'bv', 'bw', 'by', 'bz',
33 | 'ca', 'cc', 'cd', 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm', 'cn', 'co',
34 | 'cr', 'cu', 'cv', 'cx', 'cy', 'cz', 'de', 'dj', 'dk', 'dm', 'do', 'dz',
35 | 'ec', 'ee', 'eg', 'eh', 'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm',
36 | 'fo', 'fr', 'ga', 'gb', 'gd', 'ge', 'gf', 'gg', 'gh', 'gi', 'gl', 'gm',
37 | 'gn', 'gp', 'gq', 'gr', 'gs', 'gt', 'gu', 'gw', 'gy', 'hk', 'hm', 'hn',
38 | 'hr', 'ht', 'hu', 'id', 'ie', 'il', 'im', 'in', 'io', 'iq', 'ir', 'is',
39 | 'it', 'je', 'jm', 'jo', 'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn', 'kp',
40 | 'kr', 'kw', 'ky', 'kz', 'la', 'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt',
41 | 'lu', 'lv', 'ly', 'ma', 'mc', 'md', 'me', 'mg', 'mh', 'mk', 'ml', 'mm',
42 | 'mn', 'mo', 'mp', 'mq', 'mr', 'ms', 'mt', 'mu', 'mv', 'mw', 'mx', 'my',
43 | 'mz', 'na', 'nc', 'ne', 'nf', 'ng', 'ni', 'nl', 'no', 'np', 'nr', 'nu',
44 | 'nz', 'om', 'pa', 'pe', 'pf', 'pg', 'ph', 'pk', 'pl', 'pm', 'pn', 'pr',
45 | 'ps', 'pt', 'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru', 'rw', 'sa', 'sb',
46 | 'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn', 'so',
47 | 'sr', 'st', 'sv', 'sy', 'sz', 'tc', 'td', 'tf', 'tg', 'th', 'tj', 'tk',
48 | 'tl', 'tm', 'tn', 'to', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua', 'ug', 'um',
49 | 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf', 'ws',
50 | 'ye', 'yt', 'za', 'zm', 'zw'
51 | ]
52 |
53 | ISO_LANGUAGE = [
54 | 'aa', 'ab', 'ae', 'af', 'ak', 'am', 'an', 'ar', 'as', 'av', 'ay', 'az',
55 | 'ba', 'be', 'bg', 'bh', 'bi', 'bm', 'bn', 'bo', 'br', 'bs', 'ca', 'ce',
56 | 'ch', 'co', 'cr', 'cs', 'cu', 'cv', 'cy', 'da', 'de', 'dv', 'dz', 'ee',
57 | 'el', 'en', 'eo', 'es', 'et', 'eu', 'fa', 'ff', 'fi', 'fj', 'fo', 'fr',
58 | 'fy', 'ga', 'gd', 'gl', 'gn', 'gu', 'gv', 'ha', 'he', 'hi', 'ho', 'hr',
59 | 'ht', 'hu', 'hy', 'hz', 'ia', 'id', 'ie', 'ig', 'ii', 'ik', 'io', 'is',
60 | 'it', 'iu', 'ja', 'jv', 'ka', 'kg', 'ki', 'kj', 'kk', 'kl', 'km', 'kn',
61 | 'ko', 'kr', 'ks', 'ku', 'kv', 'kw', 'ky', 'la', 'lb', 'lg', 'li', 'ln',
62 | 'lo', 'lt', 'lu', 'lv', 'mg', 'mh', 'mi', 'mk', 'ml', 'mn', 'mr', 'ms',
63 | 'mt', 'my', 'na', 'nb', 'nd', 'ne', 'ng', 'nl', 'nn', 'no', 'nr', 'nv',
64 | 'ny', 'oc', 'oj', 'om', 'or', 'os', 'pa', 'pi', 'pl', 'ps', 'pt', 'qu',
65 | 'rm', 'rn', 'ro', 'ru', 'rw', 'sa', 'sc', 'sd', 'se', 'sg', 'si', 'sk',
66 | 'sl', 'sm', 'sn', 'so', 'sq', 'sr', 'ss', 'st', 'su', 'sv', 'sw', 'ta',
67 | 'te', 'tg', 'th', 'ti', 'tk', 'tl', 'tn', 'to', 'tr', 'ts', 'tt', 'tw',
68 | 'ty', 'ug', 'uk', 'ur', 'uz', 've', 'vi', 'vo', 'wa', 'wo', 'xh', 'yi',
69 | 'yo', 'za', 'zh', 'zu'
70 | ]
71 |
72 |
73 | def look_for_fixme(func):
74 | """Decorator to fail test if text argument starts with "FIXME"."""
75 |
76 | def inner(arg):
77 | if (arg is not None) and \
78 | isinstance(arg, str) and \
79 | arg.lstrip().startswith('FIXME'):
80 | return False
81 | return func(arg)
82 | return inner
83 |
84 |
85 | @look_for_fixme
86 | def check_layout(layout):
87 | '''"layout" in YAML header must be "workshop".'''
88 |
89 | return layout == 'workshop'
90 |
91 |
92 | @look_for_fixme
93 | def check_carpentry(layout):
94 | '''"carpentry" in YAML header must be "dc" or "swc".'''
95 |
96 | return layout in CARPENTRIES
97 |
98 |
99 | @look_for_fixme
100 | def check_country(country):
101 | '''"country" must be a lowercase ISO-3166 two-letter code.'''
102 |
103 | return country in ISO_COUNTRY
104 |
105 |
106 | @look_for_fixme
107 | def check_language(language):
108 | '''"language" must be a lowercase ISO-639 two-letter code.'''
109 |
110 | return language in ISO_LANGUAGE
111 |
112 |
113 | @look_for_fixme
114 | def check_humandate(date):
115 | """
116 | 'humandate' must be a human-readable date with a 3-letter month
117 | and 4-digit year. Examples include 'Feb 18-20, 2025' and 'Feb 18
118 | and 20, 2025'. It may be in languages other than English, but the
119 | month name should be kept short to aid formatting of the main
120 | Software Carpentry web site.
121 | """
122 |
123 | if ',' not in date:
124 | return False
125 |
126 | month_dates, year = date.split(',')
127 |
128 | # The first three characters of month_dates are not empty
129 | month = month_dates[:3]
130 | if any(char == ' ' for char in month):
131 | return False
132 |
133 | # But the fourth character is empty ("February" is illegal)
134 | if month_dates[3] != ' ':
135 | return False
136 |
137 | # year contains *only* numbers
138 | try:
139 | int(year)
140 | except:
141 | return False
142 |
143 | return True
144 |
145 |
146 | @look_for_fixme
147 | def check_humantime(time):
148 | """
149 | 'humantime' is a human-readable start and end time for the
150 | workshop, such as '09:00 - 16:00'.
151 | """
152 |
153 | return bool(re.match(HUMANTIME_PATTERN, time.replace(' ', '')))
154 |
155 |
156 | def check_date(this_date):
157 | """
158 | 'startdate' and 'enddate' are machine-readable start and end dates
159 | for the workshop, and must be in YYYY-MM-DD format, e.g.,
160 | '2015-07-01'.
161 | """
162 |
163 | # YAML automatically loads valid dates as datetime.date.
164 | return isinstance(this_date, date)
165 |
166 |
167 | @look_for_fixme
168 | def check_latitude_longitude(latlng):
169 | """
170 | 'latlng' must be a valid latitude and longitude represented as two
171 | floating-point numbers separated by a comma.
172 | """
173 |
174 | try:
175 | lat, lng = latlng.split(',')
176 | lat = float(lat)
177 | long = float(lng)
178 | return (-90.0 <= lat <= 90.0) and (-180.0 <= long <= 180.0)
179 | except ValueError:
180 | return False
181 |
182 |
183 | def check_instructors(instructors):
184 | """
185 | 'instructor' must be a non-empty comma-separated list of quoted
186 | names, e.g. ['First name', 'Second name', ...']. Do not use 'TBD'
187 | or other placeholders.
188 | """
189 |
190 | # YAML automatically loads list-like strings as lists.
191 | return isinstance(instructors, list) and len(instructors) > 0
192 |
193 |
194 | def check_helpers(helpers):
195 | """
196 | 'helper' must be a comma-separated list of quoted names,
197 | e.g. ['First name', 'Second name', ...']. The list may be empty.
198 | Do not use 'TBD' or other placeholders.
199 | """
200 |
201 | # YAML automatically loads list-like strings as lists.
202 | return isinstance(helpers, list) and len(helpers) >= 0
203 |
204 |
205 | @look_for_fixme
206 | def check_emails(emails):
207 | """
208 | 'email' must be a comma-separated list of valid email addresses.
209 | The list may be empty. A valid email address consists of characters,
210 | an '@', and more characters. It should not contain the default contact
211 | """
212 |
213 | # YAML automatically loads list-like strings as lists.
214 | if (isinstance(emails, list) and len(emails) >= 0):
215 | for email in emails:
216 | if ((not bool(re.match(EMAIL_PATTERN, email))) or (email == DEFAULT_CONTACT_EMAIL)):
217 | return False
218 | else:
219 | return False
220 |
221 | return True
222 |
223 |
224 | def check_eventbrite(eventbrite):
225 | """
226 | 'eventbrite' (the Eventbrite registration key) must be 9 or more
227 | digits. It may appear as an integer or as a string.
228 | """
229 |
230 | if isinstance(eventbrite, int):
231 | return True
232 | else:
233 | return bool(re.match(EVENTBRITE_PATTERN, eventbrite))
234 |
235 |
236 | @look_for_fixme
237 | def check_collaborative_notes(collaborative_notes):
238 | """
239 | 'collaborative_notes' must be a valid URL.
240 | """
241 |
242 | return bool(re.match(URL_PATTERN, collaborative_notes))
243 |
244 |
245 | @look_for_fixme
246 | def check_pass(value):
247 | """
248 | This test always passes (it is used for 'checking' things like the
249 | workshop address, for which no sensible validation is feasible).
250 | """
251 |
252 | return True
253 |
254 |
255 | HANDLERS = {
256 | 'layout': (True, check_layout, 'layout isn\'t "workshop"'),
257 |
258 | 'carpentry': (True, check_carpentry, 'carpentry isn\'t in ' +
259 | ', '.join(CARPENTRIES)),
260 |
261 | 'country': (True, check_country,
262 | 'country invalid: must use lowercase two-letter ISO code ' +
263 | 'from ' + ', '.join(ISO_COUNTRY)),
264 |
265 | 'language': (False, check_language,
266 | 'language invalid: must use lowercase two-letter ISO code' +
267 | ' from ' + ', '.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 |
273 | 'humantime': (True, check_humantime,
274 | 'humantime doesn\'t include numbers'),
275 |
276 | 'startdate': (True, check_date,
277 | 'startdate invalid. Must be of format year-month-day, ' +
278 | 'i.e., 2014-01-31'),
279 |
280 | 'enddate': (False, check_date,
281 | 'enddate invalid. Must be of format year-month-day, i.e.,' +
282 | ' 2014-01-31'),
283 |
284 | 'latlng': (True, check_latitude_longitude,
285 | 'latlng invalid. Check that it is two floating point ' +
286 | 'numbers, separated by a comma'),
287 |
288 | 'instructor': (True, check_instructors,
289 | 'instructor list isn\'t a valid list of format ' +
290 | '["First instructor", "Second instructor",..]'),
291 |
292 | 'helper': (True, check_helpers,
293 | 'helper list isn\'t a valid list of format ' +
294 | '["First helper", "Second helper",..]'),
295 |
296 | 'email': (True, check_emails,
297 | 'contact email list isn\'t a valid list of format ' +
298 | '["me@example.org", "you@example.org",..] or contains incorrectly formatted email addresses or ' +
299 | '"{0}".'.format(DEFAULT_CONTACT_EMAIL)),
300 |
301 | 'eventbrite': (False, check_eventbrite, 'Eventbrite key appears invalid'),
302 |
303 | 'collaborative_notes': (False, check_collaborative_notes, 'Collaborative Notes URL appears invalid'),
304 |
305 | 'venue': (False, check_pass, 'venue name not specified'),
306 |
307 | 'address': (False, check_pass, 'address not specified')
308 | }
309 |
310 | # REQUIRED is all required categories.
311 | REQUIRED = set([k for k in HANDLERS if HANDLERS[k][0]])
312 |
313 | # OPTIONAL is all optional categories.
314 | OPTIONAL = set([k for k in HANDLERS if not HANDLERS[k][0]])
315 |
316 |
317 | def check_blank_lines(reporter, raw):
318 | """
319 | Blank lines are not allowed in category headers.
320 | """
321 |
322 | lines = [(i, x) for (i, x) in enumerate(raw.strip().split('\n')) if not x.strip()]
323 | reporter.check(not lines,
324 | None,
325 | 'Blank line(s) in header: {0}',
326 | ', '.join(["{0}: {1}".format(i, x.rstrip()) for (i, x) in lines]))
327 |
328 |
329 | def check_categories(reporter, left, right, msg):
330 | """
331 | Report differences (if any) between two sets of categories.
332 | """
333 |
334 | diff = left - right
335 | reporter.check(len(diff) == 0,
336 | None,
337 | '{0}: offending entries {1}',
338 | msg, sorted(list(diff)))
339 |
340 |
341 | def check_file(reporter, path, data):
342 | """
343 | Get header from file, call all other functions, and check file for
344 | validity.
345 | """
346 |
347 | # Get metadata as text and as YAML.
348 | raw, header, body = split_metadata(path, data)
349 |
350 | # Do we have any blank lines in the header?
351 | check_blank_lines(reporter, raw)
352 |
353 | # Look through all header entries. If the category is in the input
354 | # file and is either required or we have actual data (as opposed to
355 | # a commented-out entry), we check it. If it *isn't* in the header
356 | # but is required, report an error.
357 | for category in HANDLERS:
358 | required, handler, message = HANDLERS[category]
359 | if category in header:
360 | if required or header[category]:
361 | reporter.check(handler(header[category]),
362 | None,
363 | '{0}\n actual value "{1}"',
364 | message, header[category])
365 | elif required:
366 | reporter.add(None,
367 | 'Missing mandatory key "{0}"',
368 | category)
369 |
370 | # Check whether we have missing or too many categories
371 | seen_categories = set(header.keys())
372 | check_categories(reporter, REQUIRED, seen_categories,
373 | 'Missing categories')
374 | check_categories(reporter, seen_categories, REQUIRED.union(OPTIONAL),
375 | 'Superfluous categories')
376 |
377 |
378 | def check_config(reporter, filename):
379 | """
380 | Check YAML configuration file.
381 | """
382 |
383 | config = load_yaml(filename)
384 |
385 | kind = config.get('kind', None)
386 | reporter.check(kind == 'workshop',
387 | filename,
388 | 'Missing or unknown kind of event: {0}',
389 | kind)
390 |
391 | carpentry = config.get('carpentry', None)
392 | reporter.check(carpentry in ('swc', 'dc'),
393 | filename,
394 | 'Missing or unknown carpentry: {0}',
395 | carpentry)
396 |
397 |
398 | def main():
399 | '''Run as the main program.'''
400 |
401 | if len(sys.argv) != 2:
402 | print(USAGE, file=sys.stderr)
403 | sys.exit(1)
404 |
405 | root_dir = sys.argv[1]
406 | index_file = os.path.join(root_dir, 'index.md')
407 | config_file = os.path.join(root_dir, '_config.yml')
408 |
409 | reporter = Reporter()
410 | check_config(reporter, config_file)
411 | check_unwanted_files(root_dir, reporter)
412 | with open(index_file) as reader:
413 | data = reader.read()
414 | check_file(reporter, index_file, data)
415 | reporter.report()
416 |
417 |
418 | if __name__ == '__main__':
419 | main()
420 |
--------------------------------------------------------------------------------
/bin/lesson_initialize.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """Initialize a newly-created repository."""
4 |
5 |
6 | from __future__ import print_function
7 | import sys
8 | import os
9 |
10 | ROOT_AUTHORS = '''\
11 | FIXME: list authors' names and email addresses.
12 | '''
13 |
14 | ROOT_CITATION = '''\
15 | FIXME: describe how to cite this lesson.
16 | '''
17 |
18 | ROOT_CONTRIBUTING_MD = '''\
19 | # Contributing
20 |
21 | [Software Carpentry][swc-site] and [Data Carpentry][dc-site] are open source projects,
22 | and we welcome contributions of all kinds:
23 | new lessons,
24 | fixes to existing material,
25 | bug reports,
26 | and reviews of proposed changes are all welcome.
27 |
28 | ## Contributor Agreement
29 |
30 | By contributing,
31 | you agree that we may redistribute your work under [our license](LICENSE.md).
32 | In exchange,
33 | we will address your issues and/or assess your change proposal as promptly as we can,
34 | and help you become a member of our community.
35 | Everyone involved in [Software Carpentry][swc-site] and [Data Carpentry][dc-site]
36 | agrees to abide by our [code of conduct](CONDUCT.md).
37 |
38 | ## How to Contribute
39 |
40 | The easiest way to get started is to file an issue
41 | to tell us about a spelling mistake,
42 | some awkward wording,
43 | or a factual error.
44 | This is a good way to introduce yourself
45 | and to meet some of our community members.
46 |
47 | 1. If you do not have a [GitHub][github] account,
48 | you can [send us comments by email][email].
49 | However,
50 | we will be able to respond more quickly if you use one of the other methods described below.
51 |
52 | 2. If you have a [GitHub][github] account,
53 | or are willing to [create one][github-join],
54 | but do not know how to use Git,
55 | you can report problems or suggest improvements by [creating an issue][issues].
56 | This allows us to assign the item to someone
57 | and to respond to it in a threaded discussion.
58 |
59 | 3. If you are comfortable with Git,
60 | and would like to add or change material,
61 | you can submit a pull request (PR).
62 | Instructions for doing this are [included below](#using-github).
63 |
64 | ## Where to Contribute
65 |
66 | 1. If you wish to change this lesson,
67 | please work in ,
68 | which can be viewed at .
69 |
70 | 2. If you wish to change the example lesson,
71 | please work in ,
72 | which documents the format of our lessons
73 | and can be viewed at .
74 |
75 | 3. If you wish to change the template used for workshop websites,
76 | please work in .
77 | The home page of that repository explains how to set up workshop websites,
78 | while the extra pages in
79 | provide more background on our design choices.
80 |
81 | 4. If you wish to change CSS style files, tools,
82 | or HTML boilerplate for lessons or workshops stored in `_includes` or `_layouts`,
83 | please work in .
84 |
85 | ## What to Contribute
86 |
87 | There are many ways to contribute,
88 | from writing new exercises and improving existing ones
89 | to updating or filling in the documentation
90 | and and submitting [bug reports][issues]
91 | about things that don't work, aren't clear, or are missing.
92 | If you are looking for ideas,
93 | please see [the list of issues for this repository][issues],
94 | or the issues for [Data Carpentry][dc-issues]
95 | and [Software Carpentry][swc-issues] projects.
96 |
97 | Comments on issues and reviews of pull requests are just as welcome:
98 | we are smarter together than we are on our own.
99 | Reviews from novices and newcomers are particularly valuable:
100 | it's easy for people who have been using these lessons for a while
101 | to forget how impenetrable some of this material can be,
102 | so fresh eyes are always welcome.
103 |
104 | ## What *Not* to Contribute
105 |
106 | Our lessons already contain more material than we can cover in a typical workshop,
107 | so we are usually *not* looking for more concepts or tools to add to them.
108 | As a rule,
109 | if you want to introduce a new idea,
110 | you must (a) estimate how long it will take to teach
111 | and (b) explain what you would take out to make room for it.
112 | The first encourages contributors to be honest about requirements;
113 | the second, to think hard about priorities.
114 |
115 | We are also not looking for exercises or other material that only run on one platform.
116 | Our workshops typically contain a mixture of Windows, macOS, and Linux users;
117 | in order to be usable,
118 | our lessons must run equally well on all three.
119 |
120 | ## Using GitHub
121 |
122 | If you choose to contribute via GitHub,
123 | you may want to look at
124 | [How to Contribute to an Open Source Project on GitHub][how-contribute].
125 | In brief:
126 |
127 | 1. The published copy of the lesson is in the `gh-pages` branch of the repository
128 | (so that GitHub will regenerate it automatically).
129 | Please create all branches from that,
130 | and merge the [master repository][repo]'s `gh-pages` branch into your `gh-pages` branch
131 | before starting work.
132 | Please do *not* work directly in your `gh-pages` branch,
133 | since that will make it difficult for you to work on other contributions.
134 |
135 | 2. We use [GitHub flow][github-flow] to manage changes:
136 | 1. Create a new branch in your desktop copy of this repository for each significant change.
137 | 2. Commit the change in that branch.
138 | 3. Push that branch to your fork of this repository on GitHub.
139 | 4. Submit a pull request from that branch to the [master repository][repo].
140 | 5. If you receive feedback,
141 | make changes on your desktop and push to your branch on GitHub:
142 | the pull request will update automatically.
143 |
144 | Each lesson has two maintainers who review issues and pull requests
145 | or encourage others to do so.
146 | The maintainers are community volunteers,
147 | and have final say over what gets merged into the lesson.
148 |
149 | ## Other Resources
150 |
151 | General discussion of [Software Carpentry][swc-site] and [Data Carpentry][dc-site]
152 | happens on the [discussion mailing list][discuss-list],
153 | which everyone is welcome to join.
154 | You can also [reach us by email][email].
155 |
156 | [email]: mailto:admin@software-carpentry.org
157 | [dc-issues]: https://github.com/issues?q=user%3Adatacarpentry
158 | [dc-lessons]: http://datacarpentry.org/lessons/
159 | [dc-site]: http://datacarpentry.org/
160 | [discuss-list]: http://lists.software-carpentry.org/listinfo/discuss
161 | [github]: http://github.com
162 | [github-flow]: https://guides.github.com/introduction/flow/
163 | [github-join]: https://github.com/join
164 | [how-contribute]: https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github
165 | [issues]: https://github.com/swcarpentry/FIXME/issues/
166 | [repo]: https://github.com/swcarpentry/FIXME/
167 | [swc-issues]: https://github.com/issues?q=user%3Aswcarpentry
168 | [swc-lessons]: http://software-carpentry.org/lessons/
169 | [swc-site]: http://software-carpentry.org/
170 | '''
171 |
172 | ROOT_CONFIG_YML = '''\
173 | #------------------------------------------------------------
174 | # Values for this lesson.
175 | #------------------------------------------------------------
176 |
177 | # Which carpentry is this ("swc", "dc", or "lc")?
178 | carpentry: "swc"
179 |
180 | # Overall title for pages.
181 | title: "Lesson Title"
182 |
183 | # Contact. This *must* include the protocol: if it's an email
184 | # address, it must look like "mailto:lessons@software-carpentry.org",
185 | # or if it's a URL, "https://gitter.im/username/ProjectName".
186 | email: "mailto:lessons@software-carpentry.org"
187 |
188 | #------------------------------------------------------------
189 | # Generic settings (should not need to change).
190 | #------------------------------------------------------------
191 |
192 | # What kind of thing is this ("workshop" or "lesson")?
193 | kind: "lesson"
194 |
195 | # Magic to make URLs resolve both locally and on GitHub.
196 | # See https://help.github.com/articles/repository-metadata-on-github-pages/.
197 | repository: /
198 |
199 | # Sites.
200 | amy_site: "https://amy.software-carpentry.org/workshops"
201 | dc_site: "http://datacarpentry.org"
202 | swc_github: "https://github.com/swcarpentry"
203 | swc_site: "https://software-carpentry.org"
204 | swc_pages: "https://swcarpentry.github.io"
205 | lc_site: "http://librarycarpentry.github.io/"
206 | template_repo: "https://github.com/swcarpentry/styles"
207 | example_repo: "https://github.com/swcarpentry/lesson-example"
208 | example_site: "https://swcarpentry.github.com/lesson-example"
209 | workshop_repo: "https://github.com/swcarpentry/workshop-template"
210 | workshop_site: "https://swcarpentry.github.io/workshop-template"
211 | training_site: "https://swcarpentry.github.io/instructor-training"
212 |
213 | # Surveys.
214 | swc_pre_survey: "https://www.surveymonkey.com/r/swc_pre_workshop_v1?workshop_id="
215 | swc_post_survey: "https://www.surveymonkey.com/r/swc_post_workshop_v1?workshop_id="
216 | dc_pre_survey: "https://www.surveymonkey.com/r/dcpostworkshopassessment?workshop_id="
217 | dc_post_survey: "https://www.surveymonkey.com/r/dcpostworkshopassessment?workshop_id="
218 | training_post_survey: "https://www.surveymonkey.com/r/post-instructor-training"
219 |
220 | # Start time in minutes (0 to be clock-independent, 540 to show a start at 09:00 am).
221 | start_time: 0
222 |
223 | # Specify that things in the episodes collection should be output.
224 | collections:
225 | episodes:
226 | output: true
227 | permalink: /:path/index.html
228 | extras:
229 | output: true
230 | permalink: /:path/index.html
231 |
232 | # Set the default layout for things in the episodes collection.
233 | defaults:
234 | - values:
235 | root: ..
236 | - scope:
237 | path: ""
238 | type: episodes
239 | values:
240 | layout: episode
241 |
242 | # Files and directories that are not to be copied.
243 | exclude:
244 | - Makefile
245 | - bin
246 |
247 | # Turn on built-in syntax highlighting.
248 | highlighter: rouge
249 | '''
250 |
251 | ROOT_INDEX_MD = '''\
252 | ---
253 | layout: lesson
254 | root: .
255 | permalink: index.html # Is the only page that don't follow the partner /:path/index.html
256 | ---
257 | FIXME: home page introduction
258 |
259 | > ## Prerequisites
260 | >
261 | > FIXME
262 | {: .prereq}
263 | '''
264 |
265 | ROOT_REFERENCE_MD = '''\
266 | ---
267 | layout: reference
268 | root: .
269 | ---
270 |
271 | ## Glossary
272 |
273 | FIXME
274 | '''
275 |
276 | ROOT_SETUP_MD = '''\
277 | ---
278 | layout: page
279 | title: Setup
280 | root: .
281 | ---
282 | FIXME
283 | '''
284 |
285 | ROOT_AIO_MD = '''\
286 | ---
287 | layout: page
288 | root: .
289 | ---
290 |
316 | {% comment %}
317 | Create anchor for each one of the episodes.
318 | {% endcomment %}
319 | {% for episode in site.episodes %}
320 |
321 | {% endfor %}
322 | '''
323 |
324 | EPISODES_INTRODUCTION_MD = '''\
325 | ---
326 | title: "Introduction"
327 | teaching: 0
328 | exercises: 0
329 | questions:
330 | - "Key question"
331 | objectives:
332 | - "First objective."
333 | keypoints:
334 | - "First key point."
335 | ---
336 | '''
337 |
338 | EXTRAS_ABOUT_MD = '''\
339 | ---
340 | layout: page
341 | title: About
342 | ---
343 | {% include carpentries.html %}
344 | '''
345 |
346 | EXTRAS_DISCUSS_MD = '''\
347 | ---
348 | layout: page
349 | title: Discussion
350 | ---
351 | FIXME
352 | '''
353 |
354 | EXTRAS_FIGURES_MD = '''\
355 | ---
356 | layout: page
357 | title: Figures
358 | ---
359 |
388 | {% comment %}
389 | Create anchor for each one of the episodes.
390 | {% endcomment %}
391 | {% for episode in site.episodes %}
392 |
393 | {% endfor %}
394 | '''
395 |
396 | EXTRAS_GUIDE_MD = '''\
397 | ---
398 | layout: page
399 | title: "Instructor Notes"
400 | ---
401 | FIXME
402 | '''
403 |
404 | BOILERPLATE = (
405 | ('AUTHORS', ROOT_AUTHORS),
406 | ('CITATION', ROOT_CITATION),
407 | ('CONTRIBUTING.md', ROOT_CONTRIBUTING_MD),
408 | ('_config.yml', ROOT_CONFIG_YML),
409 | ('index.md', ROOT_INDEX_MD),
410 | ('reference.md', ROOT_REFERENCE_MD),
411 | ('setup.md', ROOT_SETUP_MD),
412 | ('aio.md', ROOT_AIO_MD),
413 | ('_episodes/01-introduction.md', EPISODES_INTRODUCTION_MD),
414 | ('_extras/about.md', EXTRAS_ABOUT_MD),
415 | ('_extras/discuss.md', EXTRAS_DISCUSS_MD),
416 | ('_extras/figures.md', EXTRAS_FIGURES_MD),
417 | ('_extras/guide.md', EXTRAS_GUIDE_MD),
418 | )
419 |
420 |
421 | def main():
422 | """Check for collisions, then create."""
423 |
424 | # Check.
425 | errors = False
426 | for (path, _) in BOILERPLATE:
427 | if os.path.exists(path):
428 | print('Warning: {0} already exists.'.format(path), file=sys.stderr)
429 | errors = True
430 | if errors:
431 | print('**Exiting without creating files.**', file=sys.stderr)
432 | sys.exit(1)
433 |
434 | # Create.
435 | for (path, content) in BOILERPLATE:
436 | with open(path, 'w') as writer:
437 | writer.write(content)
438 |
439 |
440 | if __name__ == '__main__':
441 | main()
442 |
--------------------------------------------------------------------------------
/assets/img/lc-icon-black.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/bin/lesson_check.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """
4 | Check lesson files and their contents.
5 | """
6 |
7 | from __future__ import print_function
8 | import sys
9 | import os
10 | import glob
11 | import json
12 | import re
13 | from optparse import OptionParser
14 |
15 | from util import Reporter, read_markdown, load_yaml, check_unwanted_files, require, IMAGE_FILE_SUFFIX
16 |
17 | __version__ = '0.3'
18 |
19 | # Where to look for source Markdown files.
20 | SOURCE_DIRS = ['', '_episodes', '_extras']
21 |
22 | # Required files: each entry is ('path': YAML_required).
23 | # FIXME: We do not yet validate whether any files have the required
24 | # YAML headers, but should in the future.
25 | # The '%' is replaced with the source directory path for checking.
26 | # Episodes are handled specially, and extra files in '_extras' are also handled specially.
27 | # This list must include all the Markdown files listed in the 'bin/initialize' script.
28 | REQUIRED_FILES = {
29 | '%/CONDUCT.md': True,
30 | '%/CONTRIBUTING.md': False,
31 | '%/LICENSE.md': True,
32 | '%/README.md': False,
33 | '%/_extras/discuss.md': True,
34 | '%/_extras/guide.md': True,
35 | '%/index.md': True,
36 | '%/reference.md': True,
37 | '%/setup.md': True,
38 | }
39 |
40 | # Episode filename pattern.
41 | P_EPISODE_FILENAME = re.compile(r'/_episodes/(\d\d)-[-\w]+.md$')
42 |
43 | # Pattern to match lines ending with whitespace.
44 | P_TRAILING_WHITESPACE = re.compile(r'\s+$')
45 |
46 | # Pattern to match figure references in HTML.
47 | P_FIGURE_REFS = re.compile(r']+src="([^"]+)"[^>]*>')
48 |
49 | # Pattern to match internally-defined Markdown links.
50 | P_INTERNAL_LINK_REF = re.compile(r'\[([^\]]+)\]\[([^\]]+)\]')
51 |
52 | # Pattern to match reference links (to resolve internally-defined references).
53 | P_INTERNAL_LINK_DEF = re.compile(r'^\[([^\]]+)\]:\s*(.+)')
54 |
55 | # What kinds of blockquotes are allowed?
56 | KNOWN_BLOCKQUOTES = {
57 | 'callout',
58 | 'challenge',
59 | 'checklist',
60 | 'discussion',
61 | 'keypoints',
62 | 'objectives',
63 | 'prereq',
64 | 'quotation',
65 | 'solution',
66 | 'testimonial'
67 | }
68 |
69 | # What kinds of code fragments are allowed?
70 | KNOWN_CODEBLOCKS = {
71 | 'error',
72 | 'output',
73 | 'source',
74 | 'language-bash',
75 | 'html',
76 | 'language-make',
77 | 'language-matlab',
78 | 'language-python',
79 | 'language-r',
80 | 'language-shell',
81 | 'language-sql'
82 | }
83 |
84 | # What fields are required in teaching episode metadata?
85 | TEACHING_METADATA_FIELDS = {
86 | ('title', str),
87 | ('teaching', int),
88 | ('exercises', int),
89 | ('questions', list),
90 | ('objectives', list),
91 | ('keypoints', list)
92 | }
93 |
94 | # What fields are required in break episode metadata?
95 | BREAK_METADATA_FIELDS = {
96 | ('layout', str),
97 | ('title', str),
98 | ('break', int)
99 | }
100 |
101 | # How long are lines allowed to be?
102 | MAX_LINE_LEN = 100
103 |
104 | def main():
105 | """Main driver."""
106 |
107 | args = parse_args()
108 | args.reporter = Reporter()
109 | check_config(args.reporter, args.source_dir)
110 | args.references = read_references(args.reporter, args.reference_path)
111 |
112 | docs = read_all_markdown(args.source_dir, args.parser)
113 | check_fileset(args.source_dir, args.reporter, docs.keys())
114 | check_unwanted_files(args.source_dir, args.reporter)
115 | for filename in docs.keys():
116 | checker = create_checker(args, filename, docs[filename])
117 | checker.check()
118 |
119 | args.reporter.report()
120 |
121 |
122 | def parse_args():
123 | """Parse command-line arguments."""
124 |
125 | parser = OptionParser()
126 | parser.add_option('-l', '--linelen',
127 | default=False,
128 | action="store_true",
129 | dest='line_lengths',
130 | help='Check line lengths')
131 | parser.add_option('-p', '--parser',
132 | default=None,
133 | dest='parser',
134 | help='path to Markdown parser')
135 | parser.add_option('-r', '--references',
136 | default=None,
137 | dest='reference_path',
138 | help='path to Markdown file of external references')
139 | parser.add_option('-s', '--source',
140 | default=os.curdir,
141 | dest='source_dir',
142 | help='source directory')
143 | parser.add_option('-w', '--whitespace',
144 | default=False,
145 | action="store_true",
146 | dest='trailing_whitespace',
147 | help='Check for trailing whitespace')
148 |
149 | args, extras = parser.parse_args()
150 | require(args.parser is not None,
151 | 'Path to Markdown parser not provided')
152 | require(not extras,
153 | 'Unexpected trailing command-line arguments "{0}"'.format(extras))
154 |
155 | return args
156 |
157 |
158 | def check_config(reporter, source_dir):
159 | """Check configuration file."""
160 |
161 | config_file = os.path.join(source_dir, '_config.yml')
162 | config = load_yaml(config_file)
163 | reporter.check_field(config_file, 'configuration', config, 'kind', 'lesson')
164 | reporter.check_field(config_file, 'configuration', config, 'carpentry', ('swc', 'dc', 'lc'))
165 | reporter.check_field(config_file, 'configuration', config, 'title')
166 | reporter.check_field(config_file, 'configuration', config, 'email')
167 |
168 | reporter.check({'values': {'root': '..'}} in config.get('defaults', []),
169 | 'configuration',
170 | '"root" not set to ".." in configuration')
171 |
172 |
173 | def read_references(reporter, ref_path):
174 | """Read shared file of reference links, returning dictionary of valid references
175 | {symbolic_name : URL}
176 | """
177 |
178 | result = {}
179 | urls_seen = set()
180 | if ref_path:
181 | with open(ref_path, 'r') as reader:
182 | for (num, line) in enumerate(reader):
183 | line_num = num + 1
184 | m = P_INTERNAL_LINK_DEF.search(line)
185 | require(m,
186 | '{0}:{1} not valid reference:\n{2}'.format(ref_path, line_num, line.rstrip()))
187 | name = m.group(1)
188 | url = m.group(2)
189 | require(name,
190 | 'Empty reference at {0}:{1}'.format(ref_path, line_num))
191 | reporter.check(name not in result,
192 | ref_path,
193 | 'Duplicate reference {0} at line {1}',
194 | name, line_num)
195 | reporter.check(url not in urls_seen,
196 | ref_path,
197 | 'Duplicate definition of URL {0} at line {1}',
198 | url, line_num)
199 | result[name] = url
200 | urls_seen.add(url)
201 | return result
202 |
203 |
204 | def read_all_markdown(source_dir, parser):
205 | """Read source files, returning
206 | {path : {'metadata':yaml, 'metadata_len':N, 'text':text, 'lines':[(i, line, len)], 'doc':doc}}
207 | """
208 |
209 | all_dirs = [os.path.join(source_dir, d) for d in SOURCE_DIRS]
210 | all_patterns = [os.path.join(d, '*.md') for d in all_dirs]
211 | result = {}
212 | for pat in all_patterns:
213 | for filename in glob.glob(pat):
214 | data = read_markdown(parser, filename)
215 | if data:
216 | result[filename] = data
217 | return result
218 |
219 |
220 | def check_fileset(source_dir, reporter, filenames_present):
221 | """Are all required files present? Are extraneous files present?"""
222 |
223 | # Check files with predictable names.
224 | required = [p.replace('%', source_dir) for p in REQUIRED_FILES]
225 | missing = set(required) - set(filenames_present)
226 | for m in missing:
227 | reporter.add(None, 'Missing required file {0}', m)
228 |
229 | # Check episode files' names.
230 | seen = []
231 | for filename in filenames_present:
232 | if '_episodes' not in filename:
233 | continue
234 | m = P_EPISODE_FILENAME.search(filename)
235 | if m and m.group(1):
236 | seen.append(m.group(1))
237 | else:
238 | reporter.add(None, 'Episode {0} has badly-formatted filename', filename)
239 |
240 | # Check for duplicate episode numbers.
241 | reporter.check(len(seen) == len(set(seen)),
242 | None,
243 | 'Duplicate episode numbers {0} vs {1}',
244 | sorted(seen), sorted(set(seen)))
245 |
246 | # Check that numbers are consecutive.
247 | seen = [int(s) for s in seen]
248 | seen.sort()
249 | clean = True
250 | for i in range(len(seen) - 1):
251 | clean = clean and ((seen[i+1] - seen[i]) == 1)
252 | reporter.check(clean,
253 | None,
254 | 'Missing or non-consecutive episode numbers {0}',
255 | seen)
256 |
257 |
258 | def create_checker(args, filename, info):
259 | """Create appropriate checker for file."""
260 |
261 | for (pat, cls) in CHECKERS:
262 | if pat.search(filename):
263 | return cls(args, filename, **info)
264 |
265 |
266 | class CheckBase(object):
267 | """Base class for checking Markdown files."""
268 |
269 | def __init__(self, args, filename, metadata, metadata_len, text, lines, doc):
270 | """Cache arguments for checking."""
271 |
272 | super(CheckBase, self).__init__()
273 | self.args = args
274 | self.reporter = self.args.reporter # for convenience
275 | self.filename = filename
276 | self.metadata = metadata
277 | self.metadata_len = metadata_len
278 | self.text = text
279 | self.lines = lines
280 | self.doc = doc
281 |
282 | self.layout = None
283 |
284 |
285 | def check(self):
286 | """Run tests."""
287 |
288 | self.check_metadata()
289 | self.check_line_lengths()
290 | self.check_trailing_whitespace()
291 | self.check_blockquote_classes()
292 | self.check_codeblock_classes()
293 | self.check_defined_link_references()
294 |
295 |
296 | def check_metadata(self):
297 | """Check the YAML metadata."""
298 |
299 | self.reporter.check(self.metadata is not None,
300 | self.filename,
301 | 'Missing metadata entirely')
302 |
303 | if self.metadata and (self.layout is not None):
304 | self.reporter.check_field(self.filename, 'metadata', self.metadata, 'layout', self.layout)
305 |
306 |
307 | def check_line_lengths(self):
308 | """Check the raw text of the lesson body."""
309 |
310 | if self.args.line_lengths:
311 | over = [i for (i, l, n) in self.lines if (n > MAX_LINE_LEN) and (not l.startswith('!'))]
312 | self.reporter.check(not over,
313 | self.filename,
314 | 'Line(s) are too long: {0}',
315 | ', '.join([str(i) for i in over]))
316 |
317 |
318 | def check_trailing_whitespace(self):
319 | """Check for whitespace at the ends of lines."""
320 |
321 | if self.args.trailing_whitespace:
322 | trailing = [i for (i, l, n) in self.lines if P_TRAILING_WHITESPACE.match(l)]
323 | self.reporter.check(not trailing,
324 | self.filename,
325 | 'Line(s) end with whitespace: {0}',
326 | ', '.join([str(i) for i in trailing]))
327 |
328 |
329 | def check_blockquote_classes(self):
330 | """Check that all blockquotes have known classes."""
331 |
332 | for node in self.find_all(self.doc, {'type' : 'blockquote'}):
333 | cls = self.get_val(node, 'attr', 'class')
334 | self.reporter.check(cls in KNOWN_BLOCKQUOTES,
335 | (self.filename, self.get_loc(node)),
336 | 'Unknown or missing blockquote type {0}',
337 | cls)
338 |
339 |
340 | def check_codeblock_classes(self):
341 | """Check that all code blocks have known classes."""
342 |
343 | for node in self.find_all(self.doc, {'type' : 'codeblock'}):
344 | cls = self.get_val(node, 'attr', 'class')
345 | self.reporter.check(cls in KNOWN_CODEBLOCKS,
346 | (self.filename, self.get_loc(node)),
347 | 'Unknown or missing code block type {0}',
348 | cls)
349 |
350 |
351 | def check_defined_link_references(self):
352 | """Check that defined links resolve in the file.
353 |
354 | Internally-defined links match the pattern [text][label].
355 | """
356 |
357 | result = set()
358 | for node in self.find_all(self.doc, {'type' : 'text'}):
359 | for match in P_INTERNAL_LINK_REF.findall(node['value']):
360 | text = match[0]
361 | link = match[1]
362 | if link not in self.args.references:
363 | result.add('"{0}"=>"{1}"'.format(text, link))
364 | self.reporter.check(not result,
365 | self.filename,
366 | 'Internally-defined links may be missing definitions: {0}',
367 | ', '.join(sorted(result)))
368 |
369 |
370 | def find_all(self, node, pattern, accum=None):
371 | """Find all matches for a pattern."""
372 |
373 | assert type(pattern) == dict, 'Patterns must be dictionaries'
374 | if accum is None:
375 | accum = []
376 | if self.match(node, pattern):
377 | accum.append(node)
378 | for child in node.get('children', []):
379 | self.find_all(child, pattern, accum)
380 | return accum
381 |
382 |
383 | def match(self, node, pattern):
384 | """Does this node match the given pattern?"""
385 |
386 | for key in pattern:
387 | if key not in node:
388 | return False
389 | val = pattern[key]
390 | if type(val) == str:
391 | if node[key] != val:
392 | return False
393 | elif type(val) == dict:
394 | if not self.match(node[key], val):
395 | return False
396 | return True
397 |
398 |
399 | def get_val(self, node, *chain):
400 | """Get value one or more levels down."""
401 |
402 | curr = node
403 | for selector in chain:
404 | curr = curr.get(selector, None)
405 | if curr is None:
406 | break
407 | return curr
408 |
409 |
410 | def get_loc(self, node):
411 | """Convenience method to get node's line number."""
412 |
413 | result = self.get_val(node, 'options', 'location')
414 | if self.metadata_len is not None:
415 | result += self.metadata_len
416 | return result
417 |
418 |
419 | class CheckNonJekyll(CheckBase):
420 | """Check a file that isn't translated by Jekyll."""
421 |
422 | def __init__(self, args, filename, metadata, metadata_len, text, lines, doc):
423 | super(CheckNonJekyll, self).__init__(args, filename, metadata, metadata_len, text, lines, doc)
424 |
425 |
426 | def check_metadata(self):
427 | self.reporter.check(self.metadata is None,
428 | self.filename,
429 | 'Unexpected metadata')
430 |
431 |
432 | class CheckIndex(CheckBase):
433 | """Check the main index page."""
434 |
435 | def __init__(self, args, filename, metadata, metadata_len, text, lines, doc):
436 | super(CheckIndex, self).__init__(args, filename, metadata, metadata_len, text, lines, doc)
437 | self.layout = 'lesson'
438 |
439 | def check_metadata(self):
440 | super(CheckIndex, self).check_metadata()
441 | self.reporter.check(self.metadata.get('root', '') == '.',
442 | self.filename,
443 | 'Root not set to "."')
444 |
445 |
446 | class CheckEpisode(CheckBase):
447 | """Check an episode page."""
448 |
449 | def __init__(self, args, filename, metadata, metadata_len, text, lines, doc):
450 | super(CheckEpisode, self).__init__(args, filename, metadata, metadata_len, text, lines, doc)
451 |
452 |
453 | def check(self):
454 | """Run extra tests."""
455 |
456 | super(CheckEpisode, self).check()
457 | self.check_reference_inclusion()
458 |
459 |
460 | def check_metadata(self):
461 | super(CheckEpisode, self).check_metadata()
462 | if self.metadata:
463 | if 'layout' in self.metadata:
464 | if self.metadata['layout'] == 'break':
465 | self.check_metadata_fields(BREAK_METADATA_FIELDS)
466 | else:
467 | self.reporter.add(self.filename,
468 | 'Unknown episode layout "{0}"',
469 | self.metadata['layout'])
470 | else:
471 | self.check_metadata_fields(TEACHING_METADATA_FIELDS)
472 |
473 |
474 | def check_metadata_fields(self, expected):
475 | for (name, type_) in expected:
476 | if name not in self.metadata:
477 | self.reporter.add(self.filename,
478 | 'Missing metadata field {0}',
479 | name)
480 | elif type(self.metadata[name]) != type_:
481 | self.reporter.add(self.filename,
482 | '"{0}" has wrong type in metadata ({1} instead of {2})',
483 | name, type(self.metadata[name]), type_)
484 |
485 |
486 | def check_reference_inclusion(self):
487 | """Check that links file has been included."""
488 |
489 | if not self.args.reference_path:
490 | return
491 |
492 | for (i, last_line, line_len) in reversed(self.lines):
493 | if last_line:
494 | break
495 |
496 | require(last_line,
497 | 'No non-empty lines in {0}'.format(self.filename))
498 |
499 | include_filename = os.path.split(self.args.reference_path)[-1]
500 | if include_filename not in last_line:
501 | self.reporter.add(self.filename,
502 | 'episode does not include "{0}"',
503 | include_filename)
504 |
505 |
506 | class CheckReference(CheckBase):
507 | """Check the reference page."""
508 |
509 | def __init__(self, args, filename, metadata, metadata_len, text, lines, doc):
510 | super(CheckReference, self).__init__(args, filename, metadata, metadata_len, text, lines, doc)
511 | self.layout = 'reference'
512 |
513 |
514 | class CheckGeneric(CheckBase):
515 | """Check a generic page."""
516 |
517 | def __init__(self, args, filename, metadata, metadata_len, text, lines, doc):
518 | super(CheckGeneric, self).__init__(args, filename, metadata, metadata_len, text, lines, doc)
519 | self.layout = 'page'
520 |
521 |
522 | CHECKERS = [
523 | (re.compile(r'CONTRIBUTING\.md'), CheckNonJekyll),
524 | (re.compile(r'README\.md'), CheckNonJekyll),
525 | (re.compile(r'index\.md'), CheckIndex),
526 | (re.compile(r'reference\.md'), CheckReference),
527 | (re.compile(r'_episodes/.*\.md'), CheckEpisode),
528 | (re.compile(r'.*\.md'), CheckGeneric)
529 | ]
530 |
531 |
532 | if __name__ == '__main__':
533 | main()
534 |
--------------------------------------------------------------------------------