diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..1d7d564
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,25 @@
+root = true
+
+[*]
+charset = utf-8
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+indent_size = 2
+indent_style = space
+max_line_length = 100 # Please keep this in sync with bin/lesson_check.py!
+
+[*.r]
+max_line_length = 80
+
+[*.py]
+indent_size = 4
+indent_style = space
+max_line_length = 79
+
+[*.sh]
+end_of_line = lf
+
+[Makefile]
+indent_style = tab
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index 6cc9e52..ec2d4fe 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -1,9 +1,11 @@
-Please delete the text below before submitting your contribution.
+Please delete this line and the text below before submitting your contribution.
---
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.
-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).
+If this issue is about a specific episode within a lesson, please provide its link or filename.
+
+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 The Carpentries Team at team@carpentries.org.
---
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 6cc9e52..d9eb8c5 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,9 +1,9 @@
-Please delete the text below before submitting your contribution.
+Please delete this line and the text below before submitting your contribution.
---
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.
-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).
+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 The Carpentries Team at team@carpentries.org.
---
diff --git a/.gitignore b/.gitignore
index cf3b859..128437d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,7 @@
.sass-cache
__pycache__
_site
+.Rproj.user
+.Rhistory
+.RData
+
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..c3b9669
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,11 @@
+---
+layout: page
+title: "Contributor Code of Conduct"
+---
+As contributors and maintainers of this project,
+we pledge to follow the [Carpentry Code of Conduct][coc].
+
+Instances of abusive, harassing, or otherwise unacceptable behavior
+may be reported by following our [reporting guidelines][coc-reporting].
+
+{% include links.md %}
diff --git a/CONDUCT.md b/CONDUCT.md
deleted file mode 100644
index 5e4943b..0000000
--- a/CONDUCT.md
+++ /dev/null
@@ -1,45 +0,0 @@
----
-layout: page
-title: "Contributor Code of Conduct"
-permalink: /conduct/
----
-As contributors and maintainers of this project,
-we pledge to respect all people who contribute through reporting issues,
-posting feature requests,
-updating documentation,
-submitting pull requests or patches,
-and other activities.
-
-We are committed to making participation in this project a harassment-free experience for everyone,
-regardless of level of experience,
-gender,
-gender identity and expression,
-sexual orientation,
-disability,
-personal appearance,
-body size,
-race,
-ethnicity,
-age,
-or religion.
-
-Examples of unacceptable behavior by participants include the use of sexual language or imagery,
-derogatory comments or personal attacks,
-trolling,
-public or private harassment,
-insults,
-or other unprofessional conduct.
-
-Project maintainers have the right and responsibility to remove, edit, or reject
-comments, commits, code, wiki edits, issues, and other contributions
-that are not aligned to our [Code of Conduct][coc].
-Project maintainers who do not follow the Code of Conduct may be removed from the project team.
-
-Instances of abusive, harassing, or otherwise unacceptable behavior
-may be reported by following our [reporting guidelines][coc-reporting].
-
-
-- [Software and Data Carpentry Code of Conduct][coc]
-- [Code of Conduct Reporting Guide][coc-reporting]
-
-{% include links.md %}
diff --git a/LICENSE.md b/LICENSE.md
index b81c3b8..1f4c6b6 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,11 +1,10 @@
---
layout: page
title: "Licenses"
-permalink: /license/
---
## Instructional Material
-All Software Carpentry and Data Carpentry instructional material is
+All Software Carpentry, Data Carpentry, and Library Carpentry instructional material is
made available under the [Creative Commons Attribution
license][cc-by-human]. The following is a human-readable summary of
(and not a substitute for) the [full legal text of the CC BY 4.0
@@ -78,7 +77,6 @@ are registered trademarks of [Community Initiatives][CI].
[cc-by-human]: https://creativecommons.org/licenses/by/4.0/
[cc-by-legal]: https://creativecommons.org/licenses/by/4.0/legalcode
-[mit-license]: http://opensource.org/licenses/mit-license.html
+[mit-license]: https://opensource.org/licenses/mit-license.html
[ci]: http://communityin.org/
-[osi]: http://opensource.org
-
+[osi]: https://opensource.org
diff --git a/Makefile b/Makefile
index b5dfe2f..ac587b8 100644
--- a/Makefile
+++ b/Makefile
@@ -4,6 +4,7 @@
# Settings
MAKEFILES=Makefile $(wildcard *.mk)
JEKYLL=jekyll
+JEKYLL_VERSION=3.7.3
PARSER=bin/markdown_ast.rb
DST=_site
@@ -16,6 +17,10 @@ all : commands
commands :
@grep -h -E '^##' ${MAKEFILES} | sed -e 's/## //g'
+## docker-serve : use docker to build the site
+docker-serve :
+ docker run --rm -it -v ${PWD}:/srv/jekyll -p 127.0.0.1:4000:4000 jekyll/jekyll:${JEKYLL_VERSION} make serve
+
## serve : run a local server.
serve : lesson-md
${JEKYLL} serve
@@ -38,7 +43,7 @@ clean :
@find . -name '*.pyc' -exec rm {} \;
## clean-rmd : clean intermediate R files (that need to be committed to the repo).
-clear-rmd :
+clean-rmd :
@rm -rf ${RMD_DST}
@rm -rf fig/rmd-*
@@ -63,11 +68,11 @@ RMD_DST = $(patsubst _episodes_rmd/%.Rmd,_episodes/%.md,$(RMD_SRC))
# Lesson source files in the order they appear in the navigation menu.
MARKDOWN_SRC = \
index.md \
- CONDUCT.md \
+ CODE_OF_CONDUCT.md \
setup.md \
- $(wildcard _episodes/*.md) \
+ $(sort $(wildcard _episodes/*.md)) \
reference.md \
- $(wildcard _extras/*.md) \
+ $(sort $(wildcard _extras/*.md)) \
LICENSE.md
# Generated lesson files in the order they appear in the navigation menu.
@@ -75,33 +80,28 @@ HTML_DST = \
${DST}/index.html \
${DST}/conduct/index.html \
${DST}/setup/index.html \
- $(patsubst _episodes/%.md,${DST}/%/index.html,$(wildcard _episodes/*.md)) \
+ $(patsubst _episodes/%.md,${DST}/%/index.html,$(sort $(wildcard _episodes/*.md))) \
${DST}/reference/index.html \
- $(patsubst _extras/%.md,${DST}/%/index.html,$(wildcard _extras/*.md)) \
+ $(patsubst _extras/%.md,${DST}/%/index.html,$(sort $(wildcard _extras/*.md))) \
${DST}/license/index.html
## lesson-md : convert Rmarkdown files to markdown
lesson-md : ${RMD_DST}
-# Use of .NOTPARALLEL makes rule execute only once
-${RMD_DST} : ${RMD_SRC}
- @bin/knit_lessons.sh ${RMD_SRC}
+_episodes/%.md: _episodes_rmd/%.Rmd
+ @bin/knit_lessons.sh $< $@
## lesson-check : validate lesson Markdown.
-lesson-check :
+lesson-check : lesson-fixme
@bin/lesson_check.py -s . -p ${PARSER} -r _includes/links.md
## lesson-check-all : validate lesson Markdown, checking line lengths and trailing whitespace.
lesson-check-all :
- @bin/lesson_check.py -s . -p ${PARSER} -l -w
-
-## lesson-figures : re-generate inclusion displaying all figures.
-lesson-figures :
- @bin/extract_figures.py -p ${PARSER} ${MARKDOWN_SRC} > _includes/all_figures.html
+ @bin/lesson_check.py -s . -p ${PARSER} -r _includes/links.md -l -w --permissive
## unittest : run unit tests on checking tools.
unittest :
- python bin/test_lesson_check.py
+ @bin/test_lesson_check.py
## lesson-files : show expected names of generated files for debugging.
lesson-files :
diff --git a/_episodes/01-basics.md b/_episodes/01-basics.md
index be1482e..09f81c9 100644
--- a/_episodes/01-basics.md
+++ b/_episodes/01-basics.md
@@ -51,9 +51,9 @@ the same base document.
A version control system is a tool that keeps track of these changes for us and
helps us version and merge our files. It allows you to
decide which changes make up the next version, called a
-[commit]({{ page.root }}/reference/#commit), and keeps useful metadata about them. The
+[commit]({% link reference.md %}#commit), and keeps useful metadata about them. The
complete history of commits for a particular project and their metadata make up
-a [repository]({{ page.root }}/reference/#repository). Repositories can be kept in sync
+a [repository]({% link reference.md %}#repository). Repositories can be kept in sync
across different computers facilitating collaboration among different people.
> ## The Long History of Version Control Systems
diff --git a/_episodes/03-create-repo.md b/_episodes/03-create-repo.md
index 2754cd4..a4c58be 100644
--- a/_episodes/03-create-repo.md
+++ b/_episodes/03-create-repo.md
@@ -20,7 +20,7 @@ Lots of projects
(and therefore version control repositories)
start with a single text file in which someone writes down some initial ideas.
-Now that we have Mercurial [configured]({{ page.root }}/02-configuration/),
+Now that we have Mercurial [configured]({% link _episodes/02-configuration.md %}),
we can start using it.
Let's create a directory for Susan's NEMO forecast project:
@@ -30,7 +30,7 @@ $ cd forecast
~~~
{: .bash}
-and tell Mercurial to make it a [repository]({{ page.root }}/reference/#repository):
+and tell Mercurial to make it a [repository]({% link reference.md %}#repository):
a place where Mercurial can store versions of our files:
~~~
diff --git a/_episodes/04-tracking.md b/_episodes/04-tracking.md
index 5077433..550a783 100644
--- a/_episodes/04-tracking.md
+++ b/_episodes/04-tracking.md
@@ -110,8 +110,8 @@ $ hg commit -m "Starting to plan the daily NEMO forecast system."
When we run `hg commit`,
Mercurial takes the file we have told it about by using `hg add` and stores
a copy permanently inside the special `.hg` directory.
-That permanent copy is called a [commit]({{ page.root }}/reference/#commit)
-(or [revision]({{ page.root }}/reference/#revision)).
+That permanent copy is called a [commit]({% link reference.md %}#commit)
+(or [revision]({% link reference.md %}#revision)).
We use the `-m` flag (for "message") to record a comment that will help us
remember later on what we did and why.
@@ -156,7 +156,7 @@ summary: Starting to plan the daily NEMO forecast system.
`hg log` lists all changes committed to a repository,
starting with the most recent.
-The listing for each [changeset]({{ page.root }}/reference/#changeset) includes:
+The listing for each [changeset]({% link reference.md %}#changeset) includes:
* the changeset's revision number and identifier
(`0` and `1320339bbcae` in this case,
diff --git a/_episodes/09-remote-repos.md b/_episodes/09-remote-repos.md
index e598837..ca9394f 100644
--- a/_episodes/09-remote-repos.md
+++ b/_episodes/09-remote-repos.md
@@ -235,6 +235,6 @@ this command would download them to our local repository.
> that you can generate for yourself.
> Getting that set up is a distraction from the topics of this workshop,
> but after the workshop you should read the Bitbucket
-> [Setting up SSH for Mercurial](https://confluence.atlassian.com/bitbucket/set-up-ssh-for-mercurial-728138122.html)
+> [Setting up SSH for Mercurial](https://confluence.atlassian.com/bitbucket/set-up-an-ssh-key-728138079.html)
> documentation on your own and set up ssh keys for yourself.
{: .callout}
diff --git a/_episodes/12-merges.md b/_episodes/12-merges.md
index c8a991b..e62aede 100644
--- a/_episodes/12-merges.md
+++ b/_episodes/12-merges.md
@@ -94,7 +94,7 @@ abort: push creates new remote head e0747e3feea1!
Mercurial detects that our changes have diverged with the changes in the remote
repo on Bitbucket and stops us from creating a confusing situation on there.
What we have to do is pull the changes from Bitbucket,
-[merge]({{ page.root }}/reference/#merge) them into the copy we're currently working in,
+[merge]({% link reference.md %}#merge) them into the copy we're currently working in,
and then push that.
Let's start by pulling:
diff --git a/_episodes/15-licensing.md b/_episodes/15-licensing.md
index 763688c..42855e5 100644
--- a/_episodes/15-licensing.md
+++ b/_episodes/15-licensing.md
@@ -54,7 +54,7 @@ the most popular, including the following:
The GPL is different from most other open source licenses in that it
is
-[infective]({{ page.root }}/reference/#infective):
+[infective]({% link reference.md %}#infective):
anyone who distributes a modified version of the code, or anything
that includes GPL'ed code, must make *their* code freely available as
well.
diff --git a/_extras/about.md b/_extras/about.md
index aa7beea..8aa5f94 100644
--- a/_extras/about.md
+++ b/_extras/about.md
@@ -1,6 +1,5 @@
---
layout: page
title: About
-permalink: /about/
---
{% include carpentries.html %}
diff --git a/_extras/discuss.md b/_extras/discuss.md
index 0667f88..7a240bc 100644
--- a/_extras/discuss.md
+++ b/_extras/discuss.md
@@ -13,7 +13,7 @@ it won't be covered by the instructor.
## More Advanced Mercurial Configuration
-In the [Setting Up]({{ page.root }}/01-backup/#setting-up) section we edited a
+In the [Setting Up]({% link _episodes/01-basics.md %}#setting-up) section we edited a
Mercurial configuration file in our home directory called
`$USERPROFILE/Mercurial.ini` or `~/.hgrc`. You can quickly open that
file for editing with the command `hg config --edit`.
@@ -41,7 +41,7 @@ editing with the command `hg config --local`.
## Non-text Files
-Recall when we discussed [Conflicts]({{ page.root }}/03-conflict/) there was a
+Recall when we discussed [Conflicts]({% link _episodes/13-conflicts.md %}) there was a
challenge that asked, "What does hg do when there is a conflict in an
image or some other non-textual file that is stored in version
control?" We will now revisit this in more detail.
diff --git a/_extras/figures.md b/_extras/figures.md
index de99a57..ba06db2 100644
--- a/_extras/figures.md
+++ b/_extras/figures.md
@@ -1,6 +1,5 @@
---
layout: page
title: Figures
-permalink: /figures/
---
{% include all_figures.html %}
diff --git a/_extras/guide.md b/_extras/guide.md
index 6a13a1e..a29693c 100644
--- a/_extras/guide.md
+++ b/_extras/guide.md
@@ -1,7 +1,6 @@
---
layout: page
title: "Instructor Notes"
-permalink: /guide/
---
Using a software tool to handle the versions of your project files
lets you focus on the more interesting/innovative aspects of your project.
@@ -49,9 +48,9 @@ working alone or in teams because it is
## Teaching Notes
-* Make sure the network is working *before* starting the [Collaborating}({{ page.root }}/02-collab/) section of this lesson.
- [A Better Kind of Backup}({{ page.root }}/01-backup/) focuses on individual use of Mercurial on and can be done without network access.
- [Conflicts}({{ page.root }}/03-conflict/) can also be taught without network access and Bitbucket by creating 2 clones of the repository and pulling changes between them.
+* Make sure the network is working *before* starting the [Collaborating]({% link _episodes/11-collaboration.md %}) section of this lesson.
+ [A Better Kind of Backup]({% link _episodes/01-basics.md %}) focuses on individual use of Mercurial on and can be done without network access.
+ [Conflicts]({% link _episodes/13-conflicts.md %}) can also be taught without network access and Bitbucket by creating 2 clones of the repository and pulling changes between them.
* Drawings are particularly useful in this lesson:
if you have a whiteboard,
@@ -69,7 +68,7 @@ working alone or in teams because it is
(which is available for Windows, OS/X, and Linux)
on their desktop at some point during this lesson.
-* The [Conflicts}({{ page.root }}/03-conflict/) section of the lesson uses the [KDiff3](http://kdiff3.sourceforge.net/) graphical diff/merge tool.
+* The [Conflicts]({% link _episodes/13-conflicts.md %}) section of the lesson uses the [KDiff3](http://kdiff3.sourceforge.net/) graphical diff/merge tool.
The workshop installation instructions should include directions for OS X and Linux users to install KDiff3.
For Windows users it is bundled with TortoiseHg.
The Mercurial wiki has some [notes on using Mercurial with kdiff3](https://www.mercurial-scm.org/wiki/KDiff3).
@@ -92,7 +91,7 @@ working alone or in teams because it is
The differences between Mercurial and Git are largely syntactic,
so learning one provides the conceptual framework to use either.
-## [A Better Kind of Backup}({{ page.root }}/01-backup/)
+## [A Better Kind of Backup]({% link _episodes/01-basics.md %})
* Ask, "Who uses 'undo' in their editor?"
All say "Me".
@@ -139,7 +138,7 @@ working alone or in teams because it is
* This is a good moment to show a diff with the KDiff3 graphical diff tool.
If you skip it because you're short on time,
- show the graphical diff view of a commit on Bitbucket in the [Collaborating}({{ page.root }}/02-collab/) section.
+ show the graphical diff view of a commit on Bitbucket in the [Collaborating]({% link _episodes/11-collaboration.md %}) section.
**Exploring History** and **Recovering Old Versions**
@@ -147,7 +146,7 @@ working alone or in teams because it is
by listing the file names one after the other in the `hg revert` command,
and that shell wildcard characters can also be used.
-## [Collaborating}({{ page.root }}/02-collab/)
+## [Collaborating]({% link _episodes/11-collaboration.md %})
* Make it clear that Mercurial and Bitbucket are not the same thing:
Mercurial is an open source version control tool,
@@ -222,7 +221,7 @@ working alone or in teams because it is
sometimes weird,
will start to arise. Stay tight: conflicts are next.
-## [Conflicts}({{ page.root }}/03-conflict/)
+## [Conflicts]({% link _episodes/13-conflicts.md %})
* Explain that conflict are not a very common occurrence.
Mercurial is good at merging changes made by different people,
@@ -261,7 +260,7 @@ working alone or in teams because it is
* Use `hg merge --tool=kdiff3` to ensure that the KDiff3 GUI diff/merge tool is launched to handle resolution of the conflict.
-## [Open Science}({{ page.root }}/04-open/)
+## [Open Science]({% link _episodes/14-open.md %})
**Licensing**
diff --git a/_includes/all_keypoints.html b/_includes/all_keypoints.html
index 8563df3..e4fd289 100644
--- a/_includes/all_keypoints.html
+++ b/_includes/all_keypoints.html
@@ -1,13 +1,16 @@
{% comment %}
Display key points of all episodes for reference.
{% endcomment %}
+
+{% include base_path.html %}
+
Key Points
{% for episode in site.episodes %}
{% unless episode.break %}
diff --git a/_includes/base_path.html b/_includes/base_path.html
new file mode 100644
index 0000000..7efb357
--- /dev/null
+++ b/_includes/base_path.html
@@ -0,0 +1,27 @@
+{% comment %}
+This is adapted from: https://ricostacruz.com/til/relative-paths-in-jekyll
+
+`page.url` gives the URL of the current page with a leading /:
+
+- when the URL ends with the extension (e.g., /foo/bar.html) then we can get
+ the depth by counting the number of / and remove - 1
+- when the URL ends with a / (e.g. /foo/bar/) then the number / gives the depth
+ directly
+{% endcomment %}
+
+{% assign relative_root_path = '' %}
+
+{% assign last_char = page.url | slice: -1 %}
+
+{% if last_char == "/"}
+{% assign offset = 0 %}
+{% else %}
+{% assign offset = 1 %}
+{% endif %}
+
+{% assign depth = page.url | split: '/' | size | minus: offset %}
+{% if depth <= 1 %}{% assign relative_root_path = '.' %}
+{% elsif depth == 2 %}{% assign relative_root_path = '..' %}
+{% elsif depth == 3 %}{% assign relative_root_path = '../..' %}
+{% elsif depth == 4 %}{% assign relative_root_path = '../../..' %}
+{% endif %}
diff --git a/_includes/carpentries.html b/_includes/carpentries.html
index a0e0181..c032bd5 100644
--- a/_includes/carpentries.html
+++ b/_includes/carpentries.html
@@ -1,44 +1,70 @@
{% comment %}
- General description of Software and Data Carpentry.
+ General description of Software, Data, and Library Carpentry.
{% endcomment %}
+
+{% include base_path.html %}
+
-
+
- Since 1998,
- Software Carpentry
- has been teaching researchers in science, engineering, medicine, and related disciplines
- the computing skills they need to get more done in less time and with less pain.
- Its volunteer instructors have run hundreds of events
- for thousands of learners in the past two and a half years.
+
The Carpentries comprises
+ Software Carpentry, Data Carpentry, and Library Carpentry communities of Instructors, Trainers,
+ Maintainers, helpers, and supporters who share a mission to teach
+ foundational coding and data science skills to researchers and people
+ working in library- and information-related roles. In January,
+ 2018, The Carpentries was formed by the merger of Software Carpentry and
+ Data Carpentry. Library Carpentry became an official Carpentries Lesson Program in November 2018.
+
+
While individual lessons and workshops continue to be run under each
+ lesson project, The Carpentries provide overall staffing and governance, as
+ well as support for assessment, instructor training and mentoring.
+ Memberships are joint, and the Carpentries project maintains a shared Code
+ of Conduct. The Carpentries is a fiscally sponsored project of Community
+ Initiatives, a registered 501(c)3 non-profit based in California, USA.
+
+
+
+
+
+
+
+
Since 1998, Software Carpentry has
+ been teaching researchers across all disciplines the foundational coding
+ skills they need to get more done in less time and with less pain. Its
+ volunteer instructors have run hundreds of events for thousands of learners
+ around the world. Now that all research involves some degree of
+ computational work, whether with big data, cloud computing, or simple task
+ automation, these skills are needed more than ever.
-
+
- Data Carpentry develops and teaches workshops on the fundamental data skills needed to conduct research.
- Its target audience is researchers who have little to no prior computational experience,
- and its lessons are domain specific,
- building on learners' existing knowledge to enable them to quickly apply skills learned to their own research.
+
Data Carpentry develops and teaches
+ workshops on the fundamental data skills needed to conduct research. Its
+ target audience is researchers who have little to no prior computational
+ experience, and its lessons are domain specific, building on learners'
+ existing knowledge to enable them to quickly apply skills learned to their
+ own research. Data Carpentry workshops take researchers through the entire
+ data life cycle.
-
+
- Library Carpentry is made by librarians to help librarians
- automate repetitive, boring, error-prone tasks;
- create, maintain and analyse sustainable and reusable data;
- work effectively with IT and systems colleagues;
- better understand the use of software in research;
- and much more.
- Library Carpentry was the winner of the 2016
- British Library Labs Teaching and Learning Award.
+
Library Carpentry develops lessons and
+ teaches workshops for and with people working in library- and
+ information-related roles. Its goal is to create an on-ramp to empower this
+ community to use software and data in their own work, as well as be
+ advocates for and train others in efficient, effective and reproducible data
+ and software practices.
diff --git a/_includes/episode_navbar.html b/_includes/episode_navbar.html
index b9f85f6..ea368eb 100644
--- a/_includes/episode_navbar.html
+++ b/_includes/episode_navbar.html
@@ -1,27 +1,35 @@
+{% comment %}
+For some reason, the relative_root_path seems out of scope in this file, so we
+need to re-assign it here
+{% endcomment %}
+
+{% include base_path.html %}
+
{% comment %}
Navigation bar for an episode.
{% endcomment %}
+
',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0},sanitize:!0,sanitizeFn:null,whiteList:t},m.prototype.init=function(t,e,i){if(this.enabled=!0,this.type=t,this.$element=g(e),this.options=this.getOptions(i),this.$viewport=this.options.viewport&&g(document).find(g.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var o=this.options.trigger.split(" "),n=o.length;n--;){var s=o[n];if("click"==s)this.$element.on("click."+this.type,this.options.selector,g.proxy(this.toggle,this));else if("manual"!=s){var a="hover"==s?"mouseenter":"focusin",r="hover"==s?"mouseleave":"focusout";this.$element.on(a+"."+this.type,this.options.selector,g.proxy(this.enter,this)),this.$element.on(r+"."+this.type,this.options.selector,g.proxy(this.leave,this))}}this.options.selector?this._options=g.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},m.prototype.getDefaults=function(){return m.DEFAULTS},m.prototype.getOptions=function(t){var e=this.$element.data();for(var i in e)e.hasOwnProperty(i)&&-1!==g.inArray(i,o)&&delete e[i];return(t=g.extend({},this.getDefaults(),e,t)).delay&&"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),t.sanitize&&(t.template=n(t.template,t.whiteList,t.sanitizeFn)),t},m.prototype.getDelegateOptions=function(){var i={},o=this.getDefaults();return this._options&&g.each(this._options,function(t,e){o[t]!=e&&(i[t]=e)}),i},m.prototype.enter=function(t){var e=t instanceof this.constructor?t:g(t.currentTarget).data("bs."+this.type);if(e||(e=new this.constructor(t.currentTarget,this.getDelegateOptions()),g(t.currentTarget).data("bs."+this.type,e)),t instanceof g.Event&&(e.inState["focusin"==t.type?"focus":"hover"]=!0),e.tip().hasClass("in")||"in"==e.hoverState)e.hoverState="in";else{if(clearTimeout(e.timeout),e.hoverState="in",!e.options.delay||!e.options.delay.show)return e.show();e.timeout=setTimeout(function(){"in"==e.hoverState&&e.show()},e.options.delay.show)}},m.prototype.isInStateTrue=function(){for(var t in this.inState)if(this.inState[t])return!0;return!1},m.prototype.leave=function(t){var e=t instanceof this.constructor?t:g(t.currentTarget).data("bs."+this.type);if(e||(e=new this.constructor(t.currentTarget,this.getDelegateOptions()),g(t.currentTarget).data("bs."+this.type,e)),t instanceof g.Event&&(e.inState["focusout"==t.type?"focus":"hover"]=!1),!e.isInStateTrue()){if(clearTimeout(e.timeout),e.hoverState="out",!e.options.delay||!e.options.delay.hide)return e.hide();e.timeout=setTimeout(function(){"out"==e.hoverState&&e.hide()},e.options.delay.hide)}},m.prototype.show=function(){var t=g.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(t);var e=g.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(t.isDefaultPrevented()||!e)return;var i=this,o=this.tip(),n=this.getUID(this.type);this.setContent(),o.attr("id",n),this.$element.attr("aria-describedby",n),this.options.animation&&o.addClass("fade");var s="function"==typeof this.options.placement?this.options.placement.call(this,o[0],this.$element[0]):this.options.placement,a=/\s?auto?\s?/i,r=a.test(s);r&&(s=s.replace(a,"")||"top"),o.detach().css({top:0,left:0,display:"block"}).addClass(s).data("bs."+this.type,this),this.options.container?o.appendTo(g(document).find(this.options.container)):o.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var l=this.getPosition(),h=o[0].offsetWidth,d=o[0].offsetHeight;if(r){var p=s,c=this.getPosition(this.$viewport);s="bottom"==s&&l.bottom+d>c.bottom?"top":"top"==s&&l.top-dc.width?"left":"left"==s&&l.left-ha.top+a.height&&(n.top=a.top+a.height-l)}else{var h=e.left-s,d=e.left+s+i;ha.right&&(n.left=a.left+a.width-d)}return n},m.prototype.getTitle=function(){var t=this.$element,e=this.options;return t.attr("data-original-title")||("function"==typeof e.title?e.title.call(t[0]):e.title)},m.prototype.getUID=function(t){for(;t+=~~(1e6*Math.random()),document.getElementById(t););return t},m.prototype.tip=function(){if(!this.$tip&&(this.$tip=g(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},m.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},m.prototype.enable=function(){this.enabled=!0},m.prototype.disable=function(){this.enabled=!1},m.prototype.toggleEnabled=function(){this.enabled=!this.enabled},m.prototype.toggle=function(t){var e=this;t&&((e=g(t.currentTarget).data("bs."+this.type))||(e=new this.constructor(t.currentTarget,this.getDelegateOptions()),g(t.currentTarget).data("bs."+this.type,e))),t?(e.inState.click=!e.inState.click,e.isInStateTrue()?e.enter(e):e.leave(e)):e.tip().hasClass("in")?e.leave(e):e.enter(e)},m.prototype.destroy=function(){var t=this;clearTimeout(this.timeout),this.hide(function(){t.$element.off("."+t.type).removeData("bs."+t.type),t.$tip&&t.$tip.detach(),t.$tip=null,t.$arrow=null,t.$viewport=null,t.$element=null})},m.prototype.sanitizeHtml=function(t){return n(t,this.options.whiteList,this.options.sanitizeFn)};var e=g.fn.tooltip;g.fn.tooltip=function i(o){return this.each(function(){var t=g(this),e=t.data("bs.tooltip"),i="object"==typeof o&&o;!e&&/destroy|hide/.test(o)||(e||t.data("bs.tooltip",e=new m(this,i)),"string"==typeof o&&e[o]())})},g.fn.tooltip.Constructor=m,g.fn.tooltip.noConflict=function(){return g.fn.tooltip=e,this}}(jQuery),function(n){"use strict";var s=function(t,e){this.init("popover",t,e)};if(!n.fn.tooltip)throw new Error("Popover requires tooltip.js");s.VERSION="3.4.1",s.DEFAULTS=n.extend({},n.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:'
'}),((s.prototype=n.extend({},n.fn.tooltip.Constructor.prototype)).constructor=s).prototype.getDefaults=function(){return s.DEFAULTS},s.prototype.setContent=function(){var t=this.tip(),e=this.getTitle(),i=this.getContent();if(this.options.html){var o=typeof i;this.options.sanitize&&(e=this.sanitizeHtml(e),"string"===o&&(i=this.sanitizeHtml(i))),t.find(".popover-title").html(e),t.find(".popover-content").children().detach().end()["string"===o?"html":"append"](i)}else t.find(".popover-title").text(e),t.find(".popover-content").children().detach().end().text(i);t.removeClass("fade top bottom left right in"),t.find(".popover-title").html()||t.find(".popover-title").hide()},s.prototype.hasContent=function(){return this.getTitle()||this.getContent()},s.prototype.getContent=function(){var t=this.$element,e=this.options;return t.attr("data-content")||("function"==typeof e.content?e.content.call(t[0]):e.content)},s.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var t=n.fn.popover;n.fn.popover=function e(o){return this.each(function(){var t=n(this),e=t.data("bs.popover"),i="object"==typeof o&&o;!e&&/destroy|hide/.test(o)||(e||t.data("bs.popover",e=new s(this,i)),"string"==typeof o&&e[o]())})},n.fn.popover.Constructor=s,n.fn.popover.noConflict=function(){return n.fn.popover=t,this}}(jQuery),function(s){"use strict";function n(t,e){this.$body=s(document.body),this.$scrollElement=s(t).is(document.body)?s(window):s(t),this.options=s.extend({},n.DEFAULTS,e),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",s.proxy(this.process,this)),this.refresh(),this.process()}function e(o){return this.each(function(){var t=s(this),e=t.data("bs.scrollspy"),i="object"==typeof o&&o;e||t.data("bs.scrollspy",e=new n(this,i)),"string"==typeof o&&e[o]()})}n.VERSION="3.4.1",n.DEFAULTS={offset:10},n.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},n.prototype.refresh=function(){var t=this,o="offset",n=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),s.isWindow(this.$scrollElement[0])||(o="position",n=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var t=s(this),e=t.data("target")||t.attr("href"),i=/^#./.test(e)&&s(e);return i&&i.length&&i.is(":visible")&&[[i[o]().top+n,e]]||null}).sort(function(t,e){return t[0]-e[0]}).each(function(){t.offsets.push(this[0]),t.targets.push(this[1])})},n.prototype.process=function(){var t,e=this.$scrollElement.scrollTop()+this.options.offset,i=this.getScrollHeight(),o=this.options.offset+i-this.$scrollElement.height(),n=this.offsets,s=this.targets,a=this.activeTarget;if(this.scrollHeight!=i&&this.refresh(),o<=e)return a!=(t=s[s.length-1])&&this.activate(t);if(a&&e=n[t]&&(n[t+1]===undefined||e .active"),n=i&&r.support.transition&&(o.length&&o.hasClass("fade")||!!e.find("> .fade").length);function s(){o.removeClass("active").find("> .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),t.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),n?(t[0].offsetWidth,t.addClass("in")):t.removeClass("fade"),t.parent(".dropdown-menu").length&&t.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),i&&i()}o.length&&n?o.one("bsTransitionEnd",s).emulateTransitionEnd(a.TRANSITION_DURATION):s(),o.removeClass("in")};var t=r.fn.tab;r.fn.tab=e,r.fn.tab.Constructor=a,r.fn.tab.noConflict=function(){return r.fn.tab=t,this};var i=function(t){t.preventDefault(),e.call(r(this),"show")};r(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',i).on("click.bs.tab.data-api",'[data-toggle="pill"]',i)}(jQuery),function(l){"use strict";var h=function(t,e){this.options=l.extend({},h.DEFAULTS,e);var i=this.options.target===h.DEFAULTS.target?l(this.options.target):l(document).find(this.options.target);this.$target=i.on("scroll.bs.affix.data-api",l.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",l.proxy(this.checkPositionWithEventLoop,this)),this.$element=l(t),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};function i(o){return this.each(function(){var t=l(this),e=t.data("bs.affix"),i="object"==typeof o&&o;e||t.data("bs.affix",e=new h(this,i)),"string"==typeof o&&e[o]()})}h.VERSION="3.4.1",h.RESET="affix affix-top affix-bottom",h.DEFAULTS={offset:0,target:window},h.prototype.getState=function(t,e,i,o){var n=this.$target.scrollTop(),s=this.$element.offset(),a=this.$target.height();if(null!=i&&"top"==this.affixed)return n 0)
+ {
+ if($('#life-cycle').data('size') == 'big')
+ {
+ $('#life-cycle').data('size','small');
+ $('#life-cycle').stop().animate({
+ padding: '5px'
+ },100);
+ }
+ }
+ else
+ {
+ if($('#life-cycle').data('size') == 'small')
+ {
+ $('#life-cycle').data('size','big');
+ $('#life-cycle').stop().animate({
+ padding: '15px'
+ },100);
+ }
+ }
+});
diff --git a/bin/boilerplate/.travis.yml b/bin/boilerplate/.travis.yml
new file mode 100644
index 0000000..4f23be8
--- /dev/null
+++ b/bin/boilerplate/.travis.yml
@@ -0,0 +1,23 @@
+dist: xenial # Ubuntu 16.04 (required for python 3.7)
+language: python
+python: 3.7
+branches:
+ only:
+ - gh-pages
+ - /.*/
+before_install:
+ - sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys E084DAB9
+ - echo "deb https://cran.rstudio.com/bin/linux/ubuntu trusty/" | sudo tee -a /etc/apt/sources.list
+ - sudo apt-get update -y
+ - sudo apt-get install -y r-base
+ - sudo Rscript -e "install.packages('knitr', repos = 'https://', dependencies = TRUE)"
+ - sudo Rscript -e "install.packages('stringr', repos = 'https://cran.rstudio.com', dependencies = TRUE)"
+ - sudo Rscript -e "install.packages('checkpoint', repos = 'https://cran.rstudio.com', dependencies = TRUE)"
+ - sudo Rscript -e "install.packages('ggplot2', repos = 'https://cran.rstudio.com', dependencies = TRUE)"
+ - rvm default
+ - gem install json kramdown jekyll
+install:
+ - pip install pyyaml
+script:
+ - make lesson-check-all
+ - make --always-make site
diff --git a/bin/boilerplate/AUTHORS b/bin/boilerplate/AUTHORS
new file mode 100644
index 0000000..04e1f5a
--- /dev/null
+++ b/bin/boilerplate/AUTHORS
@@ -0,0 +1 @@
+FIXME: list authors' names and email addresses.
\ No newline at end of file
diff --git a/bin/boilerplate/CITATION b/bin/boilerplate/CITATION
new file mode 100644
index 0000000..56ece3c
--- /dev/null
+++ b/bin/boilerplate/CITATION
@@ -0,0 +1 @@
+FIXME: describe how to cite this lesson.
\ No newline at end of file
diff --git a/bin/boilerplate/CONTRIBUTING.md b/bin/boilerplate/CONTRIBUTING.md
new file mode 100644
index 0000000..7925cef
--- /dev/null
+++ b/bin/boilerplate/CONTRIBUTING.md
@@ -0,0 +1,151 @@
+# Contributing
+
+[The Carpentries][c-site] ([Software Carpentry][swc-site], [Data Carpentry][dc-site], and [Library Carpentry][lc-site]) are open source projects,
+and we welcome contributions of all kinds:
+new lessons,
+fixes to existing material,
+bug reports,
+and reviews of proposed changes are all welcome.
+
+## Contributor Agreement
+
+By contributing,
+you agree that we may redistribute your work under [our license](LICENSE.md).
+In exchange,
+we will address your issues and/or assess your change proposal as promptly as we can,
+and help you become a member of our community.
+Everyone involved in [The Carpentries][c-site]
+agrees to abide by our [code of conduct](CODE_OF_CONDUCT.md).
+
+## How to Contribute
+
+The easiest way to get started is to file an issue
+to tell us about a spelling mistake,
+some awkward wording,
+or a factual error.
+This is a good way to introduce yourself
+and to meet some of our community members.
+
+1. If you do not have a [GitHub][github] account,
+ you can [send us comments by email][email].
+ However,
+ we will be able to respond more quickly if you use one of the other methods described below.
+
+2. If you have a [GitHub][github] account,
+ or are willing to [create one][github-join],
+ but do not know how to use Git,
+ you can report problems or suggest improvements by [creating an issue][issues].
+ This allows us to assign the item to someone
+ and to respond to it in a threaded discussion.
+
+3. If you are comfortable with Git,
+ and would like to add or change material,
+ you can submit a pull request (PR).
+ Instructions for doing this are [included below](#using-github).
+
+## Where to Contribute
+
+1. If you wish to change this lesson,
+ please work in ,
+ which can be viewed at .
+
+2. If you wish to change the example lesson,
+ please work in ,
+ which documents the format of our lessons
+ and can be viewed at .
+
+3. If you wish to change the template used for workshop websites,
+ please work in .
+ The home page of that repository explains how to set up workshop websites,
+ while the extra pages in
+ provide more background on our design choices.
+
+4. If you wish to change CSS style files, tools,
+ or HTML boilerplate for lessons or workshops stored in `_includes` or `_layouts`,
+ please work in .
+
+## What to Contribute
+
+There are many ways to contribute,
+from writing new exercises and improving existing ones
+to updating or filling in the documentation
+and submitting [bug reports][issues]
+about things that don't work, aren't clear, or are missing.
+If you are looking for ideas, please see the 'Issues' tab for
+a list of issues associated with this repository,
+or you may also look at the issues for [Data Carpentry][dc-issues],
+[Software Carpentry][swc-issues], and [Library Carpentry][lc-issues] projects.
+
+Comments on issues and reviews of pull requests are just as welcome:
+we are smarter together than we are on our own.
+Reviews from novices and newcomers are particularly valuable:
+it's easy for people who have been using these lessons for a while
+to forget how impenetrable some of this material can be,
+so fresh eyes are always welcome.
+
+## What *Not* to Contribute
+
+Our lessons already contain more material than we can cover in a typical workshop,
+so we are usually *not* looking for more concepts or tools to add to them.
+As a rule,
+if you want to introduce a new idea,
+you must (a) estimate how long it will take to teach
+and (b) explain what you would take out to make room for it.
+The first encourages contributors to be honest about requirements;
+the second, to think hard about priorities.
+
+We are also not looking for exercises or other material that only run on one platform.
+Our workshops typically contain a mixture of Windows, macOS, and Linux users;
+in order to be usable,
+our lessons must run equally well on all three.
+
+## Using GitHub
+
+If you choose to contribute via GitHub, you may want to look at
+[How to Contribute to an Open Source Project on GitHub][how-contribute].
+To manage changes, we follow [GitHub flow][github-flow].
+Each lesson has two maintainers who review issues and pull requests or encourage others to do so.
+The maintainers are community volunteers and have final say over what gets merged into the lesson.
+To use the web interface for contributing to a lesson:
+
+1. Fork the originating repository to your GitHub profile.
+2. Within your version of the forked repository, move to the `gh-pages` branch and
+create a new branch for each significant change being made.
+3. Navigate to the file(s) you wish to change within the new branches and make revisions as required.
+4. Commit all changed files within the appropriate branches.
+5. Create individual pull requests from each of your changed branches
+to the `gh-pages` branch within the originating repository.
+6. If you receive feedback, make changes using your issue-specific branches of the forked
+repository and the pull requests will update automatically.
+7. Repeat as needed until all feedback has been addressed.
+
+When starting work, please make sure your clone of the originating `gh-pages` branch is up-to-date
+before creating your own revision-specific branch(es) from there.
+Additionally, please only work from your newly-created branch(es) and *not*
+your clone of the originating `gh-pages` branch.
+Lastly, published copies of all the lessons are available in the `gh-pages` branch of the originating
+repository for reference while revising.
+
+## Other Resources
+
+General discussion of [Software Carpentry][swc-site] and [Data Carpentry][dc-site]
+happens on the [discussion mailing list][discuss-list],
+which everyone is welcome to join.
+You can also [reach us by email][email].
+
+[email]: mailto:admin@software-carpentry.org
+[dc-issues]: https://github.com/issues?q=user%3Adatacarpentry
+[dc-lessons]: http://datacarpentry.org/lessons/
+[dc-site]: http://datacarpentry.org/
+[discuss-list]: http://lists.software-carpentry.org/listinfo/discuss
+[github]: https://github.com
+[github-flow]: https://guides.github.com/introduction/flow/
+[github-join]: https://github.com/join
+[how-contribute]: https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github
+[issues]: https://guides.github.com/features/issues/
+[swc-issues]: https://github.com/issues?q=user%3Aswcarpentry
+[swc-lessons]: https://software-carpentry.org/lessons/
+[swc-site]: https://software-carpentry.org/
+[c-site]: https://carpentries.org/
+[lc-site]: https://librarycarpentry.org/
+[lc-issues]: https://github.com/issues?q=user%3Alibrarycarpentry
diff --git a/bin/boilerplate/README.md b/bin/boilerplate/README.md
new file mode 100644
index 0000000..ed47f43
--- /dev/null
+++ b/bin/boilerplate/README.md
@@ -0,0 +1,40 @@
+# FIXME Lesson title
+
+[](https://swc-slack-invite.herokuapp.com/)
+
+This repository generates the corresponding lesson website from [The Carpentries](https://carpentries.org/) repertoire of lessons.
+
+## Contributing
+
+We welcome all contributions to improve the lesson! Maintainers will do their best to help you if you have any
+questions, concerns, or experience any difficulties along the way.
+
+We'd like to ask you to familiarize yourself with our [Contribution Guide](CONTRIBUTING.md) and have a look at
+the [more detailed guidelines][lesson-example] on proper formatting, ways to render the lesson locally, and even
+how to write new episodes.
+
+Please see the current list of [issues][FIXME] for ideas for contributing to this
+repository. For making your contribution, we use the GitHub flow, which is
+nicely explained in the chapter [Contributing to a Project](http://git-scm.com/book/en/v2/GitHub-Contributing-to-a-Project) in Pro Git
+by Scott Chacon.
+Look for the tag . This indicates that the mantainers will welcome a pull request fixing this issue.
+
+
+## Maintainer(s)
+
+Current maintainers of this lesson are
+
+* FIXME
+* FIXME
+* FIXME
+
+
+## Authors
+
+A list of contributors to the lesson can be found in [AUTHORS](AUTHORS)
+
+## Citation
+
+To cite this lesson, please consult with [CITATION](CITATION)
+
+[lesson-example]: https://carpentries.github.io/lesson-example
diff --git a/bin/boilerplate/_config.yml b/bin/boilerplate/_config.yml
new file mode 100644
index 0000000..3c3f4f2
--- /dev/null
+++ b/bin/boilerplate/_config.yml
@@ -0,0 +1,101 @@
+#------------------------------------------------------------
+# Values for this lesson.
+#------------------------------------------------------------
+
+# Which carpentry is this ("swc", "dc", "lc", or "cp")?
+# swc: Software Carpentry
+# dc: Data Carpentry
+# lc: Library Carpentry
+# cp: Carpentries (to use for instructor traning for instance)
+carpentry: "swc"
+
+# Overall title for pages.
+title: "Lesson Title"
+
+# Life cycle stage of the lesson
+# possible values: "pre-alpha", "alpha", "beta", "stable"
+life_cycle: "pre-alpha"
+
+#------------------------------------------------------------
+# Generic settings (should not need to change).
+#------------------------------------------------------------
+
+# What kind of thing is this ("workshop" or "lesson")?
+kind: "lesson"
+
+# Magic to make URLs resolve both locally and on GitHub.
+# See https://help.github.com/articles/repository-metadata-on-github-pages/.
+# Please don't change it: / is correct.
+repository: /
+
+# Email address, no mailto:
+email: "team@carpentries.org"
+
+# Sites.
+amy_site: "https://amy.software-carpentry.org/workshops"
+carpentries_github: "https://github.com/carpentries"
+carpentries_pages: "https://carpentries.github.io"
+carpentries_site: "https://carpentries.org/"
+dc_site: "http://datacarpentry.org"
+example_repo: "https://github.com/carpentries/lesson-example"
+example_site: "https://carpentries.github.io/lesson-example"
+lc_site: "https://librarycarpentry.org/"
+swc_github: "https://github.com/swcarpentry"
+swc_pages: "https://swcarpentry.github.io"
+swc_site: "https://software-carpentry.org"
+template_repo: "https://github.com/carpentries/styles"
+training_site: "https://carpentries.github.io/instructor-training"
+workshop_repo: "https://github.com/carpentries/workshop-template"
+workshop_site: "https://carpentries.github.io/workshop-template"
+cc_by_human: "https://creativecommons.org/licenses/by/4.0/"
+
+# Surveys.
+swc_pre_survey: "https://www.surveymonkey.com/r/swc_pre_workshop_v1?workshop_id="
+swc_post_survey: "https://www.surveymonkey.com/r/swc_post_workshop_v1?workshop_id="
+training_post_survey: "https://www.surveymonkey.com/r/post-instructor-training"
+dc_pre_survey: "https://www.surveymonkey.com/r/dcpreworkshopassessment?workshop_id="
+dc_post_survey: "https://www.surveymonkey.com/r/dcpostworkshopassessment?workshop_id="
+lc_pre_survey: "https://www.surveymonkey.com/r/lcpreworkshopsurvey?workshop_id="
+lc_post_survey: "https://www.surveymonkey.com/r/lcpostworkshopsurvey?workshop_id="
+instructor_pre_survey: "https://www.surveymonkey.com/r/instructor_training_pre_survey?workshop_id="
+instructor_post_survey: "https://www.surveymonkey.com/r/instructor_training_post_survey?workshop_id="
+
+
+# Start time in minutes (0 to be clock-independent, 540 to show a start at 09:00 am).
+start_time: 0
+
+# Specify that things in the episodes collection should be output.
+collections:
+ episodes:
+ output: true
+ permalink: /:path/index.html
+ extras:
+ output: true
+ permalink: /:path/index.html
+
+# Set the default layout for things in the episodes collection.
+defaults:
+ - values:
+ root: .
+ layout: page
+ - scope:
+ path: ""
+ type: episodes
+ values:
+ root: ..
+ layout: episode
+ - scope:
+ path: ""
+ type: extras
+ values:
+ root: ..
+ layout: page
+
+# Files and directories that are not to be copied.
+exclude:
+ - Makefile
+ - bin/
+ - .Rproj.user/
+
+# Turn on built-in syntax highlighting.
+highlighter: rouge
diff --git a/bin/boilerplate/_episodes/01-introduction.md b/bin/boilerplate/_episodes/01-introduction.md
new file mode 100644
index 0000000..2e156c2
--- /dev/null
+++ b/bin/boilerplate/_episodes/01-introduction.md
@@ -0,0 +1,15 @@
+---
+title: "Introduction"
+teaching: 0
+exercises: 0
+questions:
+- "Key question (FIXME)"
+objectives:
+- "First learning objective. (FIXME)"
+keypoints:
+- "First key point. Brief Answer to questions. (FIXME)"
+---
+FIXME
+
+{% include links.md %}
+
diff --git a/bin/boilerplate/_extras/about.md b/bin/boilerplate/_extras/about.md
new file mode 100644
index 0000000..5f07f65
--- /dev/null
+++ b/bin/boilerplate/_extras/about.md
@@ -0,0 +1,5 @@
+---
+title: About
+---
+{% include carpentries.html %}
+{% include links.md %}
diff --git a/bin/boilerplate/_extras/discuss.md b/bin/boilerplate/_extras/discuss.md
new file mode 100644
index 0000000..bfc33c5
--- /dev/null
+++ b/bin/boilerplate/_extras/discuss.md
@@ -0,0 +1,6 @@
+---
+title: Discussion
+---
+FIXME
+
+{% include links.md %}
diff --git a/bin/boilerplate/_extras/figures.md b/bin/boilerplate/_extras/figures.md
new file mode 100644
index 0000000..ee5b650
--- /dev/null
+++ b/bin/boilerplate/_extras/figures.md
@@ -0,0 +1,68 @@
+---
+title: Figures
+---
+
+{% include base_path.html %}
+
+
+{% comment %}
+Create anchor for each one of the episodes.
+{% endcomment %}
+{% for episode in site.episodes %}
+
+{% endfor %}
+
+{% include links.md %}
diff --git a/bin/boilerplate/_extras/guide.md b/bin/boilerplate/_extras/guide.md
new file mode 100644
index 0000000..50f266f
--- /dev/null
+++ b/bin/boilerplate/_extras/guide.md
@@ -0,0 +1,6 @@
+---
+title: "Instructor Notes"
+---
+FIXME
+
+{% include links.md %}
diff --git a/bin/boilerplate/aio.md b/bin/boilerplate/aio.md
new file mode 100644
index 0000000..523e7dd
--- /dev/null
+++ b/bin/boilerplate/aio.md
@@ -0,0 +1,37 @@
+---
+---
+
+{% include base_path.html %}
+
+
+{% comment %}
+Create an anchor for every episode.
+{% endcomment %}
+{% for episode in site.episodes %}
+
+{% endfor %}
diff --git a/bin/boilerplate/index.md b/bin/boilerplate/index.md
new file mode 100644
index 0000000..95ccdbd
--- /dev/null
+++ b/bin/boilerplate/index.md
@@ -0,0 +1,17 @@
+---
+layout: lesson
+root: . # Is the only page that doesn't follow the pattern /:path/index.html
+permalink: index.html # Is the only page that doesn't follow the pattern /:path/index.html
+---
+FIXME: home page introduction
+
+
+
+{% comment %} This is a comment in Liquid {% endcomment %}
+
+> ## Prerequisites
+>
+> FIXME
+{: .prereq}
+
+{% include links.md %}
diff --git a/bin/boilerplate/reference.md b/bin/boilerplate/reference.md
new file mode 100644
index 0000000..8c82616
--- /dev/null
+++ b/bin/boilerplate/reference.md
@@ -0,0 +1,9 @@
+---
+layout: reference
+---
+
+## Glossary
+
+FIXME
+
+{% include links.md %}
diff --git a/bin/boilerplate/setup.md b/bin/boilerplate/setup.md
new file mode 100644
index 0000000..b8c5032
--- /dev/null
+++ b/bin/boilerplate/setup.md
@@ -0,0 +1,7 @@
+---
+title: Setup
+---
+FIXME
+
+
+{% include links.md %}
diff --git a/bin/chunk-options.R b/bin/chunk-options.R
index d956f60..6bd4aef 100644
--- a/bin/chunk-options.R
+++ b/bin/chunk-options.R
@@ -20,7 +20,7 @@ knitr_fig_path <- function(prefix) {
opts_chunk$set(fig.path = new_path)
}
-## We use the rmd- prefix for the figures generated by the lssons so
+## We use the rmd- prefix for the figures generated by the lessons so
## they can be easily identified and deleted by `make clean-rmd`. The
## working directory when the lessons are generated is the root so the
## figures need to be saved in fig/, but when the site is generated,
@@ -29,7 +29,9 @@ knitr_fig_path <- function(prefix) {
opts_chunk$set(tidy = FALSE, results = "markup", comment = NA,
fig.align = "center", fig.path = "fig/rmd-",
- fig.process = fix_fig_path)
+ fig.process = fix_fig_path,
+ fig.width = 8.5, fig.height = 8.5,
+ fig.retina = 2)
# The hooks below add html tags to the code chunks and their output so that they
# are properly formatted when the site is built.
@@ -37,13 +39,13 @@ opts_chunk$set(tidy = FALSE, results = "markup", comment = NA,
hook_in <- function(x, options) {
stringr::str_c("\n\n~~~\n",
paste0(x, collapse="\n"),
- "\n~~~\n{: .r}\n\n")
+ "\n~~~\n{: .language-r}\n\n")
}
hook_out <- function(x, options) {
x <- gsub("\n$", "", x)
stringr::str_c("\n\n~~~\n",
- paste0(x, collapse="\n"),
+ paste0(x, collapse="\n"),
"\n~~~\n{: .output}\n\n")
}
diff --git a/bin/extract_figures.py b/bin/extract_figures.py
deleted file mode 100755
index 63a7752..0000000
--- a/bin/extract_figures.py
+++ /dev/null
@@ -1,98 +0,0 @@
-#!/usr/bin/env python
-
-from __future__ import print_function
-import sys
-import os
-import glob
-from optparse import OptionParser
-
-from util import Reporter, read_markdown, IMAGE_FILE_SUFFIX
-
-def main():
- """Main driver."""
-
- args = parse_args()
- images = []
- for filename in args.filenames:
- images += get_images(args.parser, filename)
- save(sys.stdout, images)
-
-
-def parse_args():
- """Parse command-line arguments."""
-
- parser = OptionParser()
- parser.add_option('-p', '--parser',
- default=None,
- dest='parser',
- help='path to Markdown parser')
-
- args, extras = parser.parse_args()
- require(args.parser is not None,
- 'Path to Markdown parser not provided')
- require(extras,
- 'No filenames specified')
-
- args.filenames = extras
- return args
-
-
-def get_filenames(source_dir):
- """Get all filenames to be searched for images."""
-
- return glob.glob(os.path.join(source_dir, '*.md'))
-
-
-def get_images(parser, filename):
- """Extract all images from file."""
-
- content = read_markdown(parser, filename)
- result = []
- find_image_nodes(content['doc'], result)
- find_image_links(content['doc'], result)
- return result
-
-
-def find_image_nodes(doc, result):
- """Find all nested nodes representing images."""
-
- if (doc['type'] == 'img') or \
- ((doc['type'] == 'html_element') and (doc['value'] == 'img')):
- alt = doc['attr'].get('alt', '')
- result.append({'alt': alt, 'src': doc['attr']['src']})
- else:
- for child in doc.get('children', []):
- find_image_nodes(child, result)
-
-
-def find_image_links(doc, result):
- """Find all links to files in the 'fig' directory."""
-
- if ((doc['type'] == 'a') and ('attr' in doc) and ('href' in doc['attr'])) \
- or \
- ((doc['type'] == 'html_element') and (doc['value'] == 'a') and ('href' in doc['attr'])):
- path = doc['attr']['href']
- if os.path.splitext(path)[1].lower() in IMAGE_FILE_SUFFIX:
- result.append({'alt':'', 'src': doc['attr']['href']})
- else:
- for child in doc.get('children', []):
- find_image_links(child, result)
-
-
-def save(stream, images):
- """Save results as Markdown."""
-
- text = '\n\n'.join(['
'.format(img['alt'], img['src']) for img in images])
- print(text, file=stream)
-
-
-def require(condition, message):
- """Fail if condition not met."""
-
- if not condition:
- print(message, file=sys.stderr)
- sys.exit(1)
-
-
-if __name__ == '__main__':
- main()
diff --git a/bin/generate_md_episodes.R b/bin/generate_md_episodes.R
index f2a40ba..7f37a7b 100644
--- a/bin/generate_md_episodes.R
+++ b/bin/generate_md_episodes.R
@@ -1,36 +1,59 @@
generate_md_episodes <- function() {
- if (require("knitr") && packageVersion("knitr") < '1.9.19')
- stop("knitr must be version 1.9.20 or higher")
-
- if (!require("stringr"))
- stop("The package stringr is required for generating the lessons.")
-
- if (require("checkpoint") && packageVersion("checkpoint") >= '0.4.0') {
- required_pkgs <-
- checkpoint:::scanForPackages(project = "_episodes_rmd",
- verbose=FALSE, use.knitr = TRUE)$pkgs
- } else {
- stop("The checkpoint package (>= 0.4.0) is required to build the lessons.")
- }
-
- missing_pkgs <- required_pkgs[!(required_pkgs %in% rownames(installed.packages()))]
-
- if (length(missing_pkgs)) {
- message("Installing missing required packages: ",
- paste(missing_pkgs, collapse=", "))
- install.packages(missing_pkgs)
- }
-
- ## find all the Rmd files, and generate the paths for their respective outputs
- src_rmd <- list.files(pattern = "??-*.Rmd$", path = "_episodes_rmd", full.names = TRUE)
- dest_md <- file.path("_episodes", gsub("Rmd$", "md", basename(src_rmd)))
-
- ## knit the Rmd into markdown
- mapply(function(x, y) {
- knitr::knit(x, output = y)
- }, src_rmd, dest_md)
-
+ library("methods")
+
+ if (!require("remotes", quietly = TRUE)) {
+ install.packages("remotes", repos = c(CRAN = "https://cloud.r-project.org/"))
+ }
+
+ if (!require("requirements", quietly = TRUE)) {
+ remotes::install_github("hadley/requirements")
+ }
+
+ required_pkgs <- unique(c(
+ ## Packages for episodes
+ requirements:::req_dir("_episodes_rmd"),
+ ## Pacakges for tools
+ requirements:::req_dir("bin")
+ ))
+
+ missing_pkgs <- setdiff(required_pkgs, rownames(installed.packages()))
+
+ if (length(missing_pkgs)) {
+ message("Installing missing required packages: ",
+ paste(missing_pkgs, collapse=", "))
+ install.packages(missing_pkgs)
+ }
+
+ if (require("knitr") && packageVersion("knitr") < '1.9.19')
+ stop("knitr must be version 1.9.20 or higher")
+
+ ## get the Rmd file to process from the command line, and generate the path for their respective outputs
+ args <- commandArgs(trailingOnly = TRUE)
+ if (!identical(length(args), 2L)) {
+ stop("input and output file must be passed to the script")
+ }
+
+ src_rmd <- args[1]
+ dest_md <- args[2]
+
+ ## knit the Rmd into markdown
+ knitr::knit(src_rmd, output = dest_md)
+
+ # Read the generated md files and add comments advising not to edit them
+ vapply(dest_md, function(y) {
+ con <- file(y)
+ mdfile <- readLines(con)
+ if (mdfile[1] != "---")
+ stop("Input file does not have a valid header")
+ mdfile <- append(mdfile, "# Please do not edit this file directly; it is auto generated.", after = 1)
+ mdfile <- append(mdfile, paste("# Instead, please edit",
+ basename(y), "in _episodes_rmd/"), after = 2)
+ writeLines(mdfile, con)
+ close(con)
+ return(paste("Warning added to YAML header of", y))
+ },
+ character(1))
}
generate_md_episodes()
diff --git a/bin/knit_lessons.sh b/bin/knit_lessons.sh
index 3a2395f..141c136 100755
--- a/bin/knit_lessons.sh
+++ b/bin/knit_lessons.sh
@@ -3,6 +3,6 @@
# Only try running R to translate files if there are some files present.
# The Makefile passes in the names of files.
-if [ $# -ne 0 ] ; then
- Rscript -e "source('bin/generate_md_episodes.R')"
+if [ $# -eq 2 ] ; then
+ Rscript -e "source('bin/generate_md_episodes.R')" "$@"
fi
diff --git a/bin/lesson_check.py b/bin/lesson_check.py
index 8244222..b0b5581 100755
--- a/bin/lesson_check.py
+++ b/bin/lesson_check.py
@@ -1,37 +1,39 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
"""
Check lesson files and their contents.
"""
-from __future__ import print_function
-import sys
+
import os
import glob
-import json
import re
-from optparse import OptionParser
+from argparse import ArgumentParser
-from util import Reporter, read_markdown, load_yaml, check_unwanted_files, require, IMAGE_FILE_SUFFIX
+from util import (Reporter, read_markdown, load_yaml, check_unwanted_files,
+ require)
__version__ = '0.3'
# Where to look for source Markdown files.
SOURCE_DIRS = ['', '_episodes', '_extras']
+# Where to look for source Rmd files.
+SOURCE_RMD_DIRS = ['_episodes_rmd']
+
# Required files: each entry is ('path': YAML_required).
# FIXME: We do not yet validate whether any files have the required
# YAML headers, but should in the future.
# The '%' is replaced with the source directory path for checking.
-# Episodes are handled specially, and extra files in '_extras' are also handled specially.
-# This list must include all the Markdown files listed in the 'bin/initialize' script.
+# Episodes are handled specially, and extra files in '_extras' are also handled
+# specially. This list must include all the Markdown files listed in the
+# 'bin/initialize' script.
REQUIRED_FILES = {
- '%/CONDUCT.md': True,
+ '%/CODE_OF_CONDUCT.md': True,
'%/CONTRIBUTING.md': False,
'%/LICENSE.md': True,
'%/README.md': False,
'%/_extras/discuss.md': True,
- '%/_extras/figures.md': True,
'%/_extras/guide.md': True,
'%/index.md': True,
'%/reference.md': True,
@@ -72,12 +74,14 @@
'error',
'output',
'source',
- 'bash',
- 'make',
- 'matlab',
- 'python',
- 'r',
- 'sql'
+ 'language-bash',
+ 'html',
+ 'language-make',
+ 'language-matlab',
+ 'language-python',
+ 'language-r',
+ 'language-shell',
+ 'language-sql'
}
# What fields are required in teaching episode metadata?
@@ -98,55 +102,64 @@
}
# How long are lines allowed to be?
+# Please keep this in sync with .editorconfig!
MAX_LINE_LEN = 100
+
def main():
"""Main driver."""
args = parse_args()
args.reporter = Reporter()
check_config(args.reporter, args.source_dir)
+ check_source_rmd(args.reporter, args.source_dir, args.parser)
args.references = read_references(args.reporter, args.reference_path)
docs = read_all_markdown(args.source_dir, args.parser)
- check_fileset(args.source_dir, args.reporter, docs.keys())
+ check_fileset(args.source_dir, args.reporter, list(docs.keys()))
check_unwanted_files(args.source_dir, args.reporter)
- for filename in docs.keys():
+ for filename in list(docs.keys()):
checker = create_checker(args, filename, docs[filename])
checker.check()
- check_figures(args.source_dir, args.reporter)
args.reporter.report()
+ if args.reporter.messages and not args.permissive:
+ exit(1)
def parse_args():
"""Parse command-line arguments."""
- parser = OptionParser()
- parser.add_option('-l', '--linelen',
- default=False,
- action="store_true",
- dest='line_lengths',
- help='Check line lengths')
- parser.add_option('-p', '--parser',
- default=None,
- dest='parser',
- help='path to Markdown parser')
- parser.add_option('-r', '--references',
- default=None,
- dest='reference_path',
- help='path to Markdown file of external references')
- parser.add_option('-s', '--source',
- default=os.curdir,
- dest='source_dir',
- help='source directory')
- parser.add_option('-w', '--whitespace',
- default=False,
- action="store_true",
- dest='trailing_whitespace',
- help='Check for trailing whitespace')
-
- args, extras = parser.parse_args()
+ parser = ArgumentParser(description="""Check episode files in a lesson.""")
+ parser.add_argument('-l', '--linelen',
+ default=False,
+ action="store_true",
+ dest='line_lengths',
+ help='Check line lengths')
+ parser.add_argument('-p', '--parser',
+ default=None,
+ dest='parser',
+ help='path to Markdown parser')
+ parser.add_argument('-r', '--references',
+ default=None,
+ dest='reference_path',
+ help='path to Markdown file of external references')
+ parser.add_argument('-s', '--source',
+ default=os.curdir,
+ dest='source_dir',
+ help='source directory')
+ parser.add_argument('-w', '--whitespace',
+ default=False,
+ action="store_true",
+ dest='trailing_whitespace',
+ help='Check for trailing whitespace')
+ parser.add_argument('--permissive',
+ default=False,
+ action="store_true",
+ dest='permissive',
+ help='Do not raise an error even if issues are detected')
+
+ args, extras = parser.parse_known_args()
require(args.parser is not None,
'Path to Markdown parser not provided')
require(not extras,
@@ -160,15 +173,35 @@ def check_config(reporter, source_dir):
config_file = os.path.join(source_dir, '_config.yml')
config = load_yaml(config_file)
- reporter.check_field(config_file, 'configuration', config, 'kind', 'lesson')
- reporter.check_field(config_file, 'configuration', config, 'carpentry', ('swc', 'dc', 'lc'))
+ reporter.check_field(config_file, 'configuration',
+ config, 'kind', 'lesson')
+ reporter.check_field(config_file, 'configuration',
+ config, 'carpentry', ('swc', 'dc', 'lc', 'cp'))
reporter.check_field(config_file, 'configuration', config, 'title')
- reporter.check_field(config_file, 'configuration', config, 'contact')
-
- reporter.check({'values': {'root': '..'}} in config.get('defaults', []),
+ reporter.check_field(config_file, 'configuration', config, 'email')
+
+ for defaults in [
+ {'values': {'root': '.', 'layout': 'page'}},
+ {'values': {'root': '..', 'layout': 'episode'}, 'scope': {'type': 'episodes', 'path': ''}},
+ {'values': {'root': '..', 'layout': 'page'}, 'scope': {'type': 'extras', 'path': ''}}
+ ]:
+ reporter.check(defaults in config.get('defaults', []),
'configuration',
- '"root" not set to ".." in configuration')
-
+ '"root" not set to "." in configuration')
+
+def check_source_rmd(reporter, source_dir, parser):
+ """Check that Rmd episode files include `source: Rmd`"""
+
+ episode_rmd_dir = [os.path.join(source_dir, d) for d in SOURCE_RMD_DIRS]
+ episode_rmd_files = [os.path.join(d, '*.Rmd') for d in episode_rmd_dir]
+ results = {}
+ for pat in episode_rmd_files:
+ for f in glob.glob(pat):
+ data = read_markdown(parser, f)
+ dy = data['metadata']
+ if dy:
+ reporter.check_field(f, 'episode_rmd',
+ dy, 'source', 'Rmd')
def read_references(reporter, ref_path):
"""Read shared file of reference links, returning dictionary of valid references
@@ -235,17 +268,17 @@ def check_fileset(source_dir, reporter, filenames_present):
if m and m.group(1):
seen.append(m.group(1))
else:
- reporter.add(None, 'Episode {0} has badly-formatted filename', filename)
+ reporter.add(
+ None, 'Episode {0} has badly-formatted filename', filename)
# Check for duplicate episode numbers.
reporter.check(len(seen) == len(set(seen)),
- None,
- 'Duplicate episode numbers {0} vs {1}',
- sorted(seen), sorted(set(seen)))
+ None,
+ 'Duplicate episode numbers {0} vs {1}',
+ sorted(seen), sorted(set(seen)))
# Check that numbers are consecutive.
- seen = [int(s) for s in seen]
- seen.sort()
+ seen = sorted([int(s) for s in seen])
clean = True
for i in range(len(seen) - 1):
clean = clean and ((seen[i+1] - seen[i]) == 1)
@@ -255,55 +288,22 @@ def check_fileset(source_dir, reporter, filenames_present):
seen)
-def check_figures(source_dir, reporter):
- """Check that all figures are present and referenced."""
-
- # Get references.
- try:
- all_figures_html = os.path.join(source_dir, '_includes', 'all_figures.html')
- with open(all_figures_html, 'r') as reader:
- text = reader.read()
- figures = P_FIGURE_REFS.findall(text)
- referenced = [os.path.split(f)[1] for f in figures if '/fig/' in f]
- except FileNotFoundError as e:
- reporter.add(all_figures_html,
- 'File not found')
- return
-
- # Get actual image files (ignore non-image files).
- fig_dir_path = os.path.join(source_dir, 'fig')
- actual = [f for f in os.listdir(fig_dir_path) if os.path.splitext(f)[1] in IMAGE_FILE_SUFFIX]
-
- # Report differences.
- unexpected = set(actual) - set(referenced)
- reporter.check(not unexpected,
- None,
- 'Unexpected image files: {0}',
- ', '.join(sorted(unexpected)))
- missing = set(referenced) - set(actual)
- reporter.check(not missing,
- None,
- 'Missing image files: {0}',
- ', '.join(sorted(missing)))
-
-
def create_checker(args, filename, info):
"""Create appropriate checker for file."""
for (pat, cls) in CHECKERS:
if pat.search(filename):
return cls(args, filename, **info)
+ return NotImplemented
-
-class CheckBase(object):
+class CheckBase:
"""Base class for checking Markdown files."""
def __init__(self, args, filename, metadata, metadata_len, text, lines, doc):
"""Cache arguments for checking."""
- super(CheckBase, self).__init__()
self.args = args
- self.reporter = self.args.reporter # for convenience
+ self.reporter = self.args.reporter # for convenience
self.filename = filename
self.metadata = metadata
self.metadata_len = metadata_len
@@ -313,7 +313,6 @@ def __init__(self, args, filename, metadata, metadata_len, text, lines, doc):
self.layout = None
-
def check(self):
"""Run tests."""
@@ -324,7 +323,6 @@ def check(self):
self.check_codeblock_classes()
self.check_defined_link_references()
-
def check_metadata(self):
"""Check the YAML metadata."""
@@ -333,53 +331,51 @@ def check_metadata(self):
'Missing metadata entirely')
if self.metadata and (self.layout is not None):
- self.reporter.check_field(self.filename, 'metadata', self.metadata, 'layout', self.layout)
-
+ self.reporter.check_field(
+ self.filename, 'metadata', self.metadata, 'layout', self.layout)
def check_line_lengths(self):
"""Check the raw text of the lesson body."""
if self.args.line_lengths:
- over = [i for (i, l, n) in self.lines if (n > MAX_LINE_LEN) and (not l.startswith('!'))]
+ over = [i for (i, l, n) in self.lines if (
+ n > MAX_LINE_LEN) and (not l.startswith('!'))]
self.reporter.check(not over,
self.filename,
- 'Line(s) are too long: {0}',
+ 'Line(s) too long: {0}',
', '.join([str(i) for i in over]))
-
def check_trailing_whitespace(self):
"""Check for whitespace at the ends of lines."""
if self.args.trailing_whitespace:
- trailing = [i for (i, l, n) in self.lines if P_TRAILING_WHITESPACE.match(l)]
+ trailing = [
+ i for (i, l, n) in self.lines if P_TRAILING_WHITESPACE.match(l)]
self.reporter.check(not trailing,
self.filename,
'Line(s) end with whitespace: {0}',
', '.join([str(i) for i in trailing]))
-
def check_blockquote_classes(self):
"""Check that all blockquotes have known classes."""
- for node in self.find_all(self.doc, {'type' : 'blockquote'}):
+ for node in self.find_all(self.doc, {'type': 'blockquote'}):
cls = self.get_val(node, 'attr', 'class')
self.reporter.check(cls in KNOWN_BLOCKQUOTES,
(self.filename, self.get_loc(node)),
'Unknown or missing blockquote type {0}',
cls)
-
def check_codeblock_classes(self):
"""Check that all code blocks have known classes."""
- for node in self.find_all(self.doc, {'type' : 'codeblock'}):
+ for node in self.find_all(self.doc, {'type': 'codeblock'}):
cls = self.get_val(node, 'attr', 'class')
self.reporter.check(cls in KNOWN_CODEBLOCKS,
(self.filename, self.get_loc(node)),
'Unknown or missing code block type {0}',
cls)
-
def check_defined_link_references(self):
"""Check that defined links resolve in the file.
@@ -387,7 +383,7 @@ def check_defined_link_references(self):
"""
result = set()
- for node in self.find_all(self.doc, {'type' : 'text'}):
+ for node in self.find_all(self.doc, {'type': 'text'}):
for match in P_INTERNAL_LINK_REF.findall(node['value']):
text = match[0]
link = match[1]
@@ -398,11 +394,10 @@ def check_defined_link_references(self):
'Internally-defined links may be missing definitions: {0}',
', '.join(sorted(result)))
-
def find_all(self, node, pattern, accum=None):
"""Find all matches for a pattern."""
- assert type(pattern) == dict, 'Patterns must be dictionaries'
+ assert isinstance(pattern, dict), 'Patterns must be dictionaries'
if accum is None:
accum = []
if self.match(node, pattern):
@@ -411,7 +406,6 @@ def find_all(self, node, pattern, accum=None):
self.find_all(child, pattern, accum)
return accum
-
def match(self, node, pattern):
"""Does this node match the given pattern?"""
@@ -419,16 +413,16 @@ def match(self, node, pattern):
if key not in node:
return False
val = pattern[key]
- if type(val) == str:
+ if isinstance(val, str):
if node[key] != val:
return False
- elif type(val) == dict:
+ elif isinstance(val, dict):
if not self.match(node[key], val):
return False
return True
-
- def get_val(self, node, *chain):
+ @staticmethod
+ def get_val(node, *chain):
"""Get value one or more levels down."""
curr = node
@@ -438,7 +432,6 @@ def get_val(self, node, *chain):
break
return curr
-
def get_loc(self, node):
"""Convenience method to get node's line number."""
@@ -451,10 +444,6 @@ def get_loc(self, node):
class CheckNonJekyll(CheckBase):
"""Check a file that isn't translated by Jekyll."""
- def __init__(self, args, filename, metadata, metadata_len, text, lines, doc):
- super(CheckNonJekyll, self).__init__(args, filename, metadata, metadata_len, text, lines, doc)
-
-
def check_metadata(self):
self.reporter.check(self.metadata is None,
self.filename,
@@ -465,11 +454,11 @@ class CheckIndex(CheckBase):
"""Check the main index page."""
def __init__(self, args, filename, metadata, metadata_len, text, lines, doc):
- super(CheckIndex, self).__init__(args, filename, metadata, metadata_len, text, lines, doc)
+ super().__init__(args, filename, metadata, metadata_len, text, lines, doc)
self.layout = 'lesson'
def check_metadata(self):
- super(CheckIndex, self).check_metadata()
+ super().check_metadata()
self.reporter.check(self.metadata.get('root', '') == '.',
self.filename,
'Root not set to "."')
@@ -478,19 +467,14 @@ def check_metadata(self):
class CheckEpisode(CheckBase):
"""Check an episode page."""
- def __init__(self, args, filename, metadata, metadata_len, text, lines, doc):
- super(CheckEpisode, self).__init__(args, filename, metadata, metadata_len, text, lines, doc)
-
-
def check(self):
"""Run extra tests."""
- super(CheckEpisode, self).check()
+ super().check()
self.check_reference_inclusion()
-
def check_metadata(self):
- super(CheckEpisode, self).check_metadata()
+ super().check_metadata()
if self.metadata:
if 'layout' in self.metadata:
if self.metadata['layout'] == 'break':
@@ -502,19 +486,18 @@ def check_metadata(self):
else:
self.check_metadata_fields(TEACHING_METADATA_FIELDS)
-
def check_metadata_fields(self, expected):
+ """Check metadata fields."""
for (name, type_) in expected:
if name not in self.metadata:
self.reporter.add(self.filename,
'Missing metadata field {0}',
name)
- elif type(self.metadata[name]) != type_:
+ elif not isinstance(self.metadata[name], type_):
self.reporter.add(self.filename,
'"{0}" has wrong type in metadata ({1} instead of {2})',
name, type(self.metadata[name]), type_)
-
def check_reference_inclusion(self):
"""Check that links file has been included."""
@@ -539,7 +522,7 @@ class CheckReference(CheckBase):
"""Check the reference page."""
def __init__(self, args, filename, metadata, metadata_len, text, lines, doc):
- super(CheckReference, self).__init__(args, filename, metadata, metadata_len, text, lines, doc)
+ super().__init__(args, filename, metadata, metadata_len, text, lines, doc)
self.layout = 'reference'
@@ -547,8 +530,7 @@ class CheckGeneric(CheckBase):
"""Check a generic page."""
def __init__(self, args, filename, metadata, metadata_len, text, lines, doc):
- super(CheckGeneric, self).__init__(args, filename, metadata, metadata_len, text, lines, doc)
- self.layout = 'page'
+ super().__init__(args, filename, metadata, metadata_len, text, lines, doc)
CHECKERS = [
@@ -557,6 +539,7 @@ def __init__(self, args, filename, metadata, metadata_len, text, lines, doc):
(re.compile(r'index\.md'), CheckIndex),
(re.compile(r'reference\.md'), CheckReference),
(re.compile(r'_episodes/.*\.md'), CheckEpisode),
+ (re.compile(r'aio\.md'), CheckNonJekyll),
(re.compile(r'.*\.md'), CheckGeneric)
]
diff --git a/bin/lesson_initialize.py b/bin/lesson_initialize.py
index fc7baf7..a5eb6d0 100755
--- a/bin/lesson_initialize.py
+++ b/bin/lesson_initialize.py
@@ -1,391 +1,28 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
"""Initialize a newly-created repository."""
-from __future__ import print_function
import sys
import os
-
-ROOT_AUTHORS = '''\
-FIXME: list authors' names and email addresses.
-'''
-
-ROOT_CITATION = '''\
-FIXME: describe how to cite this lesson.
-'''
-
-ROOT_CONTRIBUTING_MD = '''\
-# Contributing
-
-[Software Carpentry][swc-site] and [Data Carpentry][dc-site] are open source projects,
-and we welcome contributions of all kinds:
-new lessons,
-fixes to existing material,
-bug reports,
-and reviews of proposed changes are all welcome.
-
-## Contributor Agreement
-
-By contributing,
-you agree that we may redistribute your work under [our license](LICENSE.md).
-In exchange,
-we will address your issues and/or assess your change proposal as promptly as we can,
-and help you become a member of our community.
-Everyone involved in [Software Carpentry][swc-site] and [Data Carpentry][dc-site]
-agrees to abide by our [code of conduct](CONDUCT.md).
-
-## How to Contribute
-
-The easiest way to get started is to file an issue
-to tell us about a spelling mistake,
-some awkward wording,
-or a factual error.
-This is a good way to introduce yourself
-and to meet some of our community members.
-
-1. If you do not have a [GitHub][github] account,
- you can [send us comments by email][contact].
- However,
- we will be able to respond more quickly if you use one of the other methods described below.
-
-2. If you have a [GitHub][github] account,
- or are willing to [create one][github-join],
- but do not know how to use Git,
- you can report problems or suggest improvements by [creating an issue][issues].
- This allows us to assign the item to someone
- and to respond to it in a threaded discussion.
-
-3. If you are comfortable with Git,
- and would like to add or change material,
- you can submit a pull request (PR).
- Instructions for doing this are [included below](#using-github).
-
-## Where to Contribute
-
-1. If you wish to change this lesson,
- please work in ,
- which can be viewed at .
-
-2. If you wish to change the example lesson,
- please work in ,
- which documents the format of our lessons
- and can be viewed at .
-
-3. If you wish to change the template used for workshop websites,
- please work in .
- The home page of that repository explains how to set up workshop websites,
- while the extra pages in
- provide more background on our design choices.
-
-4. If you wish to change CSS style files, tools,
- or HTML boilerplate for lessons or workshops stored in `_includes` or `_layouts`,
- please work in .
-
-## What to Contribute
-
-There are many ways to contribute,
-from writing new exercises and improving existing ones
-to updating or filling in the documentation
-and and submitting [bug reports][issues]
-about things that don't work, aren't clear, or are missing.
-If you are looking for ideas,
-please see [the list of issues for this repository][issues],
-or the issues for [Data Carpentry][dc-issues]
-and [Software Carpentry][swc-issues] projects.
-
-Comments on issues and reviews of pull requests are just as welcome:
-we are smarter together than we are on our own.
-Reviews from novices and newcomers are particularly valuable:
-it's easy for people who have been using these lessons for a while
-to forget how impenetrable some of this material can be,
-so fresh eyes are always welcome.
-
-## What *Not* to Contribute
-
-Our lessons already contain more material than we can cover in a typical workshop,
-so we are usually *not* looking for more concepts or tools to add to them.
-As a rule,
-if you want to introduce a new idea,
-you must (a) estimate how long it will take to teach
-and (b) explain what you would take out to make room for it.
-The first encourages contributors to be honest about requirements;
-the second, to think hard about priorities.
-
-We are also not looking for exercises or other material that only run on one platform.
-Our workshops typically contain a mixture of Windows, Mac OS X, and Linux users;
-in order to be usable,
-our lessons must run equally well on all three.
-
-## Using GitHub
-
-If you choose to contribute via GitHub,
-you may want to look at
-[How to Contribute to an Open Source Project on GitHub][how-contribute].
-In brief:
-
-1. The published copy of the lesson is in the `gh-pages` branch of the repository
- (so that GitHub will regenerate it automatically).
- Please create all branches from that,
- and merge the [master repository][repo]'s `gh-pages` branch into your `gh-pages` branch
- before starting work.
- Please do *not* work directly in your `gh-pages` branch,
- since that will make it difficult for you to work on other contributions.
-
-2. We use [GitHub flow][github-flow] to manage changes:
- 1. Create a new branch in your desktop copy of this repository for each significant change.
- 2. Commit the change in that branch.
- 3. Push that branch to your fork of this repository on GitHub.
- 4. Submit a pull request from that branch to the [master repository][repo].
- 5. If you receive feedback,
- make changes on your desktop and push to your branch on GitHub:
- the pull request will update automatically.
-
-Each lesson has two maintainers who review issues and pull requests
-or encourage others to do so.
-The maintainers are community volunteers,
-and have final say over what gets merged into the lesson.
-
-## Other Resources
-
-General discussion of [Software Carpentry][swc-site] and [Data Carpentry][dc-site]
-happens on the [discussion mailing list][discuss-list],
-which everyone is welcome to join.
-You can also [reach us by email][contact].
-
-[contact]: mailto:admin@software-carpentry.org
-[dc-issues]: https://github.com/issues?q=user%3Adatacarpentry
-[dc-lessons]: http://datacarpentry.org/lessons/
-[dc-site]: http://datacarpentry.org/
-[discuss-list]: http://lists.software-carpentry.org/listinfo/discuss
-[github]: http://github.com
-[github-flow]: https://guides.github.com/introduction/flow/
-[github-join]: https://github.com/join
-[how-contribute]: https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github
-[issues]: https://github.com/swcarpentry/FIXME/issues/
-[repo]: https://github.com/swcarpentry/FIXME/
-[swc-issues]: https://github.com/issues?q=user%3Aswcarpentry
-[swc-lessons]: http://software-carpentry.org/lessons/
-[swc-site]: http://software-carpentry.org/
-'''
-
-ROOT_CONFIG_YML = '''\
-#------------------------------------------------------------
-# Values for this lesson.
-#------------------------------------------------------------
-
-# Which carpentry is this ("swc", "dc", or "lc")?
-carpentry: "swc"
-
-# Overall title for pages.
-title: "Lesson Title"
-
-# Contact. This *must* include the protocol: if it's an email
-# address, it must look like "mailto:lessons@software-carpentry.org",
-# or if it's a URL, "https://gitter.im/username/ProjectName".
-contact: "mailto:lessons@software-carpentry.org"
-
-#------------------------------------------------------------
-# Generic settings (should not need to change).
-#------------------------------------------------------------
-
-# What kind of thing is this ("workshop" or "lesson")?
-kind: "lesson"
-
-# Magic to make URLs resolve both locally and on GitHub.
-# See https://help.github.com/articles/repository-metadata-on-github-pages/.
-repository: /
-
-# Sites.
-amy_site: "https://amy.software-carpentry.org/workshops"
-dc_site: "http://datacarpentry.org"
-swc_github: "https://github.com/swcarpentry"
-swc_site: "https://software-carpentry.org"
-swc_pages: "https://swcarpentry.github.io"
-lc_site: "http://librarycarpentry.github.io/"
-template_repo: "https://github.com/swcarpentry/styles"
-example_repo: "https://github.com/swcarpentry/lesson-example"
-example_site: "https://swcarpentry.github.com/lesson-example"
-workshop_repo: "https://github.com/swcarpentry/workshop-template"
-workshop_site: "https://swcarpentry.github.io/workshop-template"
-training_site: "https://swcarpentry.github.io/instructor-training"
-
-# Surveys.
-pre_survey: "https://www.surveymonkey.com/r/swc_pre_workshop_v1?workshop_id="
-post_survey: "https://www.surveymonkey.com/r/swc_post_workshop_v1?workshop_id="
-training_post_survey: "https://www.surveymonkey.com/r/post-instructor-training"
-
-# Start time in minutes (0 to be clock-independent, 540 to show a start at 09:00 am).
-start_time: 0
-
-# Specify that things in the episodes collection should be output.
-collections:
- episodes:
- output: true
- permalink: /:path/
- extras:
- output: true
-
-# Set the default layout for things in the episodes collection.
-defaults:
- - values:
- root: ..
- - scope:
- path: ""
- type: episodes
- values:
- layout: episode
-
-# Files and directories that are not to be copied.
-exclude:
- - Makefile
- - bin
-
-# Turn off built-in syntax highlighting.
-highlighter: false
-'''
-
-ROOT_INDEX_MD = '''\
----
-layout: lesson
-root: .
----
-FIXME: home page introduction
-
-> ## Prerequisites
->
-> FIXME
-{: .prereq}
-'''
-
-ROOT_REFERENCE_MD = '''\
----
-layout: reference
-permalink: /reference/
----
-
-## Glossary
-
-FIXME
-'''
-
-ROOT_SETUP_MD = '''\
----
-layout: page
-title: Setup
-permalink: /setup/
----
-FIXME
-'''
-
-ROOT_AIO_MD = '''\
----
-layout: page
-permalink: /aio/
----
-
-{% comment %}
-Create anchor for each one of the episodes.
-{% endcomment %}
-{% for episode in site.episodes %}
-
-{% endfor %}
-'''
-
-EPISODES_INTRODUCTION_MD = '''\
----
-title: "Introduction"
-teaching: 0
-exercises: 0
-questions:
-- "Key question"
-objectives:
-- "First objective."
-keypoints:
-- "First key point."
----
-'''
-
-EXTRAS_ABOUT_MD = '''\
----
-layout: page
-title: About
-permalink: /about/
----
-{% include carpentries.html %}
-'''
-
-EXTRAS_DISCUSS_MD = '''\
----
-layout: page
-title: Discussion
-permalink: /discuss/
----
-FIXME
-'''
-
-EXTRAS_FIGURES_MD = '''\
----
-layout: page
-title: Figures
-permalink: /figures/
----
-{% include all_figures.html %}
-'''
-
-EXTRAS_GUIDE_MD = '''\
----
-layout: page
-title: "Instructor Notes"
-permalink: /guide/
----
-FIXME
-'''
-
-INCLUDES_ALL_FIGURES_HTML = '''\
-
-'''
+import shutil
BOILERPLATE = (
- ('AUTHORS', ROOT_AUTHORS),
- ('CITATION', ROOT_CITATION),
- ('CONTRIBUTING.md', ROOT_CONTRIBUTING_MD),
- ('_config.yml', ROOT_CONFIG_YML),
- ('index.md', ROOT_INDEX_MD),
- ('reference.md', ROOT_REFERENCE_MD),
- ('setup.md', ROOT_SETUP_MD),
- ('aio.md', ROOT_AIO_MD),
- ('_episodes/01-introduction.md', EPISODES_INTRODUCTION_MD),
- ('_extras/about.md', EXTRAS_ABOUT_MD),
- ('_extras/discuss.md', EXTRAS_DISCUSS_MD),
- ('_extras/figures.md', EXTRAS_FIGURES_MD),
- ('_extras/guide.md', EXTRAS_GUIDE_MD),
- ('_includes/all_figures.html', INCLUDES_ALL_FIGURES_HTML)
+ '.travis.yml',
+ 'AUTHORS',
+ 'CITATION',
+ 'CONTRIBUTING.md',
+ 'README.md',
+ '_config.yml',
+ '_episodes/01-introduction.md',
+ '_extras/about.md',
+ '_extras/discuss.md',
+ '_extras/figures.md',
+ '_extras/guide.md',
+ 'aio.md',
+ 'index.md',
+ 'reference.md',
+ 'setup.md',
)
@@ -394,7 +31,7 @@ def main():
# Check.
errors = False
- for (path, _) in BOILERPLATE:
+ for path in BOILERPLATE:
if os.path.exists(path):
print('Warning: {0} already exists.'.format(path), file=sys.stderr)
errors = True
@@ -403,9 +40,11 @@ def main():
sys.exit(1)
# Create.
- for (path, content) in BOILERPLATE:
- with open(path, 'w') as writer:
- writer.write(content)
+ for path in BOILERPLATE:
+ shutil.copyfile(
+ "bin/boilerplate/{}".format(path),
+ path
+ )
if __name__ == '__main__':
diff --git a/bin/repo_check.py b/bin/repo_check.py
index fd04ce9..af4b782 100755
--- a/bin/repo_check.py
+++ b/bin/repo_check.py
@@ -1,17 +1,17 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
"""
Check repository settings.
"""
-from __future__ import print_function
+
import sys
import os
from subprocess import Popen, PIPE
import re
-from optparse import OptionParser
+from argparse import ArgumentParser
-from util import Reporter, load_yaml, require
+from util import Reporter, require
# Import this way to produce a more useful error message.
try:
@@ -35,15 +35,26 @@
# Expected labels and colors.
EXPECTED = {
- 'bug' : 'bd2c00',
- 'discussion' : 'fc8dc1',
- 'enhancement' : '9cd6dc',
- 'help-wanted' : 'f4fd9c',
- 'instructor-training' : '6e5494',
- 'newcomer-friendly' : 'eec275',
- 'question' : '808040',
- 'template-and-tools' : '2b3990',
- 'work-in-progress' : '7ae78e'
+ 'help wanted': 'dcecc7',
+ 'status:in progress': '9bcc65',
+ 'status:changes requested': '679f38',
+ 'status:wait': 'fff2df',
+ 'status:refer to cac': 'ffdfb2',
+ 'status:need more info': 'ee6c00',
+ 'status:blocked': 'e55100',
+ 'status:out of scope': 'eeeeee',
+ 'status:duplicate': 'bdbdbd',
+ 'type:typo text': 'f8bad0',
+ 'type:bug': 'eb3f79',
+ 'type:formatting': 'ac1357',
+ 'type:template and tools': '7985cb',
+ 'type:instructor guide': '00887a',
+ 'type:discussion': 'b2e5fc',
+ 'type:enhancement': '7fdeea',
+ 'type:clarification': '00acc0',
+ 'type:teaching example': 'ced8dc',
+ 'good first issue': 'ffeb3a',
+ 'high priority': 'd22e2e'
}
@@ -54,7 +65,7 @@ def main():
args = parse_args()
reporter = Reporter()
- repo_url = get_repo_url(args.source_dir, args.repo_url)
+ repo_url = get_repo_url(args.repo_url)
check_labels(reporter, repo_url)
reporter.report()
@@ -64,24 +75,24 @@ def parse_args():
Parse command-line arguments.
"""
- parser = OptionParser()
- parser.add_option('-r', '--repo',
- default=None,
- dest='repo_url',
- help='repository URL')
- parser.add_option('-s', '--source',
- default=os.curdir,
- dest='source_dir',
- help='source directory')
-
- args, extras = parser.parse_args()
+ parser = ArgumentParser(description="""Check repository settings.""")
+ parser.add_argument('-r', '--repo',
+ default=None,
+ dest='repo_url',
+ help='repository URL')
+ parser.add_argument('-s', '--source',
+ default=os.curdir,
+ dest='source_dir',
+ help='source directory')
+
+ args, extras = parser.parse_known_args()
require(not extras,
'Unexpected trailing command-line arguments "{0}"'.format(extras))
return args
-def get_repo_url(source_dir, repo_url):
+def get_repo_url(repo_url):
"""
Figure out which repository to query.
"""
@@ -92,7 +103,8 @@ def get_repo_url(source_dir, repo_url):
# Guess.
cmd = 'git remote -v'
- p = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, close_fds=True, universal_newlines=True)
+ p = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE,
+ close_fds=True, universal_newlines=True)
stdout_data, stderr_data = p.communicate()
stdout_data = stdout_data.split('\n')
matches = [P_GIT_REMOTE.match(line) for line in stdout_data]
@@ -101,10 +113,12 @@ def get_repo_url(source_dir, repo_url):
'Unexpected output from git remote command: "{0}"'.format(matches))
username = matches[0].group(1)
- require(username, 'empty username in git remote output {0}'.format(matches[0]))
+ require(
+ username, 'empty username in git remote output {0}'.format(matches[0]))
project_name = matches[0].group(2)
- require(username, 'empty project name in git remote output {0}'.format(matches[0]))
+ require(
+ username, 'empty project name in git remote output {0}'.format(matches[0]))
url = F_REPO_URL.format(username, project_name)
return url
@@ -131,7 +145,7 @@ def check_labels(reporter, repo_url):
overlap = set(EXPECTED.keys()).intersection(set(actual.keys()))
for name in sorted(overlap):
- reporter.check(EXPECTED[name] == actual[name],
+ reporter.check(EXPECTED[name].lower() == actual[name].lower(),
None,
'Color mis-match for label {0} in {1}: expected {2}, found {3}',
name, repo_url, EXPECTED[name], actual[name])
@@ -143,13 +157,15 @@ def get_labels(repo_url):
"""
m = P_REPO_URL.match(repo_url)
- require(m, 'repository URL {0} does not match expected pattern'.format(repo_url))
+ require(
+ m, 'repository URL {0} does not match expected pattern'.format(repo_url))
username = m.group(1)
require(username, 'empty username in repository URL {0}'.format(repo_url))
project_name = m.group(2)
- require(username, 'empty project name in repository URL {0}'.format(repo_url))
+ require(
+ username, 'empty project name in repository URL {0}'.format(repo_url))
url = F_API_URL.format(username, project_name)
r = requests.get(url)
diff --git a/bin/test_lesson_check.py b/bin/test_lesson_check.py
index 743d0cf..960059e 100755
--- a/bin/test_lesson_check.py
+++ b/bin/test_lesson_check.py
@@ -1,11 +1,14 @@
+#!/usr/bin/env python3
+
import unittest
import lesson_check
import util
+
class TestFileList(unittest.TestCase):
def setUp(self):
- self.reporter = util.Reporter() ## TODO: refactor reporter class.
+ self.reporter = util.Reporter() # TODO: refactor reporter class.
def test_file_list_has_expected_entries(self):
# For first pass, simply assume that all required files are present
@@ -15,5 +18,6 @@ def test_file_list_has_expected_entries(self):
lesson_check.check_fileset('', self.reporter, all_filenames)
self.assertEqual(len(self.reporter.messages), 0)
+
if __name__ == "__main__":
unittest.main()
diff --git a/bin/util.py b/bin/util.py
index 0cc8de6..f9dc12f 100644
--- a/bin/util.py
+++ b/bin/util.py
@@ -1,4 +1,3 @@
-from __future__ import print_function
import sys
import os
import json
@@ -29,16 +28,14 @@
# (Can't use 'None' because that might be a legitimate value.)
REPORTER_NOT_SET = []
-class Reporter(object):
+
+class Reporter:
"""Collect and report errors."""
def __init__(self):
"""Constructor."""
-
- super(Reporter, self).__init__()
self.messages = []
-
def check_field(self, filename, name, values, key, expected=REPORTER_NOT_SET):
"""Check that a dictionary has an expected value."""
@@ -48,10 +45,11 @@ def check_field(self, filename, name, values, key, expected=REPORTER_NOT_SET):
pass
elif type(expected) in (tuple, set, list):
if values[key] not in expected:
- self.add(filename, '{0} {1} value {2} is not in {3}', name, key, values[key], expected)
+ self.add(
+ filename, '{0} {1} value {2} is not in {3}', name, key, values[key], expected)
elif values[key] != expected:
- self.add(filename, '{0} {1} is {2} not {3}', name, key, values[key], expected)
-
+ self.add(filename, '{0} {1} is {2} not {3}',
+ name, key, values[key], expected)
def check(self, condition, location, fmt, *args):
"""Append error if condition not met."""
@@ -59,12 +57,36 @@ def check(self, condition, location, fmt, *args):
if not condition:
self.add(location, fmt, *args)
-
def add(self, location, fmt, *args):
"""Append error unilaterally."""
self.messages.append((location, fmt.format(*args)))
+ @staticmethod
+ def pretty(item):
+ location, message = item
+ if isinstance(location, type(None)):
+ return message
+ elif isinstance(location, str):
+ return location + ': ' + message
+ elif isinstance(location, tuple):
+ return '{0}:{1}: '.format(*location) + message
+
+ print('Unknown item "{0}"'.format(item), file=sys.stderr)
+ return NotImplemented
+
+ @staticmethod
+ def key(item):
+ location, message = item
+ if isinstance(location, type(None)):
+ return ('', -1, message)
+ elif isinstance(location, str):
+ return (location, -1, message)
+ elif isinstance(location, tuple):
+ return (location[0], location[1], message)
+
+ print('Unknown item "{0}"'.format(item), file=sys.stderr)
+ return NotImplemented
def report(self, stream=sys.stdout):
"""Report all messages in order."""
@@ -72,30 +94,8 @@ def report(self, stream=sys.stdout):
if not self.messages:
return
- def pretty(item):
- location, message = item
- if isinstance(location, type(None)):
- return message
- elif isinstance(location, str):
- return location + ': ' + message
- elif isinstance(location, tuple):
- return '{0}:{1}: '.format(*location) + message
- else:
- assert False, 'Unknown item "{0}"'.format(item)
-
- def key(item):
- location, message = item
- if isinstance(location, type(None)):
- return ('', -1, message)
- elif isinstance(location, str):
- return (location, -1, message)
- elif isinstance(location, tuple):
- return (location[0], location[1], message)
- else:
- assert False, 'Unknown item "{0}"'.format(item)
-
- for m in sorted(self.messages, key=key):
- print(pretty(m), file=stream)
+ for m in sorted(self.messages, key=self.key):
+ print(self.pretty(m), file=stream)
def read_markdown(parser, path):
@@ -111,11 +111,13 @@ def read_markdown(parser, path):
# Split into lines.
metadata_len = 0 if metadata_raw is None else metadata_raw.count('\n')
- lines = [(metadata_len+i+1, line, len(line)) for (i, line) in enumerate(body.split('\n'))]
+ lines = [(metadata_len+i+1, line, len(line))
+ for (i, line) in enumerate(body.split('\n'))]
# Parse Markdown.
cmd = 'ruby {0}'.format(parser)
- p = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, close_fds=True, universal_newlines=True)
+ p = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE,
+ close_fds=True, universal_newlines=True)
stdout_data, stderr_data = p.communicate(body)
doc = json.loads(stdout_data)
@@ -136,16 +138,16 @@ def split_metadata(path, text):
metadata_raw = None
metadata_yaml = None
- metadata_len = None
pieces = text.split('---', 2)
if len(pieces) == 3:
metadata_raw = pieces[1]
text = pieces[2]
try:
- metadata_yaml = yaml.load(metadata_raw)
+ metadata_yaml = yaml.load(metadata_raw, Loader=yaml.FullLoader)
except yaml.YAMLError as e:
- print('Unable to parse YAML header in {0}:\n{1}'.format(path, e), file=sys.stderr)
+ print('Unable to parse YAML header in {0}:\n{1}'.format(
+ path, e), file=sys.stderr)
sys.exit(1)
return metadata_raw, metadata_yaml, text
@@ -159,9 +161,10 @@ def load_yaml(filename):
try:
with open(filename, 'r') as reader:
- return yaml.load(reader)
- except (yaml.YAMLError, FileNotFoundError) as e:
- print('Unable to load YAML file {0}:\n{1}'.format(filename, e), file=sys.stderr)
+ return yaml.load(reader, Loader=yaml.FullLoader)
+ except (yaml.YAMLError, IOError) as e:
+ print('Unable to load YAML file {0}:\n{1}'.format(
+ filename, e), file=sys.stderr)
sys.exit(1)
diff --git a/bin/workshop_check.py b/bin/workshop_check.py
index d3051bf..0523d0c 100755
--- a/bin/workshop_check.py
+++ b/bin/workshop_check.py
@@ -1,10 +1,10 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
'''Check that a workshop's index.html metadata is valid. See the
docstrings on the checking functions for a summary of the checks.
'''
-from __future__ import print_function
+
import sys
import os
import re
@@ -18,10 +18,10 @@
URL_PATTERN = r'https?://.+'
# Defaults.
-CARPENTRIES = ("dc", "swc")
+CARPENTRIES = ("dc", "swc", "lc", "cp")
DEFAULT_CONTACT_EMAIL = 'admin@software-carpentry.org'
-USAGE = 'Usage: "check-workshop path/to/root/directory"'
+USAGE = 'Usage: "workshop_check.py path/to/root/directory"'
# Country and language codes. Note that codes mean different things: 'ar'
# is 'Arabic' as a language but 'Argentina' as a country.
@@ -91,7 +91,7 @@ def check_layout(layout):
@look_for_fixme
def check_carpentry(layout):
- '''"carpentry" in YAML header must be "dc" or "swc".'''
+ '''"carpentry" in YAML header must be "dc", "swc", "lc", or "cp".'''
return layout in CARPENTRIES
@@ -117,7 +117,7 @@ def check_humandate(date):
and 4-digit year. Examples include 'Feb 18-20, 2025' and 'Feb 18
and 20, 2025'. It may be in languages other than English, but the
month name should be kept short to aid formatting of the main
- Software Carpentry web site.
+ Carpentries web site.
"""
if ',' not in date:
@@ -174,8 +174,8 @@ def check_latitude_longitude(latlng):
try:
lat, lng = latlng.split(',')
lat = float(lat)
- long = float(lng)
- return (-90.0 <= lat <= 90.0) and (-180.0 <= long <= 180.0)
+ lng = float(lng)
+ return (-90.0 <= lat <= 90.0) and (-180.0 <= lng <= 180.0)
except ValueError:
return False
@@ -203,15 +203,22 @@ def check_helpers(helpers):
@look_for_fixme
-def check_email(email):
+def check_emails(emails):
"""
- 'contact' must be a valid email address consisting of characters,
- an '@', and more characters. It should not be the default contact
- email address 'admin@software-carpentry.org'.
+ 'emails' must be a comma-separated list of valid email addresses.
+ The list may be empty. A valid email address consists of characters,
+ an '@', and more characters. It should not contain the default contact
"""
- return bool(re.match(EMAIL_PATTERN, email)) and \
- (email != DEFAULT_CONTACT_EMAIL)
+ # YAML automatically loads list-like strings as lists.
+ if (isinstance(emails, list) and len(emails) >= 0):
+ for email in emails:
+ if ((not bool(re.match(EMAIL_PATTERN, email))) or (email == DEFAULT_CONTACT_EMAIL)):
+ return False
+ else:
+ return False
+
+ return True
def check_eventbrite(eventbrite):
@@ -227,12 +234,12 @@ def check_eventbrite(eventbrite):
@look_for_fixme
-def check_etherpad(etherpad):
+def check_collaborative_notes(collaborative_notes):
"""
- 'etherpad' must be a valid URL.
+ 'collaborative_notes' must be a valid URL.
"""
- return bool(re.match(URL_PATTERN, etherpad))
+ return bool(re.match(URL_PATTERN, collaborative_notes))
@look_for_fixme
@@ -286,13 +293,14 @@ def check_pass(value):
'helper list isn\'t a valid list of format ' +
'["First helper", "Second helper",..]'),
- 'contact': (True, check_email,
- 'contact email invalid or still set to ' +
- '"{0}".'.format(DEFAULT_CONTACT_EMAIL)),
+ 'email': (True, check_emails,
+ 'contact email list isn\'t a valid list of format ' +
+ '["me@example.org", "you@example.org",..] or contains incorrectly formatted email addresses or ' +
+ '"{0}".'.format(DEFAULT_CONTACT_EMAIL)),
'eventbrite': (False, check_eventbrite, 'Eventbrite key appears invalid'),
- 'etherpad': (False, check_etherpad, 'Etherpad URL appears invalid'),
+ 'collaborative_notes': (False, check_collaborative_notes, 'Collaborative Notes URL appears invalid'),
'venue': (False, check_pass, 'venue name not specified'),
@@ -300,10 +308,10 @@ def check_pass(value):
}
# REQUIRED is all required categories.
-REQUIRED = set([k for k in HANDLERS if HANDLERS[k][0]])
+REQUIRED = {k for k in HANDLERS if HANDLERS[k][0]}
# OPTIONAL is all optional categories.
-OPTIONAL = set([k for k in HANDLERS if not HANDLERS[k][0]])
+OPTIONAL = {k for k in HANDLERS if not HANDLERS[k][0]}
def check_blank_lines(reporter, raw):
@@ -311,7 +319,8 @@ def check_blank_lines(reporter, raw):
Blank lines are not allowed in category headers.
"""
- lines = [(i, x) for (i, x) in enumerate(raw.strip().split('\n')) if not x.strip()]
+ lines = [(i, x) for (i, x) in enumerate(
+ raw.strip().split('\n')) if not x.strip()]
reporter.check(not lines,
None,
'Blank line(s) in header: {0}',
@@ -381,7 +390,7 @@ def check_config(reporter, filename):
kind)
carpentry = config.get('carpentry', None)
- reporter.check(carpentry in ('swc', 'dc'),
+ reporter.check(carpentry in ('swc', 'dc', 'lc', 'cp'),
filename,
'Missing or unknown carpentry: {0}',
carpentry)
diff --git a/favicon-dc.ico b/favicon-dc.ico
deleted file mode 100644
index 4937f2e..0000000
Binary files a/favicon-dc.ico and /dev/null differ
diff --git a/favicon-lc.ico b/favicon-lc.ico
deleted file mode 100644
index f4f3c93..0000000
Binary files a/favicon-lc.ico and /dev/null differ
diff --git a/favicon-swc.ico b/favicon-swc.ico
deleted file mode 100644
index 34f80ad..0000000
Binary files a/favicon-swc.ico and /dev/null differ
diff --git a/index.md b/index.md
index fec928b..62aaded 100644
--- a/index.md
+++ b/index.md
@@ -1,6 +1,5 @@
---
layout: lesson
-root: .
---
Version control is the lab notebook of the digital world:
diff --git a/reference.md b/reference.md
index cecb361..7835462 100644
--- a/reference.md
+++ b/reference.md
@@ -1,6 +1,5 @@
---
layout: reference
-permalink: /reference/
---
## A Better Kind of Backup
diff --git a/setup.md b/setup.md
index 2747cf0..87fce20 100644
--- a/setup.md
+++ b/setup.md
@@ -1,7 +1,6 @@
---
layout: page
title: "Setup"
-permalink: /setup/
---
Please use [TortoiseHg](http://tortoisehg.bitbucket.org/) to install Mercurial on Windows,