diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 25fdd00ea..371eea9c7 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -9,19 +9,23 @@ updates:
directory: "/"
schedule:
interval: "daily"
- open-pull-requests-limit: 10
+ open-pull-requests-limit: 999
- package-ecosystem: "npm"
directory: "/csunplugged/"
schedule:
interval: "daily"
- open-pull-requests-limit: 10
+ open-pull-requests-limit: 999
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "daily"
- open-pull-requests-limit: 10
+ open-pull-requests-limit: 999
+ ignore:
+ # Ignore updates to Django package until LTS version
+ - dependency-name: "django"
+ versions: ["4.0.X", "4.1.X", "5.0.X", "5.1.X", "6.0.X", "6.1.X", "7.0.X", "7.1.X"]
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
- open-pull-requests-limit: 10
+ open-pull-requests-limit: 999
diff --git a/.github/workflows/auto-merge-dependency-updates.yaml b/.github/workflows/auto-merge-dependency-updates.yaml
new file mode 100644
index 000000000..53f271eaa
--- /dev/null
+++ b/.github/workflows/auto-merge-dependency-updates.yaml
@@ -0,0 +1,30 @@
+name: Auto-merge minor dependency updates
+
+on: pull_request
+
+permissions:
+ pull-requests: write
+ contents: write
+
+jobs:
+ check-pull-request:
+ runs-on: ubuntu-latest
+ if: ${{ github.actor == 'dependabot[bot]' }}
+ steps:
+ - name: Dependabot metadata
+ id: metadata
+ uses: dependabot/fetch-metadata@v1.3.3
+ with:
+ github-token: "${{ secrets.GITHUB_TOKEN }}"
+ - name: Approve
+ if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor' || steps.metadata.outputs.update-type == 'version-update:semver-patch'}}
+ run: gh pr review --body "Automatic approval for minor dependency update." --approve "$PR_URL"
+ env:
+ PR_URL: ${{github.event.pull_request.html_url}}
+ GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
+ - name: Merge
+ if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor' || steps.metadata.outputs.update-type == 'version-update:semver-patch'}}
+ run: gh pr merge --auto --merge "$PR_URL"
+ env:
+ PR_URL: ${{github.event.pull_request.html_url}}
+ GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
diff --git a/.github/workflows/crowdin-actions.yaml b/.github/workflows/crowdin-actions.yaml
index 859d55ba7..fb25df266 100644
--- a/.github/workflows/crowdin-actions.yaml
+++ b/.github/workflows/crowdin-actions.yaml
@@ -16,15 +16,15 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
- name: Upload or update source files to Crowdin
- uses: crowdin/github-action@1.4.6
+ uses: crowdin/github-action@1.4.12
with:
upload_sources: true
- name: Download Chinese (simplified) translations
- uses: crowdin/github-action@1.4.6
+ uses: crowdin/github-action@1.4.12
with:
upload_sources: false
download_translations: true
@@ -42,7 +42,7 @@ jobs:
config: crowdin.yaml
- name: Download French translations
- uses: crowdin/github-action@1.4.6
+ uses: crowdin/github-action@1.4.12
with:
upload_sources: false
download_translations: true
@@ -60,7 +60,7 @@ jobs:
config: crowdin.yaml
- name: Download German translations
- uses: crowdin/github-action@1.4.6
+ uses: crowdin/github-action@1.4.12
with:
upload_sources: false
download_translations: true
@@ -78,7 +78,7 @@ jobs:
config: crowdin.yaml
- name: Download Indonesian translations
- uses: crowdin/github-action@1.4.6
+ uses: crowdin/github-action@1.4.12
with:
upload_sources: false
download_translations: true
@@ -96,7 +96,7 @@ jobs:
config: crowdin.yaml
- name: Download Italian translations
- uses: crowdin/github-action@1.4.6
+ uses: crowdin/github-action@1.4.12
with:
upload_sources: false
download_translations: true
@@ -114,7 +114,7 @@ jobs:
config: crowdin.yaml
- name: Download Te Reo Māori translations
- uses: crowdin/github-action@1.4.6
+ uses: crowdin/github-action@1.4.12
with:
upload_sources: false
download_translations: true
@@ -132,7 +132,7 @@ jobs:
config: crowdin.yaml
- name: Download Turkish translations
- uses: crowdin/github-action@1.4.6
+ uses: crowdin/github-action@1.4.12
with:
upload_sources: false
download_translations: true
diff --git a/.github/workflows/test-and-deploy.yaml b/.github/workflows/test-and-deploy.yaml
index 4aac92b0f..d777b275f 100644
--- a/.github/workflows/test-and-deploy.yaml
+++ b/.github/workflows/test-and-deploy.yaml
@@ -16,70 +16,91 @@ jobs:
runs-on: ubuntu-20.04
steps:
- name: Checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
- name: Create Docker network
- run: docker network create uccser-development-proxy
+ run: docker network create uccser-development-stack
- name: Start systems
- run: docker-compose -f docker-compose.local.yml up -d
+ run: docker compose -f docker-compose.local.yml up -d
- name: Run Django system check
- run: docker-compose -f docker-compose.local.yml run --rm django python ./manage.py check --fail-level WARNING
+ run: docker compose -f docker-compose.local.yml run --rm django python ./manage.py check --fail-level WARNING
test-content:
name: Tests - Content
runs-on: ubuntu-20.04
steps:
- name: Checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
- name: Create Docker network
- run: docker network create uccser-development-proxy
+ run: docker network create uccser-development-stack
- name: Start systems
- run: docker-compose -f docker-compose.local.yml up -d
+ run: docker compose -f docker-compose.local.yml up -d
- name: Create static files
- run: docker-compose -f docker-compose.local.yml run --rm --user="root" node npm run generate-assets
+ run: docker compose -f docker-compose.local.yml run --rm --user="root" node npm run generate-assets
- name: Collect static files
- run: docker-compose -f docker-compose.local.yml run --rm --user="root" django python ./manage.py collectstatic --no-input
+ run: docker compose -f docker-compose.local.yml run --rm --user="root" django python ./manage.py collectstatic --no-input
- name: Migrate database
- run: docker-compose -f docker-compose.local.yml run --rm django python ./manage.py migrate
+ run: docker compose -f docker-compose.local.yml run --rm django python ./manage.py migrate
- name: Load content
- run: docker-compose -f docker-compose.local.yml run --rm django python ./manage.py updatedata
+ run: docker compose -f docker-compose.local.yml run --rm django python ./manage.py updatedata
- name: Make resource thumbnails
- run: docker-compose -f docker-compose.local.yml run --rm --user="root" django python ./manage.py makeresourcethumbnails
+ run: docker compose -f docker-compose.local.yml run --rm --user="root" django python ./manage.py makeresourcethumbnails
- name: Collect static files
- run: docker-compose -f docker-compose.local.yml run --rm --user="root" django python ./manage.py collectstatic --no-input
+ run: docker compose -f docker-compose.local.yml run --rm --user="root" django python ./manage.py collectstatic --no-input
test-general:
name: Tests - General
runs-on: ubuntu-20.04
steps:
- name: Checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
- name: Run general tests
run: ./dev ci test_general
+ - name: Create coverage file
+ run: docker compose -f docker-compose.local.yml run --rm --user="root" django coverage xml -i
+ - name: Upload coverage file
+ uses: codecov/codecov-action@v3
+ with:
+ files: ./csunplugged/coverage.xml
+ verbose: true
test-resources:
name: Tests - Resources
runs-on: ubuntu-20.04
steps:
- name: Checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
- name: Run resource tests
run: ./dev ci test_resources
+ - name: Create coverage file
+ run: docker compose -f docker-compose.local.yml run --rm --user="root" django coverage xml -i
+ - name: Upload coverage file
+ uses: codecov/codecov-action@v3
+ with:
+ files: ./csunplugged/coverage.xml
+ verbose: true
test-management:
name: Tests - Management
runs-on: ubuntu-20.04
steps:
- name: Checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
- name: Run management tests
run: ./dev ci test_management
+ - name: Create coverage file
+ run: docker compose -f docker-compose.local.yml run --rm --user="root" django coverage xml -i
+ - name: Upload coverage file
+ uses: codecov/codecov-action@v3
+ with:
+ files: ./csunplugged/coverage.xml
+ verbose: true
test-style:
name: Tests - Style
runs-on: ubuntu-20.04
steps:
- name: Checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
- name: Run style tests
run: ./dev ci style
@@ -88,7 +109,7 @@ jobs:
runs-on: ubuntu-20.04
steps:
- name: Checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v2
with:
@@ -118,25 +139,25 @@ jobs:
]
steps:
- name: Checkout repository
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
- name: Create Docker network
- run: docker network create uccser-development-proxy
+ run: docker network create uccser-development-stack
- name: Start system
- run: docker-compose -f docker-compose.local.yml up -d
+ run: docker compose -f docker-compose.local.yml up -d
- name: Create production static files
- run: docker-compose -f docker-compose.local.yml run --rm --user="root" node npm run build
+ run: docker compose -f docker-compose.local.yml run --rm --user="root" node npm run build
- name: Collect staticfiles
- run: docker-compose -f docker-compose.local.yml run --rm --user="root" django python manage.py collectstatic --no-input
+ run: docker compose -f docker-compose.local.yml run --rm --user="root" django python manage.py collectstatic --no-input
- name: Archive static files
run: tar czf static-files.tar.gz --directory csunplugged/staticfiles/ .
- name: Upload artifact
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v3
with:
name: static-files
path: static-files.tar.gz
@@ -160,36 +181,97 @@ jobs:
]
steps:
- name: Checkout repository
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
- name: Create Docker network
- run: docker network create uccser-development-proxy
+ run: docker network create uccser-development-stack
- name: Start system
- run: docker-compose -f docker-compose.local.yml up -d
+ run: docker compose -f docker-compose.local.yml up -d
- name: Create production static files
- run: docker-compose -f docker-compose.local.yml run --rm --user="root" node npm run build
+ run: docker compose -f docker-compose.local.yml run --rm --user="root" node npm run build
- name: Migrate database
- run: docker-compose -f docker-compose.local.yml run --rm django python ./manage.py migrate
+ run: docker compose -f docker-compose.local.yml run --rm django python ./manage.py migrate
- name: Load resources
- run: docker-compose -f docker-compose.local.yml run --rm django python ./manage.py loadresources
+ run: docker compose -f docker-compose.local.yml run --rm django python ./manage.py loadresources
- name: Make resource thumbnails
- run: docker-compose -f docker-compose.local.yml run --rm --user="root" django python ./manage.py makeresourcethumbnails --all-languages
+ run: docker compose -f docker-compose.local.yml run --rm --user="root" django python ./manage.py makeresourcethumbnails --all-languages
- name: Archive static files
run: tar czf resource-thumbnails.tar.gz --directory csunplugged/build/img/resources/ .
- name: Upload artifact
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v3
with:
name: resource-thumbnails
path: resource-thumbnails.tar.gz
retention-days: 3
+ create-at-a-distance-files:
+ name: Create at a distance files
+ if: |
+ (github.ref == 'refs/heads/develop')
+ || startsWith(github.ref, 'refs/heads/research-study-')
+ || github.event_name == 'release'
+ runs-on: ubuntu-20.04
+ strategy:
+ matrix:
+ language: [
+ 'en',
+ ]
+ fail-fast: true
+
+ needs: [
+ test-django-system-check,
+ test-content,
+ test-general,
+ test-resources,
+ test-management,
+ test-style,
+ test-docs
+ ]
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - name: Create Docker network
+ run: docker network create uccser-development-stack
+
+ - name: Start system
+ run: docker compose -f docker-compose.local.yml up -d
+
+ - name: Create standard static files
+ run: docker compose -f docker-compose.local.yml run --rm --user="root" node npm run generate-assets
+
+ - name: Collect staticfiles
+ run: docker compose -f docker-compose.local.yml run --rm --user="root" django python manage.py collectstatic --no-input
+
+ - name: Migrate database
+ run: docker compose -f docker-compose.local.yml run --rm django python ./manage.py migrate
+
+ - name: Load at a distance data
+ run: docker compose -f docker-compose.local.yml run --rm django python ./manage.py load_at_a_distance_data
+
+ - name: Run decktape script
+ run: docker compose -f docker-compose.local.yml run --rm --user="root" decktape --language ${{ matrix.language }}
+
+ - name: Create speaker notes PDFs
+ run: docker compose -f docker-compose.local.yml run --rm --user="root" django python ./manage.py create_lesson_speaker_notes_pdfs --language ${{ matrix.language }}
+
+ - name: Archive static files
+ run: tar czf at-a-distance-files.tar.gz --directory csunplugged/build/slides/ .
+
+ - name: Upload artifact
+ uses: actions/upload-artifact@v3
+ with:
+ name: at-a-distance-files-${{ matrix.language }}
+ path: at-a-distance-files.tar.gz
+ retention-days: 3
+
create-resources:
name: Create resources
if: |
@@ -219,31 +301,31 @@ jobs:
]
steps:
- name: Checkout repository
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
- name: Create Docker network
- run: docker network create uccser-development-proxy
+ run: docker network create uccser-development-stack
- name: Start system
- run: docker-compose -f docker-compose.local.yml up -d
+ run: docker compose -f docker-compose.local.yml up -d
- name: Create production static files
- run: docker-compose -f docker-compose.local.yml run --rm --user="root" node npm run build
+ run: docker compose -f docker-compose.local.yml run --rm --user="root" node npm run build
- name: Migrate database
- run: docker-compose -f docker-compose.local.yml run --rm django python ./manage.py migrate
+ run: docker compose -f docker-compose.local.yml run --rm django python ./manage.py migrate
- name: Load resources
- run: docker-compose -f docker-compose.local.yml run --rm django python ./manage.py loadresources
+ run: docker compose -f docker-compose.local.yml run --rm django python ./manage.py loadresources
- name: Make resources
- run: docker-compose -f docker-compose.local.yml run --rm --user="root" django python ./manage.py makeresources --language ${{ matrix.language }}
+ run: docker compose -f docker-compose.local.yml run --rm --user="root" django python ./manage.py makeresources --language ${{ matrix.language }}
- name: Archive static files
run: tar czf resources.tar.gz --directory csunplugged/build/resources/ .
- name: Upload artifact
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v3
with:
name: resources-${{ matrix.language }}
path: resources.tar.gz
@@ -260,13 +342,14 @@ jobs:
create-static-files,
create-resource-thumbnails,
create-resources,
+ create-at-a-distance-files,
]
steps:
- name: Checkout repository
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
- name: Download all workflow run artifacts
- uses: actions/download-artifact@v2
+ uses: actions/download-artifact@v3
with:
path: artifacts/
@@ -281,9 +364,11 @@ jobs:
tar -xz --file artifacts/resource-thumbnails/resource-thumbnails.tar.gz --directory csunplugged/staticfiles/img/resources
mkdir -p csunplugged/staticfiles/resources
ls artifacts/resources-*/resources.tar.gz | xargs -n1 tar -xz --directory csunplugged/staticfiles/resources --file
+ mkdir -p csunplugged/staticfiles/slides
+ ls artifacts/at-a-distance-files-*/at-a-distance-files.tar.gz | xargs -n1 tar -xz --directory csunplugged/staticfiles/slides --file
- name: Log in to the Container registry
- uses: docker/login-action@v1.12.0
+ uses: docker/login-action@v2.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -291,7 +376,7 @@ jobs:
- name: Setup Docker metadata
id: meta
- uses: docker/metadata-action@v3
+ uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
diff --git a/LICENCE b/LICENCE
index a472b2d64..08c45d33b 100644
--- a/LICENCE
+++ b/LICENCE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2016-2017 University of Canterbury Computer Science Education Research Group
+Copyright (c) 2016-2022 University of Canterbury Computer Science Education Research Group
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/LICENCE-THIRD-PARTY b/LICENCE-THIRD-PARTY
index d2d64d2aa..8a5cb2a70 100644
--- a/LICENCE-THIRD-PARTY
+++ b/LICENCE-THIRD-PARTY
@@ -31,6 +31,17 @@ Licensed under Apache License Version 2.0 License.
third-party-licenses/coverage.txt
==============================================================================
+==============================================================================
+CS Unplugged Headings
+------------------------------------------------------------------------------
+https://github.com/uccser/cs-unplugged-font
+Copyright (c) 2008, Haley Fiege (haley@kingdomofawesome.com)
+Copyright (c) 2012, Brenda Gallo (gbrenda1987@gmail.com)
+Copyright (c) 2013, Pablo Impallari (www.impallari.com|impallari@gmail.com)
+Copyright (c) 2022, University of Canterbury Computer Science Education Research Group (https://github.com/uccser/cs-unplugged-font|csse-education-research@canterbury.ac.nz)
+Licensed under SIL Open Font License, Version 1.1.
+==============================================================================
+
==============================================================================
details-element-polyfill
------------------------------------------------------------------------------
@@ -261,16 +272,6 @@ Licensed under MIT License.
third-party-licenses/pyyaml.txt
==============================================================================
-==============================================================================
-Sniglet
-------------------------------------------------------------------------------
-https://fonts.google.com/specimen/Sniglet
-https://github.com/theleagueof/sniglet
-Copyright (c) 2008 Haley Fiege
-Licensed under SIL Open Font License, Version 1.1.
-third-party-licenses/sniglet.txt
-==============================================================================
-
==============================================================================
Sphinx
------------------------------------------------------------------------------
diff --git a/crowdin.yaml b/crowdin.yaml
index cbbe15d03..ea2627aa0 100644
--- a/crowdin.yaml
+++ b/crowdin.yaml
@@ -10,7 +10,6 @@ files: [
two_letters_code: {
zh-CN: zh_Hans,
zh-TW: zh_Hant,
- en-UD: xx_LR,
}
}
},
@@ -21,7 +20,6 @@ files: [
two_letters_code: {
zh-CN: zh_Hans,
zh-TW: zh_Hant,
- en-UD: xx_LR,
}
}
},
@@ -32,7 +30,6 @@ files: [
two_letters_code: {
zh-CN: zh_Hans,
zh-TW: zh_Hant,
- en-UD: xx_LR,
}
}
},
@@ -43,7 +40,6 @@ files: [
two_letters_code: {
zh-CN: zh_Hans,
zh-TW: zh_Hant,
- en-UD: xx_LR,
}
}
},
@@ -54,7 +50,6 @@ files: [
two_letters_code: {
zh-CN: zh_Hans,
zh-TW: zh_Hant,
- en-UD: xx_LR,
}
}
},
diff --git a/csunplugged/.coveragerc b/csunplugged/.coveragerc
index 952af223b..27883a0cd 100644
--- a/csunplugged/.coveragerc
+++ b/csunplugged/.coveragerc
@@ -1,25 +1,12 @@
[run]
branch = True
-source =
- classic
- config
- general
- locale
- resources
- search
- static
- templates
- topics
- utils
- plugging_it_in
+source = .
omit =
# Omit migration files
*/migrations/*
- # Omit settings files for local and production environments
- # TODO: Add integration tests for local and production environments
- */config/settings/*
# Omit pregenerated files
- */config/wsgi.py
+ config/wsgi.py
+ manage.py
[report]
fail_under=20
diff --git a/csunplugged/at_a_distance/__init__.py b/csunplugged/at_a_distance/__init__.py
new file mode 100644
index 000000000..cf6819cd4
--- /dev/null
+++ b/csunplugged/at_a_distance/__init__.py
@@ -0,0 +1 @@
+"""Module for the at a distance section of the CS Unplugged website."""
diff --git a/csunplugged/at_a_distance/apps.py b/csunplugged/at_a_distance/apps.py
new file mode 100644
index 000000000..ff911f120
--- /dev/null
+++ b/csunplugged/at_a_distance/apps.py
@@ -0,0 +1,9 @@
+"""Application configuration for the at a distance application."""
+
+from django.apps import AppConfig
+
+
+class AtADistanceConfig(AppConfig):
+ """Configuration object for the at a distance application."""
+
+ name = "at_a_distance"
diff --git a/csunplugged/at_a_distance/content/en/algorithms/introduction.md b/csunplugged/at_a_distance/content/en/algorithms/introduction.md
new file mode 100644
index 000000000..cff1cfdc4
--- /dev/null
+++ b/csunplugged/at_a_distance/content/en/algorithms/introduction.md
@@ -0,0 +1,21 @@
+# Algorithms
+
+The term "algorithm" comes up a lot these days - they are fundamental to how we write programs for computers.
+But what is an algorithm?
+They can be hard to define in a satisfying way, so in this activity we will learn by doing - students get to solve a simple problem, and then use that experience to articulate what the algorithm is that they used, and in turn, distinguish an algorithm from a computer program.
+
+## Facilitator preparation
+
+If you aren’t familiar with this activity, review the following here:
+
+- [Watch the video on the introduction page](https://www.csfieldguide.org.nz/en/chapters/algorithms/) of the CS Field Guide for an overview of what an algorithm is.
+- Read the first 5 paragraphs and have a trial run of the High Score boxes interactive in [CS Field Guide: Algorithms, programs and informal instructions](https://www.csfieldguide.org.nz/en/chapters/algorithms/whats-the-big-picture/#algorithms-programs-and-informal-instructions).
+
+### Preparation and teaching resources:
+
+- Read through the [guide on how to deliver this content]('at_a_distance:delivery-guide').
+- Prepare the lesson and practice it in the video conference software that you are going to present with.
+
+### Resources:
+
+- Paper and pen to record first number revealed.
diff --git a/csunplugged/at_a_distance/content/en/algorithms/supporting-resources.yaml b/csunplugged/at_a_distance/content/en/algorithms/supporting-resources.yaml
new file mode 100644
index 000000000..1db8e9fd7
--- /dev/null
+++ b/csunplugged/at_a_distance/content/en/algorithms/supporting-resources.yaml
@@ -0,0 +1,4 @@
+- text: Computer Science Field Guide on Algorithms
+ url: https://www.csfieldguide.org.nz/en/chapters/algorithms/
+- text: High score interactive
+ url: https://www.csfieldguide.org.nz/en/interactives/high-score-boxes/
diff --git a/csunplugged/at_a_distance/content/en/binary-representation/introduction.md b/csunplugged/at_a_distance/content/en/binary-representation/introduction.md
new file mode 100644
index 000000000..a6ee54b7d
--- /dev/null
+++ b/csunplugged/at_a_distance/content/en/binary-representation/introduction.md
@@ -0,0 +1,15 @@
+# Binary Representation
+
+All data on computers is represented as digits - that's why we call them digital devices!
+The digits on computers are incredibly simple - they have just two values (usually written as 0 and 1), because it's cheaper and faster to build devices that way.
+Using this two-digit (i.e. Binary) system is the basis of all data stored on computers, and all data transmitted on the internet.
+The Binary Representation activity explores how it is possible to represent all sorts of things - numbers, letters, dates and more - using such simple digits.
+At the same time, we'll be reinforcing some basic facts from mathematics, and getting a new view of our own decimal digit system.
+
+## Facilitator preparation
+
+- If you aren’t familiar with this unplugged activity, review the [in-person version of the lessons]('topics:topic' 'binary-numbers').
+- Work out your birth month, or a month you want to use, in binary i.e. ‘no, yes, no, yes, no’ for October.
+- Optional: Ask participants to make a set of binary cards as a task prior to the session, so they can experience binary cards first hand.
+- Optional: Instead of using the interactive binary cards, you could use a document reader to demonstrate binary representation.
+ Using a document camera is closer to the way that it would be done physically in a classroom; if you use the online interactive, you should emphasise that in practice it’s good to have a set of cards, and at least hold up a set to your camera to show what the online version is simulating.
diff --git a/csunplugged/at_a_distance/content/en/binary-representation/supporting-resources.yaml b/csunplugged/at_a_distance/content/en/binary-representation/supporting-resources.yaml
new file mode 100644
index 000000000..e17b1ee19
--- /dev/null
+++ b/csunplugged/at_a_distance/content/en/binary-representation/supporting-resources.yaml
@@ -0,0 +1,2 @@
+- text: Binary Cards interactive
+ url: https://www.csfieldguide.org.nz/en/interactives/binary-cards/
diff --git a/csunplugged/at_a_distance/content/en/finite-state-automata/introduction.md b/csunplugged/at_a_distance/content/en/finite-state-automata/introduction.md
new file mode 100644
index 000000000..e6e52c02c
--- /dev/null
+++ b/csunplugged/at_a_distance/content/en/finite-state-automata/introduction.md
@@ -0,0 +1,13 @@
+# Finite State Automata
+
+Finite State Automata (or FSA for short) is a very engaging topic from computer science for students, as it can be presented based on following simple maps.
+Yet it appears in advanced computer science courses because it opens all sorts of possibilities and answers the very idea of what computing is!
+In fact, the FSA is behind some deep philosophical reasoning about the limits of what computers can do.
+For our purposes, it's a fun exercise in reasoning that has some widely used everyday applications.
+
+## Facilitator preparation
+
+- Prepare the lessons and trial them with the video conference software that you are going to present with.
+- If you aren’t familiar with this activity there is a written description in the classic section of the CS Unplugged website called [Finite State Automata - Treasure Hunt](https://classic.csunplugged.org/activities/finite-state-automata/), and you can see it in action in the [Treasure Hunt](https://www.youtube.com/watch?v=8kagtp2gWhU) video (this version is based on a commuting service run by pirates!).
+ A slightly different version based on a commuter train service is in the “Formal Languages” chapter of the Computer Science Field Guide, which is aimed at high school students; you can find it in the section on [finite state automata](https://www.csfieldguide.org.nz/en/chapters/formal-languages/finite-state-automata/).
+- The interactive web page that we use for distance teaching is from here: [Computer Science Field Guide Trainsylvania interactive](https://www.csfieldguide.org.nz/en/interactives/trainsylvania/).
diff --git a/csunplugged/at_a_distance/content/en/finite-state-automata/supporting-resources.yaml b/csunplugged/at_a_distance/content/en/finite-state-automata/supporting-resources.yaml
new file mode 100644
index 000000000..b3a7b850d
--- /dev/null
+++ b/csunplugged/at_a_distance/content/en/finite-state-automata/supporting-resources.yaml
@@ -0,0 +1,8 @@
+- text: Classic CS Unplugged - Treasure Hunt activity
+ url: https://classic.csunplugged.org/activities/finite-state-automata/
+- text: Classic CS Unplugged - Video on FSA
+ url: https://classic.csunplugged.org/videos/#finite-state-automata
+- text: Supporting high school learning - Computer Science Field Guide
+ url: https://csfieldguide.org.nz/en/chapters/formal-languages/finite-state-automata/
+- text: Video supporting high school learning - Regular Languages (especially up to 4:13)
+ url: https://www.youtube.com/watch?v=PK3wL7DXuuw
diff --git a/csunplugged/at_a_distance/content/en/parity-magic/introduction.md b/csunplugged/at_a_distance/content/en/parity-magic/introduction.md
new file mode 100644
index 000000000..93d06fcd9
--- /dev/null
+++ b/csunplugged/at_a_distance/content/en/parity-magic/introduction.md
@@ -0,0 +1,9 @@
+# Parity Magic
+
+This activity involves a 'magic trick' that makes the audience think you have an amazing memory, while at the same time revealing an idea that is used to protect the data we use on digital devices.
+The data stored on a computer and transmitted on the internet uses very vulnerable media - tiny magnetised particles on magnetic disks, small charges of electricity, and rapid pulses of light, so it is very easy for some of the data to change unintentionally, perhaps because of electrical interference or imperfections in the way a device has been made.
+Error control is a clever idea that enables computers to put data back to how it was before it was messed up, usually without the person using the system even knowing that it has happened.
+
+## Facilitator preparation
+
+- If you aren’t familiar with this unplugged activity, you can review the in-person version in the [Error detection and correction topic]('topics:topic' 'error-detection-and-correction').
diff --git a/csunplugged/at_a_distance/content/en/stroop-effect/introduction.md b/csunplugged/at_a_distance/content/en/stroop-effect/introduction.md
new file mode 100644
index 000000000..ab689db8c
--- /dev/null
+++ b/csunplugged/at_a_distance/content/en/stroop-effect/introduction.md
@@ -0,0 +1,11 @@
+# The Stroop Effect
+
+Ask someone if they have ever been frustrated using a computer, and you'll probably be given many examples.
+Often this is because computer interfaces have been badly designed, and don't take account of how people think and interact with computers.
+This exercise turns things around, and uses a deliberately bad design to see how much people are slowed down when given clear but hard-to-process instructions.
+It's a lot of fun when you're deliberately tricking people, but it sheds light on how important it is that computer interfaces are created so that the user feels the computer is helping, and not hindering them.
+
+## Facilitator preparation
+
+- If you aren’t familiar with this unplugged activity, you can review the in-person version in Section 9.1 of the [EdX MOOC: Teaching Computational Thinking](https://www.edx.org/course/teaching-computational-thinking).
+- Create your coloured word list in your participants’ first language (if you use English and it’s not their first language, it can be easier to say the colours, and the point is to make it confusing!)
diff --git a/csunplugged/at_a_distance/content/en/stroop-effect/supporting-resources.yaml b/csunplugged/at_a_distance/content/en/stroop-effect/supporting-resources.yaml
new file mode 100644
index 000000000..4e0265ffb
--- /dev/null
+++ b/csunplugged/at_a_distance/content/en/stroop-effect/supporting-resources.yaml
@@ -0,0 +1,4 @@
+- text: Online Course (MOOC) - Teaching Computational Thinking (Section 9)
+ url: https://www.edx.org/course/teaching-computational-thinking
+- text: Computer Science Field Guide - Human Computer Interaction
+ url: https://www.csfieldguide.org.nz/en/chapters/human-computer-interaction/
diff --git a/csunplugged/at_a_distance/content/structure/algorithms/algorithms.yaml b/csunplugged/at_a_distance/content/structure/algorithms/algorithms.yaml
new file mode 100644
index 000000000..06455f3ee
--- /dev/null
+++ b/csunplugged/at_a_distance/content/structure/algorithms/algorithms.yaml
@@ -0,0 +1,4 @@
+# icon: img/at_a_distance/algorithms/algorithms-icon.svg
+suitable-for-teaching-students: suitable
+suitable-for-teaching-educators: suitable
+supporting-resources: true
diff --git a/csunplugged/at_a_distance/content/structure/binary-representation/binary-representation.yaml b/csunplugged/at_a_distance/content/structure/binary-representation/binary-representation.yaml
new file mode 100644
index 000000000..63c72874f
--- /dev/null
+++ b/csunplugged/at_a_distance/content/structure/binary-representation/binary-representation.yaml
@@ -0,0 +1,4 @@
+# icon: img/at_a_distance/binary-representation/binary-representation-icon.svg
+suitable-for-teaching-students: not-recommended
+suitable-for-teaching-educators: suitable
+supporting-resources: true
diff --git a/csunplugged/at_a_distance/content/structure/stroop-effect/stroop-effect.yaml b/csunplugged/at_a_distance/content/structure/stroop-effect/stroop-effect.yaml
new file mode 100644
index 000000000..930221276
--- /dev/null
+++ b/csunplugged/at_a_distance/content/structure/stroop-effect/stroop-effect.yaml
@@ -0,0 +1,4 @@
+# icon: img/at_a_distance/stroop-effect/stroop-effect-icon.svg
+suitable-for-teaching-students: suitable
+suitable-for-teaching-educators: suitable
+supporting-resources: true
diff --git a/csunplugged/at_a_distance/content/structure/structure.yaml b/csunplugged/at_a_distance/content/structure/structure.yaml
new file mode 100644
index 000000000..3b1a95215
--- /dev/null
+++ b/csunplugged/at_a_distance/content/structure/structure.yaml
@@ -0,0 +1,10 @@
+lessons:
+ - stroop-effect
+ - algorithms
+ - binary-representation
+ # - parity-magic
+ # - qr-codes
+ # - product-code-check-digits
+ # - image-representation
+ # - information-theory
+ # - finite-state-automata
diff --git a/csunplugged/at_a_distance/management/__init__.py b/csunplugged/at_a_distance/management/__init__.py
new file mode 100644
index 000000000..40b59d043
--- /dev/null
+++ b/csunplugged/at_a_distance/management/__init__.py
@@ -0,0 +1 @@
+"""Module for the management of the at a distance application."""
diff --git a/csunplugged/at_a_distance/management/commands/_LessonLoader.py b/csunplugged/at_a_distance/management/commands/_LessonLoader.py
new file mode 100644
index 000000000..0737d2f75
--- /dev/null
+++ b/csunplugged/at_a_distance/management/commands/_LessonLoader.py
@@ -0,0 +1,148 @@
+"""Custom loader for loading an at a distance lesson."""
+
+from django.db import transaction
+from utils.TranslatableModelLoader import TranslatableModelLoader
+from utils.check_required_files import find_image_files
+from utils.errors.CouldNotFindYAMLFileError import CouldNotFindYAMLFileError
+from utils.errors.MissingRequiredFieldError import MissingRequiredFieldError
+from utils.errors.InvalidYAMLValueError import InvalidYAMLValueError
+from utils.language_utils import (
+ get_available_languages,
+ get_default_language,
+)
+from at_a_distance.models import Lesson, SupportingResource
+from at_a_distance.settings import (
+ AT_A_DISTANCE_INTRODUCTION_FILENAME,
+ AT_A_DISTANCE_SUPPORTING_RESOURCES_FILENAME,
+)
+
+
+class AtADistanceLessonLoader(TranslatableModelLoader):
+ """Custom loader for loading an lesson."""
+
+ def __init__(self, lesson_number, **kwargs):
+ """Create the loader for loading a lesson.
+
+ Args:
+ lesson_number: Number of the lesson (int).
+ """
+ super().__init__(**kwargs)
+ self.lesson_number = lesson_number
+ self.lesson_slug = self.content_path
+
+ @transaction.atomic
+ def load(self):
+ """Load the content for an at a distance lesson.
+
+ Raise:
+ MissingRequiredFieldError: when no object can be found with the matching attribute.
+ """
+ lesson_structure = self.load_yaml_file(self.structure_file_path)
+
+ lesson_translations = self.get_blank_translation_dictionary()
+
+ icon_path = lesson_structure.get('icon')
+ if icon_path:
+ find_image_files([icon_path], self.structure_file_path)
+
+ # Suitability values
+ suitability_options = [i[0] for i in Lesson.SUITABILITY_CHOICES]
+
+ try:
+ suitable_teaching_students = lesson_structure['suitable-for-teaching-students']
+ except KeyError:
+ raise MissingRequiredFieldError(
+ self.structure_file_path,
+ [
+ "suitable-for-teaching-students",
+ ],
+ "Lesson"
+ )
+ else:
+ if suitable_teaching_students not in suitability_options:
+ raise InvalidYAMLValueError(
+ self.structure_file_path,
+ "suitable-for-teaching-students",
+ suitability_options,
+ )
+
+ try:
+ suitable_teaching_educators = lesson_structure['suitable-for-teaching-educators']
+ except KeyError:
+ raise MissingRequiredFieldError(
+ self.structure_file_path,
+ [
+ "suitable-for-teaching-educators",
+ ],
+ "Lesson"
+ )
+ else:
+ if suitable_teaching_educators not in suitability_options:
+ raise InvalidYAMLValueError(
+ self.structure_file_path,
+ "suitable-for-teaching-educators",
+ suitability_options,
+ )
+
+ # Introduction content
+ content_translations = self.get_markdown_translations(AT_A_DISTANCE_INTRODUCTION_FILENAME)
+ for language, content in content_translations.items():
+ lesson_translations[language]['name'] = content.title
+ lesson_translations[language]['introduction'] = content.html_string
+
+ # Create or update lesson objects and save to the database
+ lesson, created = Lesson.objects.update_or_create(
+ slug=self.lesson_slug,
+ defaults={
+ 'order_number': self.lesson_number,
+ 'icon': icon_path,
+ 'suitable_for_teaching_students': suitable_teaching_students,
+ 'suitable_for_teaching_educators': suitable_teaching_educators,
+ },
+ )
+
+ self.populate_translations(lesson, lesson_translations)
+ self.mark_translation_availability(lesson, required_fields=['name', 'introduction'])
+ lesson.save()
+
+ # Supporting resources
+ lesson.supporting_resources.all().delete()
+ supporting_resources = lesson_structure.get('supporting-resources')
+ if supporting_resources:
+ self.add_supporting_resource_translations(lesson)
+
+ if created:
+ term = 'Created'
+ else:
+ term = 'Updated'
+ self.log(f'{term} At A Distance Lesson: {lesson}')
+
+ def add_supporting_resource_translations(self, lesson):
+ """Get dictionary of translations of supporting resources.
+
+ Returns:
+ Dictionary mapping language codes to HTML.
+
+ Raises:
+ CouldNotFindYAMLFileError: If the requested file could not be found
+ in the /en directory tree
+ """
+ for language in get_available_languages():
+ yaml_file_path = self.get_localised_file(
+ language,
+ AT_A_DISTANCE_SUPPORTING_RESOURCES_FILENAME,
+ )
+ try:
+ supporting_resources = self.load_yaml_file(yaml_file_path)
+ except CouldNotFindYAMLFileError:
+ if language == get_default_language():
+ raise
+ else:
+ for (index, supporting_resource) in enumerate(supporting_resources):
+ SupportingResource.objects.create(
+ order_number=index,
+ text=supporting_resource['text'],
+ url=supporting_resource['url'],
+ language=language,
+ lesson=lesson,
+ )
diff --git a/csunplugged/at_a_distance/management/commands/__init__.py b/csunplugged/at_a_distance/management/commands/__init__.py
new file mode 100644
index 000000000..cd538629a
--- /dev/null
+++ b/csunplugged/at_a_distance/management/commands/__init__.py
@@ -0,0 +1 @@
+"""Module for the custom commands for the at a distance appliation."""
diff --git a/csunplugged/at_a_distance/management/commands/create_lesson_speaker_notes_pdfs.py b/csunplugged/at_a_distance/management/commands/create_lesson_speaker_notes_pdfs.py
new file mode 100644
index 000000000..6b4542623
--- /dev/null
+++ b/csunplugged/at_a_distance/management/commands/create_lesson_speaker_notes_pdfs.py
@@ -0,0 +1,122 @@
+"""Module for the custom Django create_lesson_speaker_notes_pdfs command."""
+
+import os.path
+from django.conf import settings
+from django.core.management.base import BaseCommand
+from at_a_distance.models import Lesson
+from at_a_distance.settings import (
+ AT_A_DISTANCE_FILE_GENERATION_LOCATION,
+ AT_A_DISTANCE_SLIDE_RESOLUTION,
+)
+from at_a_distance.utils import get_lesson_speaker_notes
+from django.template.loader import render_to_string
+
+
+class Command(BaseCommand):
+ """Required command class for the custom Django create_lesson_speaker_notes_pdfs command."""
+
+ help = "Create PDF of lessons."
+
+ def add_arguments(self, parser):
+ """Add optional parameter to create_lesson_speaker_notes_pdfs command."""
+ parser.add_argument(
+ "--language",
+ default=None,
+ dest="language",
+ help="The language to generate speaker notes PDF for.",
+ )
+
+ def handle(self, *args, **options):
+ """Automatically called when the create_lesson_speaker_notes_pdfs command is given."""
+ # If deployed, throw error
+ if settings.DEPLOYED:
+ raise Exception("The 'create_lesson_pdfs' command cannot be run when deployed")
+
+ given_language_parameter = options["language"]
+ if given_language_parameter == 'all':
+ languages = settings.DEFAULT_LANGUAGES
+ print("Generating speaker notes PDF files for all languages")
+ elif given_language_parameter:
+ languages = [(given_language_parameter, "")]
+ print(f"Generating speaker notes PDF files for {given_language_parameter} only")
+ else:
+ languages = [("en", "")]
+ print("Generating speaker notes PDF files for 'en' only")
+
+ for language_code, _ in languages:
+ for lesson in Lesson.objects.all():
+ # Gather speaker notes
+ speaker_notes = get_lesson_speaker_notes(lesson)
+
+ context = dict()
+ slides = []
+ image_directory = os.path.join(
+ AT_A_DISTANCE_FILE_GENERATION_LOCATION,
+ language_code,
+ lesson.slug,
+ )
+ filename = f"{lesson.slug}_{{}}_{AT_A_DISTANCE_SLIDE_RESOLUTION}.png"
+
+ for (slide_number, speaker_note) in enumerate(speaker_notes, start=1):
+ slides.append(
+ {
+ 'number': slide_number,
+ 'image': os.path.join(
+ image_directory,
+ filename.format(slide_number),
+ ),
+ 'notes': speaker_note,
+ }
+ )
+ context["slides"] = slides
+
+ # Render to PDF using Weasyprint
+ (pdf_file, filename) = create_speaker_notes_pdf(lesson, context)
+
+ pdf_directory = os.path.join(
+ AT_A_DISTANCE_FILE_GENERATION_LOCATION,
+ language_code,
+ lesson.slug,
+ )
+ if not os.path.exists(pdf_directory):
+ os.makedirs(pdf_directory)
+
+ filename = f"{filename}.pdf"
+ pdf_file_output = open(os.path.join(pdf_directory, filename), "wb")
+ pdf_file_output.write(pdf_file)
+ pdf_file_output.close()
+ print(f" - Created '{language_code}' speaker notes PDF for {lesson.slug}")
+
+
+def create_speaker_notes_pdf(lesson, context):
+ """Return PDF for speaker notes lesson.
+
+ Args:
+ lesson (Lesson): The lesson the PDF is related too.
+ context (dict): Data for rendering in the PDF.
+
+ Return:
+ PDF file.
+ """
+ # Only import weasyprint when required as production environment
+ # does not have it installed.
+ from weasyprint import HTML, CSS
+
+ filename = f"{lesson.slug}-speaker-notes"
+
+ context["lesson"] = lesson
+ context["filename"] = filename
+
+ pdf_html = render_to_string(
+ "at_a_distance/components/base-speaker-notes-pdf.html",
+ context,
+ )
+ html = HTML(string=pdf_html, base_url=settings.BUILD_ROOT)
+ css_file = os.path.join(
+ settings.BUILD_ROOT,
+ "css/at-a-distance/speaker-notes-pdf.print.css",
+ )
+ css_string = open(css_file, encoding="UTF-8").read()
+ base_css = CSS(string=css_string)
+ pdf = html.write_pdf(stylesheets=[base_css])
+ return (pdf, filename)
diff --git a/csunplugged/at_a_distance/management/commands/load_at_a_distance_data.py b/csunplugged/at_a_distance/management/commands/load_at_a_distance_data.py
new file mode 100644
index 000000000..8c05c15ed
--- /dev/null
+++ b/csunplugged/at_a_distance/management/commands/load_at_a_distance_data.py
@@ -0,0 +1,58 @@
+"""Module for the custom Django load_at_a_distance_data command."""
+
+import os.path
+from django.core.management.base import BaseCommand
+from django.conf import settings
+from utils.BaseLoader import BaseLoader
+from utils.LoaderFactory import LoaderFactory
+from utils.errors.MissingRequiredFieldError import MissingRequiredFieldError
+from utils.errors.InvalidYAMLValueError import InvalidYAMLValueError
+
+
+class Command(BaseCommand):
+ """Required command class for the custom Django load_at_a_distance_data command."""
+
+ help = "Loads load_at_a_distance_data content in database."
+
+ def handle(self, *args, **options):
+ """Automatically called when the load_at_a_distance_data command is given.
+
+ Raise:
+ MissingRequiredFieldError: when no object can be found with the matching
+ attribute.
+ """
+ factory = LoaderFactory()
+
+ # Get structure and content files
+ base_loader = BaseLoader()
+ base_path = settings.AT_A_DISTANCE_CONTENT_BASE_PATH
+
+ structure_file_path = os.path.join(
+ base_path,
+ base_loader.structure_dir,
+ "structure.yaml"
+ )
+
+ structure_file = base_loader.load_yaml_file(structure_file_path)
+
+ if structure_file.get("lessons", None) is None:
+ raise MissingRequiredFieldError(
+ structure_file_path,
+ ["lessons"],
+ "Application Structure"
+ )
+ elif not isinstance(structure_file["lessons"], list):
+ raise InvalidYAMLValueError(
+ structure_file_path,
+ ["lessons"],
+ "list"
+ )
+ else:
+ for lesson_number, lesson_slug in enumerate(structure_file["lessons"]):
+ lesson_structure_file = f"{lesson_slug}.yaml"
+ factory.create_at_a_distance_lesson_loader(
+ lesson_number,
+ base_path=base_path,
+ content_path=lesson_slug,
+ structure_filename=lesson_structure_file,
+ ).load()
diff --git a/csunplugged/at_a_distance/migrations/0001_initial.py b/csunplugged/at_a_distance/migrations/0001_initial.py
new file mode 100644
index 000000000..592414751
--- /dev/null
+++ b/csunplugged/at_a_distance/migrations/0001_initial.py
@@ -0,0 +1,31 @@
+# Generated by Django 3.2.13 on 2022-06-16 00:09
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Lesson',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('languages', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=10), default=list, size=None)),
+ ('slug', models.SlugField(max_length=100)),
+ ('name', models.CharField(default='', max_length=200)),
+ ('icon', models.CharField(max_length=150, null=True)),
+ ('order_number', models.PositiveSmallIntegerField(unique=True)),
+ ('introduction', models.TextField(default='')),
+ ('video', models.URLField(blank=True)),
+ ],
+ options={
+ 'ordering': ['order_number'],
+ },
+ ),
+ ]
diff --git a/csunplugged/at_a_distance/migrations/0002_auto_20220616_0235.py b/csunplugged/at_a_distance/migrations/0002_auto_20220616_0235.py
new file mode 100644
index 000000000..443c61a5a
--- /dev/null
+++ b/csunplugged/at_a_distance/migrations/0002_auto_20220616_0235.py
@@ -0,0 +1,93 @@
+# Generated by Django 3.2.13 on 2022-06-16 02:35
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('at_a_distance', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='lesson',
+ name='introduction_de',
+ field=models.TextField(default='', null=True),
+ ),
+ migrations.AddField(
+ model_name='lesson',
+ name='introduction_en',
+ field=models.TextField(default='', null=True),
+ ),
+ migrations.AddField(
+ model_name='lesson',
+ name='introduction_es',
+ field=models.TextField(default='', null=True),
+ ),
+ migrations.AddField(
+ model_name='lesson',
+ name='introduction_fr',
+ field=models.TextField(default='', null=True),
+ ),
+ migrations.AddField(
+ model_name='lesson',
+ name='introduction_mi',
+ field=models.TextField(default='', null=True),
+ ),
+ migrations.AddField(
+ model_name='lesson',
+ name='introduction_xx_lr',
+ field=models.TextField(default='', null=True),
+ ),
+ migrations.AddField(
+ model_name='lesson',
+ name='introduction_yy_rl',
+ field=models.TextField(default='', null=True),
+ ),
+ migrations.AddField(
+ model_name='lesson',
+ name='introduction_zh_hans',
+ field=models.TextField(default='', null=True),
+ ),
+ migrations.AddField(
+ model_name='lesson',
+ name='name_de',
+ field=models.CharField(default='', max_length=200, null=True),
+ ),
+ migrations.AddField(
+ model_name='lesson',
+ name='name_en',
+ field=models.CharField(default='', max_length=200, null=True),
+ ),
+ migrations.AddField(
+ model_name='lesson',
+ name='name_es',
+ field=models.CharField(default='', max_length=200, null=True),
+ ),
+ migrations.AddField(
+ model_name='lesson',
+ name='name_fr',
+ field=models.CharField(default='', max_length=200, null=True),
+ ),
+ migrations.AddField(
+ model_name='lesson',
+ name='name_mi',
+ field=models.CharField(default='', max_length=200, null=True),
+ ),
+ migrations.AddField(
+ model_name='lesson',
+ name='name_xx_lr',
+ field=models.CharField(default='', max_length=200, null=True),
+ ),
+ migrations.AddField(
+ model_name='lesson',
+ name='name_yy_rl',
+ field=models.CharField(default='', max_length=200, null=True),
+ ),
+ migrations.AddField(
+ model_name='lesson',
+ name='name_zh_hans',
+ field=models.CharField(default='', max_length=200, null=True),
+ ),
+ ]
diff --git a/csunplugged/at_a_distance/migrations/0003_supportingresource.py b/csunplugged/at_a_distance/migrations/0003_supportingresource.py
new file mode 100644
index 000000000..cc4678f05
--- /dev/null
+++ b/csunplugged/at_a_distance/migrations/0003_supportingresource.py
@@ -0,0 +1,28 @@
+# Generated by Django 3.2.13 on 2022-07-12 22:13
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('at_a_distance', '0002_auto_20220616_0235'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='SupportingResource',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('order_number', models.PositiveSmallIntegerField(unique=True)),
+ ('text', models.TextField()),
+ ('url', models.URLField()),
+ ('language', models.CharField(max_length=10)),
+ ('lesson', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='supporting_resources', to='at_a_distance.lesson')),
+ ],
+ options={
+ 'ordering': ['order_number'],
+ },
+ ),
+ ]
diff --git a/csunplugged/at_a_distance/migrations/0004_alter_supportingresource_order_number.py b/csunplugged/at_a_distance/migrations/0004_alter_supportingresource_order_number.py
new file mode 100644
index 000000000..4dff2efee
--- /dev/null
+++ b/csunplugged/at_a_distance/migrations/0004_alter_supportingresource_order_number.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.13 on 2022-07-12 22:51
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('at_a_distance', '0003_supportingresource'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='supportingresource',
+ name='order_number',
+ field=models.PositiveSmallIntegerField(),
+ ),
+ ]
diff --git a/csunplugged/at_a_distance/migrations/0005_auto_20220726_0020.py b/csunplugged/at_a_distance/migrations/0005_auto_20220726_0020.py
new file mode 100644
index 000000000..b1c6f6d0b
--- /dev/null
+++ b/csunplugged/at_a_distance/migrations/0005_auto_20220726_0020.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.2.13 on 2022-07-26 00:20
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('at_a_distance', '0004_alter_supportingresource_order_number'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='lesson',
+ name='suitable_for_teaching_educators',
+ field=models.CharField(choices=[('not-suitable', 'Not suitable'), ('suitable', 'Suitable'), ('not-recommended', 'Not recommended')], default='not-suitable', max_length=15),
+ ),
+ migrations.AddField(
+ model_name='lesson',
+ name='suitable_for_teaching_students',
+ field=models.CharField(choices=[('not-suitable', 'Not suitable'), ('suitable', 'Suitable'), ('not-recommended', 'Not recommended')], default='not-suitable', max_length=15),
+ ),
+ ]
diff --git a/csunplugged/at_a_distance/migrations/0006_alter_lesson_order_number.py b/csunplugged/at_a_distance/migrations/0006_alter_lesson_order_number.py
new file mode 100644
index 000000000..8694ee332
--- /dev/null
+++ b/csunplugged/at_a_distance/migrations/0006_alter_lesson_order_number.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.13 on 2022-07-26 03:03
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('at_a_distance', '0005_auto_20220726_0020'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='lesson',
+ name='order_number',
+ field=models.PositiveSmallIntegerField(),
+ ),
+ ]
diff --git a/csunplugged/at_a_distance/migrations/0007_auto_20220821_2359.py b/csunplugged/at_a_distance/migrations/0007_auto_20220821_2359.py
new file mode 100644
index 000000000..8c9f32320
--- /dev/null
+++ b/csunplugged/at_a_distance/migrations/0007_auto_20220821_2359.py
@@ -0,0 +1,29 @@
+# Generated by Django 3.2.15 on 2022-08-21 23:59
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('at_a_distance', '0006_alter_lesson_order_number'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='lesson',
+ name='introduction_xx_lr',
+ ),
+ migrations.RemoveField(
+ model_name='lesson',
+ name='introduction_yy_rl',
+ ),
+ migrations.RemoveField(
+ model_name='lesson',
+ name='name_xx_lr',
+ ),
+ migrations.RemoveField(
+ model_name='lesson',
+ name='name_yy_rl',
+ ),
+ ]
diff --git a/csunplugged/at_a_distance/migrations/__init__.py b/csunplugged/at_a_distance/migrations/__init__.py
new file mode 100644
index 000000000..7a189655e
--- /dev/null
+++ b/csunplugged/at_a_distance/migrations/__init__.py
@@ -0,0 +1 @@
+"""Module for migrations for the at a distance application."""
diff --git a/csunplugged/at_a_distance/models.py b/csunplugged/at_a_distance/models.py
new file mode 100644
index 000000000..4a83b61a5
--- /dev/null
+++ b/csunplugged/at_a_distance/models.py
@@ -0,0 +1,114 @@
+"""Models for the at a distance application."""
+
+from os.path import join
+from django.urls import reverse
+from django.db import models
+from django.utils.translation import get_language
+from django.utils.translation import ugettext_lazy as _
+from utils.TranslatableModel import TranslatableModel
+from at_a_distance.settings import AT_A_DISTANCE_SLIDES_TEMPLATE_BASE_PATH
+
+
+class LessonManager(models.Manager):
+ """Custom manager for Lesson class."""
+
+ def get_queryset(self):
+ """Prefetch related supporting resources."""
+ return super().get_queryset().prefetch_related('supporting_resources')
+
+
+class Lesson(TranslatableModel):
+ """Model for an at a distance lesson in database."""
+
+ MODEL_NAME = _("At A Disance Lesson")
+
+ slug = models.SlugField(max_length=100)
+ name = models.CharField(max_length=200, default="")
+ order_number = models.PositiveSmallIntegerField()
+ icon = models.CharField(max_length=150, null=True)
+ objects = LessonManager()
+
+ # Suitability attributes
+ NOT_SUITABLE = 'not-suitable'
+ SUITABLE = 'suitable'
+ NOT_RECOMMENDED = 'not-recommended'
+ SUITABILITY_CHOICES = [
+ (NOT_SUITABLE, _('Not suitable')),
+ (SUITABLE, _('Suitable')),
+ (NOT_RECOMMENDED, _('Not recommended')),
+ ]
+ suitable_for_teaching_students = models.CharField(
+ max_length=15,
+ choices=SUITABILITY_CHOICES,
+ default=NOT_SUITABLE,
+ )
+ suitable_for_teaching_educators = models.CharField(
+ max_length=15,
+ choices=SUITABILITY_CHOICES,
+ default=NOT_SUITABLE,
+ )
+
+ # Content
+ introduction = models.TextField(default="")
+ video = models.URLField(blank=True)
+
+ def get_absolute_url(self):
+ """Return the canonical URL for an activity.
+
+ Returns:
+ URL as string.
+ """
+ return reverse("at_a_distance:lesson", kwargs={"lesson_slug": self.slug})
+
+ def get_slides_path(self):
+ """Return the path to the lesson slides."""
+ return join(
+ AT_A_DISTANCE_SLIDES_TEMPLATE_BASE_PATH,
+ f'{self.slug}.html'
+ )
+
+ def __str__(self):
+ """Text representation of a lesson object.
+
+ Returns:
+ Name of a lesson (str).
+ """
+ return self.name
+
+ class Meta:
+ """Set consistent ordering of lessons."""
+
+ ordering = ["order_number", ]
+
+
+class SupportingResourceManager(models.Manager):
+ """Custom manager for SupportingResource class."""
+
+ def get_queryset(self):
+ """Return supporting resources for the current language."""
+ return super().get_queryset().filter(language=get_language())
+
+
+class SupportingResource(models.Model):
+ """Model for a supporting resource to a lesson.
+
+ This model is not translatable as supporting resources may be
+ completely different for each language.
+ """
+
+ order_number = models.PositiveSmallIntegerField()
+ text = models.TextField()
+ url = models.URLField()
+ language = models.CharField(max_length=10)
+ lesson = models.ForeignKey(
+ Lesson,
+ null=True,
+ related_name="supporting_resources",
+ on_delete=models.CASCADE,
+ )
+ objects = SupportingResourceManager()
+
+ class Meta:
+ """Set consistent ordering of supporting resources."""
+
+ ordering = ["order_number", ]
diff --git a/csunplugged/at_a_distance/settings.py b/csunplugged/at_a_distance/settings.py
new file mode 100644
index 000000000..6f1f6eb83
--- /dev/null
+++ b/csunplugged/at_a_distance/settings.py
@@ -0,0 +1,20 @@
+"""Settings for the at a distance application."""
+
+import os.path
+from django.conf import settings
+
+
+AT_A_DISTANCE_FILE_GENERATION_LOCATION = os.path.join(
+ str(settings.ROOT_DIR.path("build")),
+ "slides",
+)
+
+AT_A_DISTANCE_INTRODUCTION_FILENAME = 'introduction.md'
+AT_A_DISTANCE_SUPPORTING_RESOURCES_FILENAME = 'supporting-resources.yaml'
+AT_A_DISTANCE_SLIDES_TEMPLATE_BASE_PATH = 'at_a_distance/lesson-slides'
+
+AT_A_DISTANCE_SLIDE_RESOLUTION_HEIGHT = "1080"
+AT_A_DISTANCE_SLIDE_RESOLUTION_WIDTH = "1920"
+
+# Settings computed from above settings
+AT_A_DISTANCE_SLIDE_RESOLUTION = f'{AT_A_DISTANCE_SLIDE_RESOLUTION_WIDTH}x{AT_A_DISTANCE_SLIDE_RESOLUTION_HEIGHT}'
diff --git a/csunplugged/at_a_distance/translation.py b/csunplugged/at_a_distance/translation.py
new file mode 100644
index 000000000..01771b1b8
--- /dev/null
+++ b/csunplugged/at_a_distance/translation.py
@@ -0,0 +1,23 @@
+"""Translation options for localised models.
+
+Utilised by django-modeltranslation. See http://django-modeltranslation.readthedocs.io
+"""
+
+from modeltranslation.translator import translator, TranslationOptions
+from at_a_distance.models import Lesson
+
+
+class LessonTranslationOptions(TranslationOptions):
+ """Translation options for Activity model."""
+
+ fields = (
+ "name",
+ "introduction",
+ )
+ fallback_undefined = {
+ "name": None,
+ "introduction": None,
+ }
+
+
+translator.register(Lesson, LessonTranslationOptions)
diff --git a/csunplugged/at_a_distance/urls.py b/csunplugged/at_a_distance/urls.py
new file mode 100644
index 000000000..77de23af4
--- /dev/null
+++ b/csunplugged/at_a_distance/urls.py
@@ -0,0 +1,49 @@
+"""URL redirect routing for the at a distance section of the CS Unplugged website."""
+
+from django.urls import path
+from at_a_distance import views
+
+app_name = 'at_a_distance'
+urlpatterns = [
+ # eg: /at-a-distance/
+ path(
+ '',
+ views.IndexView.as_view(),
+ name='index'
+ ),
+ # eg: /at-a-distance/how-to-teach/
+ path(
+ 'delivery-guide/',
+ views.DeliveryGuideView.as_view(),
+ name='delivery-guide'
+ ),
+ # eg: /at-a-distance/speaker-notes/
+ path(
+ 'speaker-notes/',
+ views.LessonSlideSpeakerNotesView.as_view(),
+ name='speaker-notes'
+ ),
+ path(
+ "slides-file-generation-json/",
+ views.slides_file_generation_json,
+ name='slides_file_generation_json'
+ ),
+ # eg: /at-a-distance/stroop-effect/
+ path(
+ '/',
+ views.LessonView.as_view(),
+ name='lesson'
+ ),
+ # eg: /at-a-distance/stroop-effect/slides/
+ path(
+ '/slides/',
+ views.LessonSlidesView.as_view(),
+ name='lesson_slides'
+ ),
+ # eg: /at-a-distance/stroop-effect/slides/
+ path(
+ '/slides-file-generation/',
+ views.LessonFileGenerationView.as_view(),
+ name='lesson_file_generation'
+ ),
+]
diff --git a/csunplugged/at_a_distance/utils.py b/csunplugged/at_a_distance/utils.py
new file mode 100644
index 000000000..0ebe7ce9d
--- /dev/null
+++ b/csunplugged/at_a_distance/utils.py
@@ -0,0 +1,53 @@
+"""Utility functions for the at a distance application."""
+
+from django.template.loader import render_to_string
+from at_a_distance.models import Lesson
+from lxml import html
+
+
+def get_lesson_speaker_notes(lesson):
+ """Get speaker notes for lesson slides suitable for PDF creation.
+
+ Args:
+ lesson (Lesson): Lesson object for rendering HTML.
+
+ Returns:
+ Array of HTML of lesson slides speaker notes.
+ """
+ slide_html = render_to_string(
+ "at_a_distance/components/reveal-slides-structure.html",
+ {
+ "lesson": lesson,
+ }
+ )
+ root = html.fromstring(slide_html)
+ speaker_notes = []
+ slide_elements = root.cssselect('div.slides section')
+ for slide_element in slide_elements:
+ slide_notes = slide_element.cssselect('aside.notes')
+ if slide_notes:
+ speaker_note_html = html.tostring(slide_notes[0], pretty_print=True, encoding='unicode')
+ speaker_notes.append(speaker_note_html)
+ else:
+ speaker_notes.append(False)
+ return speaker_notes
+
+
+def get_slide_lengths():
+ """Return slide lengths for every lesson.
+
+ Returns:
+ Dictionary of lesson slugs mapped to slide counts.
+ """
+ data = dict()
+ for lesson in Lesson.objects.all():
+ slide_html = render_to_string(
+ "at_a_distance/components/reveal-slides-structure.html",
+ {
+ "lesson": lesson,
+ }
+ )
+ root = html.fromstring(slide_html)
+ slide_count = len(root.cssselect('div.slides section'))
+ data[lesson.slug] = slide_count
+ return data
diff --git a/csunplugged/at_a_distance/views.py b/csunplugged/at_a_distance/views.py
new file mode 100644
index 000000000..5e36053a3
--- /dev/null
+++ b/csunplugged/at_a_distance/views.py
@@ -0,0 +1,123 @@
+"""Views for the at a distance application."""
+
+import os.path
+from django.views import generic
+from django.utils import translation
+from django.conf import settings
+from django.http import JsonResponse
+from django.utils.translation import get_language
+from at_a_distance.models import Lesson
+from at_a_distance.settings import AT_A_DISTANCE_SLIDE_RESOLUTION
+from at_a_distance.utils import get_slide_lengths
+
+
+class IndexView(generic.ListView):
+ """View for the at a distance application homepage."""
+
+ template_name = "at_a_distance/index.html"
+ model = Lesson
+ context_object_name = "lessons"
+
+
+class DeliveryGuideView(generic.TemplateView):
+ """View for the devliery guide page."""
+
+ template_name = "at_a_distance/delivery-guide.html"
+
+
+class LessonView(generic.DetailView):
+ """View for a specific lesson."""
+
+ model = Lesson
+ template_name = "at_a_distance/lesson.html"
+ context_object_name = "lesson"
+ slug_url_kwarg = "lesson_slug"
+
+ def get_context_data(self, **kwargs):
+ """Provide the context data for the index view.
+
+ Returns:
+ Dictionary of context data.
+ """
+ # Call the base implementation first to get a context
+ context = super().get_context_data(**kwargs)
+ context['slides_pdf'] = os.path.join(
+ "slides",
+ get_language(),
+ self.object.slug,
+ f"{self.object.slug}-slides.pdf"
+ )
+ context['notes_pdf'] = os.path.join(
+ "slides",
+ get_language(),
+ self.object.slug,
+ f"{self.object.slug}-speaker-notes.pdf"
+ )
+ return context
+
+
+class LessonSlidesView(generic.DetailView):
+ """View for a specific lesson's slides."""
+
+ model = Lesson
+ template_name = "at_a_distance/lesson-slides.html"
+ context_object_name = "lesson"
+ slug_url_kwarg = "lesson_slug"
+
+
+class LessonFileGenerationView(generic.DetailView):
+ """View for generating a specific lesson's files."""
+
+ model = Lesson
+ template_name = "at_a_distance/lesson-slides.html"
+ context_object_name = "lesson"
+ slug_url_kwarg = "lesson_slug"
+
+ def get_context_data(self, **kwargs):
+ """Provide the context data for the index view.
+
+ Returns:
+ Dictionary of context data.
+ """
+ # Call the base implementation first to get a context
+ context = super().get_context_data(**kwargs)
+ context['fragments'] = 'false'
+ context['slide_number'] = 'false'
+ return context
+
+
+class LessonSlideSpeakerNotesView(generic.TemplateView):
+ """View for speaker notes window."""
+
+ template_name = "at_a_distance/reveal-speaker-notes-plugin/speaker-notes-window.html"
+
+
+def slides_file_generation_json(request, **kwargs):
+ """Provide JSON data for creating thumbnails.
+
+ Args:
+ request: The HTTP request.
+
+ Returns:
+ JSON response is sent containing data for thumbnails.
+ """
+ data = dict()
+
+ if request.GET.get("language", False) == "all":
+ languages = settings.DEFAULT_LANGUAGES
+ elif request.GET.get("language", False):
+ languages = [(request.GET.get("language"), "")]
+ else:
+ languages = [("en", "")]
+
+ # For each language{}
+ data["languages"] = dict()
+ for language_code, _ in languages:
+ with translation.override(language_code):
+ data["languages"][language_code] = list(Lesson.translated_objects.values_list('slug', flat=True))
+
+ # Other values
+ data["resolution"] = AT_A_DISTANCE_SLIDE_RESOLUTION
+ data["slide_counts"] = get_slide_lengths()
+
+ return JsonResponse(data, safe=False)
diff --git a/csunplugged/at_home/content/en/binary-challenge/more-information.md b/csunplugged/at_home/content/en/binary-challenge/more-information.md
index 0ee5e173f..2b5dfdf4d 100644
--- a/csunplugged/at_home/content/en/binary-challenge/more-information.md
+++ b/csunplugged/at_home/content/en/binary-challenge/more-information.md
@@ -2,5 +2,5 @@ To see this in action check out these links:
- Here are [some examples of learning about binary digits](https://vimeo.com/342521353) in a similar style to this activity.
- Here’s [binary digits being taught in a classroom](https://vimeo.com/437725275) using larger cards.
-- Here are some [related lesson plans](https://csunplugged.org/en/topics/binary-numbers/unit-plan/) on binary representation.
+- Here are some [related lesson plans]('topics:topic' 'binary-numbers') on binary representation.
- Here’s an [online version of the card activity](https://csfieldguide.org.nz/en/interactives/binary-cards/?digits=5) - it’s easier to set up, but not as much fun as using real cards.
diff --git a/csunplugged/at_home/content/en/kidbots/more-information.md b/csunplugged/at_home/content/en/kidbots/more-information.md
index 0bb8e9024..0895fbd7e 100644
--- a/csunplugged/at_home/content/en/kidbots/more-information.md
+++ b/csunplugged/at_home/content/en/kidbots/more-information.md
@@ -1,4 +1,4 @@
- Often we do this with a third helper, a “tester”, who reads out the instructions to the bot, but in the home version the bot themselves can test the instructions.
[Here](https://player.vimeo.com/video/292222421) is a demonstration of this in action.
-- [Here’s](https://csunplugged.org/en/topics/kidbots/unit-plan/rescue-mission/) a version where the challenge is given as a Rescue Mission.
+- [Here’s]('topics:lesson' 'kidbots' 'rescue-mission') a version where the challenge is given as a Rescue Mission.
- There are online versions that can be used to explore and extend this kind of programming; for example, [Lightbot](https://lightbot.com/), [ScratchJr](https://www.scratchjr.org/) and Bee-Bot for [Android](https://play.google.com/store/apps/details?id=com.tts.beebot&hl=en) and [iOS](https://apps.apple.com/us/app/bee-bot/id500131639).
diff --git a/csunplugged/at_home/content/en/unlocking-the-secret-in-product-codes/challenges.yaml b/csunplugged/at_home/content/en/unlocking-the-secret-in-product-codes/challenges.yaml
index 3c9b1e845..103d8c422 100644
--- a/csunplugged/at_home/content/en/unlocking-the-secret-in-product-codes/challenges.yaml
+++ b/csunplugged/at_home/content/en/unlocking-the-secret-in-product-codes/challenges.yaml
@@ -2,18 +2,18 @@
question: If a product scans with the 13-digit code 9 414942 119252, is it likely to have been scanned correctly? (Type in yes or no).
answer: "no"
2:
- question: What is the check digit that’s missing from this 12-digit product code? 7 19821 37345 _
+ question: What is the check digit that’s missing from this 12-digit product code? 7 19821 37345 _
answer: "2"
3:
question: What is the check digit that’s missing from this 13-digit product code? 9 300652 80460_
answer: "7"
4:
- question: The first digit on this product is smudged - can you work out what it is?
+ question: The first digit on this product is missing - can you work out what it is?
answer: "9"
- image: img/at_home/unlocking-the-secret-in-product-codes/unlocking-the-secret-in-product-codes-challenge-4.jpg
- image_description: A thirteen digit product code with the first digit smudged. The rest of the digits are 400547010059.
+ image: img/at_home/unlocking-the-secret-in-product-codes/unlocking-the-secret-in-product-codes-challenge-4.svg
+ image_description: A thirteen digit product code with the first digit missing. The rest of the digits are 400547010059.
5:
- question: The second digit on this product is smudged - can you work out what it is?
+ question: The second digit on this product is missing - can you work out what it is?
answer: "3"
- image: img/at_home/unlocking-the-secret-in-product-codes/unlocking-the-secret-in-product-codes-challenge-5.jpg
- image_description: A thirteen digit product code with the second digit smudged. The digits are 9, followed by the smudged digit, followed by 00657233860.
+ image: img/at_home/unlocking-the-secret-in-product-codes/unlocking-the-secret-in-product-codes-challenge-5.svg
+ image_description: A thirteen digit product code with the second digit missing. The digits are 9, followed by the smudged digit, followed by 00657233860.
diff --git a/csunplugged/at_home/content/en/unlocking-the-secret-in-product-codes/more-information.md b/csunplugged/at_home/content/en/unlocking-the-secret-in-product-codes/more-information.md
index 84d3913e1..dcb93fd32 100644
--- a/csunplugged/at_home/content/en/unlocking-the-secret-in-product-codes/more-information.md
+++ b/csunplugged/at_home/content/en/unlocking-the-secret-in-product-codes/more-information.md
@@ -1,2 +1,2 @@
- There’s a poster that shows how to do these calculations [here](https://csunplugged.org/en/resources/barcode-checksum-poster/).
-- There are more details about how check digits (and related methods) are used [here](https://csunplugged.org/en/topics/error-detection-and-correction/unit-plan/description/).
+- There are more details about how check digits (and related methods) are used [here]('topics:topic_whats_it_all_about' 'error-detection-and-correction').
diff --git a/csunplugged/at_home/management/commands/_ActivityLoader.py b/csunplugged/at_home/management/commands/_ActivityLoader.py
index 889c18b3d..c6beb660a 100644
--- a/csunplugged/at_home/management/commands/_ActivityLoader.py
+++ b/csunplugged/at_home/management/commands/_ActivityLoader.py
@@ -136,7 +136,6 @@ def load(self):
else:
self.log('Updated Activity: {}'.format(activity.name))
- # structure_filename = os.path.join(unit_plan_file_path)
self.factory.create_challenge_loader(
activity,
base_path=self.base_path,
diff --git a/csunplugged/at_home/migrations/0013_auto_20220520_0506.py b/csunplugged/at_home/migrations/0013_auto_20220520_0506.py
new file mode 100644
index 000000000..527022ff8
--- /dev/null
+++ b/csunplugged/at_home/migrations/0013_auto_20220520_0506.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.2.11 on 2022-05-20 05:06
+
+import django.contrib.postgres.indexes
+import django.contrib.postgres.search
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('at_home', '0012_auto_20210929_2238'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='activity',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(null=True),
+ ),
+ migrations.AddIndex(
+ model_name='activity',
+ index=django.contrib.postgres.indexes.GinIndex(fields=['search_vector'], name='at_home_act_search__0470ab_gin'),
+ ),
+ ]
diff --git a/csunplugged/at_home/migrations/0014_auto_20220821_2359.py b/csunplugged/at_home/migrations/0014_auto_20220821_2359.py
new file mode 100644
index 000000000..f4189dd90
--- /dev/null
+++ b/csunplugged/at_home/migrations/0014_auto_20220821_2359.py
@@ -0,0 +1,85 @@
+# Generated by Django 3.2.15 on 2022-08-21 23:59
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('at_home', '0013_auto_20220520_0506'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='activity',
+ name='activity_extra_information_xx_lr',
+ ),
+ migrations.RemoveField(
+ model_name='activity',
+ name='activity_extra_information_yy_rl',
+ ),
+ migrations.RemoveField(
+ model_name='activity',
+ name='activity_steps_xx_lr',
+ ),
+ migrations.RemoveField(
+ model_name='activity',
+ name='activity_steps_yy_rl',
+ ),
+ migrations.RemoveField(
+ model_name='activity',
+ name='inside_the_computer_xx_lr',
+ ),
+ migrations.RemoveField(
+ model_name='activity',
+ name='inside_the_computer_yy_rl',
+ ),
+ migrations.RemoveField(
+ model_name='activity',
+ name='introduction_xx_lr',
+ ),
+ migrations.RemoveField(
+ model_name='activity',
+ name='introduction_yy_rl',
+ ),
+ migrations.RemoveField(
+ model_name='activity',
+ name='more_information_xx_lr',
+ ),
+ migrations.RemoveField(
+ model_name='activity',
+ name='more_information_yy_rl',
+ ),
+ migrations.RemoveField(
+ model_name='activity',
+ name='name_xx_lr',
+ ),
+ migrations.RemoveField(
+ model_name='activity',
+ name='name_yy_rl',
+ ),
+ migrations.RemoveField(
+ model_name='activity',
+ name='project_xx_lr',
+ ),
+ migrations.RemoveField(
+ model_name='activity',
+ name='project_yy_rl',
+ ),
+ migrations.RemoveField(
+ model_name='challenge',
+ name='answer_xx_lr',
+ ),
+ migrations.RemoveField(
+ model_name='challenge',
+ name='answer_yy_rl',
+ ),
+ migrations.RemoveField(
+ model_name='challenge',
+ name='question_xx_lr',
+ ),
+ migrations.RemoveField(
+ model_name='challenge',
+ name='question_yy_rl',
+ ),
+ ]
diff --git a/csunplugged/at_home/models.py b/csunplugged/at_home/models.py
index b4411353b..7bd5b11d8 100644
--- a/csunplugged/at_home/models.py
+++ b/csunplugged/at_home/models.py
@@ -4,17 +4,20 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
from utils.TranslatableModel import TranslatableModel
+from django.contrib.postgres.search import SearchVectorField
+from django.contrib.postgres.indexes import GinIndex
class Activity(TranslatableModel):
"""Model for an activity in database."""
- MODEL_NAME = _("Activity")
+ MODEL_NAME = _("At Home Activity")
slug = models.SlugField(max_length=100)
name = models.CharField(max_length=150, default="")
icon = models.CharField(max_length=150, null=True)
order_number = models.PositiveSmallIntegerField(unique=True)
+ search_vector = SearchVectorField(null=True)
# The following are stored as HTML from Markdown files
introduction = models.TextField(default="")
inside_the_computer = models.TextField(default="")
@@ -40,10 +43,30 @@ def __str__(self):
"""
return self.name
+ def index_contents(self):
+ """Return dictionary for search indexing.
+
+ Returns:
+ Dictionary of content for search indexing. The dictionary keys
+ are the weightings of content, and the dictionary values
+ are strings of content to index.
+ """
+ text = (
+ self.introduction + self.inside_the_computer +
+ self.project + self.more_information
+ )
+ return {
+ 'A': self.name,
+ 'B': text,
+ }
+
class Meta:
"""Set consistent ordering of activities."""
ordering = ["order_number", ]
+ indexes = [
+ GinIndex(fields=['search_vector'])
+ ]
class Challenge(TranslatableModel):
diff --git a/csunplugged/classic/migrations/0004_auto_20220520_0454.py b/csunplugged/classic/migrations/0004_auto_20220520_0454.py
new file mode 100644
index 000000000..c3994c71f
--- /dev/null
+++ b/csunplugged/classic/migrations/0004_auto_20220520_0454.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.2.11 on 2022-05-20 04:54
+
+import django.contrib.postgres.indexes
+import django.contrib.postgres.search
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('classic', '0003_alter_classicpage_id'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='classicpage',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(null=True),
+ ),
+ migrations.AddIndex(
+ model_name='classicpage',
+ index=django.contrib.postgres.indexes.GinIndex(fields=['search_vector'], name='classic_cla_search__3a2336_gin'),
+ ),
+ ]
diff --git a/csunplugged/classic/models.py b/csunplugged/classic/models.py
index a27eaf391..3ae393dc1 100644
--- a/csunplugged/classic/models.py
+++ b/csunplugged/classic/models.py
@@ -2,17 +2,20 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
+from django.contrib.postgres.search import SearchVectorField
+from django.contrib.postgres.indexes import GinIndex
class ClassicPage(models.Model):
"""Model for Classic CS Unplugged page in database."""
- MODEL_NAME = _("Classic CS Unplugged page")
+ MODEL_NAME = _("Classic CS Unplugged Page")
# Auto-incrementing 'id' field is automatically set by Django
slug = models.SlugField(unique=True)
name = models.CharField(max_length=200)
redirect = models.URLField()
+ search_vector = SearchVectorField(null=True)
def get_absolute_url(self):
"""Return the canonical URL for a ClassicPage.
@@ -29,3 +32,22 @@ def __str__(self):
Name of page (str).
"""
return self.name
+
+ def index_contents(self):
+ """Return dictionary for search indexing.
+
+ Returns:
+ Dictionary of content for search indexing. The dictionary keys
+ are the weightings of content, and the dictionary values
+ are strings of content to index.
+ """
+ return {
+ 'A': self.name,
+ }
+
+ class Meta:
+ """Meta options for model."""
+
+ indexes = [
+ GinIndex(fields=['search_vector'])
+ ]
diff --git a/csunplugged/classic/search_indexes.py b/csunplugged/classic/search_indexes.py
deleted file mode 100644
index cb5a71531..000000000
--- a/csunplugged/classic/search_indexes.py
+++ /dev/null
@@ -1,31 +0,0 @@
-"""Search index for ClassicPage model."""
-
-from haystack import indexes
-from classic.models import ClassicPage
-
-
-class ClassicPageIndex(indexes.SearchIndex, indexes.Indexable):
- """Search index for ClassicPage model."""
-
- text = indexes.CharField(document=True, use_template=True)
-
- def prepare(self, obj):
- """Set boost of ClassicPage model for index.
-
- Args:
- obj (ClassicPage): ClassicPage object.
-
- Returns:
- Dictionary of data.
- """
- data = super(ClassicPageIndex, self).prepare(obj)
- data["_boost"] = 0.6
- return data
-
- def get_model(self):
- """Return the Resource model.
-
- Returns:
- Resource object.
- """
- return ClassicPage
diff --git a/csunplugged/config/__init__.py b/csunplugged/config/__init__.py
index 6d7e8ea0e..23460af86 100644
--- a/csunplugged/config/__init__.py
+++ b/csunplugged/config/__init__.py
@@ -1,3 +1,3 @@
"""Module for Django system configuration."""
-__version__ = "6.5.0"
+__version__ = "7.0.0"
diff --git a/csunplugged/config/settings/base.py b/csunplugged/config/settings/base.py
index c5e69ba0b..1cd219991 100644
--- a/csunplugged/config/settings/base.py
+++ b/csunplugged/config/settings/base.py
@@ -12,11 +12,7 @@
import environ
import os.path
import logging.config
-
-# Add custom languages not provided by Django
import django.conf.locale
-from django.conf import global_settings
-from django.utils.translation import ugettext_lazy as _
# cs-unplugged/csunplugged/config/settings/base.py - 3 = csunplugged/
ROOT_DIR = environ.Path(__file__) - 3
@@ -33,11 +29,10 @@
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
+ # Full text search
"django.contrib.postgres",
-
# Useful template tags
"django.contrib.humanize",
-
# Admin
"django.contrib.admin",
]
@@ -45,8 +40,6 @@
THIRD_PARTY_APPS = [
"corsheaders",
"django_bootstrap_breadcrumbs",
- "haystack",
- "widget_tweaks",
"modeltranslation",
"bidiutils",
]
@@ -61,6 +54,7 @@
"classic.apps.ClassicConfig",
"at_home.apps.AtHomeConfig",
"moocs.apps.MoocsConfig",
+ "at_a_distance.apps.AtADistanceConfig",
]
# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
@@ -120,13 +114,6 @@
# See: https://docs.djangoproject.com/en/dev/ref/settings/#language-code
LANGUAGE_CODE = "en"
-INCONTEXT_L10N_PSEUDOLANGUAGE = "xx-lr"
-INCONTEXT_L10N_PSEUDOLANGUAGE_BIDI = "yy-rl"
-INCONTEXT_L10N_PSEUDOLANGUAGES = (
- INCONTEXT_L10N_PSEUDOLANGUAGE,
- INCONTEXT_L10N_PSEUDOLANGUAGE_BIDI
-)
-
DEFAULT_LANGUAGES = (
("en", "English"),
("de", "Deutsche"),
@@ -146,36 +133,6 @@
'name_local': "Te Reo Māori",
}
}
-
-if env.bool("INCLUDE_INCONTEXT_L10N", False):
- EXTRA_LANGUAGES = [
- (INCONTEXT_L10N_PSEUDOLANGUAGE, "Translation mode"),
- (INCONTEXT_L10N_PSEUDOLANGUAGE_BIDI, "Translation mode (Bi-directional)"),
- ]
-
- EXTRA_LANG_INFO.update({
- INCONTEXT_L10N_PSEUDOLANGUAGE: {
- 'bidi': False,
- 'code': INCONTEXT_L10N_PSEUDOLANGUAGE,
- 'name': "Translation mode",
- 'name_local': _("Translation mode"),
- },
- INCONTEXT_L10N_PSEUDOLANGUAGE_BIDI: {
- 'bidi': True,
- 'code': INCONTEXT_L10N_PSEUDOLANGUAGE_BIDI,
- 'name': "Translation mode (Bi-directional)",
- 'name_local': _("Translation mode (Bi-directional)"),
- }
- })
-
- # Add new languages to the list of all django languages
- global_settings.LANGUAGES = global_settings.LANGUAGES + EXTRA_LANGUAGES
- global_settings.LANGUAGES_BIDI = (global_settings.LANGUAGES_BIDI +
- [INCONTEXT_L10N_PSEUDOLANGUAGE_BIDI.split('-')[0]])
- # Add new languages to the list of languages used for this project
- LANGUAGES += tuple(EXTRA_LANGUAGES)
- LANGUAGES_BIDI = global_settings.LANGUAGES_BIDI
-
django.conf.locale.LANG_INFO.update(EXTRA_LANG_INFO)
# See: https://docs.djangoproject.com/en/dev/ref/settings/#site-id
@@ -223,10 +180,11 @@
"bidiutils.context_processors.bidi",
],
"libraries": {
+ "custom_tags": "config.templatetags.custom_tags",
+ "query_replace": "config.templatetags.query_replace",
+ "read_static_file": "config.templatetags.read_static_file",
"render_html_field": "config.templatetags.render_html_field",
"translate_url": "config.templatetags.translate_url",
- "query_replace": "config.templatetags.query_replace",
- 'custom_tags': 'config.templatetags.custom_tags'
},
},
},
@@ -336,25 +294,11 @@
},
]
-# SEARCH CONFIGURATION
-# ------------------------------------------------------------------------------
-# See: http://django-haystack.readthedocs.io/en/v2.6.0/settings.html
-HAYSTACK_CONNECTIONS = {
- 'default': {
- 'ENGINE': 'haystack.backends.elasticsearch5_backend.Elasticsearch5SearchEngine',
- 'URL': 'elasticsearch:9200',
- 'INDEX_NAME': 'haystack',
- },
-}
-HAYSTACK_SEARCH_RESULTS_PER_PAGE = 10
-
# OTHER SETTINGS
# ------------------------------------------------------------------------------
DEPLOYED = env.bool("DEPLOYED")
GIT_SHA = env("GIT_SHA", default=None)
-if GIT_SHA:
- GIT_SHA = GIT_SHA[:8]
-else:
+if not GIT_SHA:
GIT_SHA = "local development"
PRODUCTION_ENVIRONMENT = False
STAGING_ENVIRONMENT = False
@@ -369,6 +313,7 @@
CLASSIC_PAGES_CONTENT_BASE_PATH = os.path.join(str(ROOT_DIR.path("classic")), "content")
GENERAL_PAGES_CONTENT_BASE_PATH = os.path.join(str(ROOT_DIR.path("general")), "content")
ACTIVITIES_CONTENT_BASE_PATH = os.path.join(str(ROOT_DIR.path("at_home")), "content")
+AT_A_DISTANCE_CONTENT_BASE_PATH = os.path.join(str(ROOT_DIR.path("at_a_distance")), "content")
BREADCRUMBS_TEMPLATE = "django_bootstrap_breadcrumbs/bootstrap4.html"
JOBE_SERVER_URL = "http://jobe"
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
@@ -378,3 +323,5 @@
"http://localhost:8000",
"https://canterbury.ac.nz"
]
+# Used by speaker notes for at a distance slides
+X_FRAME_OPTIONS = "SAMEORIGIN"
diff --git a/csunplugged/config/settings/local.py b/csunplugged/config/settings/local.py
index ea1a277dd..2a9fad5af 100644
--- a/csunplugged/config/settings/local.py
+++ b/csunplugged/config/settings/local.py
@@ -3,7 +3,6 @@
Django settings for local development environment.
- Run in Debug mode
-- Add custom dev application
- Add Django Debug Toolbar
- Add django-extensions
- Use console backend for emails
@@ -63,21 +62,19 @@
INTERNAL_IPS = ["127.0.0.1", "10.0.2.2", ]
ALLOWED_HOSTS = [
- ".canterbury.ac.nz",
- "localhost",
"cs-unplugged.localhost",
- "127.0.0.1",
- "[::1]",
+ "localhost",
+ "django",
]
def show_django_debug_toolbar(request):
- """Show Django Debug Toolbar in every request when running locally.
+ """Show toolbar in request unless parameter is given.
Args:
request: The request object.
"""
- return True
+ return "hide-debug-toolbar" not in request.GET
DEBUG_TOOLBAR_CONFIG = {
@@ -86,7 +83,6 @@ def show_django_debug_toolbar(request):
],
"SHOW_TEMPLATE_CONTEXT": True,
"SHOW_TOOLBAR_CALLBACK": show_django_debug_toolbar,
-
}
# django-extensions
@@ -97,7 +93,9 @@ def show_django_debug_toolbar(request):
# ----------------------------------------------------------------------------
TEST_RUNNER = "django.test.runner.DiscoverRunner"
-
-# Your local stuff: Below this line define 3rd party library settings
-# ----------------------------------------------------------------------------
-INSTALLED_APPS += ["dev.apps.DevConfig"] # noqa: F405
+# LOGGING
+# ------------------------------------------------------------------------------
+# Based off https://lincolnloop.com/blog/django-logging-right-way/
+# Suppress these loggers in local development for less noise in logs
+logging.getLogger('gunicorn.access').handlers = [] # noqa F405
+logging.getLogger('gunicorn.error').handlers = [] # noqa F405
diff --git a/csunplugged/config/settings/production.py b/csunplugged/config/settings/production.py
index b58700af2..60162f74b 100644
--- a/csunplugged/config/settings/production.py
+++ b/csunplugged/config/settings/production.py
@@ -71,4 +71,3 @@
# SECURE_SSL_REDIRECT = env.bool("DJANGO_SECURE_SSL_REDIRECT", default=True) # noqa: F405
# CSRF_COOKIE_SECURE = True
# CSRF_COOKIE_HTTPONLY = True
-# X_FRAME_OPTIONS = "DENY"
diff --git a/csunplugged/config/settings/testing.py b/csunplugged/config/settings/testing.py
index 661e6f7eb..9559f4098 100644
--- a/csunplugged/config/settings/testing.py
+++ b/csunplugged/config/settings/testing.py
@@ -65,7 +65,6 @@
# ----------------------------------------------------------------------------
INSTALLED_APPS += [ # noqa: F405
"test_without_migrations",
- "dev.apps.DevConfig",
# Model for TranslatableModel tests
"tests.utils.translatable_model",
]
diff --git a/csunplugged/config/templatetags/read_static_file.py b/csunplugged/config/templatetags/read_static_file.py
new file mode 100644
index 000000000..d636c3dde
--- /dev/null
+++ b/csunplugged/config/templatetags/read_static_file.py
@@ -0,0 +1,32 @@
+"""Module for the custom read_file template tag."""
+
+import os.path
+from django import template
+from django.conf import settings
+from django.utils.safestring import mark_safe
+
+register = template.Library()
+
+
+@register.simple_tag
+def read_static_file(filepath):
+ """Read file and return contents.
+
+ This tag should not be used on any files provided
+ by users, as they contents are automatically marked
+ as safe.
+
+ Args:
+ filepath (str): File to read.
+
+ Returns:
+ Contents of file.
+ """
+ filepath = settings.STATIC_ROOT + filepath
+
+ if os.path.isfile(filepath):
+ with open(filepath) as file_obj:
+ contents = mark_safe(file_obj.read())
+ else:
+ raise FileNotFoundError(f'No static file found: {filepath}')
+ return contents
diff --git a/csunplugged/config/urls.py b/csunplugged/config/urls.py
index d18383969..44f36ee8a 100644
--- a/csunplugged/config/urls.py
+++ b/csunplugged/config/urls.py
@@ -18,6 +18,7 @@
path('topics/', include('topics.urls', namespace='topics')),
path('resources/', include('resources.urls', namespace='resources')),
path('at-home/', include('at_home.urls', namespace='at_home')),
+ path('at-a-distance/', include('at_a_distance.urls', namespace='at_a_distance')),
path('plugging-it-in/', include('plugging_it_in.urls', namespace='plugging_it_in')),
path('moocs/', include('moocs.urls', namespace='moocs')),
)
@@ -35,9 +36,6 @@
urlpatterns += [
path('__debug__/', include(debug_toolbar.urls)),
]
- urlpatterns += i18n_patterns(
- path('__dev__/', include('dev.urls', namespace='dev')),
- )
# These patterns allows these error pages to be debugged during development.
from django.views import defaults
urlpatterns += [
diff --git a/csunplugged/dev/__init__.py b/csunplugged/dev/__init__.py
deleted file mode 100644
index 2429f52e9..000000000
--- a/csunplugged/dev/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Module for the dev application."""
diff --git a/csunplugged/dev/apps.py b/csunplugged/dev/apps.py
deleted file mode 100644
index 63f2887dd..000000000
--- a/csunplugged/dev/apps.py
+++ /dev/null
@@ -1,9 +0,0 @@
-"""Application configuration for the dev application."""
-
-from django.apps import AppConfig
-
-
-class DevConfig(AppConfig):
- """Configuration object for the dev application."""
-
- name = "dev"
diff --git a/csunplugged/dev/migrations/__init__.py b/csunplugged/dev/migrations/__init__.py
deleted file mode 100644
index e53e5429b..000000000
--- a/csunplugged/dev/migrations/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Module for migrations for the dev application."""
diff --git a/csunplugged/dev/urls.py b/csunplugged/dev/urls.py
deleted file mode 100644
index a9a37ab14..000000000
--- a/csunplugged/dev/urls.py
+++ /dev/null
@@ -1,15 +0,0 @@
-"""URL routing for the dev application."""
-
-from django.urls import path
-
-from . import views
-
-app_name = 'dev'
-urlpatterns = [
- # eg: /dev/
- path(
- '',
- views.IndexView.as_view(),
- name="index"
- ),
-]
diff --git a/csunplugged/dev/views.py b/csunplugged/dev/views.py
deleted file mode 100644
index 941e0ada2..000000000
--- a/csunplugged/dev/views.py
+++ /dev/null
@@ -1,64 +0,0 @@
-"""Views for the dev application."""
-
-from django.views import generic
-from utils.group_lessons_by_age import group_lessons_by_age
-from topics.models import (
- Topic,
- CurriculumArea,
- CurriculumIntegration,
- ProgrammingChallenge,
- ProgrammingChallengeDifficulty,
- ProgrammingChallengeLanguage,
- LearningOutcome,
- GlossaryTerm,
-)
-
-
-class IndexView(generic.TemplateView):
- """View for the dev application homepage."""
-
- template_name = "dev/index.html"
- context_object_name = "all_topics"
-
- def get_context_data(self, **kwargs):
- """Return context for dev homepage.
-
- Returns:
- A dictionary of context data.
- """
- context = super(IndexView, self).get_context_data(**kwargs)
-
- # Get topic, unit plan and lesson lists
- context["topics"] = Topic.objects.order_by("name")
-
- # Build dictionaries for each unit plan and lesson
- for topic in context["topics"]:
- unit_plans = []
- for unit_plan in topic.unit_plans.all():
- unit_plan.grouped_lessons = group_lessons_by_age(unit_plan.lessons.all())
- unit_plans.append(unit_plan)
- topic.units = unit_plans
- topic.integrations = CurriculumIntegration.objects.filter(topic=topic).order_by("number")
- topic.programming_challenges = ProgrammingChallenge.objects.filter(topic=topic).order_by(
- "challenge_set_number", "challenge_number"
- )
-
- # Get curriculum area list
- context["curriculum_areas"] = []
- for parent in CurriculumArea.objects.filter(parent=None).order_by("name"):
- children = list(CurriculumArea.objects.filter(parent=parent).order_by("name"))
- context["curriculum_areas"].append((parent, children))
-
- # Get learning outcome list
- context["learning_outcomes"] = LearningOutcome.objects.all()
-
- # Get learning outcome list
- context["programming_challenge_languages"] = ProgrammingChallengeLanguage.objects.all()
-
- # Get learning outcome list
- context["programming_challenge_difficulties"] = ProgrammingChallengeDifficulty.objects.all()
-
- # Get glossary term list
- context["glossary_terms"] = GlossaryTerm.objects.all().order_by("term")
-
- return context
diff --git a/csunplugged/general/management/commands/updatedata.py b/csunplugged/general/management/commands/updatedata.py
index 91fcffa4c..2985d3d72 100644
--- a/csunplugged/general/management/commands/updatedata.py
+++ b/csunplugged/general/management/commands/updatedata.py
@@ -26,4 +26,5 @@ def handle(self, *args, **options):
management.call_command("loadgeneralpages", lite_load=lite_load)
management.call_command("loadclassicpages", lite_load=lite_load)
management.call_command("loadactivities", lite_load=lite_load)
- management.call_command("rebuild_index", "--noinput")
+ management.call_command("load_at_a_distance_data")
+ management.call_command("rebuild_search_indexes")
diff --git a/csunplugged/general/migrations/0003_auto_20220520_0501.py b/csunplugged/general/migrations/0003_auto_20220520_0501.py
new file mode 100644
index 000000000..ce455555c
--- /dev/null
+++ b/csunplugged/general/migrations/0003_auto_20220520_0501.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.2.11 on 2022-05-20 05:01
+
+import django.contrib.postgres.indexes
+import django.contrib.postgres.search
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('general', '0002_alter_generalpage_id'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='generalpage',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(null=True),
+ ),
+ migrations.AddIndex(
+ model_name='generalpage',
+ index=django.contrib.postgres.indexes.GinIndex(fields=['search_vector'], name='general_gen_search__2e3762_gin'),
+ ),
+ ]
diff --git a/csunplugged/general/models.py b/csunplugged/general/models.py
index 132fa7122..946244070 100644
--- a/csunplugged/general/models.py
+++ b/csunplugged/general/models.py
@@ -3,18 +3,22 @@
from django.db import models
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
+from django.contrib.postgres.search import SearchVectorField
+from django.contrib.postgres.indexes import GinIndex
+from search.utils import get_template_text
class GeneralPage(models.Model):
"""Model for general page in database."""
- MODEL_NAME = _("General page")
+ MODEL_NAME = _("General Page")
# Auto-incrementing 'id' field is automatically set by Django
slug = models.SlugField(unique=True)
name = models.CharField(max_length=100)
template = models.CharField(max_length=100)
url_name = models.CharField(max_length=100)
+ search_vector = SearchVectorField(null=True)
def get_absolute_url(self):
"""Return the canonical URL for a GeneralPage.
@@ -31,3 +35,23 @@ def __str__(self):
Name of page (str).
"""
return self.name
+
+ def index_contents(self):
+ """Return dictionary for search indexing.
+
+ Returns:
+ Dictionary of content for search indexing. The dictionary keys
+ are the weightings of content, and the dictionary values
+ are strings of content to index.
+ """
+ return {
+ 'A': self.name,
+ 'B': get_template_text(self.template),
+ }
+
+ class Meta:
+ """Meta options for model."""
+
+ indexes = [
+ GinIndex(fields=['search_vector'])
+ ]
diff --git a/csunplugged/general/search_indexes.py b/csunplugged/general/search_indexes.py
deleted file mode 100644
index 976d43048..000000000
--- a/csunplugged/general/search_indexes.py
+++ /dev/null
@@ -1,56 +0,0 @@
-"""Search index for GeneralPage model."""
-
-from haystack import indexes
-from lxml.html import fromstring
-from lxml.cssselect import CSSSelector
-from django.template.loader import render_to_string
-from django.template.exceptions import TemplateSyntaxError
-from general.models import GeneralPage
-
-CONTENT_NOT_FOUND_ERROR_MESSAGE = ("General page requires content wrapped in "
- "an element with ID 'general-page-content'")
-
-
-class GeneralPageIndex(indexes.SearchIndex, indexes.Indexable):
- """Search index for GeneralPage model."""
-
- text = indexes.CharField(document=True)
-
- def prepare(self, obj):
- """Set boost of GeneralPage model for index.
-
- Args:
- obj (GeneralPage): GeneralPage object.
-
- Returns:
- Dictionary of data.
- """
- data = super(GeneralPageIndex, self).prepare(obj)
- data["_boost"] = 0.8
- return data
-
- def prepare_text(self, obj):
- """Return text for indexing.
-
- Args:
- obj (GeneralPage): Object for indexing.
-
- Returns:
- String for indexing.
- """
- rendered = render_to_string(obj.template, {"LANGUAGE_CODE": "en"})
- html = fromstring(rendered)
- selector = CSSSelector("#general-page-content")
- try:
- contents = selector(html)[0].text_content()
- except IndexError:
- raise TemplateSyntaxError(CONTENT_NOT_FOUND_ERROR_MESSAGE)
- return contents
-
- def get_model(self):
- """Return the GeneralPage model.
-
- Returns:
- GeneralPage object.
- """
- return GeneralPage
diff --git a/csunplugged/gulpfile.js b/csunplugged/gulpfile.js
index 0471bb91c..b97941896 100644
--- a/csunplugged/gulpfile.js
+++ b/csunplugged/gulpfile.js
@@ -3,7 +3,7 @@
////////////////////////////////
// Gulp and package
-const { src, dest, parallel, series, watch } = require('gulp')
+const { src, dest, parallel, series, watch, lastRun } = require('gulp')
const pjson = require('./package.json')
// Plugins
@@ -14,6 +14,7 @@ const buffer = require('vinyl-buffer');
const c = require('ansi-colors')
const concat = require('gulp-concat')
const cssnano = require('cssnano')
+const dependents = require('gulp-dependents')
const errorHandler = require('gulp-error-handle')
const filter = require('gulp-filter')
const gulpif = require('gulp-if');
@@ -51,6 +52,7 @@ function pathsConfig(appName) {
js_source: `${staticSourceRoot}/js`,
images_source: `${staticSourceRoot}/img`,
files_source: `${staticSourceRoot}/files`,
+ fonts_source: `${staticSourceRoot}/fonts`,
vendor_js_source: [
`${vendorsRoot}/jquery/dist/jquery.js`,
`${vendorsRoot}/popper.js/dist/umd/popper.js`,
@@ -61,10 +63,10 @@ function pathsConfig(appName) {
],
// Output files
css_output: `${staticOutputRoot}/css`,
- fonts_output: `${staticOutputRoot}/fonts`,
images_output: `${staticOutputRoot}/img`,
js_output: `${staticOutputRoot}/js`,
files_output: `${staticOutputRoot}/files`,
+ fonts_output: `${staticOutputRoot}/fonts`,
}
}
@@ -88,6 +90,9 @@ const processCss = [
pixrem(), // add fallbacks for rem units
postcssFlexbugFixes(), // adds flexbox fixes
]
+const printProcessCss = [
+ pixrem(), // add fallbacks for rem units
+]
const minifyCss = [
cssnano({ preset: 'default' }) // minify result
]
@@ -117,8 +122,16 @@ function css() {
}
function scss() {
- return src(`${paths.scss_source}/**/*.scss`)
+ function postcss_callback(file) {
+ if (file.basename.endsWith('.print.css')) {
+ return { plugins: printProcessCss }
+ } else {
+ return { plugins: processCss }
+ }
+ }
+ return src(`${paths.scss_source}/**/*.scss`, { since: lastRun(scss) })
.pipe(errorHandler(catchError))
+ .pipe(dependents())
.pipe(sourcemaps.init())
.pipe(sass({
includePaths: [
@@ -127,7 +140,7 @@ function scss() {
],
sourceComments: !PRODUCTION,
}).on('error', sass.logError))
- .pipe(postcss(processCss))
+ .pipe(postcss(postcss_callback))
.pipe(sourcemaps.write())
.pipe(gulpif(PRODUCTION, postcss(minifyCss))) // Minifies the result
.pipe(dest(paths.css_output))
@@ -139,7 +152,7 @@ function js() {
return src([
`${paths.js_source}/**/*.js`,
`!${paths.js_source}/modules/**/*.js`
- ])
+ ], { since: lastRun(js) })
.pipe(errorHandler(catchError))
.pipe(sourcemaps.init())
.pipe(js_filter)
@@ -175,6 +188,12 @@ function files() {
.pipe(dest(paths.files_output))
}
+// Custom fonts files
+function fonts() {
+ return src(`${paths.fonts_source}/**/*`)
+ .pipe(dest(paths.fonts_output))
+}
+
// Browser sync server for live reload
// TODO: Not yet working
// function initBrowserSync() {
@@ -211,7 +230,8 @@ const generateAssets = parallel(
js,
vendorJs,
img,
- files
+ files,
+ fonts
)
// Set up dev environment
diff --git a/csunplugged/locale/xx_LR/LC_MESSAGES/django.po b/csunplugged/locale/xx_LR/LC_MESSAGES/django.po
deleted file mode 100644
index bd1122c66..000000000
--- a/csunplugged/locale/xx_LR/LC_MESSAGES/django.po
+++ /dev/null
@@ -1,979 +0,0 @@
-msgid ""
-msgstr ""
-"Project-Id-Version: cs-unplugged\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-02-06 12:04+0000\n"
-"PO-Revision-Date: 2018-02-07 10:05-0500\n"
-"Last-Translator: uccser-admin \n"
-"Language-Team: English (upside down)\n"
-"Language: en_UD\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-"X-Generator: crowdin.com\n"
-"X-Crowdin-Project: cs-unplugged\n"
-"X-Crowdin-Language: en-UD\n"
-"X-Crowdin-File: /csunplugged/locale/en/LC_MESSAGES/django.po\n"
-
-msgid "Translation mode"
-msgstr "crwdns18197:0crwdne18197:0"
-
-msgid "Translation mode (Bi-directional)"
-msgstr "crwdns18198:0crwdne18198:0"
-
-msgid "12 digits"
-msgstr "crwdns15756:0crwdne15756:0"
-
-msgid "13 digits"
-msgstr "crwdns15757:0crwdne15757:0"
-
-msgid "Barcode length"
-msgstr "crwdns15755:0crwdne15755:0"
-
-msgid "{} Digit Barcode"
-msgstr "crwdns18199:0crwdne18199:0"
-
-msgid "Separate!"
-msgstr "crwdns18200:0crwdne18200:0"
-
-msgid "Operate!"
-msgstr "crwdns18201:0crwdne18201:0"
-
-msgid "Calculate!"
-msgstr "crwdns18202:0crwdne18202:0"
-
-msgid "Remember that this algorithm uses modulo 10, so we are only interested in the number in the one's column."
-msgstr "crwdns18203:0crwdne18203:0"
-
-msgid "Display Numbers"
-msgstr "crwdns15769:0crwdne15769:0"
-
-msgid "Black on Card Back"
-msgstr "crwdns15766:0crwdne15766:0"
-
-msgid "Yes - Uses a lot of black ink, but conveys clearer card state. Print double sided."
-msgstr "crwdns18204:0crwdne18204:0"
-
-msgid "No - Print single sided."
-msgstr "crwdns18205:0crwdne18205:0"
-
-msgid "Four (1 to 8)"
-msgstr "crwdns15760:0crwdne15760:0"
-
-msgid "Eight (1 to 128)"
-msgstr "crwdns15761:0crwdne15761:0"
-
-msgid "Twelve (1 to 2048)"
-msgstr "crwdns15762:0crwdne15762:0"
-
-msgid "Number of Bits"
-msgstr "crwdns15759:0crwdne15759:0"
-
-msgid "Display Dot Counts"
-msgstr "crwdns15763:0crwdne15763:0"
-
-msgid "Student"
-msgstr "crwdns15772:0crwdne15772:0"
-
-msgid "Teacher (solutions)"
-msgstr "crwdns15773:0crwdne15773:0"
-
-msgid "Worksheet Version"
-msgstr "crwdns15771:0crwdne15771:0"
-
-msgid "Binary (0 or 1)"
-msgstr "crwdns15776:0crwdne15776:0"
-
-msgid "Lightbulb (off or on)"
-msgstr "crwdns15777:0crwdne15777:0"
-
-msgid "Value Representation"
-msgstr "crwdns15775:0crwdne15775:0"
-
-msgid "Hello, I'm a"
-msgstr "crwdns19548:0crwdne19548:0"
-
-msgid "Programmer"
-msgstr "crwdns19549:0crwdne19549:0"
-
-msgid "Tester"
-msgstr "crwdns19550:0crwdne19550:0"
-
-msgid "Bot"
-msgstr "crwdns19551:0crwdne19551:0"
-
-msgid "Modulo"
-msgstr "crwdns15783:0crwdne15783:0"
-
-msgid "Black"
-msgstr "crwdns15787:0crwdne15787:0"
-
-msgid "Blue"
-msgstr "crwdns15788:0crwdne15788:0"
-
-msgid "Green"
-msgstr "crwdns15789:0crwdne15789:0"
-
-msgid "Purple"
-msgstr "crwdns15790:0crwdne15790:0"
-
-msgid "Red"
-msgstr "crwdns15791:0crwdne15791:0"
-
-msgid "Card back colour"
-msgstr "crwdns15786:0crwdne15786:0"
-
-msgid "None"
-msgstr "crwdns15793:0crwdne15793:0"
-
-msgid "Piano keys to highlight"
-msgstr "crwdns15792:0crwdne15792:0"
-
-msgid "Black and White (2 possible binary values)"
-msgstr "crwdns18206:0crwdne18206:0"
-
-msgid "Black and White (2 possible binary values) in Run Length Encoding"
-msgstr "crwdns18207:0crwdne18207:0"
-
-msgid "Greyscale (4 possible binary values)"
-msgstr "crwdns18208:0crwdne18208:0"
-
-msgid "Colour (8 possible binary values)"
-msgstr "crwdns18209:0crwdne18209:0"
-
-msgid "Fish - 6 pages"
-msgstr "crwdns18210:0crwdne18210:0"
-
-msgid "Hot air balloon - 8 pages"
-msgstr "crwdns18211:0crwdne18211:0"
-
-msgid "Boat - 9 pages"
-msgstr "crwdns18212:0crwdne18212:0"
-
-msgid "Parrots - 32 pages"
-msgstr "crwdns18213:0crwdne18213:0"
-
-msgid "Black and White"
-msgstr "crwdns18214:0crwdne18214:0"
-
-msgid "Run length encoding"
-msgstr "crwdns18215:0crwdne18215:0"
-
-msgid "Greyscale"
-msgstr "crwdns18216:0crwdne18216:0"
-
-msgid "Colour"
-msgstr "crwdns18217:0crwdne18217:0"
-
-msgid "Boat"
-msgstr "crwdns18218:0crwdne18218:0"
-
-msgid "Fish"
-msgstr "crwdns18219:0crwdne18219:0"
-
-msgid "Hot air balloon"
-msgstr "crwdns18220:0crwdne18220:0"
-
-msgid "Parrots"
-msgstr "crwdns15729:0crwdne15729:0"
-
-msgid "Colouring type"
-msgstr "crwdns18221:0crwdne18221:0"
-
-msgid "Image"
-msgstr "crwdns18222:0crwdne18222:0"
-
-msgid "Number of cards (15 or 31)"
-msgstr "crwdns15817:0crwdne15817:0"
-
-msgid "0 to 99"
-msgstr "crwdns15818:0crwdne15818:0"
-
-msgid "0 to 999"
-msgstr "crwdns15819:0crwdne15819:0"
-
-msgid "Blank"
-msgstr "crwdns15784:0crwdne15784:0"
-
-msgid "Number of cards"
-msgstr "crwdns15815:0crwdne15815:0"
-
-msgid "Range of numbers"
-msgstr "crwdns15816:0crwdne15816:0"
-
-msgid "Include teacher guide sheet"
-msgstr "crwdns15820:0crwdne15820:0"
-
-msgid "Small numbers (1 to 6)"
-msgstr "crwdns15823:0crwdne15823:0"
-
-msgid "Large numbers (7 digit numbers)"
-msgstr "crwdns15824:0crwdne15824:0"
-
-msgid "Letters"
-msgstr "crwdns15828:0crwdne15828:0"
-
-msgid "Words"
-msgstr "crwdns15827:0crwdne15827:0"
-
-msgid "Fractions"
-msgstr "crwdns15825:0crwdne15825:0"
-
-msgid "Māori colours"
-msgstr "crwdns15829:0crwdne15829:0"
-
-msgid "Māori numbers"
-msgstr "crwdns15826:0crwdne15826:0"
-
-msgid "Butterfly life cycle"
-msgstr "crwdns15831:0crwdne15831:0"
-
-msgid "Little Red Riding Hood"
-msgstr "crwdns15830:0crwdne15830:0"
-
-msgid "Card Type"
-msgstr "crwdns15822:0crwdne15822:0"
-
-msgid "Easy Numbers (1 digits)"
-msgstr "crwdns18223:0crwdne18223:0"
-
-msgid "Medium Numbers (2 digits)"
-msgstr "crwdns15837:0crwdne15837:0"
-
-msgid "Hard Numbers (3 digits)"
-msgstr "crwdns15838:0crwdne15838:0"
-
-msgid "None (Blank - Useful as template) "
-msgstr "crwdns18224:0crwdne18224:0"
-
-msgid "Prefill with Numbers"
-msgstr "crwdns15834:0crwdne15834:0"
-
-msgid "Circular"
-msgstr "crwdns15840:0crwdne15840:0"
-
-msgid "Twisted"
-msgstr "crwdns15841:0crwdne15841:0"
-
-msgid "Train track shape"
-msgstr "crwdns15839:0crwdne15839:0"
-
-msgid "Easy Numbers (2 digits)"
-msgstr "crwdns15842:0crwdne15842:0"
-
-msgid "Medium Numbers (3 digits)"
-msgstr "crwdns15843:0crwdne15843:0"
-
-msgid "Hard Numbers (4 digits)"
-msgstr "crwdns15844:0crwdne15844:0"
-
-msgid "None (Blank)"
-msgstr "crwdns15845:0crwdne15845:0"
-
-msgid "Sorted Numbers"
-msgstr "crwdns15848:0crwdne15848:0"
-
-msgid "Unsorted Numbers"
-msgstr "crwdns15847:0crwdne15847:0"
-
-msgid "Table only - Little ink usage."
-msgstr "crwdns15852:0crwdne15852:0"
-
-msgid "Table and artwork - Uses a lot of colour ink."
-msgstr "crwdns15851:0crwdne15851:0"
-
-msgid "Numbers Unsorted/Sorted"
-msgstr "crwdns15846:0crwdne15846:0"
-
-msgid "Include instruction sheets"
-msgstr "crwdns15849:0crwdne15849:0"
-
-msgid "Printing mode"
-msgstr "crwdns15850:0crwdne15850:0"
-
-msgid "A4"
-msgstr "crwdns15802:0crwdne15802:0"
-
-msgid "US Letter"
-msgstr "crwdns15803:0crwdne15803:0"
-
-msgid "Paper Size"
-msgstr "crwdns15801:0crwdne15801:0"
-
-msgid "Header Text"
-msgstr "crwdns15805:0crwdne15805:0"
-
-msgid "Example School: Room Four"
-msgstr "crwdns18225:0crwdne18225:0"
-
-msgid "Number of Copies"
-msgstr "crwdns15806:0crwdne15806:0"
-
-msgid "Local Generation Only"
-msgstr "crwdns15804:0crwdne15804:0"
-
-msgid "Yes"
-msgstr "crwdns15764:0crwdne15764:0"
-
-msgid "No"
-msgstr "crwdns15765:0crwdne15765:0"
-
-msgid "Page not found"
-msgstr "crwdns20515:0crwdne20515:0"
-
-msgid "We are sorry but we could not find a page for the link you provided."
-msgstr "crwdns20516:0crwdne20516:0"
-
-msgid "You may find one of the links below useful:"
-msgstr "crwdns20517:0crwdne20517:0"
-
-msgid "Home page"
-msgstr "crwdns20518:0crwdne20518:0"
-
-msgid "Search"
-msgstr "crwdns19542:0crwdne19542:0"
-
-msgid "Classic CS Unplugged"
-msgstr "crwdns15722:0crwdne15722:0"
-
-msgid "CS Unplugged"
-msgstr "crwdns15645:0crwdne15645:0"
-
-msgid "Topics"
-msgstr "crwdns15646:0crwdne15646:0"
-
-msgid "Printables"
-msgstr "crwdns19552:0crwdne19552:0"
-
-msgid "About"
-msgstr "crwdns15648:0crwdne15648:0"
-
-msgid "Looking for something for high schools? Check out the Computer Science Field Guide."
-msgstr "crwdns15658:0crwdne15658:0"
-
-msgid "The primary goal of the Unplugged project is to promote Computer Science (and computing in general) to young people as an interesting, engaging, and intellectually stimulating discipline."
-msgstr "crwdns18226:0crwdne18226:0"
-
-#, python-format
-msgid "Read more about our principles here."
-msgstr "crwdns18227:0%(principles_url)scrwdne18227:0"
-
-msgid "Useful Links"
-msgstr "crwdns15649:0crwdne15649:0"
-
-msgid "Community"
-msgstr "crwdns15650:0crwdne15650:0"
-
-msgid "Twitter"
-msgstr "crwdns15651:0crwdne15651:0"
-
-msgid "YouTube"
-msgstr "crwdns15652:0crwdne15652:0"
-
-msgid "GitHub"
-msgstr "crwdns15653:0crwdne15653:0"
-
-msgid "Help"
-msgstr "crwdns15654:0crwdne15654:0"
-
-msgid "Glossary"
-msgstr "crwdns15655:0crwdne15655:0"
-
-msgid "Feedback"
-msgstr "crwdns15656:0crwdne15656:0"
-
-msgid "Contact"
-msgstr "crwdns15657:0crwdne15657:0"
-
-msgid "The CS Unplugged material is open source on GitHub, and this website's content is shared under a Creative Commons Attribution-ShareAlike 4.0 International license. The CS Unplugged is a project by the Computer Science Education Research Group at the University of Canterbury, New Zealand."
-msgstr "crwdns15659:0crwdne15659:0"
-
-#, python-format
-msgid "This definition is not available in %(language)s, sorry!"
-msgstr "crwdns18228:0%(language)scrwdne18228:0"
-
-msgid "Close"
-msgstr "crwdns15660:0crwdne15660:0"
-
-msgid "Unit Plan:"
-msgstr "crwdns15661:0crwdne15661:0"
-
-msgid "CS Unplugged is a collection of free learning activities that teach Computer Science through engaging games and puzzles that use cards, string, crayons and lots of running around. We originally developed this so that young students could dive head-first into Computer Science, experiencing the kinds of questions and challenges that computer scientists experience, but without having to learn programming first."
-msgstr "crwdns15662:0crwdne15662:0"
-
-msgid "The collection was originally intended as a resource for outreach and extension, but with the adoption of computing and computational thinking into many classrooms around the world, it is now widely used for teaching. The material has been used in many contexts outside the classroom as well, including science shows, talks for senior citizens, and special events."
-msgstr "crwdns15663:0crwdne15663:0"
-
-msgid "Thanks to generous sponsorships we have been able to create associated resources such as the videos, which are intended to help teachers see how the activities work (please don’t show them to your classes – let them experience the activities themselves!). All of the activities that we provide are “open source” – they are released under a Creative Commons BY-NC-SA licence, so you can copy, share and modify the material."
-msgstr "crwdns15664:0crwdne15664:0"
-
-#, python-format
-msgid "To view the team of contributors who work on this project, see our people page."
-msgstr "crwdns15665:0%(people_url)scrwdne15665:0"
-
-#, python-format
-msgid "For details on how to contact us, see our contact us page."
-msgstr "crwdns15666:0%(contact_url)scrwdne15666:0"
-
-#, python-format
-msgid "For more information about the principles behind CS Unplugged, see our principles page."
-msgstr "crwdns15667:0%(principles_url)scrwdne15667:0"
-
-msgid "About pages"
-msgstr "crwdns15668:0crwdne15668:0"
-
-msgid "Computational Thinking and CS Unplugged"
-msgstr "crwdns15669:0crwdne15669:0"
-
-msgid "Principles"
-msgstr "crwdns15672:0crwdne15672:0"
-
-msgid "People"
-msgstr "crwdns15670:0crwdne15670:0"
-
-msgid "Contact Us"
-msgstr "crwdns15671:0crwdne15671:0"
-
-msgid "What is Computational Thinking?"
-msgstr "crwdns15673:0crwdne15673:0"
-
-msgid "The world we live in has become a digital one, filled with technology and driven by Computer Science. Software and technology have transformed every subject and job area, from science and medicine, to art history and psychology. Digital technology is ubiquitous. To be informed and empowered citizens, the next generation of students need to understand this digital world that they live in."
-msgstr "crwdns15674:0crwdne15674:0"
-
-msgid "This is why Computational Thinking has been called the ‘21st Century Skill Set’, and is important for everyone to learn. It is critical to understanding how the digital world works, for harnessing the power of computers to solve tough problems, and making great things happen! It also enables us to think critically about not just the benefits of certain technologies, but also the potential harm, ethical implications, or unintended consequences of these."
-msgstr "crwdns15675:0crwdne15675:0"
-
-msgid "But what exactly is Computational Thinking? Let’s have a look at a technical definition..."
-msgstr "crwdns15676:0crwdne15676:0"
-
-msgid "\"Computational Thinking is the thought processes involved in formulating problems and their solutions so that the solutions are represented in a form that can be effectively carried out by an information-processing agent.\""
-msgstr "crwdns15677:0crwdne15677:0"
-
-msgid "Phew, it’s quite a mouthful isn’t it? But, as we like to say at CS Unplugged, it’s just big words for simple ideas! 'Information-processing agent' means anything that follows a set of instructions to complete a task (we call this 'computing'). Most of the time this 'agent' means a computer or other type of digital device - but it could also be a human! We’ll refer to it as a computer to make things a bit simpler. To represent solutions in a way that a computer can carry them out, we have to represent them as a step by step process - an algorithm. To create these algorithmic solutions we apply some special problem solving skills to. These skill are what make up Computational Thinking! And they are skills that are transferrable to any field."
-msgstr "crwdns15678:0crwdne15678:0"
-
-msgid "Computational Thinking could be described as 'thinking like a Computer Scientist', but it is now an important skill for everyone to learn, whether they want to be a Computer Scientist or not! It’s interesting, and important, to note that Computational Thinking, and Computer Science, aren’t entirely about computers, they are more about people. You might think that we write programs for computers, but really we write programs for people - to help them communicate, find information, and solve problems."
-msgstr "crwdns15679:0crwdne15679:0"
-
-msgid "For example, you might use an app on a smartphone to get directions to a friend's house; the app is an example of a computer program, and the smartphone is the \"information processing agent\" that runs the program for us. Whoever designed the algorithm for working out the best route, and all the details like the interface and how to store the map, applied computational thinking to design the system. But they didn't design it for the sake of the smartphone; they designed it to help the person using the smartphone."
-msgstr "crwdns15680:0crwdne15680:0"
-
-msgid "Computational Thinking in CS Unplugged"
-msgstr "crwdns15681:0crwdne15681:0"
-
-msgid "
Throughout the lessons and units in CS Unplugged there are many links to Computational Thinking. Teaching Computational Thinking through CS Unplugged activities teaches students how to:
describe a problem,
identify the important details needed to solve this problem,
break the problem down into small, logical steps,
use these steps to create a process (algorithm) that solves the problem,
and then evaluate this process.
"
-msgstr "crwdns15682:0crwdne15682:0"
-
-msgid "These skills are transferable to any other curriculum area, but are particularly relevant to developing digital systems and solving problems using the capabilities of computers.
"
-msgstr "crwdns15683:0crwdne15683:0"
-
-msgid "These Computational Thinking concepts are all connected to each other and support each other, but it's important to note that not all aspects of Computational Thinking will necessarily happen in every unit or lesson. In each unit and lesson we've highlighted the important connections for you to observe your students in action."
-msgstr "crwdns15684:0crwdne15684:0"
-
-msgid "There are a number of definitions of Computational Thinking, but most have a set of 5 or 6 problem solving skills that Computational Thinking embodies. For the Unplugged project we've identified the following six CT skills that are often mentioned in the literature; they are described below, and at the end of each Unplugged lesson we've identified ways that these skills appeared in the lesson, to help you see the CT connection with the lessons."
-msgstr "crwdns15685:0crwdne15685:0"
-
-msgid "Computational Thinking skills"
-msgstr "crwdns15686:0crwdne15686:0"
-
-msgid "Algorithmic thinking"
-msgstr "crwdns15687:0crwdne15687:0"
-
-msgid "Algorithms are at the heart of Computational Thinking and Computer Science, because in Computer Science the solutions to problems are not simply an answer (e.g. ‘42’, or a fact), they are algorithms. An algorithm is a step-by-step process that solves a problem or completes a task. If you follow the algorithm's steps correctly, you will arrive at a correct solution, even for different inputs. For example, we can use an algorithm to find the shortest route between two locations on a map; the same algorithm can be used for any pair of starting and finishing points, so the solution depends on the input to the algorithm. If we know the algorithm for solving a problem then we can solve that problem easily, whenever we want, without having to think! We can just follow the steps. Computers can’t think for themselves, so they need to be given algorithms to do things."
-msgstr "crwdns15688:0crwdne15688:0"
-
-msgid "Algorithmic thinking is the process of creating algorithms. When we create an algorithm to solve a problem, we call this an algorithmic solution."
-msgstr "crwdns15689:0crwdne15689:0"
-
-msgid "Computational algorithms (the kind that can run on digital devices) have relatively few ingredients because digital devices only have a few types of instruction that they can follow; the main things they can do are receive input, provide output, store values, follow instructions in a sequence, choose between options, and repeat instructions in a loop. Despite how limited this range of instructions is, we've described everything that digital devices can compute, and this is why algorithms are described restricted to these elements."
-msgstr "crwdns15690:0crwdne15690:0"
-
-msgid "Abstraction"
-msgstr "crwdns15691:0crwdne15691:0"
-
-msgid "Abstraction is all about simplifying things to help us manage complexity. It requires identifying what the most important aspects of a problem are and hiding the other specific details that we don’t need to focus on. The important aspects can be used to create a model, or simplified representation, of the original thing we were dealing with. We can then work with this model to solve the problem, rather than having to deal with all the nitty gritty details at once. Computer Scientists often work with multiple levels of abstraction."
-msgstr "crwdns15692:0crwdne15692:0"
-
-msgid "We use abstraction often in our everyday lives, for example when we use maps. Maps show us a simplified version of the world by leaving out unnecessary details, like where every individual tree in a park is, and only keeping the most relevant information the map reader will need, such as roads and street names."
-msgstr "crwdns15693:0crwdne15693:0"
-
-msgid "Digital devices use abstraction all the time; they try to hide as much unnecessary information from the user as possible. For example, let's say you took a nice scenic photo on your last camping trip, and now you want to edit it on your laptop and adjust the colours in it. Normally we could do this by opening a picture editing program, adjusting some colour sliders or maybe choosing a filter. When you do this there are a lot of complicated operations happening which the computer is hiding from you."
-msgstr "crwdns15694:0crwdne15694:0"
-
-msgid "The picture you took is stored on the computer as a big list of pixels, which are each a different colour, and each colour is represented by a set of numbers, and each of these numbers are stored as binary digits! That’s a lot of information. Imagine if when you adjusted the colours you had to go through and look at all the colour values of every pixel and change each and every one of those! That’s what the computer is doing for you, but since you don’t need to know this to accomplish your goal the computer hides this information away."
-msgstr "crwdns15695:0crwdne15695:0"
-
-msgid "Decomposition"
-msgstr "crwdns15696:0crwdne15696:0"
-
-msgid "Decomposition is about breaking down problems into smaller, more manageable, parts, and then focusing on solving each of these smaller problems. We can break a complex problem down until the smaller parts are so simple they become easy to solve. The solutions to each of these smaller, and simpler, problems build up to a solution to the big problem we started with. Decomposition helps make large problems much less intimidating!"
-msgstr "crwdns15697:0crwdne15697:0"
-
-msgid "Decomposition is an important skill for creating algorithms and processes that can be implemented on a computing device, because computers need very specific instructions. They need to be told each of the tiny steps they need to follow in order to do things."
-msgstr "crwdns15698:0crwdne15698:0"
-
-msgid "For example the overall task of making a cake can be decomposed into several smaller tasks, each of which can be performed easily."
-msgstr "crwdns15699:0crwdne15699:0"
-
-msgid "
Make cake
Bake cake
Put ingredients in bowl (butter, sugar, egg, flour)
Mix
Pour into tin
Put in oven for 30mins
Take out of tin
Make icing
Put on cake
"
-msgstr "crwdns15700:0crwdne15700:0"
-
-msgid "Generalising and patterns"
-msgstr "crwdns15701:0crwdne15701:0"
-
-msgid "Generalising is also referred to as 'pattern recognition and generalisation'. Generalisation is taking a solution (or part of a solution) to a problem and generalising it so it can be applied to other similar problems and tasks. Since solutions in Computer Science are algorithms, this means we take an algorithm and make it general enough that it can be used for a range of problems. This process involves abstraction, because to make something more general we have to remove unnecessary details that are related to a specific problem or situation, but are not important to how the algorithm functions."
-msgstr "crwdns15702:0crwdne15702:0"
-
-msgid "Spotting patterns is an important part of this process, when we think about problems we might recognise similarities between them and that they can be solved in similar ways. This is called pattern matching, and it’s something we do naturally all the time in our daily life."
-msgstr "crwdns15703:0crwdne15703:0"
-
-msgid "Generalised algorithms can be reused for a whole group of similar problems, which means we can come up with solutions quickly and effectively."
-msgstr "crwdns15704:0crwdne15704:0"
-
-msgid "Evaluation"
-msgstr "crwdns15705:0crwdne15705:0"
-
-msgid "Evaluation is about identifying the possible solutions to a problem and judging which is the best to use, if they will work in some situations but not others, and how they can be improved. When judging our solutions we need to think about a range of factors. For example how much time it will take these processes (algorithms) to solve the problem and will it reliably solve the problem, or if there are certain situations where it will perform in a very different way. Evaluation is something we do a lot in our everyday lives."
-msgstr "crwdns15706:0crwdne15706:0"
-
-msgid "There are different ways we can evaluate our algorithmic solutions. We can test their speed by implementing them on a computer; or we can analyse them by counting or calculating how many steps they are likely to take. We can test that they work correctly by giving our solution lots of different inputs, and checking it works as expected. When we do this we need to think about the different inputs we test, because we don’t want to check every possible input (often there's an infinite number of different possible inputs!), but we still need to know if it will work for them. Testing is something Computer Scientists and programmers do all the time. But because we can't usually test every possible input, we also try to evaluate a system using logical reasoning."
-msgstr "crwdns15707:0crwdne15707:0"
-
-msgid "Logic"
-msgstr "crwdns15708:0crwdne15708:0"
-
-msgid "When trying to solve problems we need to think logically. Logical reasoning is about trying to make sense of things by observing, collecting data, thinking about the facts you know, and then figuring things out based on what you already know. It helps us use our existing knowledge to establish rules and check facts."
-msgstr "crwdns15709:0crwdne15709:0"
-
-msgid "For example, suppose you are writing software that works out the shortest route to a location from your house. In the following map it's 2 minutes to the library if you head north from your house, but if you head south it's 3 minutes to the next intersection. You might wonder if there's a better route to the library if you start by heading south, but logically there can't be because you'll already have walked for 3 minutes to get to the intersection."
-msgstr "crwdns15710:0crwdne15710:0"
-
-msgid "At a deeper level, computers are built entirely on logic. They use 'True' and 'False' values, and use something called 'Boolean expressions', like “is age > 5”, to make decisions in computer programs."
-msgstr "crwdns15711:0crwdne15711:0"
-
-msgid "Tracking down a bug in a program also requires logical thinking, to work out where, and why, something in the program is going wrong."
-msgstr "crwdns15712:0crwdne15712:0"
-
-msgid "Email: You can email the CS Unplugged team here."
-msgstr "crwdns15713:0crwdne15713:0"
-
-msgid "Twitter:@UCCSEd"
-msgstr "crwdns15714:0crwdne15714:0"
-
-msgid "Community Google Group: We have a Google Group you can join and talk about CS Unplugged."
-msgstr "crwdns15715:0crwdne15715:0"
-
-msgid "How do I teach CS Unplugged?"
-msgstr "crwdns18344:0crwdne18344:0"
-
-msgid "CS Unplugged is very much based on a constructivist approach: students are given challenges based on a few simple rules, and in the process of solving those challenges they uncover powerful ideas on their own. Not only is this a more memorable way to learn, but it empowers them to realise that these are ideas within their grasp. The activities are also very kinesthetic - the bigger the materials, the better."
-msgstr "crwdns18345:0crwdne18345:0"
-
-msgid "Because of this approach, you can also learn alongside the students. You'll need to read through the whole activity so that you're prepared for it, and we have provided videos for many of them so you can visualise them, but as the students discover how these ideas work out, you'll start to see patterns and ideas that the students are discovering as they understand the principles behind these topics from Computer Science."
-msgstr "crwdns18346:0crwdne18346:0"
-
-msgid "If you're working within a school curriculum, you can find appropriate activities based on the learning objectives and age group. In several countries there are guides to the local curriculum that link it to Unplugged (e.g. Digital Technologies Hub for Australia). But the unit plans listed follow the common topics that are appearing in school curricula, so you are likely to find something relevant from the titles. Note that CS Unplugged does not teach programming - the Kidbots activity is an excellent lead in to programming and the \"Plugging it in\" sections give some programming exercises to follow up with - but in general we are trying to show students what they could do with programming, before they have to learn it."
-msgstr "crwdns18347:0crwdne18347:0"
-
-msgid "But reading about Unplugged isn't the fun way to engage with it - pick a lesson for your students, and dive into it!"
-msgstr "crwdns18348:0crwdne18348:0"
-
-msgid "View available topics"
-msgstr "crwdns18349:0crwdne18349:0"
-
-msgid "Computer Science without a computer"
-msgstr "crwdns15716:0crwdne15716:0"
-
-msgid "CS Unplugged is a collection of free teaching material that teaches Computer Science through engaging games and puzzles that use cards, string, crayons and lots of running around."
-msgstr "crwdns15717:0crwdne15717:0"
-
-msgid "Welcome to the new CS Unplugged!"
-msgstr "crwdns20512:0crwdne20512:0"
-
-msgid "This updated website has unit plans, lesson plans, teaching videos, curriculum integration activities, and programming exercises to plug in the Computer Science concepts they have just learnt unplugged."
-msgstr "crwdns20513:0crwdne20513:0"
-
-msgid "The original activities are still available at classic.csunplugged.org."
-msgstr "crwdns20519:0crwdne20519:0"
-
-msgid "What is Computer Science?"
-msgstr "crwdns18350:0crwdne18350:0"
-
-msgid "Curriculum Integrations"
-msgstr "crwdns15721:0crwdne15721:0"
-
-msgid "version 4.0.0 onwards"
-msgstr "crwdns19553:0crwdne19553:0"
-
-msgid "Founders"
-msgstr "crwdns15724:0crwdne15724:0"
-
-msgid "Advisers"
-msgstr "crwdns15725:0crwdne15725:0"
-
-msgid "Current Team"
-msgstr "crwdns15726:0crwdne15726:0"
-
-msgid "Previous Team Members"
-msgstr "crwdns19554:0crwdne19554:0"
-
-msgid "up to version 3.2.2"
-msgstr "crwdns19555:0crwdne19555:0"
-
-msgid "Contributors"
-msgstr "crwdns15727:0crwdne15727:0"
-
-msgid "Website"
-msgstr "crwdns15728:0crwdne15728:0"
-
-msgid "The primary goal of the Unplugged project is to promote Computer Science (and computing in general) to young people as an interesting, engaging, and intellectually stimulating discipline. We want to capture people’s imagination and address common misconceptions about what it means to be a computer scientist. We want to convey fundamentals that do not depend on particular software or systems, ideas that will still be fresh in 10 years. We want to reach kids in elementary schools and provide supplementary material for university courses. We want to tread where high-tech educational solutions are infeasible; to cross the divide between the information-rich and information-poor, between industrialized countries and the developing world."
-msgstr "crwdns15730:0crwdne15730:0"
-
-msgid "There are many worthy projects for promoting Computer Science. The main principles that distinguish the Unplugged activities are:"
-msgstr "crwdns15731:0crwdne15731:0"
-
-msgid "No Computers Required"
-msgstr "crwdns15732:0crwdne15732:0"
-
-msgid "The activities do not depend on computers. This avoids confusing Computer Science with programming or learning application software, makes the activities available to those who aren’t able to or don’t want to work with computers, and skips the barrier of learning to program before being able to explore ideas. It also provides physical, kinaesthetic experiences as part of learning computing, which can be a welcome break from sitting in front of a screen. For example, the parity magic trick is a card game that happens to use the same principle as error correction in computer memory. Unplugged isn’t a completely Luddite approach – we do exploit the internet and other computing facilities to share and develop the activities, and we hope that students will have to opportunity to learn to program so that they can put wheels on the ideas they have been exploring."
-msgstr "crwdns15733:0crwdne15733:0"
-
-msgid "Real Computer Science"
-msgstr "crwdns15734:0crwdne15734:0"
-
-msgid "Unplugged presents fundamental concepts in Computer Science such as algorithms, artificial intelligence, graphics, information theory, human computer interfaces, programming languages, and so on. We want to emphasize that programming is a means, not an end. Wikipedia provides a definition of Computer Science, and Peter Denning’s Great Principles project provides a more detailed analysis of the topics it covers."
-msgstr "crwdns15735:0crwdne15735:0"
-
-msgid "Learning by doing"
-msgstr "crwdns15736:0crwdne15736:0"
-
-msgid "The activities tend to be kinaesthetic, often on a large scale and involving team work. For example, the Sorting Network activity has teams of six running through a network drawn on the ground. The activities tend to allow students to discover answers for themselves, rather than just being given solutions or algorithms to follow; that is, a constructivist approach is encouraged (where the teacher uses the scaffolding provided by Unplugged to ask questions that lead them to discover the knowledge themselves), as we want students to realize that they are capable of finding solutions to problems on their own, rather than being given a solution to apply to the problem. For example, students don’t really need to be able to convert numbers to binary, but it is valuable for them to discover the patterns such as the doubling value of bits, patterns when you count in binary, and how the range increases exponentially as you add bits."
-msgstr "crwdns15737:0crwdne15737:0"
-
-msgid "Fun"
-msgstr "crwdns15738:0crwdne15738:0"
-
-msgid "The activities are fun and engaging, not just busy-work for the sake of it. Usually the explanations are quite brief – the teacher lays out the materials and a few rules, and the students follow the challenge from there. There are puzzles, challenges, competitions, problem solving and humour. Unplugged activities should leave students with a sense of genuine achievement. There is often a strong sense of story in the activities; problems are presented as part of a story rather than as an abstract mathematical challenge. Children are more interested in pirates than privacy, and absurd fictitious stories can be more memorable than compelling business applications."
-msgstr "crwdns15739:0crwdne15739:0"
-
-msgid "No specialised equipment"
-msgstr "crwdns15740:0crwdne15740:0"
-
-msgid "The activities are low cost, using equipment commonly found in classrooms or stationery stores. Most require only paper and pencil, and perhaps cards, string, chalk, whiteboard markers, balls or similar items."
-msgstr "crwdns15741:0crwdne15741:0"
-
-msgid "Variations encouraged"
-msgstr "crwdns15742:0crwdne15742:0"
-
-msgid "Unplugged is published under a Creative Commons licence, which permits free sharing (with acknowledgement). Variations, adaptations and extensions are encouraged. This also allows local publishing arrangements to take account of the kind of packaging that would make the material more accessible to local educational practitioners."
-msgstr "crwdns15743:0crwdne15743:0"
-
-msgid "
Two specific situations that we get asked about are:
For-profit classes (e.g. clubs with a subscription fee): It is fine to charge a fee for people to attend classes that you're running that uses this material.
Selling packs of support material (such as pre-printed cards): It is fine to do this. We hope that you'll do it for a reasonable price, and it's even better if you can get someone to sponsor you so that you can give it away to educators, but the license allows you to set your own price.
"
-msgstr "crwdns15744:0crwdne15744:0"
-
-msgid "For everyone"
-msgstr "crwdns15745:0crwdne15745:0"
-
-msgid "The programme is strongly international – we encourage variations that are relevant to local cultures (for example, some activities that require a large playground can be changed to a board game for schools that have very little open space; others use contexts that might not be familiar to students in a different culture). Translators should try the activities locally and involve teachers. It is better to adapt activities rather than translate them faithfully to something that would be less meaningful in the local culture. The activities are intended to be inclusive."
-msgstr "crwdns15746:0crwdne15746:0"
-
-msgid "Co-operative"
-msgstr "crwdns15747:0crwdne15747:0"
-
-msgid "We encourage co-operation, communication and problem solving. Competition can also be effective if it is used appropriately, especially between teams rather than individuals, but having students working cooperatively is a great way to learn about problem solving."
-msgstr "crwdns15748:0crwdne15748:0"
-
-msgid "Stand-alone Activities"
-msgstr "crwdns15749:0crwdne15749:0"
-
-msgid "As much as possible, the activities are stand-alone modules that can be used independently of each other, so that they can be used for enrichment in curricula or for outreach on their own rather than having to be used as a series. The ones that have been presented as lesson plans will sometimes require a series of lessons to be followed, but we will indicate if particular preparation is required."
-msgstr "crwdns15750:0crwdne15750:0"
-
-msgid "Resilient"
-msgstr "crwdns15751:0crwdne15751:0"
-
-msgid "The activities are resilient to errors made by students; they should not depend on getting many difficult steps exactly right, and minor mistakes should not prevent participants from understanding the principles. The instructions are usually just one or two rules and a goal that can be expressed in a single sentence (e.g. “Each card is either fully visible or not; how can you display exactly 11 dots?”, or “We need to get from any house to any other house; what is the smallest number of paving stones that make this possible”)."
-msgstr "crwdns15752:0crwdne15752:0"
-
-msgid "The term \"Computer Science\" has become widely used in recent years as the skills associated with the subject have become crucial to developing innovative digital technology, and qualifications in this area are highly sought after."
-msgstr "crwdns18352:0crwdne18352:0"
-
-msgid "The CS Unplugged activities are intended to give you a feel for what the subject is - you can learn what it is by doing. At this early stage, rather than define it formally, let's think about how it might influence our daily lives. For example, think of your favourite search engine. On the surface it seems like a fairly simple interface: a text box where you type what you want to search for, and a button to start the search. The level of programming knowledge needed to implement a text box and a button is fairly rudimentary, and you could implement a search by writing a short program (probably less than 20 lines) to go through all the text on the web and displaying any that match. But obviously there's more to it than this! There are billions of searches are made every day, on billions on web pages, and the approach above will give answers (eventually), but will be so slow and ineffective that no-one would use it."
-msgstr "crwdns18353:0crwdne18353:0"
-
-msgid "This is where computer science comes in; many areas of Computer Science are employed to make the system work well, and most of them are illustrated through Unplugged. How could you search billions of items in a fraction of a second (Searching Algorithms)? How do you make sure that it's easy to use (Human-Computer Interaction)? We need to keep it secure - users don't want other people to know what they are searching for, and the search engine doesn't want commercial interests to manipulate search ranking (Computer Security and Encryption)? Search engines generally predict what you are about to search for (Artificial Intelligence). It needs to be reliable - a small mistake from one of the thousands of programmers at a search engine company shouldn't prevent the site working (Software Engineering). It needs to scale well - if it becomes 10 times as popular, you don't want it to need 100 times the computing resources (Algorithms). The relevance of a search generally depends on the relationships between web sites - you need a map showing which sites are linked to which other ones (Graphs)."
-msgstr "crwdns18354:0crwdne18354:0"
-
-msgid "The areas above cover much of what the subject of computer science is about. Programming is just a tool for implementing ideas (well, it's a very powerful tool, and requires considerable skill to use well). But programming on its own isn't enough to create software that people love to use, and Computer Science is the area that gives programmers the inside knowledge to make their software fast, efficient, reliable, secure, usable, intelligent, scalable, and even delightful!"
-msgstr "crwdns18355:0crwdne18355:0"
-
-msgid "That's why we developed CS Unplugged - we want young students to be empowered to understand the great ideas that computer science covers, without having to become expert programmers first. They won't be learning exactly how to build the next search engine, social network or game app, but they will have an idea of what sort of techniques are needed to make it successful. We don't want them to see digital systems as some kind of magic that they can't participate in, but as something that they could understand and, for some, create themselves. Actually, it is kind of magic when you start understanding what can and can't be done."
-msgstr "crwdns18356:0crwdne18356:0"
-
-msgid "Read 'How do I teach CS Unplugged?'"
-msgstr "crwdns18357:0crwdne18357:0"
-
-msgid "This page diplays a complete list of all available printables. If a lesson uses a printable, the lesson will contain a direct link to the printable with a description on how to use it."
-msgstr "crwdns19556:0crwdne19556:0"
-
-msgid "No printables are available."
-msgstr "crwdns19557:0crwdne19557:0"
-
-msgid "Generate Printable"
-msgstr "crwdns19558:0crwdne19558:0"
-
-#, python-format
-msgid "The download of this Printable includes %(copies_amount)s unique copies."
-msgstr "crwdns19559:0%(copies_amount)scrwdne19559:0"
-
-msgid "Preview"
-msgstr "crwdns15809:0crwdne15809:0"
-
-msgid "Related Lessons"
-msgstr "crwdns15810:0crwdne15810:0"
-
-msgid "Topic"
-msgstr "crwdns15811:0crwdne15811:0"
-
-msgid "Ages"
-msgstr "crwdns15812:0crwdne15812:0"
-
-msgid "Number"
-msgstr "crwdns15813:0crwdne15813:0"
-
-msgid "Lesson"
-msgstr "crwdns15814:0crwdne15814:0"
-
-msgid "Within topic"
-msgstr "crwdns19543:0crwdne19543:0"
-
-#, python-format
-msgid "Ages %(lower)s to %(upper)s: Lesson %(number)s"
-msgstr "crwdns15866:0%(lower)scrwdnd15866:0%(upper)scrwdnd15866:0%(number)scrwdne15866:0"
-
-msgid "within unit plan"
-msgstr "crwdns19544:0crwdne19544:0"
-
-msgid "Challenge Level:"
-msgstr "crwdns15885:0crwdne15885:0"
-
-msgid "The following table lists curriculum integrations for all topics in the CS Unplugged content."
-msgstr "crwdns15853:0crwdne15853:0"
-
-msgid "Activity"
-msgstr "crwdns15854:0crwdne15854:0"
-
-msgid "Curriculum Areas"
-msgstr "crwdns15855:0crwdne15855:0"
-
-msgid "Prerequisite Lessons?"
-msgstr "crwdns15856:0crwdne15856:0"
-
-msgid "Seeing the Computational Thinking connections"
-msgstr "crwdns15857:0crwdne15857:0"
-
-msgid "Throughout the lessons there are links to computational thinking. Below we've noted some general links that apply to this content."
-msgstr "crwdns15858:0crwdne15858:0"
-
-msgid "Teaching computational thinking through CSUnplugged activities supports students to learn how to describe a problem, identify what are the important details they need to solve this problem, and break it down into small, logical steps so that they can then create a process which solves the problem, and then evaluate this process. These skills are transferable to any other curriculum area, but are particularly relevant to developing digital systems and solving problems using the capabilities of computers."
-msgstr "crwdns15859:0crwdne15859:0"
-
-#, python-format
-msgid "These Computational Thinking concepts are all connected to each other and support each other, but it’s important to note that not all aspects of Computational Thinking happen in every unit or lesson. We’ve highlighted the important connections for you to observe your students in action. For more background information on what our definition of Computational Thinking see our notes about computational thinking."
-msgstr "crwdns15860:0%(ct_url)scrwdne15860:0"
-
-msgid "topic"
-msgstr "crwdns18229:0crwdne18229:0"
-
-msgid "curriculum integration"
-msgstr "crwdns18230:0crwdne18230:0"
-
-msgid "Heads up!"
-msgstr "crwdns15861:0crwdne15861:0"
-
-msgid "To do this activity it's expected you understand the content covered in the following:"
-msgstr "crwdns15862:0crwdne15862:0"
-
-#, python-format
-msgid "The following glossary terms are not yet available in %(language)s. Sorry about that!"
-msgstr "crwdns18231:0%(language)scrwdne18231:0"
-
-msgid "Open a topic to see all related unit plans, lessons, curriculum integrations, and programming challenges."
-msgstr "crwdns15863:0crwdne15863:0"
-
-#, python-format
-msgid "Ages %(min_age)s to %(max_age)s"
-msgstr "crwdns18232:0%(min_age)scrwdnd18232:0%(max_age)scrwdne18232:0"
-
-msgid "lessons"
-msgstr "crwdns18233:0crwdne18233:0"
-
-msgid "curriculum integrations"
-msgstr "crwdns18234:0crwdne18234:0"
-
-msgid "programming challenges"
-msgstr "crwdns18235:0crwdne18235:0"
-
-msgid "No topics are available."
-msgstr "crwdns15864:0crwdne15864:0"
-
-msgid "unit plan"
-msgstr "crwdns18236:0crwdne18236:0"
-
-msgid "lesson"
-msgstr "crwdns18237:0crwdne18237:0"
-
-#, python-format
-msgid "Duration: %(duration)s minutes"
-msgstr "crwdns18358:0%(duration)scrwdne18358:0"
-
-msgid "Learning outcomes"
-msgstr "crwdns15867:0crwdne15867:0"
-
-msgid "Students will be able to:"
-msgstr "crwdns15868:0crwdne15868:0"
-
-msgid "Classroom resources"
-msgstr "crwdns15870:0crwdne15870:0"
-
-msgid "Programming challenges"
-msgstr "crwdns15871:0crwdne15871:0"
-
-msgid "View related programming challenges"
-msgstr "crwdns15872:0crwdne15872:0"
-
-msgid "Table of contents"
-msgstr "crwdns15873:0crwdne15873:0"
-
-msgid "Computational Thinking"
-msgstr "crwdns15874:0crwdne15874:0"
-
-#, python-format
-msgid "Ages %(lower)s to %(upper)s"
-msgstr "crwdns15875:0%(lower)scrwdnd15875:0%(upper)scrwdne15875:0"
-
-#, python-format
-msgid "Not available in %(language)s"
-msgstr "crwdns18238:0%(language)scrwdne18238:0"
-
-#, python-format
-msgid "
Sorry! This %(model_type)s is not yet available in %(language)s.
This %(model_type)s is available in the following languages:
"
-msgstr "crwdns18239:0%(model_type)scrwdnd18239:0%(language)scrwdnd18239:0%(model_type)scrwdne18239:0"
-
-msgid "or"
-msgstr "crwdns18240:0crwdne18240:0"
-
-#, python-format
-msgid "Return to the %(parent_type)s"
-msgstr "crwdns18241:0%(parent_type)scrwdne18241:0"
-
-#, python-format
-msgid "%(name)s solution"
-msgstr "crwdns15876:0%(name)scrwdne15876:0"
-
-msgid "solution"
-msgstr "crwdns15877:0crwdne15877:0"
-
-msgid "challenge"
-msgstr "crwdns18242:0crwdne18242:0"
-
-msgid "Heads Up! If you are ready to compare your programming to ours or are wanting to have a look at how we solved it, click 'View solution' below to view at least one way to write this program."
-msgstr "crwdns15878:0crwdne15878:0"
-
-msgid "View solution"
-msgstr "crwdns15879:0crwdne15879:0"
-
-msgid "This is just one of many possible solutions:"
-msgstr "crwdns15880:0crwdne15880:0"
-
-msgid "Back to programming challenge"
-msgstr "crwdns15881:0crwdne15881:0"
-
-msgid "Extra Challenge"
-msgstr "crwdns15882:0crwdne15882:0"
-
-#, python-format
-msgid "%(name)s programming challenges"
-msgstr "crwdns15883:0%(name)scrwdne15883:0"
-
-#, python-format
-msgid "No programming challenges for %(topic)s."
-msgstr "crwdns15884:0%(topic)scrwdne15884:0"
-
-msgid "programming challenge"
-msgstr "crwdns18243:0crwdne18243:0"
-
-msgid "This programming challenge is linked to the following lessons:"
-msgstr "crwdns15886:0crwdne15886:0"
-
-#, python-format
-msgid "Challenge %(set_num)s.%(chal_num)s for %(name)s"
-msgstr "crwdns15887:0%(set_num)scrwdnd15887:0%(chal_num)scrwdnd15887:0%(name)scrwdne15887:0"
-
-#, python-format
-msgid "%(lower)s to %(upper)s"
-msgstr "crwdns15888:0%(lower)scrwdnd15888:0%(upper)scrwdne15888:0"
-
-msgid "and"
-msgstr "crwdns15889:0crwdne15889:0"
-
-msgid "Languages"
-msgstr "crwdns15891:0crwdne15891:0"
-
-msgid "What it should look like"
-msgstr "crwdns15892:0crwdne15892:0"
-
-msgid "Hints"
-msgstr "crwdns15893:0crwdne15893:0"
-
-#, python-format
-msgid "Show %(implementation.language.name)s solution"
-msgstr "crwdns15894:0%(implementation.language.name)scrwdne15894:0"
-
-msgid "Name"
-msgstr "crwdns15895:0crwdne15895:0"
-
-msgid "Challenge Level"
-msgstr "crwdns15896:0crwdne15896:0"
-
-#, python-format
-msgid "%(topic.name)s other resources"
-msgstr "crwdns15897:0%(topic.name)scrwdne15897:0"
-
-msgid "Other resources"
-msgstr "crwdns15904:0crwdne15904:0"
-
-msgid "Sorry! We couldn't find any information on other resources for this topic."
-msgstr "crwdns18244:0crwdne18244:0"
-
-msgid "Return to the topic"
-msgstr "crwdns18245:0crwdne18245:0"
-
-msgid "list of topics"
-msgstr "crwdns18246:0crwdne18246:0"
-
-msgid "Unit Plans"
-msgstr "crwdns18359:0crwdne18359:0"
-
-msgid "Looking for more?"
-msgstr "crwdns15901:0crwdne15901:0"
-
-msgid "Click here for other resources"
-msgstr "crwdns15902:0crwdne15902:0"
-
-msgid "Description"
-msgstr "crwdns18360:0crwdne18360:0"
-
-msgid "Unit plan:"
-msgstr "crwdns15898:0crwdne15898:0"
-
-msgid "What's it all about?"
-msgstr "crwdns18361:0crwdne18361:0"
-
-msgid "Read the full unit plan description"
-msgstr "crwdns18362:0crwdne18362:0"
-
-msgid "Lessons"
-msgstr "crwdns15905:0crwdne15905:0"
-
diff --git a/csunplugged/locale/yy_RL b/csunplugged/locale/yy_RL
deleted file mode 120000
index cb9aa369e..000000000
--- a/csunplugged/locale/yy_RL
+++ /dev/null
@@ -1 +0,0 @@
-xx_LR
\ No newline at end of file
diff --git a/csunplugged/package.json b/csunplugged/package.json
index 317002c7f..8eef12559 100644
--- a/csunplugged/package.json
+++ b/csunplugged/package.json
@@ -4,18 +4,19 @@
"private": true,
"dependencies": {},
"devDependencies": {
- "ansi-colors": "4.1.1",
- "autoprefixer": "10.4.2",
- "bootstrap": "4.6.0",
- "browser-sync": "2.27.7",
+ "ansi-colors": "4.1.3",
+ "autoprefixer": "10.4.8",
+ "bootstrap": "4.6.1",
+ "browser-sync": "2.27.10",
"browserify": "17.0.0",
"child_process": "1.0.2",
- "codemirror": "5.65.1",
- "cssnano": "5.0.15",
+ "codemirror": "5.65.6",
+ "cssnano": "5.1.13",
"blockly": "7.20211209.2",
"details-element-polyfill": "2.4.0",
"fancy-log": "2.0.0",
"gulp-concat": "2.6.1",
+ "gulp-dependents": "1.2.5",
"gulp-error-handle": "1.0.1",
"gulp-filter": "7.0.0",
"gulp-if": "3.0.0",
@@ -31,12 +32,13 @@
"multiple-select": "1.5.2",
"pixrem": "5.0.0",
"popper.js": "1.16.1",
- "postcss": "8.4.5",
+ "postcss": "8.4.16",
"postcss-flexbugs-fixes": "5.0.2",
- "sass": "1.49.0",
+ "reveal.js": "4.3.1",
+ "sass": "1.54.4",
"scratchblocks": "uccser/scratchblocks#master",
"vinyl-buffer": "1.0.1",
- "yargs": "17.3.1"
+ "yargs": "17.5.1"
},
"engines": {
"node": ">=8"
diff --git a/csunplugged/resources/content/zh_Hans/treasure-hunt.md b/csunplugged/resources/content/zh_Hans/treasure-hunt.md
deleted file mode 100644
index 891f59ecc..000000000
--- a/csunplugged/resources/content/zh_Hans/treasure-hunt.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# 寻宝游戏
-
-此资源包含搜索算法活动的打印资料。
\ No newline at end of file
diff --git a/csunplugged/resources/generators/BinaryWindowsResourceGenerator.py b/csunplugged/resources/generators/BinaryWindowsResourceGenerator.py
index 2a05007e8..94f5bacae 100644
--- a/csunplugged/resources/generators/BinaryWindowsResourceGenerator.py
+++ b/csunplugged/resources/generators/BinaryWindowsResourceGenerator.py
@@ -10,9 +10,15 @@
FONT_PATH = "static/fonts/PatrickHand-Regular.ttf"
FONT = ImageFont.truetype(FONT_PATH, 300)
SMALL_FONT = ImageFont.truetype(FONT_PATH, 180)
+IMAGE_SIZE_X = 2334
+IMAGE_SIZE_Y = 1650
+LINE_COLOUR = "#000000"
+LINE_WIDTH = 1
NUMBER_BITS_VALUES = {
"4": _("Four (1 to 8)"),
+ "5": _("Five (1 to 16)"),
+ "6": _("Six (1 to 32)"),
"8": _("Eight (1 to 128)"),
}
@@ -55,126 +61,228 @@ def data(self):
A dictionary or list of dictionaries for each resource page.
"""
# Retrieve parameters
- number_of_bits = self.options["number_bits"].value
+ number_of_bits = int(self.options["number_bits"].value)
value_type = self.options["value_type"].value
- dot_counts = self.options["dot_counts"].value
+ show_bits_value = self.options["dot_counts"].value
+
+ # Define variables
+ column_width = IMAGE_SIZE_X / number_of_bits
- pages = []
- page_sets = [("binary-windows-1-to-8.png", 8)]
- if number_of_bits == "8":
- page_sets.append(("binary-windows-16-to-128.png", 128))
-
- for (filename, dot_count_start) in page_sets:
- image = Image.open(os.path.join(BASE_IMAGE_PATH, filename))
- image = self.add_digit_values(image, value_type, True, 660, 724, 1700, FONT)
- if dot_counts:
- image = self.add_dot_counts(image, dot_count_start, SMALL_FONT)
- image = image.rotate(90, expand=True)
- pages.append({"type": "image", "data": image})
- pages.append(self.back_page(value_type))
- pages[0]["thumbnail"] = True
+ # Get page outline
+ page_outline = self.page_outline(number_of_bits, column_width)
+ front_page = self.front_page(
+ value_type,
+ column_width,
+ show_bits_value,
+ page_outline.copy()
+ )
+ back_page = self.back_page(
+ value_type,
+ column_width,
+ page_outline.copy()
+ )
+ pages = [front_page, back_page]
return pages
- def back_page(self, value_type):
+ def page_outline(self, number_of_bits, column_width):
+ """Create outline (lines without content) for page.
+
+ Args:
+ number_of_bits (int): Number of bits on page.
+ column_width (int): Width of each bit on page.
+
+ Return:
+ Page outline as type Image.
+ """
+ page_outline = Image.new("RGB", (IMAGE_SIZE_X, IMAGE_SIZE_Y), "#fff")
+ draw = ImageDraw.Draw(page_outline)
+ image_midpoint_y = int(IMAGE_SIZE_Y / 2)
+
+ # Draw outline
+ draw.rectangle(
+ [(0, 0), (IMAGE_SIZE_X - LINE_WIDTH, IMAGE_SIZE_Y - LINE_WIDTH)],
+ outline=LINE_COLOUR,
+ fill="#ffffff",
+ width=LINE_WIDTH,
+ )
+
+ # Draw internal separator lines
+ for line_number in range(1, number_of_bits):
+ x_coord = line_number * column_width
+ draw.line(
+ [(x_coord, 0), (x_coord, image_midpoint_y)],
+ fill=LINE_COLOUR,
+ width=LINE_WIDTH,
+ )
+
+ # Draw dashed line
+ dash_size = 10
+ for x_coord in range(0, IMAGE_SIZE_X, dash_size * 3):
+ draw.line(
+ [(x_coord, image_midpoint_y), (x_coord + dash_size, image_midpoint_y)],
+ fill="#666666",
+ width=LINE_WIDTH,
+ )
+
+ return page_outline
+
+ def front_page(self, value_type, column_width, show_bits_value, page_outline):
+ """Return a Pillow object of front page of Binary Windows.
+
+ Args:
+ value_type (str): Type of value representation used.
+ column_width (float): Width of a bit column.
+ show_bits_value (bool): True if bit value labels should be shown.
+ page_outline (Pillow Draw): Outline of page to draw on.
+
+ Returns:
+ A dictionary for the back page.
+ """
+ image = self.add_dots(page_outline, column_width, show_bits_value)
+ image = self.add_digit_values(page_outline, column_width, value_type, True)
+ image = image.rotate(90, expand=True)
+ return {"type": "image", "data": image, "thumbnail": True}
+
+ def back_page(self, value_type, column_width, page_outline):
"""Return a Pillow object of back page of Binary Windows.
Args:
- value_type: Type of value representation used (str).
+ value_type (str): Type of value representation used.
+ column_width (float): Width of a bit column.
+ page_outline (Pillow Draw): Outline of page to draw on.
Returns:
A dictionary for the back page.
"""
- image = Image.open(os.path.join(BASE_IMAGE_PATH, "binary-windows-blank.png"))
- image = self.add_digit_values(image, value_type, False, 660, 724, 650, FONT)
+ image = self.add_digit_values(page_outline, column_width, value_type, False)
image = image.rotate(90, expand=True)
return {"type": "image", "data": image}
- def add_dot_counts(self, image, starting_value, font):
- """Add dot count text onto image.
+ def add_dots(self, image, column_width, show_bits_value):
+ """Add binary values onto image.
+
+ Note: This method adds dots from right to left on the page.
Args:
- image: The image to add text to (Pillow Image).
- starting_value: Number on left window (int).
- font: Font used for adding text (Pillow Font).
+ image (Pillow Image): The image to add binary values to.
+ column_width (float): Width of a bit column.
+ show_bits_value (bool): True if bit value labels should be shown.
Returns:
- Pillow Image with text added (Pillow Image).
+ Pillow Image with dots (Pillow Image).
"""
- value = starting_value
draw = ImageDraw.Draw(image)
- coord_x = 660
- coord_x_increment = 724
- coord_y = 1000
- for i in range(4):
- text = str(value)
- text_width, text_height = draw.textsize(text, font=font)
- text_coord_x = coord_x - (text_width / 2)
- text_coord_y = coord_y - (text_height / 2)
- draw.text(
- (text_coord_x, text_coord_y),
- text,
- font=font,
- fill="#000"
+ coord_x_start = IMAGE_SIZE_X - (column_width / 2)
+ coord_x_increment = column_width
+ coord_x = coord_x_start
+ base_image_coord_y = IMAGE_SIZE_Y * 0.2
+ base_text_coord_y = IMAGE_SIZE_Y * 0.4
+ dots_value = 1
+
+ while coord_x > 0:
+ # Select dots image
+ image_file = f"dots-{dots_value}.png"
+ dots_image = Image.open(os.path.join(BASE_IMAGE_PATH, image_file))
+
+ # Scale image to fit dots to 80% of column width
+ (dots_width, dots_height) = dots_image.size
+ x_scale_factor = (column_width / dots_width) * 0.9
+ y_scale_factor = (IMAGE_SIZE_Y * 0.3) / dots_height
+ scale_factor = min(x_scale_factor, y_scale_factor)
+ dots_image = dots_image.resize((
+ int(dots_width * scale_factor),
+ int(dots_height * scale_factor),
+ ))
+
+ # Paste into image
+ (dots_width, dots_height) = dots_image.size
+ coords = (
+ int(coord_x - (dots_width / 2)),
+ int(base_image_coord_y - (dots_height / 2)),
)
- coord_x += coord_x_increment
- value = int(value / 2)
+ image.paste(dots_image, box=coords, mask=dots_image)
+
+ if show_bits_value:
+ text = str(dots_value)
+ text_width, text_height = draw.textsize(text, font=SMALL_FONT)
+ text_coord_x = coord_x - (text_width / 2)
+ text_coord_y = base_text_coord_y - (text_height / 2)
+ draw.text(
+ (text_coord_x, text_coord_y),
+ text,
+ font=SMALL_FONT,
+ fill="#000"
+ )
+
+ # Update variables
+ coord_x -= coord_x_increment
+ dots_value *= 2
return image
- def add_digit_values(self, image, value_type, on, x_coord_start, x_coord_increment, base_y_coord, font):
+ def add_digit_values(self, image, column_width, value_type, on):
"""Add binary values onto image.
Args:
- image: The image to add binary values to (Pillow Image).
- value_type: Either "binary" for 0's and 1's, or "lightbulb" for
- lit and unlit lightbulbs (str).
- on: True if binary value is on/lit, otherwise False (bool).
- x_coord_start: X co-ordinate starting value (int).
- x_coord_increment: X co-ordinate increment value (int).
- base_y_coord: Y co-ordinate value (int).
- font: Font used for adding text (Pillow Font).
+ image (Pillow Image): The image to add binary values to.
+ column_width (float): Width of a bit column.
+ value_type (str): Either "binary" for 0's and 1's, or "lightbulb" for
+ lit and unlit lightbulbs.
+ on (bool): True if binary value is on/lit, otherwise False.
Returns:
Pillow Image with binary values (Pillow Image).
"""
- text_coord_x = x_coord_start
+ coord_x_start = column_width / 2
+ coord_x_increment = column_width
if value_type == "binary":
if on:
text = "1"
else:
text = "0"
- else: # lightbulb
+ else: # Lightbulb
if on:
- image_file = "col_binary_lightbulb.png"
+ image_file = "lightbulb-on.png"
else:
- image_file = "col_binary_lightbulb_off.png"
- lightbulb = Image.open(os.path.join("static/img/topics/", image_file))
- (width, height) = lightbulb.size
- scale_factor = 0.6
- lightbulb = lightbulb.resize((int(width * scale_factor), int(height * scale_factor)))
- (width, height) = lightbulb.size
- lightbulb_width = int(width / 2)
- lightbulb_height = int(height / 2)
+ image_file = "lightbulb-off.png"
+ lightbulb = Image.open(os.path.join(BASE_IMAGE_PATH, image_file))
+ (lightbulb_width, lightbulb_height) = lightbulb.size
+ # Scale image to fit lightbulb to 80% of column width
+ scale_factor = (column_width / lightbulb_width) * 0.8
+ lightbulb = lightbulb.resize((
+ int(lightbulb_width * scale_factor),
+ int(lightbulb_height * scale_factor),
+ ))
+ (lightbulb_width, lightbulb_height) = lightbulb.size
if not on:
lightbulb = lightbulb.rotate(180)
- for i in range(4):
- draw = ImageDraw.Draw(image)
- if value_type == "binary":
+ draw = ImageDraw.Draw(image)
+ coord_x = coord_x_start
- text_width, text_height = draw.textsize(text, font=font)
- coord_x = text_coord_x - (text_width / 2)
- coord_y = base_y_coord - (text_height / 2)
+ if on:
+ base_coord_y = IMAGE_SIZE_Y * 0.75
+ else:
+ base_coord_y = IMAGE_SIZE_Y * 0.25
+
+ while coord_x < IMAGE_SIZE_X:
+ if value_type == "binary":
+ text_width, text_height = draw.textsize(text, font=FONT)
+ text_coord_x = coord_x - (text_width / 2)
+ text_coord_y = base_coord_y - (text_height * .7)
draw.text(
- (coord_x, coord_y),
+ (text_coord_x, text_coord_y),
text,
- font=font,
+ font=FONT,
fill="#000"
)
else: # lightbulb
- coords = (text_coord_x - lightbulb_width, base_y_coord - lightbulb_height + 75)
+ coords = (
+ int(coord_x - (lightbulb_width / 2)),
+ int(base_coord_y - (lightbulb_height / 2)),
+ )
image.paste(lightbulb, box=coords, mask=lightbulb)
- text_coord_x += x_coord_increment
+ coord_x += coord_x_increment
return image
@property
diff --git a/csunplugged/resources/management/commands/makeresources.py b/csunplugged/resources/management/commands/makeresources.py
index 6ffbc4372..205227ee7 100644
--- a/csunplugged/resources/management/commands/makeresources.py
+++ b/csunplugged/resources/management/commands/makeresources.py
@@ -45,8 +45,7 @@ def handle(self, *args, **options):
else:
generation_languages = []
for language_code, _ in settings.LANGUAGES:
- if language_code not in settings.INCONTEXT_L10N_PSEUDOLANGUAGES:
- generation_languages.append(language_code)
+ generation_languages.append(language_code)
for resource in resources:
print("Creating {}".format(resource.name))
@@ -92,3 +91,4 @@ def create_resource_pdf(self, resource, combination, language_code, base_path):
pdf_file_output = open(os.path.join(pdf_directory, filename), "wb")
pdf_file_output.write(pdf_file)
pdf_file_output.close()
+ print(" - Created PDF '{}' in '{}'".format(filename, language_code))
diff --git a/csunplugged/resources/migrations/0021_auto_20220520_0454.py b/csunplugged/resources/migrations/0021_auto_20220520_0454.py
new file mode 100644
index 000000000..4891abe4a
--- /dev/null
+++ b/csunplugged/resources/migrations/0021_auto_20220520_0454.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.2.11 on 2022-05-20 04:54
+
+import django.contrib.postgres.indexes
+import django.contrib.postgres.search
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('resources', '0020_alter_resource_id'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='resource',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(null=True),
+ ),
+ migrations.AddIndex(
+ model_name='resource',
+ index=django.contrib.postgres.indexes.GinIndex(fields=['search_vector'], name='resources_r_search__82e125_gin'),
+ ),
+ ]
diff --git a/csunplugged/resources/migrations/0022_auto_20220821_2359.py b/csunplugged/resources/migrations/0022_auto_20220821_2359.py
new file mode 100644
index 000000000..895c7c881
--- /dev/null
+++ b/csunplugged/resources/migrations/0022_auto_20220821_2359.py
@@ -0,0 +1,29 @@
+# Generated by Django 3.2.15 on 2022-08-21 23:59
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('resources', '0021_auto_20220520_0454'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='resource',
+ name='content_xx_lr',
+ ),
+ migrations.RemoveField(
+ model_name='resource',
+ name='content_yy_rl',
+ ),
+ migrations.RemoveField(
+ model_name='resource',
+ name='name_xx_lr',
+ ),
+ migrations.RemoveField(
+ model_name='resource',
+ name='name_yy_rl',
+ ),
+ ]
diff --git a/csunplugged/resources/models.py b/csunplugged/resources/models.py
index 609baaf1e..271789ac1 100644
--- a/csunplugged/resources/models.py
+++ b/csunplugged/resources/models.py
@@ -4,6 +4,8 @@
from django.utils.translation import ugettext_lazy as _
from django.urls import reverse
from utils.TranslatableModel import TranslatableModel
+from django.contrib.postgres.search import SearchVectorField
+from django.contrib.postgres.indexes import GinIndex
class Resource(TranslatableModel):
@@ -17,6 +19,7 @@ class Resource(TranslatableModel):
generator_module = models.CharField(max_length=200)
copies = models.BooleanField()
content = models.TextField(default="")
+ search_vector = SearchVectorField(null=True)
def get_absolute_url(self):
"""Return the canonical URL for a resource.
@@ -37,7 +40,23 @@ def __str__(self):
"""
return self.name
+ def index_contents(self):
+ """Return dictionary for search indexing.
+
+ Returns:
+ Dictionary of content for search indexing. The dictionary keys
+ are the weightings of content, and the dictionary values
+ are strings of content to index.
+ """
+ return {
+ 'A': self.name,
+ 'B': self.content,
+ }
+
class Meta:
- """Meta class settings."""
+ """Meta options for model."""
ordering = ["name"]
+ indexes = [
+ GinIndex(fields=['search_vector'])
+ ]
diff --git a/csunplugged/resources/search_indexes.py b/csunplugged/resources/search_indexes.py
deleted file mode 100644
index 7a7f985b0..000000000
--- a/csunplugged/resources/search_indexes.py
+++ /dev/null
@@ -1,18 +0,0 @@
-"""Search index for resources model."""
-
-from haystack import indexes
-from resources.models import Resource
-
-
-class ResourceIndex(indexes.SearchIndex, indexes.Indexable):
- """Search index for Resource model."""
-
- text = indexes.CharField(document=True, use_template=True)
-
- def get_model(self):
- """Return the Resource model.
-
- Returns:
- Resource object.
- """
- return Resource
diff --git a/csunplugged/resources/utils/get_thumbnail.py b/csunplugged/resources/utils/get_thumbnail.py
index 81df8f8f8..6b3172ceb 100644
--- a/csunplugged/resources/utils/get_thumbnail.py
+++ b/csunplugged/resources/utils/get_thumbnail.py
@@ -35,8 +35,6 @@ def get_thumbnail_base(resource_slug):
"""
if settings.DEPLOYED:
resource_language = get_language()
- if resource_language in settings.INCONTEXT_L10N_PSEUDOLANGUAGES:
- resource_language = "en"
else:
resource_language = "en"
resource_thumbnail_base = join(
diff --git a/csunplugged/search/forms.py b/csunplugged/search/forms.py
deleted file mode 100644
index 8b0e82b91..000000000
--- a/csunplugged/search/forms.py
+++ /dev/null
@@ -1,65 +0,0 @@
-"""Module for custom search form."""
-from django import forms
-from haystack.forms import ModelSearchForm
-from topics.models import (
- Lesson,
- CurriculumIntegration,
- CurriculumArea,
-)
-
-
-class CustomSearchForm(ModelSearchForm):
- """Class for custom search form."""
-
- curriculum_areas = forms.ModelMultipleChoiceField(
- queryset=CurriculumArea.objects.all(),
- required=False,
- widget=None,
- )
-
- def search(self):
- """Search index based off query.
-
- This method overrides the default ModelSearchForm search method to
- modify the default result if a blank query string is given. The form
- returns all items instead of zero items if a blank string is given.
-
- The original search method checks if the form is valid, however
- with all fields being optional with no validation, the form is always
- valid. Therefore logic for an invalid form is removed.
-
- Returns:
- SearchQuerySet of search results.
- """
- if not self.cleaned_data.get('q'):
- search_query_set = all_items(self.searchqueryset.all())
- else:
- search_query_set = self.searchqueryset.auto_query(self.cleaned_data['q'])
- search_query_set = search_query_set.models(*self.get_models())
-
- # Filter items by curriculum areas if given in query.
- if self.cleaned_data["curriculum_areas"]:
- # Currently the given filter is provided as a QuerySet, but the search
- # index saves the curriculum areas for objects as a list of primary
- # keys, stored as strings. Because of this, the logic below must
- # covert the QuerySet of the filter into a list of primary key strings.
- query_ids = list(map(str, self.cleaned_data["curriculum_areas"].values_list("pk", flat=True)))
- # Only query models with curriculum_areas attribute.
- models = set(self.get_models()).intersection([Lesson, CurriculumIntegration])
- search_query_set = search_query_set.models(*models).filter(
- curriculum_areas__in=query_ids
- )
-
- return search_query_set
-
-
-def all_items(searchqueryset):
- """Return all items of SearchQuerySet.
-
- Args:
- searchqueryset (SearchQuerySet): QuerySet of search items.
-
- Returns:
- All items in index.
- """
- return searchqueryset.all()
diff --git a/csunplugged/search/management/commands/rebuild_index.py b/csunplugged/search/management/commands/rebuild_index.py
deleted file mode 100644
index 84421fb70..000000000
--- a/csunplugged/search/management/commands/rebuild_index.py
+++ /dev/null
@@ -1,38 +0,0 @@
-"""Module for the overriden Django Haystack rebuild_index command."""
-
-from haystack.management.commands import rebuild_index
-from haystack.query import SearchQuerySet
-from search.forms import all_items
-from resources.models import Resource
-from general.models import GeneralPage
-from classic.models import ClassicPage
-from topics.models import (
- Topic,
- UnitPlan,
- Lesson,
- ProgrammingChallenge,
- CurriculumIntegration,
-)
-
-
-class Command(rebuild_index.Command):
- """Command class for the custom Django rebuild_index command."""
-
- help = "Rebuild search index and check empty query returns all items."
-
- def handle(self, *args, **options):
- """Automatically called when the rebuild_index command is given."""
- total_objects = Resource.objects.count()
- total_objects += Topic.objects.count()
- total_objects += UnitPlan.objects.count()
- total_objects += Lesson.objects.count()
- total_objects += ProgrammingChallenge.objects.count()
- total_objects += CurriculumIntegration.objects.count()
- total_objects += GeneralPage.objects.count()
- total_objects += ClassicPage.objects.count()
- super(Command, self).handle(*args, **options)
- total_results = len(all_items(SearchQuerySet()))
- if total_objects == total_results:
- print("Search index loaded with {} items.".format(total_results))
- else: # pragma: no cover
- raise Exception("Search all_items() method does not return all items.")
diff --git a/csunplugged/search/management/commands/rebuild_search_indexes.py b/csunplugged/search/management/commands/rebuild_search_indexes.py
new file mode 100644
index 000000000..e02c5067d
--- /dev/null
+++ b/csunplugged/search/management/commands/rebuild_search_indexes.py
@@ -0,0 +1,19 @@
+"""Module for the custom Django rebuild_search_indexes command."""
+
+from django.core import management
+from django.db import transaction
+from search.utils import get_search_index_updater
+from search.settings import SEARCH_CLASSES_AND_BOOSTS
+
+
+class Command(management.base.BaseCommand):
+ """Required command class for the custom Django rebuild_search_indexes command."""
+
+ help = "Rebuild search indexes in database."
+
+ def handle(self, *args, **options):
+ """Automatically called when the command is given."""
+ for model_data in SEARCH_CLASSES_AND_BOOSTS:
+ model = model_data['class']
+ for instance in model.objects.all():
+ transaction.on_commit(get_search_index_updater(instance))
diff --git a/csunplugged/search/settings.py b/csunplugged/search/settings.py
new file mode 100644
index 000000000..b56314980
--- /dev/null
+++ b/csunplugged/search/settings.py
@@ -0,0 +1,118 @@
+"""Settings for the search application."""
+
+from topics.models import (
+ Topic,
+ Lesson,
+ ProgrammingChallenge,
+ CurriculumIntegration,
+ LessonNumber,
+ CurriculumArea,
+)
+from classic.models import ClassicPage
+from resources.models import Resource
+from general.models import GeneralPage
+from at_home.models import Activity
+from search.utils import (
+ get_search_model_types,
+ get_model_filter_options,
+)
+
+SEARCH_PAGINATION = 25
+SEARCH_RESULT_TEMPLATE_DIRECTORY = 'search/result/'
+
+
+def lesson_render_function(lesson):
+ """Provide additional context for rendering lesson results.
+
+ Args:
+ lesson (Lesson): Instance of search result.
+
+ Returns:
+ Dictionary of additional items to add to the render context.
+ """
+ lesson_ages = []
+ lesson_numbers = LessonNumber.objects.filter(lesson=lesson).select_related('age_group')
+ for lesson_number in lesson_numbers:
+ lesson_ages.append(
+ {
+ "lower": lesson_number.age_group.ages.lower,
+ "upper": lesson_number.age_group.ages.upper,
+ "number": lesson_number.number,
+ }
+ )
+ curriculum_areas = CurriculumArea.objects.filter(
+ learning_outcomes__in=lesson.learning_outcomes.all()
+ ).select_related('parent').distinct()
+
+ additional_context = {
+ 'lesson_ages': lesson_ages,
+ 'curriculum_areas': curriculum_areas,
+ }
+ return additional_context
+
+
+# List of dicts of class, boost value, required prefetches.
+SEARCH_CLASSES_AND_BOOSTS = [
+ {
+ 'class': Topic,
+ 'boost': 2,
+ 'curriculum_area_filtered': False,
+ 'select_related': [],
+ 'prefetch_related': [],
+ },
+ {
+ 'class': Lesson,
+ 'boost': 1.2,
+ 'curriculum_area_filtered': True,
+ 'select_related': ['topic'],
+ 'prefetch_related': ['learning_outcomes'],
+ 'render_function': lesson_render_function,
+ },
+ {
+ 'class': Resource,
+ 'boost': 1,
+ 'curriculum_area_filtered': False,
+ 'select_related': [],
+ 'prefetch_related': [],
+ },
+ {
+ 'class': GeneralPage,
+ 'boost': 0.9,
+ 'curriculum_area_filtered': False,
+ 'select_related': [],
+ 'prefetch_related': [],
+ },
+ {
+ 'class': Activity,
+ 'boost': 0.8,
+ 'curriculum_area_filtered': False,
+ 'select_related': [],
+ 'prefetch_related': [],
+ },
+ {
+ 'class': CurriculumIntegration,
+ 'boost': 0.7,
+ 'curriculum_area_filtered': True,
+ 'select_related': ['topic'],
+ 'prefetch_related': ['curriculum_areas', 'curriculum_areas__parent'],
+ },
+ {
+ 'class': ClassicPage,
+ 'boost': 0.6,
+ 'curriculum_area_filtered': False,
+ 'select_related': [],
+ 'prefetch_related': [],
+ },
+ {
+ 'class': ProgrammingChallenge,
+ 'boost': 0.4,
+ 'curriculum_area_filtered': False,
+ 'select_related': ['topic'],
+ 'prefetch_related': ['difficulty'],
+ },
+]
+
+
+# Settings calculated from SEARCH_MODEL_TYPES
+SEARCH_MODEL_TYPES = get_search_model_types(SEARCH_CLASSES_AND_BOOSTS)
+SEARCH_MODEL_FILTER_VALUES = get_model_filter_options(SEARCH_MODEL_TYPES)
diff --git a/csunplugged/search/templatetags/__init__.py b/csunplugged/search/templatetags/__init__.py
new file mode 100644
index 000000000..23ff4ca91
--- /dev/null
+++ b/csunplugged/search/templatetags/__init__.py
@@ -0,0 +1 @@
+"""Module for the templatetags for the search application."""
diff --git a/csunplugged/search/templatetags/search_result.py b/csunplugged/search/templatetags/search_result.py
new file mode 100644
index 000000000..7eff6e3e7
--- /dev/null
+++ b/csunplugged/search/templatetags/search_result.py
@@ -0,0 +1,20 @@
+"""Module for the search result template tag."""
+
+from os.path import join
+from django.template.defaulttags import register
+from django.template.loader import render_to_string
+from search.utils import get_search_model_id
+from search.settings import SEARCH_RESULT_TEMPLATE_DIRECTORY, SEARCH_MODEL_TYPES
+
+
+@register.simple_tag
+def search_result_template(result):
+ """Return the value in a dictionary given a key."""
+ class_id = get_search_model_id(type(result))
+ context = {'result': result}
+
+ render_func = SEARCH_MODEL_TYPES[class_id].get('render_function', None)
+ if render_func:
+ context.update(render_func(result))
+ template = join(SEARCH_RESULT_TEMPLATE_DIRECTORY, class_id + '.html')
+ return render_to_string(template, context)
diff --git a/csunplugged/search/urls.py b/csunplugged/search/urls.py
index cb409670b..18211dc43 100644
--- a/csunplugged/search/urls.py
+++ b/csunplugged/search/urls.py
@@ -9,7 +9,7 @@
# eg: /search/
path(
'',
- views.CustomSearchView.as_view(),
+ views.SearchView.as_view(),
name="index"
),
]
diff --git a/csunplugged/search/utils.py b/csunplugged/search/utils.py
new file mode 100644
index 000000000..d99ff9c2c
--- /dev/null
+++ b/csunplugged/search/utils.py
@@ -0,0 +1,138 @@
+"""Search utility functions."""
+
+import copy
+from django.db.models import Value
+from django.contrib.postgres.search import SearchVector
+from lxml.html import fromstring
+from lxml.cssselect import CSSSelector
+from django.template.loader import render_to_string
+from django.template.exceptions import TemplateSyntaxError
+
+CONTENT_NOT_FOUND_ERROR_MESSAGE = ("General page requires content wrapped in "
+ "an element with ID 'general-page-content'")
+
+
+def concat_field_values(*args):
+ """Return string of field values for search indexing.
+
+ Args:
+ Any number of QuerySet objects, the result of value_list calls.
+
+ Returns:
+ String for search indexing.
+ """
+ field_names = []
+ for queryset in args:
+ for instance in queryset:
+ for field in instance:
+ field_names.append(str(field))
+ return ' '.join(field_names)
+
+
+def get_search_index_updater(instance):
+ """Return function for updating search index of instance."""
+ components = instance.index_contents()
+ pk = instance.pk
+
+ def on_commit():
+ search_vector_list = []
+ for weight, text in components.items():
+ search_vector_list.append(
+ SearchVector(Value(text), weight=weight)
+ )
+ search_vectors = search_vector_list[0]
+ for search_vector in search_vector_list[1:]:
+ search_vectors += search_vector
+
+ instance.__class__.objects.filter(pk=pk).update(
+ search_vector=search_vectors
+ )
+ print(f'Rebuilt index for {instance} ({instance.__class__})')
+ return on_commit
+
+
+def get_template_text(template):
+ """Return text for indexing.
+
+ Args:
+ template (string): Path to template to get text from.
+
+ Returns:
+ String for indexing.
+ """
+ rendered = render_to_string(template, {"LANGUAGE_CODE": "en"})
+ html = fromstring(rendered)
+ selector = CSSSelector("#general-page-content")
+ try:
+ contents = selector(html)[0].text_content()
+ except IndexError:
+ raise TemplateSyntaxError(CONTENT_NOT_FOUND_ERROR_MESSAGE)
+ return contents
+
+
+def get_search_model_types(search_model_types_list):
+ """Return dictionary of search model types.
+
+ Args:
+ search_model_types: List of dicts of search base data to
+ to be fleshed out into full dictionary.
+
+ Return:
+ Dictionary of search model types, keyed by search model ID.
+ """
+ model_types = dict()
+ for model_data in search_model_types_list:
+ model_types[get_search_model_id(model_data['class'])] = model_data
+ return model_types
+
+
+def get_search_model_id(model):
+ """Return search ID for model.
+
+ Args:
+ model: Class to get search ID for.
+
+ Returns:
+ String identifying class.
+ """
+ module_name = model._meta.app_label.lower()
+ class_name = model._meta.object_name.lower()
+ return f'{module_name}.{class_name}'
+
+
+def get_model_filter_options(search_model_types):
+ """Return of model options for search type filter.
+
+ Args:
+ search_model_types (dict): Dictionary of search model type data.
+
+ Return:
+ List of dictionaries of model options.
+ """
+ options = []
+ for (model_id, model_data) in search_model_types.items():
+ options.append(
+ {
+ 'name': model_data['class'].MODEL_NAME,
+ 'value': model_id,
+ 'selected': False,
+ }
+ )
+ return options
+
+
+def updated_model_filter_options(model_options, selected_options):
+ """Update a set of model filters with selected options.
+
+ Args:
+ model_options (list): List of dictionaries of model options.
+ selected_options (list): List of selected values.
+
+ Returns:
+ Updated list of dictionaries of model options.
+ """
+ model_options = copy.deepcopy(model_options)
+ for model_option in model_options:
+ if model_option['value'] in selected_options:
+ model_option['selected'] = True
+ return model_options
diff --git a/csunplugged/search/views.py b/csunplugged/search/views.py
index 780b45016..a7a80b55a 100644
--- a/csunplugged/search/views.py
+++ b/csunplugged/search/views.py
@@ -1,91 +1,123 @@
"""Module for custom search view."""
-from haystack.generic_views import SearchView
-from haystack.query import EmptySearchQuerySet
-from django.db.models.functions import Concat
-from search.forms import CustomSearchForm
-from topics.models import (
- CurriculumArea,
- LessonNumber,
-)
-
-class CustomSearchView(SearchView):
- """View for custom search."""
+from itertools import chain
+from django.views import generic
+from django.db.models import (
+ F,
+ # Value,
+ # BooleanField,
+ # Case,
+ # When,
+)
+from django.contrib.postgres.search import (
+ SearchQuery,
+ SearchRank,
+)
+# from topics.models import CurriculumArea
+from search.settings import (
+ SEARCH_MODEL_TYPES,
+ SEARCH_MODEL_FILTER_VALUES,
+)
+from search.utils import updated_model_filter_options
- form_class = CustomSearchForm
- def get_queryset(self):
- """Return the list of items for this view.
+class SearchView(generic.TemplateView):
+ """View for search."""
- This method overrides the default method, as all items were being
- returned when no query parameters was given (not a blank query).
-
- Returns:
- QuerySet for view.
- """
- if self.request.GET:
- return super(CustomSearchView, self).get_queryset()
- else:
- return EmptySearchQuerySet()
+ template_name = 'search/index.html'
def get_context_data(self, *args, **kwargs):
- """Return context dictionary for custom search view.
+ """Return context dictionary for search view.
Returns:
Dictionary of context values.
"""
- context = super(CustomSearchView, self).get_context_data(*args, **kwargs)
- context["search"] = bool(self.request.GET)
-
- # Model filter
- selected_models = self.request.GET.getlist("models")
- models_tuples = context["form"].fields["models"].choices
- models = []
- for (model_value, model_name) in models_tuples:
- model_data = {
- "value": model_value,
- "name": model_name,
- }
- if model_value in selected_models:
- model_data["selected"] = "true"
- if model_value == "classic.classicpage":
- model_data["name"] = "Classic CS Unplugged pages"
- models.append(model_data)
- context["models"] = models
-
- # Curriculum area filter
- selected_curriculum_areas = self.request.GET.getlist("curriculum_areas")
- curriculum_areas = list(CurriculumArea.objects.annotate(
- display_name=Concat("parent__name", "name")
- ).order_by("display_name").values("pk", "colour", "name", "parent__name", "parent__pk"))
- grouped_curriculum_areas = []
- for curriculum_area in curriculum_areas:
- if selected_curriculum_areas:
- if str(curriculum_area["pk"]) in selected_curriculum_areas:
- curriculum_area["selected"] = "true"
- parent_pk = curriculum_area["parent__pk"]
- if parent_pk:
- grouped_curriculum_areas[-1]["children"].append(curriculum_area)
- else:
- curriculum_area["children"] = []
- grouped_curriculum_areas.append(curriculum_area)
- context["curriculum_areas"] = grouped_curriculum_areas
-
- # Update result objects
- for result in context["object_list"]:
- if result.model_name == "lesson":
- lesson_ages = []
- for age_group in result.object.age_group.order_by("ages"):
- number = LessonNumber.objects.get(lesson=result.object, age_group=age_group).number
- lesson_ages.append(
- {
- "lower": age_group.ages.lower,
- "upper": age_group.ages.upper,
- "number": number,
- }
+ context = super().get_context_data(*args, **kwargs)
+ context['models'] = SEARCH_MODEL_FILTER_VALUES
+
+ # Get request query parmaters
+ query_text = self.request.GET.get('q')
+ selected_models = self.request.GET.getlist('models')
+ # selected_curriculum_areas = self.request.GET.getlist('curriculum_areas')
+ get_request = bool(self.request.GET)
+
+ if get_request:
+ context['search'] = get_request
+
+ search_request_data = SEARCH_MODEL_TYPES.copy()
+ models_to_ignore = []
+
+ for (model_id, model_data) in search_request_data.items():
+ if not selected_models or model_id in selected_models:
+ model_data['results'] = model_data['class'].objects.all()
+ else:
+ models_to_ignore.append(model_id)
+
+ # Delete unused models after iterating
+ for model_id in models_to_ignore:
+ del search_request_data[model_id]
+
+ # Search by text query if provided
+ if query_text:
+ query = SearchQuery(query_text, search_type="websearch")
+ for (model_id, model_data) in search_request_data.items():
+ model_data['results'] = model_data['results'].filter(
+ search_vector=query
+ ).annotate(
+ rank=SearchRank(
+ F('search_vector'), query
+ ) + model_data['boost']
+ ).filter(
+ rank__gt=0
+ ).order_by(
+ '-rank'
+ )
+
+ # Concatenate all results
+ results = []
+ for model_data in search_request_data.values():
+ results.append(
+ model_data['results'].select_related(
+ *model_data['select_related']
+ ).prefetch_related(
+ *model_data['prefetch_related']
)
- result.lesson_ages = lesson_ages
- result.curriculum_areas = CurriculumArea.objects.filter(
- learning_outcomes__in=result.object.learning_outcomes.all()
- ).distinct()
+ )
+ results = chain(*results)
+
+ # Order results if query text provided
+ if query_text:
+ results = sorted(
+ results,
+ key=lambda instance: instance.rank,
+ reverse=True,
+ )
+ else:
+ results = list(results)
+
+ # Organise curriculum areas
+ # curriculum_areas = CurriculumArea.objects.order_by(
+ # 'number', '-parent', 'name'
+ # ).values(
+ # 'pk', 'colour', 'name', 'parent__pk'
+ # )
+ # grouped_curriculum_areas = []
+ # for curriculum_area in curriculum_areas:
+ # if selected_curriculum_areas:
+ # if str(curriculum_area['pk']) in selected_curriculum_areas:
+ # curriculum_area['selected'] = 'true'
+ # if curriculum_area['parent__pk']:
+ # grouped_curriculum_areas[-1]['children'].append(curriculum_area)
+ # else:
+ # curriculum_area['children'] = []
+ # grouped_curriculum_areas.append(curriculum_area)
+
+ context['query'] = query_text
+ context['models'] = updated_model_filter_options(
+ SEARCH_MODEL_FILTER_VALUES,
+ selected_models,
+ )
+ # context['curriculum_areas'] = grouped_curriculum_areas
+ context['results'] = results
+ context['results_count'] = len(results)
return context
diff --git a/third-party-licences/sniglet.txt b/csunplugged/static/fonts/CSUnplugged-Headings-LICENSE.txt
similarity index 72%
rename from third-party-licences/sniglet.txt
rename to csunplugged/static/fonts/CSUnplugged-Headings-LICENSE.txt
index 68d725136..02770b0a6 100644
--- a/third-party-licences/sniglet.txt
+++ b/csunplugged/static/fonts/CSUnplugged-Headings-LICENSE.txt
@@ -1,19 +1,18 @@
-Copyright (c) 2008, Haley Fiege , with Reserved Font Name: "Sniglet".
+Copyright (c) 2008, Haley Fiege (haley@kingdomofawesome.com)
+Copyright (c) 2012, Brenda Gallo (gbrenda1987@gmail.com)
+Copyright (c) 2013, Pablo Impallari (www.impallari.com|impallari@gmail.com)
+Copyright (c) 2022, University of Canterbury Computer Science Education Research Group (https://github.com/uccser/cs-unplugged-font|csse-education-research@canterbury.ac.nz)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
-SIL Open Font License
-====================================================
-
-
-Preamble
-----------
-
+PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
@@ -29,71 +28,63 @@ however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
-Definitions
--------------
-
-`"Font Software"` refers to the set of files released by the Copyright
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
-`"Reserved Font Name"` refers to any names specified as such after the
+"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
-`"Original Version"` refers to the collection of Font Software components as
+"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
-`"Modified Version"` refers to any derivative made by adding to, deleting,
+"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
-`"Author"` refers to any designer, engineer, programmer, technical
+"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
-Permission & Conditions
-------------------------
-
+PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
-1. Neither the Font Software nor any of its individual components,
+1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
-2. Original or Modified Versions of the Font Software may be bundled,
+2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
-3. No Modified Version of the Font Software may use the Reserved Font
+3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
-4. The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
-5. The Font Software, modified or unmodified, in part or in whole,
+5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
-Termination
------------
-
+TERMINATION
This license becomes null and void if any of the above conditions are
not met.
-
DISCLAIMER
-
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
diff --git a/csunplugged/static/fonts/CSUnplugged-Headings.otf b/csunplugged/static/fonts/CSUnplugged-Headings.otf
new file mode 100644
index 000000000..c905cbc8f
Binary files /dev/null and b/csunplugged/static/fonts/CSUnplugged-Headings.otf differ
diff --git a/csunplugged/static/img/at_a_distance/algorithms/scratch-program.svg b/csunplugged/static/img/at_a_distance/algorithms/scratch-program.svg
new file mode 100644
index 000000000..69822cb76
--- /dev/null
+++ b/csunplugged/static/img/at_a_distance/algorithms/scratch-program.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/csunplugged/static/img/at_a_distance/button-download-slides.svg b/csunplugged/static/img/at_a_distance/button-download-slides.svg
new file mode 100755
index 000000000..c990a59c4
--- /dev/null
+++ b/csunplugged/static/img/at_a_distance/button-download-slides.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/csunplugged/static/img/at_a_distance/button-download-speaker-notes.svg b/csunplugged/static/img/at_a_distance/button-download-speaker-notes.svg
new file mode 100755
index 000000000..00ea12f7e
--- /dev/null
+++ b/csunplugged/static/img/at_a_distance/button-download-speaker-notes.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/csunplugged/static/img/at_a_distance/button-open-slides.svg b/csunplugged/static/img/at_a_distance/button-open-slides.svg
new file mode 100755
index 000000000..02b596e5e
--- /dev/null
+++ b/csunplugged/static/img/at_a_distance/button-open-slides.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/csunplugged/static/img/at_a_distance/computer-thumbs-up.png b/csunplugged/static/img/at_a_distance/computer-thumbs-up.png
new file mode 100644
index 000000000..3612156eb
Binary files /dev/null and b/csunplugged/static/img/at_a_distance/computer-thumbs-up.png differ
diff --git a/csunplugged/static/img/at_a_distance/finite-state-automata/email-prompt.svg b/csunplugged/static/img/at_a_distance/finite-state-automata/email-prompt.svg
new file mode 100755
index 000000000..6525fbf2a
--- /dev/null
+++ b/csunplugged/static/img/at_a_distance/finite-state-automata/email-prompt.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/csunplugged/static/img/at_a_distance/finite-state-automata/fsa.svg b/csunplugged/static/img/at_a_distance/finite-state-automata/fsa.svg
new file mode 100755
index 000000000..26e58ce7e
--- /dev/null
+++ b/csunplugged/static/img/at_a_distance/finite-state-automata/fsa.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/csunplugged/static/img/at_a_distance/stroop-effect/airfare-prompt.png b/csunplugged/static/img/at_a_distance/stroop-effect/airfare-prompt.png
new file mode 100755
index 000000000..b62860d31
Binary files /dev/null and b/csunplugged/static/img/at_a_distance/stroop-effect/airfare-prompt.png differ
diff --git a/csunplugged/static/img/at_a_distance/stroop-effect/date-prompt.png b/csunplugged/static/img/at_a_distance/stroop-effect/date-prompt.png
new file mode 100755
index 000000000..3778e790a
Binary files /dev/null and b/csunplugged/static/img/at_a_distance/stroop-effect/date-prompt.png differ
diff --git a/csunplugged/static/img/at_a_distance/stroop-effect/quit-prompt.png b/csunplugged/static/img/at_a_distance/stroop-effect/quit-prompt.png
new file mode 100755
index 000000000..a36965799
Binary files /dev/null and b/csunplugged/static/img/at_a_distance/stroop-effect/quit-prompt.png differ
diff --git a/csunplugged/static/img/at_a_distance/teaching-online.png b/csunplugged/static/img/at_a_distance/teaching-online.png
new file mode 100755
index 000000000..c95f596b5
Binary files /dev/null and b/csunplugged/static/img/at_a_distance/teaching-online.png differ
diff --git a/csunplugged/static/img/at_a_distance/tile-background.png b/csunplugged/static/img/at_a_distance/tile-background.png
new file mode 100755
index 000000000..47ebfa604
Binary files /dev/null and b/csunplugged/static/img/at_a_distance/tile-background.png differ
diff --git a/csunplugged/static/img/at_home/unlocking-the-secret-in-product-codes/README.txt b/csunplugged/static/img/at_home/unlocking-the-secret-in-product-codes/README.txt
new file mode 100644
index 000000000..7f137727f
--- /dev/null
+++ b/csunplugged/static/img/at_home/unlocking-the-secret-in-product-codes/README.txt
@@ -0,0 +1,2 @@
+Barcode SVGs are created with https://online-barcode-generator.net/
+The parameter is set to EAN-13, and the resulting SVG is upscaled, and text changed to paths.
diff --git a/csunplugged/static/img/at_home/unlocking-the-secret-in-product-codes/unlocking-the-secret-in-product-codes-challenge-4.jpg b/csunplugged/static/img/at_home/unlocking-the-secret-in-product-codes/unlocking-the-secret-in-product-codes-challenge-4.jpg
deleted file mode 100644
index 545fbdcc8..000000000
Binary files a/csunplugged/static/img/at_home/unlocking-the-secret-in-product-codes/unlocking-the-secret-in-product-codes-challenge-4.jpg and /dev/null differ
diff --git a/csunplugged/static/img/at_home/unlocking-the-secret-in-product-codes/unlocking-the-secret-in-product-codes-challenge-4.svg b/csunplugged/static/img/at_home/unlocking-the-secret-in-product-codes/unlocking-the-secret-in-product-codes-challenge-4.svg
new file mode 100644
index 000000000..defc02d64
--- /dev/null
+++ b/csunplugged/static/img/at_home/unlocking-the-secret-in-product-codes/unlocking-the-secret-in-product-codes-challenge-4.svg
@@ -0,0 +1,320 @@
+
+
diff --git a/csunplugged/static/img/at_home/unlocking-the-secret-in-product-codes/unlocking-the-secret-in-product-codes-challenge-5.jpg b/csunplugged/static/img/at_home/unlocking-the-secret-in-product-codes/unlocking-the-secret-in-product-codes-challenge-5.jpg
deleted file mode 100644
index 93c2a4437..000000000
Binary files a/csunplugged/static/img/at_home/unlocking-the-secret-in-product-codes/unlocking-the-secret-in-product-codes-challenge-5.jpg and /dev/null differ
diff --git a/csunplugged/static/img/at_home/unlocking-the-secret-in-product-codes/unlocking-the-secret-in-product-codes-challenge-5.svg b/csunplugged/static/img/at_home/unlocking-the-secret-in-product-codes/unlocking-the-secret-in-product-codes-challenge-5.svg
new file mode 100644
index 000000000..acbb7f276
--- /dev/null
+++ b/csunplugged/static/img/at_home/unlocking-the-secret-in-product-codes/unlocking-the-secret-in-product-codes-challenge-5.svg
@@ -0,0 +1,327 @@
+
+
diff --git a/csunplugged/static/img/cs-unplugged-logo-at-a-distance-white.svg b/csunplugged/static/img/cs-unplugged-logo-at-a-distance-white.svg
new file mode 100755
index 000000000..52d94b9a9
--- /dev/null
+++ b/csunplugged/static/img/cs-unplugged-logo-at-a-distance-white.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/csunplugged/static/img/cs-unplugged-logo-at-a-distance.svg b/csunplugged/static/img/cs-unplugged-logo-at-a-distance.svg
new file mode 100755
index 000000000..0ee77df26
--- /dev/null
+++ b/csunplugged/static/img/cs-unplugged-logo-at-a-distance.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/csunplugged/static/img/plugging-it-in/tile-background.png b/csunplugged/static/img/plugging-it-in/tile-background.png
new file mode 100644
index 000000000..57eeaabcf
Binary files /dev/null and b/csunplugged/static/img/plugging-it-in/tile-background.png differ
diff --git a/csunplugged/static/img/resources/binary-windows/binary-windows-1-to-8.png b/csunplugged/static/img/resources/binary-windows/binary-windows-1-to-8.png
deleted file mode 100644
index 96738e677..000000000
Binary files a/csunplugged/static/img/resources/binary-windows/binary-windows-1-to-8.png and /dev/null differ
diff --git a/csunplugged/static/img/resources/binary-windows/binary-windows-16-to-128.png b/csunplugged/static/img/resources/binary-windows/binary-windows-16-to-128.png
deleted file mode 100644
index 935fe9b55..000000000
Binary files a/csunplugged/static/img/resources/binary-windows/binary-windows-16-to-128.png and /dev/null differ
diff --git a/csunplugged/static/img/resources/binary-windows/binary-windows-blank.png b/csunplugged/static/img/resources/binary-windows/binary-windows-blank.png
deleted file mode 100644
index c32e75c71..000000000
Binary files a/csunplugged/static/img/resources/binary-windows/binary-windows-blank.png and /dev/null differ
diff --git a/csunplugged/static/img/resources/binary-windows/dots-1.png b/csunplugged/static/img/resources/binary-windows/dots-1.png
new file mode 100755
index 000000000..0c32356e3
Binary files /dev/null and b/csunplugged/static/img/resources/binary-windows/dots-1.png differ
diff --git a/csunplugged/static/img/resources/binary-windows/dots-128.png b/csunplugged/static/img/resources/binary-windows/dots-128.png
new file mode 100755
index 000000000..b858bdff3
Binary files /dev/null and b/csunplugged/static/img/resources/binary-windows/dots-128.png differ
diff --git a/csunplugged/static/img/resources/binary-windows/dots-16.png b/csunplugged/static/img/resources/binary-windows/dots-16.png
new file mode 100755
index 000000000..792032aac
Binary files /dev/null and b/csunplugged/static/img/resources/binary-windows/dots-16.png differ
diff --git a/csunplugged/static/img/resources/binary-windows/dots-2.png b/csunplugged/static/img/resources/binary-windows/dots-2.png
new file mode 100755
index 000000000..36444bc0a
Binary files /dev/null and b/csunplugged/static/img/resources/binary-windows/dots-2.png differ
diff --git a/csunplugged/static/img/resources/binary-windows/dots-32.png b/csunplugged/static/img/resources/binary-windows/dots-32.png
new file mode 100755
index 000000000..1c762ce20
Binary files /dev/null and b/csunplugged/static/img/resources/binary-windows/dots-32.png differ
diff --git a/csunplugged/static/img/resources/binary-windows/dots-4.png b/csunplugged/static/img/resources/binary-windows/dots-4.png
new file mode 100755
index 000000000..f9b33a35a
Binary files /dev/null and b/csunplugged/static/img/resources/binary-windows/dots-4.png differ
diff --git a/csunplugged/static/img/resources/binary-windows/dots-64.png b/csunplugged/static/img/resources/binary-windows/dots-64.png
new file mode 100755
index 000000000..4312ca3f7
Binary files /dev/null and b/csunplugged/static/img/resources/binary-windows/dots-64.png differ
diff --git a/csunplugged/static/img/resources/binary-windows/dots-8.png b/csunplugged/static/img/resources/binary-windows/dots-8.png
new file mode 100755
index 000000000..0d7683da2
Binary files /dev/null and b/csunplugged/static/img/resources/binary-windows/dots-8.png differ
diff --git a/csunplugged/static/img/resources/binary-windows/lightbulb-off.png b/csunplugged/static/img/resources/binary-windows/lightbulb-off.png
new file mode 100644
index 000000000..d8139f25c
Binary files /dev/null and b/csunplugged/static/img/resources/binary-windows/lightbulb-off.png differ
diff --git a/csunplugged/static/img/resources/binary-windows/lightbulb-on.png b/csunplugged/static/img/resources/binary-windows/lightbulb-on.png
new file mode 100644
index 000000000..16e076884
Binary files /dev/null and b/csunplugged/static/img/resources/binary-windows/lightbulb-on.png differ
diff --git a/csunplugged/static/js/at-a-distance.js b/csunplugged/static/js/at-a-distance.js
new file mode 100644
index 000000000..25cac0508
--- /dev/null
+++ b/csunplugged/static/js/at-a-distance.js
@@ -0,0 +1,5 @@
+// Set to global scope to be accessible by inpage JavaScript
+// defined in templates/at_a_distance/components/reveal-initialize.html
+window.CSU_ATD_Reveal = require('reveal.js');
+window.CSU_ATD_SpeakerNotes = require('./reveal-speaker-notes-plugin/reveal-plugin.js');
+window.CSU_ATD_Highlight = require('reveal.js/plugin/highlight/highlight.js');
diff --git a/csunplugged/static/js/reveal-speaker-notes-plugin/LICENSE b/csunplugged/static/js/reveal-speaker-notes-plugin/LICENSE
new file mode 100644
index 000000000..f82a0cc76
--- /dev/null
+++ b/csunplugged/static/js/reveal-speaker-notes-plugin/LICENSE
@@ -0,0 +1,19 @@
+Copyright (C) 2011-2022 Hakim El Hattab, http://hakim.se, and reveal.js contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/csunplugged/static/js/reveal-speaker-notes-plugin/README.md b/csunplugged/static/js/reveal-speaker-notes-plugin/README.md
new file mode 100644
index 000000000..f099f1ed2
--- /dev/null
+++ b/csunplugged/static/js/reveal-speaker-notes-plugin/README.md
@@ -0,0 +1,50 @@
+# Custom reveal.js speaker notes plugin
+
+A modified version of the reveal.js speaker notes plugin.
+
+- https://github.com/hakimel/reveal.js
+- https://github.com/hakimel/reveal.js/tree/master/plugin/notes
+
+The source repository is licensed under the MIT license.
+
+The original license is available within this directory, and also in `third-party-licences/revealjs.txt`.
+
+A big thank you to Hakim El Hattab for making such a great framework.
+
+## Changes from original
+
+- HTML for speaker view is served at a specific URL by Django.
+ This allows us to easier edit and link files to the HTML.
+- JavaScript for the speaker view has been moved to its own file for easier editing.
+- CSS for the speaker view has been moved to its own file for easier editing and been converted to SCSS.
+- Remove Markdown support for speaker notes.
+- Add feature to copy <pre> tag inside speaker notes to clipboard.
+- Replaced tall layout as default, and removed wide layout.
+- Customise speaker view layout and show controls help text.
+- Removed pacing timer as we don't use this feature. Timings cannot be suggested due to wide range of speakers (style, language, confidence, etc).
+- Scroll to top of notes when slide changes.
+
+## Files related to plugin
+
+```
+csunplugged/
+ static/
+ js/
+ reveal-speaker-notes-plugin/
+ LICENSE
+ README.md
+ reveal-plugin.js
+ speaker-notes-window.js
+ scss/
+ reveal-speaker-notes-plugin/
+ speaker-notes-window.scss
+ templates/
+ at_a_distance/
+ reveal-speaker-notes-plugin/
+ speaker-notes-window.html
+third-party-licences/
+ revealjs.txt
+```
+
+Logic for serving speaker notes view is found within the `at_a_distance` application (in particular the `views.py` and `urls.py` files.
+Some SCSS partials are imported for consistency with the custom theme used.
diff --git a/csunplugged/static/js/reveal-speaker-notes-plugin/reveal-plugin.js b/csunplugged/static/js/reveal-speaker-notes-plugin/reveal-plugin.js
new file mode 100644
index 000000000..b47536075
--- /dev/null
+++ b/csunplugged/static/js/reveal-speaker-notes-plugin/reveal-plugin.js
@@ -0,0 +1,256 @@
+/**
+ * Modified version of the reveal.js speaker notes plugin.
+ *
+ * Changes from original are listed in the README file.
+ *
+ * This script handles opening of and synchronization with the
+ * speaker notes window.
+ *
+ * Handshake process:
+ * 1. This window posts 'connect' to notes window
+ * - Includes URL of presentation to show
+ * 2. Notes window responds with 'connected' when it is available
+ * 3. This window proceeds to send the current presentation state
+ * to the notes window
+ */
+
+const Plugin = () => {
+
+ let connectInterval;
+ let speakerWindow = null;
+ let deck;
+
+ /**
+ * Opens a new speaker view window.
+ */
+ function openSpeakerWindow() {
+
+ // If a window is already open, focus it
+ if (speakerWindow && !speakerWindow.closed) {
+ speakerWindow.focus();
+ }
+ else {
+ speakerWindow = window.open('/at-a-distance/speaker-notes/', 'Speaker Notes', 'width=1200,height=800');
+
+ if (!speakerWindow) {
+ alert('Speaker view popup failed to open. Please make sure popups are allowed and reopen the speaker view.');
+ return;
+ }
+
+ connect();
+ }
+
+ }
+
+ /**
+ * Reconnect with an existing speaker view window.
+ */
+ function reconnectSpeakerWindow(reconnectWindow) {
+
+ if (speakerWindow && !speakerWindow.closed) {
+ speakerWindow.focus();
+ }
+ else {
+ speakerWindow = reconnectWindow;
+ window.addEventListener('message', onPostMessage);
+ onConnected();
+ }
+
+ }
+
+ /**
+ * Connect to the notes window through a postmessage handshake.
+ * Using postmessage enables us to work in situations where the
+ * origins differ, such as a presentation being opened from the
+ * file system.
+ */
+ function connect() {
+
+ const presentationURL = deck.getConfig().url;
+
+ const url = typeof presentationURL === 'string' ? presentationURL :
+ window.location.protocol + '//' + window.location.host + window.location.pathname + window.location.search;
+
+ // Keep trying to connect until we get a 'connected' message back
+ connectInterval = setInterval(function () {
+ speakerWindow.postMessage(JSON.stringify({
+ namespace: 'reveal-notes',
+ type: 'connect',
+ state: deck.getState(),
+ url
+ }), '*');
+ }, 500);
+
+ window.addEventListener('message', onPostMessage);
+
+ }
+
+ /**
+ * Calls the specified Reveal.js method with the provided argument
+ * and then pushes the result to the notes frame.
+ */
+ function callRevealApi(methodName, methodArguments, callId) {
+
+ let result = deck[methodName].apply(deck, methodArguments);
+ speakerWindow.postMessage(JSON.stringify({
+ namespace: 'reveal-notes',
+ type: 'return',
+ result,
+ callId
+ }), '*');
+
+ }
+
+ /**
+ * Posts the current slide data to the notes window.
+ */
+ function post(event) {
+
+ let slideElement = deck.getCurrentSlide(),
+ notesElement = slideElement.querySelector('aside.notes'),
+ fragmentElement = slideElement.querySelector('.current-fragment');
+
+ let messageData = {
+ namespace: 'reveal-notes',
+ type: 'state',
+ notes: '',
+ markdown: false,
+ whitespace: 'normal',
+ state: deck.getState()
+ };
+
+ // Look for notes defined in a slide attribute
+ if (slideElement.hasAttribute('data-notes')) {
+ messageData.notes = slideElement.getAttribute('data-notes');
+ messageData.whitespace = 'pre-wrap';
+ }
+
+ // Look for notes defined in a fragment
+ if (fragmentElement) {
+ let fragmentNotes = fragmentElement.querySelector('aside.notes');
+ if (fragmentNotes) {
+ notesElement = fragmentNotes;
+ }
+ else if (fragmentElement.hasAttribute('data-notes')) {
+ messageData.notes = fragmentElement.getAttribute('data-notes');
+ messageData.whitespace = 'pre-wrap';
+
+ // In case there are slide notes
+ notesElement = null;
+ }
+ }
+
+ // Look for notes defined in an aside element
+ if (notesElement) {
+ messageData.notes = notesElement.innerHTML;
+ messageData.markdown = typeof notesElement.getAttribute('data-markdown') === 'string';
+ }
+
+ speakerWindow.postMessage(JSON.stringify(messageData), '*');
+
+ }
+
+ /**
+ * Check if the given event is from the same origin as the
+ * current window.
+ */
+ function isSameOriginEvent(event) {
+
+ try {
+ return window.location.origin === event.source.location.origin;
+ }
+ catch (error) {
+ return false;
+ }
+
+ }
+
+ function onPostMessage(event) {
+
+ // Only allow same-origin messages
+ // (added 12/5/22 as a XSS safeguard)
+ if (isSameOriginEvent(event)) {
+
+ let data = JSON.parse(event.data);
+ if (data && data.namespace === 'reveal-notes' && data.type === 'connected') {
+ clearInterval(connectInterval);
+ onConnected();
+ }
+ else if (data && data.namespace === 'reveal-notes' && data.type === 'call') {
+ callRevealApi(data.methodName, data.arguments, data.callId);
+ }
+
+ }
+
+ }
+
+ /**
+ * Called once we have established a connection to the notes
+ * window.
+ */
+ function onConnected() {
+
+ // Monitor events that trigger a change in state
+ deck.on('slidechanged', post);
+ deck.on('fragmentshown', post);
+ deck.on('fragmenthidden', post);
+ deck.on('overviewhidden', post);
+ deck.on('overviewshown', post);
+ deck.on('paused', post);
+ deck.on('resumed', post);
+
+ // Post the initial state
+ post();
+
+ }
+
+ return {
+ id: 'notes',
+
+ init: function (reveal) {
+
+ deck = reveal;
+
+ if (!/receiver/i.test(window.location.search)) {
+
+ // If the there's a 'notes' query set, open directly
+ if (window.location.search.match(/(\?|\&)notes/gi) !== null) {
+ openSpeakerWindow();
+ }
+ else {
+ // Keep listening for speaker view hearbeats. If we receive a
+ // heartbeat from an orphaned window, reconnect it. This ensures
+ // that we remain connected to the notes even if the presentation
+ // is reloaded.
+ window.addEventListener('message', event => {
+
+ if (!speakerWindow && typeof event.data === 'string') {
+ let data;
+
+ try {
+ data = JSON.parse(event.data);
+ }
+ catch (error) { }
+
+ if (data && data.namespace === 'reveal-notes' && data.type === 'heartbeat') {
+ reconnectSpeakerWindow(event.source);
+ }
+ }
+ });
+ }
+
+ // Open the notes when the 's' key is hit
+ deck.addKeyBinding({ keyCode: 83, key: 'S', description: 'Speaker notes view' }, function () {
+ openSpeakerWindow();
+ });
+
+ }
+
+ },
+
+ open: openSpeakerWindow
+ };
+
+};
+
+module.exports = Plugin;
diff --git a/csunplugged/static/js/reveal-speaker-notes-plugin/speaker-notes-window.js b/csunplugged/static/js/reveal-speaker-notes-plugin/speaker-notes-window.js
new file mode 100644
index 000000000..ff9d59c01
--- /dev/null
+++ b/csunplugged/static/js/reveal-speaker-notes-plugin/speaker-notes-window.js
@@ -0,0 +1,488 @@
+/**
+ * Modified version of the reveal.js speaker notes plugin.
+ *
+ * Changes from original are listed in the README file.
+ */
+
+(function () {
+
+ var notes,
+ notesValue,
+ currentState,
+ currentSlide,
+ upcomingSlide,
+ layoutDropdown,
+ pendingCalls = {},
+ lastRevealApiCallId = 0,
+ connected = false
+
+ var connectionStatus = document.querySelector('#connection-status');
+
+ var SPEAKER_LAYOUTS = {
+ 'default': 'Default',
+ 'notes-only': 'Notes only'
+ };
+
+ setupLayout();
+
+ let openerOrigin;
+
+
+ try {
+ openerOrigin = window.opener.location.origin;
+ }
+ catch (error) { console.warn(error) }
+
+ // In order to prevent XSS, the speaker view will only run if its
+ // opener has the same origin as itself
+ if (window.location.origin !== openerOrigin) {
+ connectionStatus.innerHTML = 'Error: The speaker notes window can only be opened from the same origin and by a slide deck.';
+ return;
+ }
+
+ var connectionTimeout = setTimeout(function () {
+ connectionStatus.innerHTML = 'Error connecting to main window. Please try closing and reopening the speaker view.';
+ }, 5000);
+
+ window.addEventListener('message', function (event) {
+
+ clearTimeout(connectionTimeout);
+ connectionStatus.style.display = 'none';
+
+ var data = JSON.parse(event.data);
+
+ // The overview mode is only useful to the reveal.js instance
+ // where navigation occurs so we don't sync it
+ if (data.state) delete data.state.overview;
+
+ // Messages sent by the notes plugin inside of the main window
+ if (data && data.namespace === 'reveal-notes') {
+ if (data.type === 'connect') {
+ handleConnectMessage(data);
+ }
+ else if (data.type === 'state') {
+ handleStateMessage(data);
+ }
+ else if (data.type === 'return') {
+ pendingCalls[data.callId](data.result);
+ delete pendingCalls[data.callId];
+ }
+ }
+ // Messages sent by the reveal.js inside of the current slide preview
+ else if (data && data.namespace === 'reveal') {
+ if (/ready/.test(data.eventName)) {
+ // Send a message back to notify that the handshake is complete
+ window.opener.postMessage(JSON.stringify({ namespace: 'reveal-notes', type: 'connected' }), '*');
+ }
+ else if (/slidechanged|fragmentshown|fragmenthidden|paused|resumed/.test(data.eventName) && currentState !== JSON.stringify(data.state)) {
+
+ dispatchStateToMainWindow(data.state);
+
+ }
+ }
+
+ });
+
+ /**
+ * Updates the presentation in the main window to match the state
+ * of the presentation in the notes window.
+ */
+ const dispatchStateToMainWindow = debounce((state) => {
+ window.opener.postMessage(JSON.stringify({ method: 'setState', args: [state] }), '*');
+ }, 500);
+
+ /**
+ * Asynchronously calls the Reveal.js API of the main frame.
+ */
+ function callRevealApi(methodName, methodArguments, callback) {
+
+ var callId = ++lastRevealApiCallId;
+ pendingCalls[callId] = callback;
+ window.opener.postMessage(JSON.stringify({
+ namespace: 'reveal-notes',
+ type: 'call',
+ callId: callId,
+ methodName: methodName,
+ arguments: methodArguments
+ }), '*');
+
+ }
+
+ /**
+ * Called when the main window is trying to establish a
+ * connection.
+ */
+ function handleConnectMessage(data) {
+
+ if (connected === false) {
+ connected = true;
+
+ setupIframes(data);
+ setupKeyboard();
+ setupNotes();
+ setupTimer();
+ setupHeartbeat();
+ }
+
+ }
+
+ /**
+ * Called when the main window sends an updated state.
+ */
+ function handleStateMessage(data) {
+
+ // Store the most recently set state to avoid circular loops
+ // applying the same state
+ currentState = JSON.stringify(data.state);
+
+ // No need for updating the notes in case of fragment changes
+ if (data.notes) {
+ notes.classList.remove('hidden');
+ notesValue.style.whiteSpace = data.whitespace;
+ notesValue.innerHTML = data.notes;
+ notesValue.scrollTop = 0;
+ setupNoteCopyEvents();
+ }
+ else {
+ notes.classList.add('hidden');
+ }
+
+ // Update the note slides
+ currentSlide.contentWindow.postMessage(JSON.stringify({ method: 'setState', args: [data.state] }), '*');
+ upcomingSlide.contentWindow.postMessage(JSON.stringify({ method: 'setState', args: [data.state] }), '*');
+ upcomingSlide.contentWindow.postMessage(JSON.stringify({ method: 'next' }), '*');
+
+ }
+
+ // Limit to max one state update per X ms
+ handleStateMessage = debounce(handleStateMessage, 200);
+
+ /**
+ * Forward keyboard events to the current slide window.
+ * This enables keyboard events to work even if focus
+ * isn't set on the current slide iframe.
+ *
+ * Block F5 default handling, it reloads and disconnects
+ * the speaker notes window.
+ */
+ function setupKeyboard() {
+
+ document.addEventListener('keydown', function (event) {
+ if (event.keyCode === 116 || (event.metaKey && event.keyCode === 82)) {
+ event.preventDefault();
+ return false;
+ }
+ currentSlide.contentWindow.postMessage(JSON.stringify({ method: 'triggerKey', args: [event.keyCode] }), '*');
+ });
+
+ }
+
+ /**
+ * Creates the preview iframes.
+ */
+ function setupIframes(data) {
+
+ var params = [
+ 'receiver',
+ 'progress=false',
+ 'history=false',
+ 'transition=none',
+ 'autoSlide=0',
+ 'backgroundTransition=none',
+ 'hide-controls-modal',
+ ].join('&');
+
+ var urlSeparator = /\?/.test(data.url) ? '&' : '?';
+ var hash = '#/' + data.state.indexh + '/' + data.state.indexv;
+ var currentURL = data.url + urlSeparator + params + '&postMessageEvents=true' + hash;
+ var upcomingURL = data.url + urlSeparator + params + '&controls=false&keyboard=false' + hash;
+
+ currentSlide = document.createElement('iframe');
+ currentSlide.setAttribute('width', 1280);
+ currentSlide.setAttribute('height', 1024);
+ currentSlide.setAttribute('src', currentURL);
+ document.querySelector('#current-slide').appendChild(currentSlide);
+
+ upcomingSlide = document.createElement('iframe');
+ upcomingSlide.setAttribute('width', 640);
+ upcomingSlide.setAttribute('height', 512);
+ upcomingSlide.setAttribute('src', upcomingURL);
+ document.querySelector('#upcoming-slide').appendChild(upcomingSlide);
+
+ }
+
+ /**
+ * Setup the notes UI.
+ */
+ function setupNotes() {
+
+ notes = document.querySelector('#speaker-controls-notes');
+ notesValue = document.querySelector('#speaker-controls-notes .value');
+ }
+
+ /**
+ * We send out a heartbeat at all times to ensure we can
+ * reconnect with the main presentation window after reloads.
+ */
+ function setupHeartbeat() {
+
+ setInterval(() => {
+ window.opener.postMessage(JSON.stringify({ namespace: 'reveal-notes', type: 'heartbeat' }), '*');
+ }, 1000);
+
+ }
+
+ /**
+ * Return the number of seconds allocated for presenting
+ * all slides up to and including this one.
+ */
+ function getTimeAllocated(timings, callback) {
+
+ callRevealApi('getSlidePastCount', [], function (currentSlide) {
+ var allocated = 0;
+ for (var i in timings.slice(0, currentSlide + 1)) {
+ allocated += timings[i];
+ }
+ callback(allocated);
+ });
+
+ }
+
+ /**
+ * Create the timer and clock and start updating them
+ * at an interval.
+ */
+ function setupTimer() {
+
+ var start = new Date(),
+ timeEl = document.querySelector('#time-elapsed'),
+ hoursEl = timeEl.querySelector('.hours-value'),
+ minutesEl = timeEl.querySelector('.minutes-value'),
+ secondsEl = timeEl.querySelector('.seconds-value'),
+ clockEl = document.querySelector('#time-clock .value');
+
+ var timings = null;
+ // Update once directly
+ _updateTimer();
+
+ // Then update every second
+ setInterval(_updateTimer, 1000);
+
+ function _resetTimer() {
+
+ if (timings == null) {
+ start = new Date();
+ _updateTimer();
+ }
+ else {
+ // Reset timer to beginning of current slide
+ getTimeAllocated(timings, function (slideEndTimingSeconds) {
+ var slideEndTiming = slideEndTimingSeconds * 1000;
+ callRevealApi('getSlidePastCount', [], function (currentSlide) {
+ var currentSlideTiming = timings[currentSlide] * 1000;
+ var previousSlidesTiming = slideEndTiming - currentSlideTiming;
+ var now = new Date();
+ start = new Date(now.getTime() - previousSlidesTiming);
+ _updateTimer();
+ });
+ });
+ }
+
+ }
+
+ timeEl.addEventListener('click', function () {
+ _resetTimer();
+ return false;
+ });
+
+ function _displayTime(hrEl, minEl, secEl, time) {
+
+ var sign = Math.sign(time) == -1 ? "-" : "";
+ time = Math.abs(Math.round(time / 1000));
+ var seconds = time % 60;
+ var minutes = Math.floor(time / 60) % 60;
+ var hours = Math.floor(time / (60 * 60));
+ hrEl.innerHTML = sign + zeroPadInteger(hours);
+ if (hours == 0) {
+ hrEl.classList.add('mute');
+ }
+ else {
+ hrEl.classList.remove('mute');
+ }
+ minEl.innerHTML = ':' + zeroPadInteger(minutes);
+ if (hours == 0 && minutes == 0) {
+ minEl.classList.add('mute');
+ }
+ else {
+ minEl.classList.remove('mute');
+ }
+ secEl.innerHTML = ':' + zeroPadInteger(seconds);
+ }
+
+ function _updateTimer() {
+
+ var diff, hours, minutes, seconds,
+ now = new Date();
+
+ diff = now.getTime() - start.getTime();
+
+ clockEl.innerHTML = now.toLocaleTimeString('en-US', { hour12: true, hour: '2-digit', minute: '2-digit' });
+ _displayTime(hoursEl, minutesEl, secondsEl, diff);
+
+ }
+ }
+
+ /**
+ * Sets up the speaker view layout and layout selector.
+ */
+ function setupLayout() {
+
+ layoutDropdown = document.querySelector('#speaker-layout select');
+
+ // Render the list of available layouts
+ for (var id in SPEAKER_LAYOUTS) {
+ var option = document.createElement('option');
+ option.setAttribute('value', id);
+ option.textContent = SPEAKER_LAYOUTS[id];
+ layoutDropdown.appendChild(option);
+ }
+
+ // Monitor the dropdown for changes
+ layoutDropdown.addEventListener('change', function (event) {
+
+ setLayout(layoutDropdown.value);
+
+ }, false);
+
+ // Restore any currently persisted layout
+ setLayout(getLayout());
+
+ }
+
+ /**
+ * Sets a new speaker view layout. The layout is persisted
+ * in local storage.
+ */
+ function setLayout(value) {
+ layoutDropdown.value = value;
+ document.body.setAttribute('data-speaker-layout', value);
+
+ // Persist locally
+ if (supportsLocalStorage()) {
+ window.localStorage.setItem('reveal-speaker-layout', value);
+ }
+
+ }
+
+ /**
+ * Returns the ID of the most recently set speaker layout
+ * or our default layout if none has been set.
+ */
+ function getLayout() {
+
+ if (supportsLocalStorage()) {
+ var layout = window.localStorage.getItem('reveal-speaker-layout');
+ if (layout) {
+ return layout;
+ }
+ }
+
+ // Default to the first record in the layouts hash
+ for (var id in SPEAKER_LAYOUTS) {
+ return id;
+ }
+
+ }
+
+ function supportsLocalStorage() {
+
+ try {
+ localStorage.setItem('test', 'test');
+ localStorage.removeItem('test');
+ return true;
+ }
+ catch (e) {
+ return false;
+ }
+
+ }
+
+ function zeroPadInteger(num) {
+
+ var str = '00' + parseInt(num);
+ return str.substring(str.length - 2);
+
+ }
+
+ /**
+ * Limits the frequency at which a function can be called.
+ */
+ function debounce(fn, ms) {
+
+ var lastTime = 0,
+ timeout;
+
+ return function () {
+
+ var args = arguments;
+ var context = this;
+
+ clearTimeout(timeout);
+
+ var timeSinceLastCall = Date.now() - lastTime;
+ if (timeSinceLastCall > ms) {
+ fn.apply(context, args);
+ lastTime = Date.now();
+ }
+ else {
+ timeout = setTimeout(function () {
+ fn.apply(context, args);
+ lastTime = Date.now();
+ }, ms - timeSinceLastCall);
+ }
+
+ }
+
+ }
+
+ /*
+ * Sets up
+ set the maximum_so_far to the first number
+
+ for each other number:
+ if the next number is larger than maximum_so_far:
+ set maximum_so_far to the number
+
+ At the end, maximum_so_far contains the largest value
+
diff --git a/csunplugged/templates/at_a_distance/components/algorithms-python.html b/csunplugged/templates/at_a_distance/components/algorithms-python.html
new file mode 100644
index 000000000..5be1ad3a6
--- /dev/null
+++ b/csunplugged/templates/at_a_distance/components/algorithms-python.html
@@ -0,0 +1,21 @@
+
Python
+
+
+
+
+ max_so_far = int(input("Type in a score: "))
+
+ for i in range(4):
+ score = int(input("Type in a score: "))
+ if score > max_so_far:
+ max_so_far = score
+
+ print("High score was", max_so_far)
+
+ These activities are based on live sessions using video conference systems such as Zoom, Google Meet, or Microsoft Teams.
+ Below is a step-by-step guide for preparing to deliver CS Unplugged at a distance sessions.
+
+
+
+ These activities are based on live sessions using video conference systems such as Zoom, Google Meet, or Microsoft Teams.
+
+
+
+ Disclaimer:
+ The software, applications and equipment listed in this section and in the lessons are all tools that we used in 2021 - 2022 while developing these resources.
+
+
+
Key principles
+
+
We followed several principles when designing lessons for online only situations:
+
+
Appropriate for all
+
+
The activities on CS Unplugged at a distance content have been taught at all age levels.
+
+
Easy to deliver
+
+
Our content has everything you need within the online slide deck. Each lesson has one key computer science concept for you to focus on.
+
+
Considerate of people’s time
+
+
These presentations are concise and easy for facilitators, as well as making the best use of the participants’ time.
+
+
Planning a session
+
+
This section supports you to plan and prepare for delivering a session.
+
+
General presentation considerations
+
+
+
Have separate notes and/or print the speaker notes. This allows you to focus on facilitating and following the suggested script and facilitation notes while avoiding the audience seeing them.
+
Opening a presentation room 15 minutes before the start time allows participants to make sure they are logged in with time to resolve any technical issues before the presentation begins.
+
Mute participants' microphones unless they are speaking.
+
+
+
+ Looking for ways to maintain the CS Unplugged feel
+
+
+ The lessons include interactives, so you don’t need to use any props.
+ You may want to incorporate physical CS Unplugged material into your presentation to demonstrate how it works face-to-face.
+
+
+
Here’s some ways you might do that:
+
+
+
+ If you are using a whiteboard, have a whiteboard behind you (or a small hand-held whiteboard).
+ Consider where you are positioned and how easy it is to transition between teaching at the whiteboard and your screen.
+
+
+ If you are holding up an object to the camera, remember that a blurred background setting may mean that your audience can’t see the object unless it’s in front of you.
+
+
+ If you have a document camera directly connected as your main camera (rather than via screen sharing), make sure special effects like blurring backgrounds or ‘touching up appearance’ are switched off.
+
+
+
+
+
+
Which online platform will you use?
+
+
+ Some organisations and schools only allow one platform such as Zoom, Microsoft Teams, or Google Meet to be used, and some will block access to other platforms.
+ Make sure that you have mentioned what platform you are using in your promotional material.
+
+
+
Familiarising yourself with the platform's settings and features
+
+
+ We recommend using the following security settings to ensure your presentation is not accidentally interrupted:
+
+
+
+ Note: Some of these settings may not be available until the meeting is running.
+
+
+
+
Removing the participants ability to share their screen.
+
You may want to consider allowing or disabling the ability for participants to unmute themselves or show their video.
+
+
+
+ We highly recommend familiarising yourself with how to mute or unmute a participant if needed.
+ Online platforms tend to switch the screen to show whoever is speaking (including background noises) which can interrupt the flow of your session.
+
+
+
How will your participants interact with this session?
+
+
+ Encouraging participation is key to making online sessions engaging.
+ Most platforms offer a variety of options for interacting with participants, but we have found the most immediate and reliable system is the standard text chat.
+ Participants can get a strong sense of the group's responses as they see them go past in the chat window.
+ Our lessons have been designed to only use this feature.
+ If it is a smaller group (fewer than 10) allowing participants to speak can also work.
+
+
+
+ Other interaction features
+
+
+ Some of the Unplugged activities would seem to work well with polls and audience responses systems, but these can take some time to set up, and you often have to wait until everyone has responded, which can interrupt the flow of the session.
+
+
+
+ Consider the financial cost (to you and your participants) and immediacy of responses with different software.
+ Sometimes “trial” versions of software have limitations that can interrupt the flow of the event.
+ Also, using features built into the video conference system (such as text chat) avoids asking participants to install more software.
+
+
+
+ Often the previous messages in the chat aren't available to people who join late; for example, if you post a link to a web page that they should be looking at, late arrivees may not be able to see the link.
+ You may need to repeat messages for new arrivals.
+
+
+
+
+
Are you recording this session?
+
+
+ Sometimes people will ask you to record a session.
+ This raises privacy risks, and can inhibit questions and conversations.
+ If someone can’t be there live, it may be better to offer an alternative repeat session, or post a summary in a different format that’s more suitable for them to read or view.
+
+
+
If you are using breakout rooms recording becomes problematic as you can’t usually record the conversations in the breakout rooms.
+
+
Running a session
+
+
Running an online session (especially the first time) can be stressful but we have provided some tips below to make it as smooth as possible.
+
+
How are your participants accessing this session
+
+
Consider the different ways people may be viewing the session, such as individually or in groups, and how this might affect the way you interact with your participants.
+
+
+ Hearing feedback?
+
+
+ Have each participant mute their microphone unless they’re speaking.
+ If feedback is still occurring, make sure the speaker’s speaker is not too loud.
+
+
+
+
+
+ Participants watching in groups?
+
+
+ Encourage each participant to sign in on their own device so they can use the chat feature individually.
+ They will need to have their microphone and speaker muted so they don’t hear the audio multiple times.
+
+
+
+
+
Familiarising your participants with the online platform
+
+
+ It’s good to ask some ice-breaker questions while people are joining your session.
+ You could ask for responses in the chat to questions like “Where are you/your group joining us from?” or “What age group do you teach?”.
+ This is a great opportunity for participants to get familiar with the chat functionality.
+
+
+
Engaging your audience
+
+
+ Using the chat functionality is an effective way to engage participants.
+ As a facilitator, be mindful that it does take time for your participants to think of an answer, type it, and for it to appear in the chat.
+ The response time depends on the question, and it could take up to 20 seconds before answers start to appear.
+ It’s important to not speak during that time to allow people to think.
+
+
+
If it’s a small group, open the discussion by allowing your participants to unmute their mics and ask them to indicate they have something to add by raising their hand.
+
+
+ Other features
+
+
+ Breakout rooms can sometimes be useful during large online workshop presentations, but we have written these resources to keep the participants together.
+ Using breakout rooms can require some experience to be effective, and can depend on having people available to facilitate them if you have a lot of participants.
+ The time it takes to organise people into breakout rooms can interrupt the flow of a session, but if you’re confident using them, they can be used with discretion to manage activities.
+
+
+
+
+
Participant privacy
+
+
Supporting the privacy of participants is important, especially if you’re recording the session. Here are some general tips:
+
+
+
+ Remind participants at the beginning of each session that others can see their background.
+ Allow them to turn their cameras off if they like.
+
+
+ Remind participants at the beginning of each session that they can change their name that appears on their screen for others to see.
+
+
+ Familiarise yourself with how to mute individual participants in case they unmute themselves and unwanted sound is heard.
+
+
+
+
If things go wrong
+
+
+ Firstly, don’t stress, things go wrong, we are all human.
+ Here are some ways you can prepare yourself for the unexpected.
+
+
+
+
+ Remember the mute functionality is great for stopping disruptions.
+
+
+ Check what the host connection rights are in the online platform if the host drops out.
+ Will the session automatically end?
+
+
+ Have a way to communicate with participants if the live session is terminated unexpectedly (e.g. have their email addresses at hand so you can quickly send information about what you will do).
+
+
+ Have a mobile device fully charged, with internet connectivity, ready to log into the online platform in case your primary device fails.
+
+ CS Unplugged at a distance is a series of short lessons adapted from CS Unplugged activities that are suitable for teaching online to students, or for training educators.
+
+
+ CS Unplugged is always better in person!
+ But if you find yourself having to facilitate online, this content is adapted to support this.
+