diff --git a/.github/workflows/auto-merge-dependency-updates.yaml b/.github/workflows/auto-merge-dependency-updates.yaml index 5451a5093..d569427b1 100644 --- a/.github/workflows/auto-merge-dependency-updates.yaml +++ b/.github/workflows/auto-merge-dependency-updates.yaml @@ -13,7 +13,7 @@ jobs: steps: - name: Dependabot metadata id: metadata - uses: dependabot/fetch-metadata@v1.3.4 + uses: dependabot/fetch-metadata@v1.3.5 with: github-token: "${{ secrets.GITHUB_TOKEN }}" - name: Approve diff --git a/.github/workflows/crowdin-actions.yaml b/.github/workflows/crowdin-actions.yaml index 4b19df1cf..90830f5c9 100644 --- a/.github/workflows/crowdin-actions.yaml +++ b/.github/workflows/crowdin-actions.yaml @@ -19,12 +19,12 @@ jobs: uses: actions/checkout@v3 - name: Upload or update source files to Crowdin - uses: crowdin/github-action@1.5.0 + uses: crowdin/github-action@1.5.2 with: upload_sources: true - name: Download German translations - uses: crowdin/github-action@1.5.0 + uses: crowdin/github-action@1.5.2 with: upload_sources: false download_translations: true @@ -42,7 +42,7 @@ jobs: config: crowdin.yaml - name: Download Spanish translations - uses: crowdin/github-action@1.5.0 + uses: crowdin/github-action@1.5.2 with: upload_sources: false download_translations: true diff --git a/.github/workflows/test-and-deploy.yaml b/.github/workflows/test-and-deploy.yaml index 3de79555d..4ae0c5d4e 100644 --- a/.github/workflows/test-and-deploy.yaml +++ b/.github/workflows/test-and-deploy.yaml @@ -19,6 +19,9 @@ jobs: uses: actions/checkout@v3 - name: Create Docker network run: docker network create uccser-development-stack + # Required for the node service + - name: Set DOCKER_UID variable + run: echo "DOCKER_UID=$(echo $UID)" >> $GITHUB_ENV - name: Start systems run: docker compose -f docker-compose.local.yml up -d - name: Run Django system check @@ -32,6 +35,9 @@ jobs: uses: actions/checkout@v3 - name: Create Docker network run: docker network create uccser-development-stack + # Required for the node service + - name: Set DOCKER_UID variable + run: echo "DOCKER_UID=$(echo $UID)" >> $GITHUB_ENV - name: Start systems run: docker compose -f docker-compose.local.yml up -d - name: Create static files @@ -125,6 +131,9 @@ jobs: - name: Create Docker network run: docker network create uccser-development-stack + # Required for the node service + - name: Set DOCKER_UID variable + run: echo "DOCKER_UID=$(echo $UID)" >> $GITHUB_ENV - name: Start system run: docker compose -f docker-compose.local.yml up -d @@ -174,6 +183,10 @@ jobs: - name: Create Docker network run: docker network create uccser-development-stack + # Required for the node service + - name: Set DOCKER_UID variable + run: echo "DOCKER_UID=$(echo $UID)" >> $GITHUB_ENV + - name: Start system run: docker compose -f docker-compose.local.yml up -d diff --git a/csfieldguide/chapters/content/en/coding-compression/sections/shannons-experiment.md b/csfieldguide/chapters/content/en/coding-compression/sections/shannons-experiment.md new file mode 100644 index 000000000..a8e346cc3 --- /dev/null +++ b/csfieldguide/chapters/content/en/coding-compression/sections/shannons-experiment.md @@ -0,0 +1,62 @@ +# Shannon's experiment + +It turns out that there are limits to how small we can compress a file, and to explore this we’re going to look at multi-million dollar frauds, and a fun game that exposes the limits of compression. + +Every so often someone claims to have invented an amazing {glossary-link term="lossless"}lossless{glossary-link end} compression method that can compress *any* file, including compressed files. If that was true, it would mean that you could use the method to compress files down to just a few {glossary-link term="byte"}bytes{glossary-link end}. Any file could be downloaded in a fraction of a second, and computers could store billions of huge video files. It would revolutionise computing! But is there a limit on how small a file can be compressed? + +{panel type="curiosity"} + +# Fraud in data compression + +A number of fake systems have been produced that claim to compress any file as small as you want. +They have even been demonstrated! But they all turn out to be a fake system - the common trick is to have a “compression” program that actually just hides the file being compressed somewhere else on a computer, and replaces it with a tiny file. The decompression program copies the hidden file back. It’s a very simple program to write, and looks very impressive because files are replaced with tiny ones, and then reproduced exactly. + +You can find a few examples of these if you search for “Pixelon”, “Adam’s platform”, “Near Zero”, or “Madison Priest” (add the terms “compression” and “fraud” if you are searching for these, as there are other legitimate organisations with similar names). Several of these organisations have taken millions of dollars from investors who didn’t understand the limits of compression, and all ended up failing. + +{panel end} + +## How small can we compress a file? + +With {glossary-link term="lossy"}lossy{glossary-link end} compression, there isn’t a limit to how small you can compress a file, since it’s just a matter of giving up quality to make the file smaller. You could compress a 10-megapixel photo down to just one pixel (perhaps the average colour of the whole photo). It wouldn’t be much use to anyone, but technically it’s a lossy version of the original photo. + +But with lossless compression, the original file needs to be able to be restored to exactly its original form. + +We’ve seen that compression works by taking advantage of patterns in the data being compressed. +In the 1950s an interesting experiment was developed by a scientist called Claude Shannon, in which he asked humans to predict English text, and he measured how good the compression would be using their ability to make predictions. +The idea is that if a computer was as good at English as a human, then that might be near the limit of what is possible. + +Shannon’s game is easy to play. +Just click on the letter that you think is coming up next in the sentence (you’ll need to start by guessing the first letter). The number of guesses you make give an indication of how predictable the letter is. +These guesses are used to estimate how small the data could be compressed -- you can see this estimate by clicking on the “Show statistics” button. +The “bits per character” is the estimate of how many bits would be needed on average to represent each character. +Plain English text is often stored in 7 or 8 bits for each character (using Unicode or ASCII), and you should find that using your predictions the experiment can do better than that, usually around 2 bits per character. +That’s equivalent to compressing a normal file (8 bits per character) to a quarter of its size. + +Try it here: + +{interactive slug="shannon-experiment" type="whole-page" alt="Shannon's experiment"} + +But it’s very hard to get smaller than 1 bit per character (one eighth of the normal size). +Shannon found that this seems to be a limit for how much we can compress English text. +And this is one reason that we should be suspicious of any system that claims to compress English text to much smaller than one eighth of its original size. + +{panel type="teacher-note"} + +# Creating your own experiment + +This interactive contains an option to create your own experiment. +For example, this could be used to tailor the sentence set to use words that are more familiar to your students. + +Additionally we have support for multiple different languages and sentence sets. +Currently, we have a sentence set for Te Reo Māori, and the original sentences used by Shannon in 1951. + +If you would like to use another language with a different set of characters and/or accents, this also works! +When creating a custom sentence, any characters that aren't already on the keyboard get added automatically. + +Lastly, you could also considering using a pattern that is easily guessed once they realise what is happening, such as "AAAAAAAAAAAAAAAA", "ABABABABABAB", or "blah blah blah blah blah blah blah blah blah blah". +These have very close to zero information content as they are very predictable. +At the other extreme, a (fake) passowrd such as "P6dQKg#S58dw66p" could be used to explore how hard it is to guess random characters. + +{panel end} + +{comment - could add more about Shannon, model at sender and received, movie about him} diff --git a/csfieldguide/chapters/content/structure/coding-compression/sections/sections.yaml b/csfieldguide/chapters/content/structure/coding-compression/sections/sections.yaml index 3b724acc2..bc3ab99f2 100644 --- a/csfieldguide/chapters/content/structure/coding-compression/sections/sections.yaml +++ b/csfieldguide/chapters/content/structure/coding-compression/sections/sections.yaml @@ -19,8 +19,11 @@ general-purpose: audio-compression: section-number: 7 -the-whole-story: +shannons-experiment: section-number: 8 -further-reading: +the-whole-story: section-number: 9 + +further-reading: + section-number: 10 \ No newline at end of file diff --git a/csfieldguide/chapters/management/commands/_ChapterSectionHeadingsLoader.py b/csfieldguide/chapters/management/commands/_ChapterSectionHeadingsLoader.py index 68c8ae808..400f46d65 100644 --- a/csfieldguide/chapters/management/commands/_ChapterSectionHeadingsLoader.py +++ b/csfieldguide/chapters/management/commands/_ChapterSectionHeadingsLoader.py @@ -39,13 +39,16 @@ def load(self): """ for language, content in self.content_translations.items(): + i = 0 if content.heading_tree: - for (i, heading_node) in enumerate(content.heading_tree): + for heading_node in content.heading_tree: self.chapter_section.headings.update_or_create( - slug=heading_node.title_slug, + number=i, + language=language, defaults={ 'name': heading_node.title, - 'language': language, - 'number': i, + 'slug': heading_node.title_slug } ) + i += 1 + self.chapter_section.headings.filter(number__gte=i, language=language).delete() diff --git a/csfieldguide/chapters/management/commands/_ChapterSectionsLoader.py b/csfieldguide/chapters/management/commands/_ChapterSectionsLoader.py index 8ce0c0430..eb4998534 100644 --- a/csfieldguide/chapters/management/commands/_ChapterSectionsLoader.py +++ b/csfieldguide/chapters/management/commands/_ChapterSectionsLoader.py @@ -33,10 +33,9 @@ def load(self): field. """ chapter_sections_structure = self.load_yaml_file(self.structure_file_path) - section_numbers = [] + next_section_number = 1 for (section_slug, section_structure) in chapter_sections_structure.items(): - if section_structure is None: raise MissingRequiredFieldError( self.structure_file_path, @@ -57,8 +56,15 @@ def load(self): "section-number - value '{}' is invalid".format(section_number), "section-number must be an integer value." ) + if section_number != next_section_number: + raise InvalidYAMLValueError( + self.structure_file_path, + "section-number - value '{}' is invalid".format(section_number), + "section-numbers must be in sequential order. The next expected number was '{}'." + .format(next_section_number) + ) - section_numbers.append(section_number) + next_section_number += 1 chapter_section_translations = self.get_blank_translation_dictionary() @@ -69,9 +75,9 @@ def load(self): chapter_section_translations[language]["name"] = content.title chapter_section, created = self.chapter.chapter_sections.update_or_create( - slug=section_slug, + number=section_number, defaults={ - 'number': section_number, + 'slug': section_slug, 'languages': list(content_translations.keys()), } ) @@ -101,11 +107,4 @@ def load(self): structure_filename=self.structure_file_path, ).load() - # assumes first section number is always 1 - for counter, section_number in enumerate(section_numbers, 1): - if section_number != counter: - raise InvalidYAMLValueError( - self.structure_file_path, - "section-number - value '{}' is invalid".format(section_number), - "section-numbers must be in sequential order. The next expected number was '{}'.".format(counter) - ) + self.chapter.chapter_sections.filter(number__gte=next_section_number).delete() diff --git a/csfieldguide/chapters/management/commands/_ChaptersLoader.py b/csfieldguide/chapters/management/commands/_ChaptersLoader.py index de660e5aa..7d53e72fa 100644 --- a/csfieldguide/chapters/management/commands/_ChaptersLoader.py +++ b/csfieldguide/chapters/management/commands/_ChaptersLoader.py @@ -69,9 +69,9 @@ def load(self): # Create or update chapter object and save to the db chapter, created = Chapter.objects.update_or_create( - slug=self.chapter_slug, + number=self.chapter_number, defaults={ - 'number': self.chapter_number, + 'slug': self.chapter_slug, 'icon': chapter_icon, 'video': video } diff --git a/csfieldguide/chapters/management/commands/loadchapters.py b/csfieldguide/chapters/management/commands/loadchapters.py index c44e195e4..3b34fb022 100644 --- a/csfieldguide/chapters/management/commands/loadchapters.py +++ b/csfieldguide/chapters/management/commands/loadchapters.py @@ -3,6 +3,7 @@ import os.path from django.core.management.base import BaseCommand from django.conf import settings +from django.db import transaction from utils.BaseLoader import BaseLoader from utils.LoaderFactory import LoaderFactory from utils.errors.MissingRequiredFieldError import MissingRequiredFieldError @@ -19,6 +20,7 @@ class Command(BaseCommand): help = "Converts Markdown files listed in structure file and stores" + @transaction.atomic def handle(self, *args, **options): """Automatically called when the loadchapters command is given.""" factory = LoaderFactory() @@ -51,6 +53,7 @@ def handle(self, *args, **options): "Application Structure" ) else: + next_chapter_number = 1 for chapter_slug in chapters: chapter_structure_file = "{}.yaml".format(chapter_slug) @@ -67,6 +70,16 @@ def handle(self, *args, **options): "chapter-number - value '{}' is invalid".format(chapter_number), "chapter-number must be an integer value." ) + if chapter_number != next_chapter_number: + raise InvalidYAMLValueError( + structure_file_path, + "chapter-number - value '{}' is invalid".format(chapter_number), + ("chapter-numbers must be in sequential order. The next expected number was '{}'." + .format(next_chapter_number)) + ) + + next_chapter_number += 1 + factory.create_chapter_loader( base_path=base_path, content_path=chapter_slug, diff --git a/csfieldguide/chapters/migrations/0037_chapter_slug_deferred.py b/csfieldguide/chapters/migrations/0037_chapter_slug_deferred.py new file mode 100644 index 000000000..8c082e195 --- /dev/null +++ b/csfieldguide/chapters/migrations/0037_chapter_slug_deferred.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.16 on 2022-11-24 02:49 + +from django.db import migrations, models +import django.db.models.constraints + + +class Migration(migrations.Migration): + + dependencies = [ + ('chapters', '0036_alter_chaptersection_options'), + ] + + operations = [ + migrations.AddConstraint( + model_name='chapter', + constraint=models.UniqueConstraint(deferrable=django.db.models.constraints.Deferrable['DEFERRED'], fields=('slug',), name='slug_deferred'), + ), + ] diff --git a/csfieldguide/chapters/migrations/0038_alter_chapter_slug.py b/csfieldguide/chapters/migrations/0038_alter_chapter_slug.py new file mode 100644 index 000000000..bbb3b7b04 --- /dev/null +++ b/csfieldguide/chapters/migrations/0038_alter_chapter_slug.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.16 on 2022-11-24 03:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('chapters', '0037_chapter_slug_deferred'), + ] + + operations = [ + migrations.AlterField( + model_name='chapter', + name='slug', + field=models.SlugField(), + ), + ] diff --git a/csfieldguide/chapters/models.py b/csfieldguide/chapters/models.py index 8d9b4d402..f35561cd5 100644 --- a/csfieldguide/chapters/models.py +++ b/csfieldguide/chapters/models.py @@ -2,7 +2,6 @@ from django.db import models from interactives.models import Interactive -from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ from utils.TranslatableModel import TranslatableModel from django.urls import reverse @@ -54,7 +53,7 @@ class Chapter(TranslatableModel): """Model for chapter in database.""" # Auto-incrementing 'id' field is automatically set by Django - slug = models.SlugField(unique=True) + slug = models.SlugField() # This is set unique in the Meta child class name = models.CharField(max_length=100, default="") number = models.SmallIntegerField(unique=True) introduction = models.TextField(default="") @@ -104,6 +103,10 @@ class Meta: verbose_name = _("chapter") verbose_name_plural = _("chapters") + constraints = [ + models.UniqueConstraint(fields=["slug"], deferrable=models.Deferrable.DEFERRED, name="slug_deferred") + ] + class ChapterSection(TranslatableModel): """Model for each section in a chapter in database.""" @@ -128,24 +131,6 @@ def __str__(self): """ return self.name - def clean(self): - """Use to check for unique section numbers. - - Raises: - ValidationError: when the section being added uses - an existing section number for this chapter. - """ - # get all sections with same section number and chapter as new section being added - sections = ChapterSection.objects.filter(number=self.number, chapter=self.chapter) - # if already exists section with same number in same chapter, then throw error! - if len(sections) > 1: - raise ValidationError(('Section number must be unique per chapter.')) - - def save(self, *args, **kwargs): - """Override save method to validate unique section numbers.""" - super(ChapterSection, self).save(*args, **kwargs) - self.clean() - class Meta: """Set consistent ordering of chapter sections.""" diff --git a/csfieldguide/config/__init__.py b/csfieldguide/config/__init__.py index ed081da1a..856f65721 100644 --- a/csfieldguide/config/__init__.py +++ b/csfieldguide/config/__init__.py @@ -1,3 +1,3 @@ """Module for Django system configuration.""" -__version__ = "3.12.6" +__version__ = "3.13.0" diff --git a/csfieldguide/package-lock.json b/csfieldguide/package-lock.json index 4db067723..411e922fe 100644 --- a/csfieldguide/package-lock.json +++ b/csfieldguide/package-lock.json @@ -2439,9 +2439,9 @@ "dev": true }, "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "dev": true }, "decompress": { @@ -3049,9 +3049,9 @@ } }, "engine.io": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.0.tgz", - "integrity": "sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.1.tgz", + "integrity": "sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==", "dev": true, "requires": { "@types/cookie": "^0.4.1", @@ -6052,9 +6052,9 @@ } }, "jquery": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.1.tgz", - "integrity": "sha512-opJeO4nCucVnsjiXOE+/PcCgYw9Gwpvs/a6B1LL/lQhwWwpbVEVYDZ1FokFr8PRc7ghYlrFPuyHuiiDNTQxmcw==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.2.tgz", + "integrity": "sha512-/e7ulNIEEYk1Z/l4X0vpxGt+B/dNsV8ghOPAWZaJs8pkGvsSC0tm33aMGylXcj/U7y4IcvwtMXPMyBFZn/gK9A==", "dev": true }, "js-yaml": { @@ -7516,9 +7516,9 @@ "dev": true }, "postcss": { - "version": "8.4.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz", - "integrity": "sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==", + "version": "8.4.20", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.20.tgz", + "integrity": "sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==", "dev": true, "requires": { "nanoid": "^3.3.4", @@ -8538,9 +8538,9 @@ "dev": true }, "sass": { - "version": "1.55.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.55.0.tgz", - "integrity": "sha512-Pk+PMy7OGLs9WaxZGJMn7S96dvlyVBwwtToX895WmCpAOr5YiJYEUJfiJidMuKb613z2xNWcXCHEuOvjZbqC6A==", + "version": "1.56.2", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.56.2.tgz", + "integrity": "sha512-ciEJhnyCRwzlBCB+h5cCPM6ie/6f8HrhZMQOf5vlU60Y1bI1rx5Zb0vlDZvaycHsg/MqFfF1Eq2eokAa32iw8w==", "dev": true, "requires": { "chokidar": ">=3.0.0 <4.0.0", @@ -9006,9 +9006,9 @@ "dev": true }, "socket.io-parser": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", - "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.5.tgz", + "integrity": "sha512-sNjbT9dX63nqUFIOv95tTVm6elyIU4RvB1m8dOeZt+IgWwcWklFDOdmGcfo3zSiRsnR/3pJkjY5lfoGqEe4Eig==", "dev": true, "requires": { "@types/component-emitter": "^1.2.10", @@ -9054,9 +9054,9 @@ } }, "socket.io-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.0.tgz", - "integrity": "sha512-tLfmEwcEwnlQTxFB7jibL/q2+q8dlVQzj4JdRLJ/W/G1+Fu9VSxCx1Lo+n1HvXxKnM//dUuD0xgiA7tQf57Vng==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.1.tgz", + "integrity": "sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==", "dev": true, "requires": { "@socket.io/component-emitter": "~3.1.0", @@ -10452,9 +10452,9 @@ "dev": true }, "yargs": { - "version": "17.6.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.0.tgz", - "integrity": "sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==", + "version": "17.6.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", + "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", "dev": true, "requires": { "cliui": "^8.0.1", @@ -10463,7 +10463,7 @@ "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^21.0.0" + "yargs-parser": "^21.1.1" }, "dependencies": { "ansi-regex": { diff --git a/csfieldguide/package.json b/csfieldguide/package.json index dcf175d8e..ede16ac86 100644 --- a/csfieldguide/package.json +++ b/csfieldguide/package.json @@ -30,16 +30,16 @@ "gulp-tap": "2.0.0", "gulp-terser": "2.1.0", "iframe-resizer": "4.3.2", - "jquery": "3.6.1", + "jquery": "3.6.2", "lity": "2.4.1", "multiple-select": "1.5.2", "pixrem": "5.0.0", "popper.js": "1.16.1", - "postcss": "8.4.18", + "postcss": "8.4.20", "postcss-flexbugs-fixes": "5.0.2", - "sass": "1.55.0", + "sass": "1.56.2", "vinyl-buffer": "1.0.1", - "yargs": "17.6.0" + "yargs": "17.6.2" }, "engines": { "node": ">=8" diff --git a/csfieldguide/static/interactives/scene-editor/package.json b/csfieldguide/static/interactives/scene-editor/package.json index 293cf4247..29f75cb73 100644 --- a/csfieldguide/static/interactives/scene-editor/package.json +++ b/csfieldguide/static/interactives/scene-editor/package.json @@ -4,7 +4,7 @@ "private": true, "dependencies": { "mathjs": "6.0.3", - "three": "0.146.0", + "three": "0.147.0", "three-orbit-controls": "82.1.0", "sprintf-js": "1.1.2" } diff --git a/csfieldguide/templates/appendices/contributors.html b/csfieldguide/templates/appendices/contributors.html index ca59d2cab..8505356f1 100644 --- a/csfieldguide/templates/appendices/contributors.html +++ b/csfieldguide/templates/appendices/contributors.html @@ -27,6 +27,7 @@