diff --git a/.travis.yml b/.travis.yml index 2b39e2d..b637672 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,8 +13,7 @@ jobs: include: - stage: test script: coverage run --rcfile=.coveragerc manage.py test tests --settings=config.settings.testing -v=3 - - script: python manage.py loadpikau - - script: python manage.py loadlicences + - script: python manage.py loaddata - script: flake8 - script: pydocstyle --count --explain after_success: diff --git a/CHANGELOG.md b/CHANGELOG.md index fdfee28..71d7fdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## 0.6.0 (Pre-release) + +*This release requires a database wipe due to new migrations.* + +- Add "Why digital technologies?" pīkau. +- Store and load readiness level for pīkau. +- Ensure pīkau title fits within pathway diagram nodes. (fixes #56) +- Fix bug where file licence warning displayed wrong licence. (fixes #60) +- Load file data from loader. +- Check files used in pīkau are listed within files application. +- Display links between files and pīkau. (fixes #61) +- Display image preview for files. (fixes #35) +- Add test cases. +- Dependency updates: + - Update django from 2.0.5 to 2.0.6. + - Update django-anymail 2.2 to 3.0. + ## 0.5.1 (Pre-release) - Fix bug where slug max length was too short for titles. diff --git a/config/__init__.py b/config/__init__.py index 106cd83..2d5feb3 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -1,3 +1,3 @@ """Module for Django system configuration.""" -__version__ = "0.5.1" +__version__ = "0.6.0" diff --git a/files/content/files.yaml b/files/content/files.yaml new file mode 100644 index 0000000..69bcb78 --- /dev/null +++ b/files/content/files.yaml @@ -0,0 +1,158 @@ +banner-mahuika-maui-png: + name: "Mahuika and Māui Banner" + filename: "banner-mahuika-maui.png" + location: "https://drive.google.com/open?id=1NJfnUmj6et78-o8Eov2i7fgZdauR_myE" + direct-link: "images/core-education/banner-mahuika-maui.png" +maui-face-png: + name: "Māui face" + filename: "maui-face.png" + location: "https://drive.google.com/open?id=1NU2glbUDS928aBwKgivX4GdoyKKkbMmo" + direct-link: "images/core-education/maui-face.png" +mahuika-face-png: + name: "Mahuika face" + filename: "mahuika-face.png" + location: "https://drive.google.com/open?id=1396r4RQjZxZdyAPVzDm3o0heDJit1m_E" + direct-link: "images/core-education/mahuika-face.png" +what-is-ct-png: + name: "What is CT?" + filename: "what-is-ct.png" + location: "images/pikau/what-is-ct.png" + direct-link: "images/pikau/what-is-ct.png" + licence: cc-by-sa-4 +ct-around-the-world-png: + name: "CT around the world" + filename: "ct-around-the-world.png" + location: "images/pikau/ct-around-the-world.png" + direct-link: "images/pikau/ct-around-the-world.png" + licence: cc-by-sa-4 +arnold-the-wonder-parrot-jpg: + name: "Arnold the Wonder Parrot" + filename: "arnold-the-wonder-parrot.jpg" + location: "images/pikau/arnold-the-wonder-parrot.jpg" + direct-link: "images/pikau/arnold-the-wonder-parrot.jpg" + licence: cc-by-sa-4 +information-theory-activity-jpg: + name: "Information Theory CS Unplugged Activity" + filename: "information-theory-activity.jpg" + location: "images/pikau/information-theory-activity.jpg" + direct-link: "images/pikau/information-theory-activity.jpg" + licence: cc-by-sa-4 +placeholder-video: + name: "Kia Takatū ā-Matihiko Placeholder Video" + filename: "https://www.youtube.com/embed/zSfzB-Z-mKM" + location: "https://www.youtube.com/embed/zSfzB-Z-mKM" + direct-link: "https://www.youtube.com/embed/zSfzB-Z-mKM" + licence: cc-by-sa-4 +ct-loading-planes-trimmed-mov: + name: "CT Loading Planes - Trimmed" + filename: "CT loading planes trimmed.mov" + location: "https://vimeo.com/273809039/eb5cb42933" + direct-link: "https://vimeo.com/273809039/eb5cb42933" + licence: cc-by-sa-4 +ct-six-elements-of-digital-devices-mp4: + name: "Six elements of digital devices" + filename: "six elements of digital devices.mp4" + location: "https://vimeo.com/273813250/f251e2a3d4" + direct-link: "https://vimeo.com/273813250/f251e2a3d4" + licence: cc-by-sa-4 +digital-images-mp4: + name: "Digital images" + filename: "digital images.mp4" + location: "https://vimeo.com/273824548/770c5c1c48" + direct-link: "https://vimeo.com/273824548/770c5c1c48" + licence: cc-by-sa-4 +csfg-algorithms-trailer-mp4: + name: "Computer Science Field Guide: Algorithms" + filename: "Computer Science Field Guide - Algorithms.mp4" + location: "https://vimeo.com/69609500" + direct-link: "https://vimeo.com/69609500" + licence: cc-by-sa-4 +getting-the-most-out-of-pikau: + name: Getting the most out of pīkau + filename: "https://vimeo.com/272882311/8e4cf1de3f" + location: "https://vimeo.com/272882311/8e4cf1de3f" + direct-link: "https://vimeo.com/272882311/8e4cf1de3f" +1983-cpa-5426-png: + name: Muḥammad ibn Mūsā al-Ḵwārizmī + filename: 1983 CPA 5426 (1).png + location: "https://commons.wikimedia.org/wiki/File:1983_CPA_5426_(1).png" + direct-link: "https://upload.wikimedia.org/wikipedia/commons/1/11/1983_CPA_5426_%281%29.png" + licence: public-domain + description: "This work is not an object of copyright according to article 1259 of Book IV of the Civil Code of the Russian Federation No. 230-FZ of December 18, 2006." +euklid-von-alexandria-1-jpg: + name: Euklid-von-Alexandria + filename: Euklid-von-Alexandria_1.jpg + location: "https://commons.wikimedia.org/wiki/File:Euklid-von-Alexandria_1.jpg" + direct-link: "https://upload.wikimedia.org/wikipedia/commons/3/30/Euklid-von-Alexandria_1.jpg" + licence: public-domain + description: "This work is in the public domain in its country of origin and other countries and areas where the copyright term is the author's life plus 70 years or less." +telescope-with-stars-jpg: + name: Telescope in front of stars + filename: science.jpg + location: "https://www.timelinecoverbanner.com/covers/telescope-knowledge-mystery-and-science-Facebook-cover/" + direct-link: "https://www.timelinecoverbanner.com/facebook-covers/2013/01/science.jpg" + licence: cc0 +design-desk-display-jpg: + name: User frustrated in front of computer + filename: design-desk-display-313690.jpg + location: "https://www.pexels.com/photo/design-desk-display-eyewear-313690/" + licence: cc0 +dt3e-interviews-with-teachers-mp4: + name: "DT3e - Interviews with teachers" + filename: "DT3e interviews with teachers.mp4" + location: "https://vimeo.com/273841538" + direct-link: "https://vimeo.com/273841538" + licence: cc-by-sa-4 +use-modify-create-diagram-svg: + name: Use-Modify-Create Learning Progression Diagram + filename: "use-modify-create-diagram.svg" + location: "images/pikau/use-modify-create-diagram.svg" + direct-link: "images/pikau/use-modify-create-diagram.svg" + licence: cc-by-sa-4 +technology-diagram-jpg: + name: NZ Curriculum Technology Diagram + filename: "technology-diagram.jpg" + location: "images/core-education/technology-diagram.jpg" + direct-link: "images/core-education/technology-diagram.jpg" +oudjat-svg: + name: Oudjat + filename: "Oudjat.SVG" + location: "https://commons.wikimedia.org/wiki/File:Oudjat.SVG" + direct-link: "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f5/Oudjat.SVG/500px-Oudjat.SVG.png" + licence: cc-by-sa-3 +eratosthene-png: + name: Eratosthene + filename: "Eratosthene.01.png" + location: "https://commons.wikimedia.org/wiki/File:Eratosthene.01.png" + direct-link: "https://upload.wikimedia.org/wikipedia/commons/b/b3/Eratosthene.01.png" + licence: public-domain +ada-lovelace-jpg: + name: Ada Lovelace + filename: "Ada Lovelace portrait.jpg" + location: "https://commons.wikimedia.org/wiki/File:Ada_Lovelace_portrait.jpg" + direct-link: "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a4/Ada_Lovelace_portrait.jpg/334px-Ada_Lovelace_portrait.jpg" + licence: public-domain +edsger-wybe-dijkstra-jpg: + name: Edsger Wybe Dijkstra + filename: "Edsger Wybe Dijkstra.jpg" + location: "https://commons.wikimedia.org/wiki/File:Edsger_Wybe_Dijkstra.jpg" + direct-link: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d9/Edsger_Wybe_Dijkstra.jpg/450px-Edsger_Wybe_Dijkstra.jpg" + licence: cc-by-sa-3 +pagerank-png: + name: PageRank + filename: "PageRank-hi-res.png" + location: "https://commons.wikimedia.org/wiki/File:PageRank-hi-res.png" + direct-link: "https://upload.wikimedia.org/wikipedia/commons/thumb/6/69/PageRank-hi-res.png/640px-PageRank-hi-res.png" + licence: cc-by-sa-2-5 +jeannette-wing-jpg: + name: Jeannette Wing + filename: "Jeannette Wing, Davos 2013.jpg" + location: "https://commons.wikimedia.org/wiki/File:Jeannette_Wing,_Davos_2013.jpg" + direct-link: "https://upload.wikimedia.org/wikipedia/commons/thumb/7/77/Jeannette_Wing%2C_Davos_2013.jpg/532px-Jeannette_Wing%2C_Davos_2013.jpg" + licence: cc-by-sa-2 +khwarizmi-amirkabir-png: + name: Khwarizmi Amirkabir University of Technology + filename: "Khwarizmi Amirkabir University of Technology.png" + location: "https://commons.wikimedia.org/wiki/File:Khwarizmi_Amirkabir_University_of_Technology.png" + direct-link: "https://upload.wikimedia.org/wikipedia/commons/a/a6/Khwarizmi_Amirkabir_University_of_Technology.png" + licence: cc-by-sa-3 diff --git a/files/content/licences.yaml b/files/content/licences.yaml index ee092c3..ff7757d 100644 --- a/files/content/licences.yaml +++ b/files/content/licences.yaml @@ -1,6 +1,24 @@ -- name: "Unknown" +unknown: + name: "Unknown" url: "https://creativecommons.org/choose/" -- name: "Creative Commons (BY-SA 4.0)" +cc0: + name: "CC0 - No Rights Reserved" + url: "https://creativecommons.org/share-your-work/public-domain/cc0/" +cc-by-sa-2: + name: "Creative Commons (BY-SA 2.0)" + url: "https://creativecommons.org/licenses/by-sa/2.0/" +cc-by-sa-2-5: + name: "Creative Commons (BY-SA 2.5)" + url: "https://creativecommons.org/licenses/by-sa/2.5/" +cc-by-sa-3: + name: "Creative Commons (BY-SA 3.0)" + url: "https://creativecommons.org/licenses/by-sa/3.0/" +cc-by-sa-4: + name: "Creative Commons (BY-SA 4.0)" url: "https://creativecommons.org/licenses/by-sa/4.0/" -- name: "Creative Commons (BY-NC-SA 4.0)" +cc-by-nc-sa-4: + name: "Creative Commons (BY-NC-SA 4.0)" url: "https://creativecommons.org/licenses/by-nc-sa/4.0/" +public-domain: + name: Public Domain + url: "https://wiki.creativecommons.org/wiki/Public_domain" diff --git a/files/management/commands/_FileLoader.py b/files/management/commands/_FileLoader.py new file mode 100644 index 0000000..ada92bb --- /dev/null +++ b/files/management/commands/_FileLoader.py @@ -0,0 +1,32 @@ +"""Custom loader for loading files.""" + +from django.db import transaction +from files.models import File, Licence +from utils.BaseLoader import BaseLoader + + +class FileLoader(BaseLoader): + """Custom loader for loading files.""" + + @transaction.atomic + def load(self): + """Load the files into the database.""" + files = self.load_yaml_file("files.yaml") + + for file_slug, file_data in files.items(): + licence = Licence.objects.get(slug=file_data.get("licence", "unknown")) + defaults = { + "name": file_data["name"], + "filename": file_data["filename"], + "location": file_data["location"], + "direct_link": file_data.get("direct-link", ""), + "licence": licence, + "description": file_data.get("description", ""), + } + file_object, created = File.objects.update_or_create( + slug=file_slug, + defaults=defaults, + ) + self.log_object_creation(created, file_object) + + self.log("All files loaded!\n") diff --git a/files/management/commands/_LicenceLoader.py b/files/management/commands/_LicenceLoader.py index 14e2dfe..1668521 100644 --- a/files/management/commands/_LicenceLoader.py +++ b/files/management/commands/_LicenceLoader.py @@ -13,12 +13,13 @@ def load(self): """Load the licences into the database.""" licences = self.load_yaml_file("licences.yaml") - for licence_data in licences: + for licence_slug, licence_data in licences.items(): defaults = { + "name": licence_data["name"], "url": licence_data["url"], } licence, created = Licence.objects.update_or_create( - name=licence_data["name"], + slug=licence_slug, defaults=defaults, ) self.log_object_creation(created, licence) diff --git a/files/management/commands/loadlicences.py b/files/management/commands/loadfiles.py similarity index 54% rename from files/management/commands/loadlicences.py rename to files/management/commands/loadfiles.py index 1733050..2130a8a 100644 --- a/files/management/commands/loadlicences.py +++ b/files/management/commands/loadfiles.py @@ -1,16 +1,18 @@ -"""Module for the custom Django loadlicences command.""" +"""Module for the custom Django loadfiles command.""" from django.conf import settings from django.core import management from files.management.commands._LicenceLoader import LicenceLoader +from files.management.commands._FileLoader import FileLoader class Command(management.base.BaseCommand): - """Required command class for the custom Django loadlicences command.""" + """Required command class for the custom Django loadfiles command.""" help = "Loads licences into the website" def handle(self, *args, **options): - """Automatically called when the loadlicences command is given.""" + """Automatically called when the loadfiles command is given.""" base_path = settings.FILES_CONTENT_BASE_PATH LicenceLoader(base_path).load() + FileLoader(base_path).load() diff --git a/files/migrations/0001_initial.py b/files/migrations/0001_initial.py index 2db3918..b005ff9 100644 --- a/files/migrations/0001_initial.py +++ b/files/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.0.5 on 2018-05-21 04:19 +# Generated by Django 2.0.5 on 2018-06-07 02:53 from django.db import migrations, models import django.db.models.deletion @@ -18,15 +18,17 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('slug', models.SlugField(unique=True)), - ('filename', models.CharField(max_length=200)), + ('name', models.CharField(max_length=200, unique=True)), + ('filename', models.CharField(max_length=200, unique=True)), ('description', models.TextField(blank=True)), - ('location', models.URLField()), + ('location', models.URLField(unique=True)), ], ), migrations.CreateModel( name='Licence', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('slug', models.SlugField(unique=True)), ('name', models.CharField(max_length=200, unique=True)), ('url', models.URLField()), ], diff --git a/files/migrations/0002_auto_20180607_2117.py b/files/migrations/0002_auto_20180607_2117.py new file mode 100644 index 0000000..419d175 --- /dev/null +++ b/files/migrations/0002_auto_20180607_2117.py @@ -0,0 +1,23 @@ +# Generated by Django 2.0.5 on 2018-06-07 09:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('files', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='file', + name='direct_link', + field=models.URLField(blank=True), + ), + migrations.AlterField( + model_name='file', + name='location', + field=models.URLField(), + ), + ] diff --git a/files/models.py b/files/models.py index eb0a2a7..6f59372 100644 --- a/files/models.py +++ b/files/models.py @@ -2,8 +2,22 @@ from django.db import models from django.core.exceptions import ObjectDoesNotExist +from django.template.loader import render_to_string from django.urls import reverse +VIDEO_PROVIDERS = ( + "youtube", + "vimeo", +) + +IMAGE_EXTENSIONS = ( + ".jpeg", + ".jpg", + ".png", + ".gif", + ".svg", +) + def default_licence(): """Return default licence object. @@ -12,7 +26,7 @@ def default_licence(): Licence 'Unknown' if available, otherwise None. """ try: - default = Licence.objects.get(name="Unknown").pk + default = Licence.objects.get(slug="unknown").pk except ObjectDoesNotExist: default = None return default @@ -21,6 +35,7 @@ def default_licence(): class Licence(models.Model): """Model for licence.""" + slug = models.SlugField(unique=True) name = models.CharField(max_length=200, unique=True) url = models.URLField() @@ -50,9 +65,11 @@ class File(models.Model): """Model for file.""" slug = models.SlugField(unique=True) - filename = models.CharField(max_length=200) - description = models.TextField(blank=True) + name = models.CharField(max_length=200, unique=True) + filename = models.CharField(max_length=200, unique=True) location = models.URLField() + direct_link = models.URLField(blank=True) + description = models.TextField(blank=True) licence = models.ForeignKey( Licence, on_delete=models.CASCADE, @@ -61,6 +78,47 @@ class File(models.Model): null=True, ) + def media_type(self): + """Return label for media type. + + Returns: + String label of media type. + """ + if any(substring in self.direct_link for substring in VIDEO_PROVIDERS): + label = "Video" + elif self.direct_link.endswith(IMAGE_EXTENSIONS): + label = "Image" + else: + label = "Unknown" + return label + + def preview_html(self): + """Return HTML for preview. + + Returns: + HTML as a string. + """ + # YouTube video + if "youtube" in self.direct_link: + context = {"direct_link": self.direct_link} + html = render_to_string("files/previews/youtube.html", context=context) + # Vimeo video + elif "vimeo" in self.direct_link: + context = {"video_id": self.direct_link.split("/")[3]} + html = render_to_string("files/previews/vimeo.html", context=context) + # Direct image URL + elif self.direct_link.startswith("http"): + context = {"direct_link": self.direct_link} + html = render_to_string("files/previews/external-image.html", context=context) + # Relative image + elif self.direct_link: + context = {"direct_link": self.direct_link} + html = render_to_string("files/previews/internal-image.html", context=context) + # Unsupported preview + else: + html = "No preview available" + return html + def get_absolute_url(self): """Return the URL for a file. @@ -75,7 +133,7 @@ def __str__(self): Returns: String describing file. """ - return self.filename + return self.name def __repr__(self): """Text representation of File object for developers. diff --git a/files/tables.py b/files/tables.py index 53bb4ad..432e9ad 100644 --- a/files/tables.py +++ b/files/tables.py @@ -1,5 +1,6 @@ """Tables for the files application.""" +from django.template.loader import render_to_string import django_tables2 as tables from files.models import ( File, @@ -9,12 +10,35 @@ class FileTable(tables.Table): """Table to display all files.""" - filename = tables.LinkColumn() + name = tables.LinkColumn() + preview = tables.TemplateColumn( + template_name="files/previews/preview.html", + verbose_name="Preview", + ) + media_type_rendered = tables.TemplateColumn( + template_name="files/previews/type-icon.html", + verbose_name="Media type", + ) licence = tables.RelatedLinkColumn() + def render_media_type(self, value): + """Render template for media type column. + + Args: + value: Value for column. + + Returns: + Rendered string for column. + """ + if value in ("Image", "Video"): + context = {"image_path": "images/icons/icons8/{}.png".format(value)} + image = render_to_string("files/previews/type-icon.html", context=context) + value = image + value + return value + class Meta: """Meta attributes for FileTable class.""" model = File - fields = ("filename", "licence") - order_by = "filename" + fields = ("name", "media_type_rendered", "licence") + order_by = "name" diff --git a/files/views.py b/files/views.py index 939f81e..c299971 100644 --- a/files/views.py +++ b/files/views.py @@ -14,6 +14,7 @@ from files.filters import FileFilter from files.models import ( File, + default_licence, ) from files.forms import ( FileForm, @@ -36,6 +37,7 @@ def get_context_data(self, **kwargs): """ context = super(FileList, self).get_context_data(**kwargs) context["unknown_licences"] = File.objects.filter(licence__name="Unknown").count() + context["unknown_licence_id"] = default_licence() return context diff --git a/pikau/content/pikau-courses.yaml b/pikau/content/pikau-courses.yaml index 6dd21c4..d3f0515 100644 --- a/pikau/content/pikau-courses.yaml +++ b/pikau/content/pikau-courses.yaml @@ -1,4 +1,5 @@ courses: - getting-the-most-out-of-pikau + - why-digital-technologies - what-is-computational-thinking - computational-thinking-the-international-perspective diff --git a/pikau/content/pikau-courses/computational-thinking-the-international-perspective/metadata.yaml b/pikau/content/pikau-courses/computational-thinking-the-international-perspective/metadata.yaml index 4f8324f..d288ea4 100644 --- a/pikau/content/pikau-courses/computational-thinking-the-international-perspective/metadata.yaml +++ b/pikau/content/pikau-courses/computational-thinking-the-international-perspective/metadata.yaml @@ -1,5 +1,6 @@ name: "Computational thinking - The international perspective" status: 2 +readiness-level: 1 language: en topic: ct level: all @@ -7,6 +8,8 @@ tags: - introductory cover-photo: images/pikau/ct-around-the-world.png trailer-video: https://www.youtube.com/embed/zSfzB-Z-mKM +prerequisites: + - what-is-computational-thinking overview: overview.md content: @@ -24,3 +27,6 @@ content: - slug: references file: references.md assessment-items: assessment-items.md + +extra-files: + - jeannette-wing-jpg diff --git a/pikau/content/pikau-courses/getting-the-most-out-of-pikau/metadata.yaml b/pikau/content/pikau-courses/getting-the-most-out-of-pikau/metadata.yaml index 99a2f97..b7705aa 100644 --- a/pikau/content/pikau-courses/getting-the-most-out-of-pikau/metadata.yaml +++ b/pikau/content/pikau-courses/getting-the-most-out-of-pikau/metadata.yaml @@ -1,5 +1,6 @@ name: "Getting the most out of pīkau" status: 6 +readiness-level: 1 overview: overview.md language: en topic: housekeeping @@ -7,6 +8,7 @@ level: all tags: - introductory cover-photo: images/core-education/banner-mahuika-maui.png +trailer-video: https://vimeo.com/272882311/8e4cf1de3f content: - slug: introduction-to-pikau file: introduction-to-pikau.md diff --git a/pikau/content/pikau-courses/what-is-computational-thinking/metadata.yaml b/pikau/content/pikau-courses/what-is-computational-thinking/metadata.yaml index 29ee69c..64f4088 100644 --- a/pikau/content/pikau-courses/what-is-computational-thinking/metadata.yaml +++ b/pikau/content/pikau-courses/what-is-computational-thinking/metadata.yaml @@ -1,5 +1,6 @@ name: "What is computational thinking?" status: 5 +readiness-level: 1 language: en topic: ct level: all @@ -7,6 +8,8 @@ tags: - introductory cover-photo: images/pikau/what-is-ct.png trailer-video: https://www.youtube.com/embed/zSfzB-Z-mKM +prerequisites: + - getting-the-most-out-of-pikau overview: overview.md content: @@ -24,3 +27,8 @@ content: - slug: references file: references.md assessment-items: assessment-items.md + +extra-files: + - ct-loading-planes-trimmed-mov + - csfg-algorithms-trailer-mp4 + - ct-six-elements-of-digital-devices-mp4 diff --git a/pikau/content/pikau-courses/why-digital-technologies/assessment-items.md b/pikau/content/pikau-courses/why-digital-technologies/assessment-items.md new file mode 100644 index 0000000..eedc397 --- /dev/null +++ b/pikau/content/pikau-courses/why-digital-technologies/assessment-items.md @@ -0,0 +1 @@ +**Ngā kiriahi question:** What excites and concerns you about teaching Digital Technologies? diff --git a/pikau/content/pikau-courses/why-digital-technologies/metadata.yaml b/pikau/content/pikau-courses/why-digital-technologies/metadata.yaml new file mode 100644 index 0000000..1bf7f96 --- /dev/null +++ b/pikau/content/pikau-courses/why-digital-technologies/metadata.yaml @@ -0,0 +1,51 @@ +name: "Why digital technologies?" +status: 5 +readiness-level: 1 +language: en +topic: general +level: all +tags: + - introductory +cover-photo: images/pikau/information-theory-activity.jpg +trailer-video: https://www.youtube.com/embed/zSfzB-Z-mKM +prerequisites: + - getting-the-most-out-of-pikau + +overview: overview.md +content: + - slug: users-or-creators + file: why-this-matters/users-or-creators.md + module: Why this matters... + - slug: already-know-this + file: why-this-matters/already-know-this.md + module: Why this matters... + - slug: how-are-students-better-off + file: why-this-matters/how-are-students-better-off.md + module: Why this matters... + - slug: is-it-all-about-devices + file: why-this-matters/is-it-all-about-devices.md + module: Why this matters... + - slug: what-about-all-the-jargon + file: why-this-matters/what-about-all-the-jargon.md + module: Why this matters... + - slug: structure-of-learning-area + file: why-this-matters/structure-of-learning-area.md + module: Why this matters... + - slug: where-to-next + file: where-to-next.md + +assessment-items: assessment-items.md + +extra-files: + - digital-images-mp4 + - 1983-cpa-5426-png + - euklid-von-alexandria-1-jpg + - telescope-with-stars-jpg + - design-desk-display-jpg + - dt3e-interviews-with-teachers-mp4 + - use-modify-create-diagram-svg + - oudjat-svg + - eratosthene-png + - ada-lovelace-jpg + - edsger-wybe-dijkstra-jpg + - pagerank-png diff --git a/pikau/content/pikau-courses/why-digital-technologies/overview.md b/pikau/content/pikau-courses/why-digital-technologies/overview.md new file mode 100644 index 0000000..d972f32 --- /dev/null +++ b/pikau/content/pikau-courses/why-digital-technologies/overview.md @@ -0,0 +1,5 @@ +By the end of this pīkau you should be able to: + +- explain the purpose of the new digital technologies content in the technology learning area +- explain how digital technologies is primarily about people, and not devices +- identify the two components of digital technologies – Computational Thinking (CT) and Designing Developing Digital Outcomes (DDDO, or Digital Outcomes) diff --git a/pikau/content/pikau-courses/why-digital-technologies/where-to-next.md b/pikau/content/pikau-courses/why-digital-technologies/where-to-next.md new file mode 100644 index 0000000..c10485d --- /dev/null +++ b/pikau/content/pikau-courses/why-digital-technologies/where-to-next.md @@ -0,0 +1,22 @@ +# Where to next? + +The readiness programme has two main supports for exploring digital technologies and getting prepared to teach it: + +The first set of supports consists of: +- the website Kia Takatū a- Matihiko: +- Te Tokorima-a-Mahuika | The online self review tool, +- Pīkau | Toolkits. + +The second set of supports are the face to face and collaborative components: +- Ki te Ahikāroa | Local meet ups +- Ngā Kiriahi | Online communities of practice +- Te Pā Pouahi | The Digital Leaders programme and the digital leaders courses/pouahi. + +These have been developed with the aim of a personalised feel and approach that will engage and sustain your long-term learning journey, about and with digital technologies, to reach the ultimate goal: being ready to effectively implement this new content by 2020. + +If you want some background reading: + +- [This report from the Royal Society in 2012](https://royalsociety.org/~/media/education/computing-in-schools/2012-01-12-computing-in-schools.pdf) explains the thinking that catalysed the introduction of the equivalent curriculum in England. +- The [following talk by Hinerangi Edwards and Tim Bell](https://vimeo.com/228628929) in Rotorua was given during the consultation before the design of the new curriculum was finalised. It touches on many of the topics discussed in this pīkau/toolkit. +- This [collection of videos](http://www.digipubs.vic.edu.au/curriculum/digitaltechnologies/digital-technologies-curriculum_why) was put together in Australia to support the introduction of Digital Technologies as an area of the Australian curriculum. The videos cover a lot of the ideas around the introduction of Digital Technologies. +- If you’re interested in looking at some significant algorithms discussed in simple terms, you may enjoy “[Nine Algorithms That Changed the Future - The Ingenious Ideas That Drive Today's Computers](https://press.princeton.edu/titles/9528.html)”, by John MacCormick. diff --git a/pikau/content/pikau-courses/why-digital-technologies/why-this-matters/already-know-this.md b/pikau/content/pikau-courses/why-digital-technologies/why-this-matters/already-know-this.md new file mode 100644 index 0000000..b774df3 --- /dev/null +++ b/pikau/content/pikau-courses/why-digital-technologies/why-this-matters/already-know-this.md @@ -0,0 +1,35 @@ +# You might already know some of this + +{comment Mahuika: You have already done some relevant teaching} + +Digital technologies relates to many ideas that are already taught in schools - but perhaps not the ones that people might think of first! + +
+ +
+ +Learning about DT will draw heavily on the capabilities that are expressed in the key competencies of [The New Zealand Curriculum](http://nzcurriculum.tki.org.nz/Key-competencies) (thinking; using language, symbols, and texts; managing self; relating to others; participating and contributing). + +For example, when we look at programming in more detail, we’ll see that it involves: + +- figuring out what you are going to program (which exercises communication skills to find out what the program should do for others) +- designing and writing the program (often working with others) +- testing that it works as needed (which exercises rigour – you want to have the self-discipline to find faults rather than have the users of your program find them) +- persistence (once a fault has been identified, the cause needs to be tracked down). + +Language, symbols, and text are a big part of digital technologies, and students will be exploring new ways to represent and communicate ideas. + +Coming up with ideas for digital outcomes involves creativity. +Perhaps counter-intuitively, creativity thrives when there are constraints on what can be done. Working within the constraints of what can be done digitally provides opportunities for creativity in the rich realm of possibilities to explore. + +These are all capabilities that you may have already been encouraging in your students. + +And while *digital* technologies may seem new, you may already have been doing parts of it while you have been teaching and learning in the technology learning area. + +Inquiry learning and problem solving activities are often located in the technology strands – in the “doing and thinking” in [technological practice](http://technology.tki.org.nz/Technology-in-the-NZC/Technological-practice) (brief development, planning, and outcome development and evaluation). + +When learners apply specialist disciplinary knowledge and understandings, this falls within the [technological knowledge strand](http://technology.tki.org.nz/Technology-in-the-NZC/Technological-knowledge) – the “knowing” (modelling/testing/trialing, and systems). + +The deeper questioning is the “why”, (such as, why does technology change, what drives it and what are the results in made outcomes?) This falls within the [nature of technology strand](http://technology.tki.org.nz/Technology-in-the-NZC/Nature-of-technology) (characteristics of technology and characteristics of technological outcomes). + +These strands are woven into the new digital technologies and hangarau matihikocontent through the progress outcomes. diff --git a/pikau/content/pikau-courses/why-digital-technologies/why-this-matters/how-are-students-better-off.md b/pikau/content/pikau-courses/why-digital-technologies/why-this-matters/how-are-students-better-off.md new file mode 100644 index 0000000..958bafe --- /dev/null +++ b/pikau/content/pikau-courses/why-digital-technologies/why-this-matters/how-are-students-better-off.md @@ -0,0 +1,29 @@ +# How are your students better off? + +{comment Mahuika: How do we look after our students?} + +You may have heard people say that young people know all about digital technology. +But many of them **don’t know what they don’t know**, and often they base their views on stereotypes of the field. +A devastating consequence of this is that some of the people most needed in the field don’t even know that it might be an exciting option for them. +Watch these videos from senior tertiary computer science students who nearly missed out on the whole opportunity. + +{video url="https://www.youtube.com/embed/zSfzB-Z-mKM"} + +{video url="https://www.youtube.com/embed/zSfzB-Z-mKM"} + +Another concern is that the subject might only appeal to a particular group of students, and shouldn’t be required for everyone. +Have a look at these comments from teachers who have been teaching digital technologies for the last few years. + +{video url="https://www.youtube.com/embed/zSfzB-Z-mKM"} + +There’s also the question of how this relates to careers. +Having digital technologies in the curriculum is not all about preparing students for careers, but we hope that some will find that this is their passion. +There are incredible opportunities for employment in this area. +Employees with strong skills in this area (combining technical and “soft” skills) are in short supply in NZ and overseas, especially in software development. + +TechHub NZ has information about the kinds of jobs available: + +- [TechHub – IT Careers](https://techhub.nz/IT-Careers) +- [TechHub – Why IT?](https://techhub.nz/Why-IT) + +{video url="https://www.youtube.com/embed/zSfzB-Z-mKM"} diff --git a/pikau/content/pikau-courses/why-digital-technologies/why-this-matters/is-it-all-about-devices.md b/pikau/content/pikau-courses/why-digital-technologies/why-this-matters/is-it-all-about-devices.md new file mode 100644 index 0000000..e109648 --- /dev/null +++ b/pikau/content/pikau-courses/why-digital-technologies/why-this-matters/is-it-all-about-devices.md @@ -0,0 +1,39 @@ +# Is it all about devices? + +{comment Mahuika: Let’s focus on people, not things} + +{video url="https://www.youtube.com/embed/zSfzB-Z-mKM"} + +{video url="https://www.youtube.com/embed/zSfzB-Z-mKM"} + +{boxed-text type="pull-out"} + +**Activity: Explore a digital view of your own photo** + +Try the Pixel viewer demonstrated in the video above by going to the [Computer Science Field Guide – Pixel Value Interactive](http://csfieldguide.org.nz/en/interactives/pixel-viewer/index.html). +You can drop your own photo onto the page to explore the colours in it. + +The RGB colour mixer used in the video is from the [Computer Science field Guide – RGB Colour Mixer](http://www.csfieldguide.org.nz/en/interactives/rgb-mixer/index.html). +You can use it to see how the three numbers specify the colour. + +{boxed-text end} + +## Green screening - the hard way! + +{image file-path="images/pikau/arnold-the-wonder-parrot.jpg" alignment="center" caption="true"} + +Arnold the Wonder Parrot + +{image end} + +This is Arnold the Wonder Parrot, the mascot of the CS Unplugged project. +You can explore this photo of Arnold at the [following link](http://csfieldguide.org.nz/en/interactives/pixel-viewer/index.html?hide-menu&no-pixel-fill&image=arnold.jpg), but it doesn’t have the colours visible, just their numbers. +Can you see where the edge of the green is? +(The G value will be high, and R and B will be low.) +How about Arnold? +(Orange pixels will have a high R and G value.) +What you’re doing manually is green-screening – working out which pixels are the background, and which are the actor in front of the green. + +The activity above is looking at green-screen pixels. +This is a good example of the relationship between CT (computational thinking) and DDDO (designing and developing digital outcomes). +In DDDO, students use existing software to do green-screening to create new digital outcomes, while in CT, students would consider how to design an algorithm to do the green-screening. diff --git a/pikau/content/pikau-courses/why-digital-technologies/why-this-matters/structure-of-learning-area.md b/pikau/content/pikau-courses/why-digital-technologies/why-this-matters/structure-of-learning-area.md new file mode 100644 index 0000000..c9b20ac --- /dev/null +++ b/pikau/content/pikau-courses/why-digital-technologies/why-this-matters/structure-of-learning-area.md @@ -0,0 +1,26 @@ +# What’s the structure of the learning area? + +{comment Mahuika: What’s the big picture? How does it all fit together?} + +Not only is there new terminology to learn about technical topics, but there are also several names to know used for the new areas in the curriculum. +We’ve been using the names already (such as “computational thinking”), but in this module we’ll look at exactly how they fit together. + +The following diagram shows how digital technologies fits into The New Zealand Curriculum. +Digital Technologies is part of the technology learning area, and is made up of two “technological areas”. +The full names of the two technological areas are “computational thinking for digital technologies” and “designing and developing digital outcomes”. +These are a bit of a mouthful, so they are sometimes referred to as just “computational thinking”, or CT for short, and digital outcomes, DDDO, or 3DO. +CT and 3DO together are referred to as “digital technologies”, or DT for short. + +{image file-path="images/core-education/technology-diagram.jpg" alignment="center"} + +The other technological areas (material outcomes, processed outcomes, and design and visual communication) have [achievement objectives](http://nzcurriculum.tki.org.nz/The-New-Zealand-Curriculum/Technology/Achievement-objectives). + +The new CT and DDDO technological areas use [progress outcomes (PO’s)](http://nzcurriculum.tki.org.nz/The-New-Zealand-Curriculum/Technology/Progress-outcomes), which describe the significant learning steps students take in building their digital capability. + +These PO’s link to the New Zealand Curriculum levels giving a broad indication of the approximate stage the learning takes place. +We’ll explore that further in a separate pīkau! + +See The New Zealand Curriculum Online: + +- [Technology – Achievement objectives](http://nzcurriculum.tki.org.nz/The-New-Zealand-Curriculum/Technology/Achievement-objectives) (for areas other than Digital Technologies) +- [Technology – Progress outcomes](http://nzcurriculum.tki.org.nz/The-New-Zealand-Curriculum/Technology/Progress-outcomes) (for Digital Technologies) diff --git a/pikau/content/pikau-courses/why-digital-technologies/why-this-matters/users-or-creators.md b/pikau/content/pikau-courses/why-digital-technologies/why-this-matters/users-or-creators.md new file mode 100644 index 0000000..6f97989 --- /dev/null +++ b/pikau/content/pikau-courses/why-digital-technologies/why-this-matters/users-or-creators.md @@ -0,0 +1,10 @@ +# Users or creators? + +{comment Māui: We’re turning conventional thinking upside down} + +How much do young people actually know about digital technologies? +Watch these videos for some ideas about this... + +{video url="https://www.youtube.com/embed/zSfzB-Z-mKM"} + +{video url="https://www.youtube.com/embed/zSfzB-Z-mKM"} diff --git a/pikau/content/pikau-courses/why-digital-technologies/why-this-matters/what-about-all-the-jargon.md b/pikau/content/pikau-courses/why-digital-technologies/why-this-matters/what-about-all-the-jargon.md new file mode 100644 index 0000000..7407091 --- /dev/null +++ b/pikau/content/pikau-courses/why-digital-technologies/why-this-matters/what-about-all-the-jargon.md @@ -0,0 +1,28 @@ +# What about all the jargon? + +{comment Māui: Some pretty weird words, but how hard could it be?} + +{video url="https://www.youtube.com/embed/zSfzB-Z-mKM"} + +{video url="https://www.youtube.com/embed/zSfzB-Z-mKM"} + +{boxed-text type="quote"} + +*“Why do you use such big words for such simple ideas?”* - +(Primary school student doing a computer science activity) + +{boxed-text end} + +{boxed-text type="example"} + +**Challenge: Algorithms in the news** + +Is the word “algorithm” used in everyday news articles? +Go to a newspaper website, and search for the world “algorithm”. +Where does it come up, and how are algorithms affecting society? + +{boxed-text end} + +To look into the past, explore the following timeline to see where the idea of an algorithm has been used in the past, where the name comes from, and how algorithms have become important to us as digital devices become more common. + + diff --git a/pikau/content/topics.yaml b/pikau/content/topics.yaml index df5a126..c6bdab8 100644 --- a/pikau/content/topics.yaml +++ b/pikau/content/topics.yaml @@ -2,3 +2,4 @@ dddo: Designing and Developing Digital Outcomes ct: Computational Thinking dl: Digital Leaders housekeeping: Housekeeping +general: General diff --git a/pikau/management/commands/_PikauCourseLoader.py b/pikau/management/commands/_PikauCourseLoader.py index 2869b62..6128972 100644 --- a/pikau/management/commands/_PikauCourseLoader.py +++ b/pikau/management/commands/_PikauCourseLoader.py @@ -3,6 +3,7 @@ import os.path from django.db import transaction from utils.BaseLoader import BaseLoader +from pikau.utils.find_file import find_file from pikau.models import ( PikauCourse, PikauUnit, @@ -62,14 +63,17 @@ def load(self): remove_title=False, ).html_string + cover_photo = pikau_course_metadata.get("cover-photo", COVER_PHOTO_DEFAULT) + trailer_video = pikau_course_metadata.get("trailer-video", "") + defaults = { "name": pikau_course_metadata["name"], "status": pikau_course_metadata["status"], "language": pikau_course_metadata["language"], "topic": Topic.objects.get(slug=pikau_course_metadata["topic"]), "level": Level.objects.get(slug=pikau_course_metadata["level"]), - "trailer_video": pikau_course_metadata.get("trailer-video", ""), - "cover_photo": pikau_course_metadata.get("cover-photo", COVER_PHOTO_DEFAULT), + "trailer_video": trailer_video, + "cover_photo": cover_photo, "overview": pikau_course_overview, "readiness_level": pikau_course_metadata.get("readiness-level"), "study_plan": pikau_course_study_plan, @@ -82,6 +86,13 @@ def load(self): defaults=defaults, ) + # Check cover photo, trailer video, and extra files are logged + pikau_course.files.add(find_file(filename=cover_photo)) + if trailer_video: + pikau_course.files.add(find_file(filename=trailer_video)) + for file_slug in pikau_course_metadata.get("extra-files", list()): + pikau_course.files.add(find_file(slug=file_slug)) + # Delete all existing units for course # since the will be loaded from raw data. PikauUnit.objects.filter(pikau_course=pikau_course).delete() @@ -92,6 +103,10 @@ def load(self): heading_required=True, remove_title=True, ) + # Check files in content + for filename in unit_content.required_files["images"]: + pikau_course.files.add(find_file(filename=filename)) + pikau_course.content.create( slug=unit_data["slug"], pikau_course=pikau_course, @@ -111,6 +126,9 @@ def load(self): for pikau_course_glossary_term_slug in pikau_course_metadata.get("glossary", list()): pikau_course.glossary_terms.add(GlossaryTerm.objects.get(slug=pikau_course_glossary_term_slug)) + for pikau_course_prerequisite_slug in pikau_course_metadata.get("prerequisites", list()): + pikau_course.prerequisites.add(PikauCourse.objects.get(slug=pikau_course_prerequisite_slug)) + self.log_object_creation(created, pikau_course) self.log("All pikau courses loaded!\n") diff --git a/pikau/management/commands/loaddata.py b/pikau/management/commands/loaddata.py new file mode 100644 index 0000000..a572b90 --- /dev/null +++ b/pikau/management/commands/loaddata.py @@ -0,0 +1,14 @@ +"""Module for the custom Django loaddata command.""" + +from django.core import management + + +class Command(management.base.BaseCommand): + """Required command class for the custom Django loaddata command.""" + + help = "Update all data from content folders for all applications" + + def handle(self, *args, **options): + """Automatically called when the updatedata command is given.""" + management.call_command("loadfiles") + management.call_command("loadpikau") diff --git a/pikau/migrations/0031_pikaucourse_files.py b/pikau/migrations/0031_pikaucourse_files.py new file mode 100644 index 0000000..af55bb2 --- /dev/null +++ b/pikau/migrations/0031_pikaucourse_files.py @@ -0,0 +1,19 @@ +# Generated by Django 2.0.5 on 2018-06-07 02:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('files', '0001_initial'), + ('pikau', '0030_auto_20180529_0956'), + ] + + operations = [ + migrations.AddField( + model_name='pikaucourse', + name='files', + field=models.ManyToManyField(blank=True, related_name='pikau_courses', to='files.File'), + ), + ] diff --git a/pikau/models.py b/pikau/models.py index 8342ccd..3a52cb6 100644 --- a/pikau/models.py +++ b/pikau/models.py @@ -5,6 +5,8 @@ from django.contrib.auth.models import User from django.template import defaultfilters from django.urls import reverse +from files.models import File + LANGUAGE_CHOICES = ( ("en", "English"), @@ -242,7 +244,11 @@ class PikauCourse(models.Model): study_plan = models.TextField(blank=True) assessment_description = models.TextField(blank=True) assessment_items = models.TextField(blank=True) - # TODO: Add resources + files = models.ManyToManyField( + File, + related_name="pikau_courses", + blank=True, + ) # Development attributes development_folder = models.URLField(blank=True) diff --git a/pikau/utils/find_file.py b/pikau/utils/find_file.py new file mode 100644 index 0000000..f106214 --- /dev/null +++ b/pikau/utils/find_file.py @@ -0,0 +1,40 @@ +"""Find file object for given filename or slug.""" + +from os.path import basename +from django.core.exceptions import ObjectDoesNotExist +from files.models import File + + +def find_file(filename=None, slug=None): + """Find file object for given filename or slug. + + Args: + filename (str): String of file filename. + slug (str): String of file slug. + + Returns: + File object. + + Raises: + ValueError: If file object cannot be found. + """ + if not filename and not slug: + raise ValueError("One keyword argument is required: filename or slug") + try: + if filename: + file_object = File.objects.get(filename=filename) + else: + file_object = File.objects.get(slug=slug) + except ObjectDoesNotExist: + if filename and not filename.startswith("http"): + try: + filename = basename(filename) + file_object = File.objects.get(filename=filename) + except ObjectDoesNotExist: + file_object = None + else: + file_object = None + if not file_object: + raise ValueError("File '{}' not listed in files list".format(filename or slug)) + else: + return file_object diff --git a/pikau/utils/pathways.py b/pikau/utils/pathways.py index 7da2dde..ae23c59 100644 --- a/pikau/utils/pathways.py +++ b/pikau/utils/pathways.py @@ -6,8 +6,8 @@ GRAPH_TEMPLATE = ( "digraph {{" - "graph [bgcolor=transparent,fontname=inherit];" - "node [shape=box,fillcolor=white,style=filled,fontname=inherit];" + 'graph [bgcolor=transparent,fontname="helvetica"];' + 'node [shape=box,fillcolor=white,style=filled,fontname="helvetica", margin=0.1];' "{nodes}" "{edges}" "}}" diff --git a/pikau/views.py b/pikau/views.py index e828429..ab607f5 100644 --- a/pikau/views.py +++ b/pikau/views.py @@ -39,6 +39,7 @@ from pikau.forms import ( GlossaryForm, ) +from files.tables import FileTable NUMBER_OF_FLAME_STAGES = 7 @@ -220,6 +221,16 @@ class PikauCourseDetailView(LoginRequiredMixin, DetailView): context_object_name = "pikau_course" model = PikauCourse + def get_context_data(self, **kwargs): + """Provide the context data for the pikau course view. + + Returns: + Dictionary of context data. + """ + context = super(PikauCourseDetailView, self).get_context_data(**kwargs) + context["table"] = FileTable(self.object.files.all()) + return context + class PikauCourseContentView(LoginRequiredMixin, DetailView): """View for a pīkau course's content.""" diff --git a/release.sh b/release.sh index 23a0bba..b86601d 100755 --- a/release.sh +++ b/release.sh @@ -1,5 +1,4 @@ #!/bin/bash python manage.py collectstatic --no-input --settings=config.settings.production python manage.py migrate --no-input --settings=config.settings.production -python manage.py loadlicences --settings=config.settings.production -python manage.py loadpikau --settings=config.settings.production +python manage.py loaddata --settings=config.settings.production diff --git a/requirements/base.txt b/requirements/base.txt index fa4ccdc..eb440e6 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,7 +1,7 @@ # Base dependencies go here # Django -django==2.0.5 +django==2.0.6 whitenoise==3.3.1 django-heroku==0.3.1 django-environ==0.4.4 @@ -22,4 +22,4 @@ argon2-cffi==18.1.0 # Users django-allauth==0.36.0 django-crispy-forms==1.7.2 -django-anymail==2.2 +django-anymail==3.0 diff --git a/static/css/website.css b/static/css/website.css index ba711b5..01b3d38 100644 --- a/static/css/website.css +++ b/static/css/website.css @@ -99,6 +99,9 @@ footer > .container { .icon { width: 2.5rem; } +.icon-1-1 { + width: 1.1rem; +} .icon-small { width: 1rem; } @@ -136,3 +139,9 @@ footer > .container { .no-underline:hover { text-decoration: none !important; } +#file-image-preview { + max-height: 25em; +} +.file-image-preview-small { + max-width: 10em; +} diff --git a/static/images/core-education/technology-diagram.jpg b/static/images/core-education/technology-diagram.jpg new file mode 100644 index 0000000..74329fb Binary files /dev/null and b/static/images/core-education/technology-diagram.jpg differ diff --git a/static/images/icons/icons8/image.png b/static/images/icons/icons8/image.png new file mode 100644 index 0000000..6918c5a Binary files /dev/null and b/static/images/icons/icons8/image.png differ diff --git a/static/images/icons/icons8/video.png b/static/images/icons/icons8/video.png new file mode 100644 index 0000000..4b875f6 Binary files /dev/null and b/static/images/icons/icons8/video.png differ diff --git a/static/images/pikau/arnold-the-wonder-parrot.jpg b/static/images/pikau/arnold-the-wonder-parrot.jpg new file mode 100644 index 0000000..2dbe6f9 Binary files /dev/null and b/static/images/pikau/arnold-the-wonder-parrot.jpg differ diff --git a/static/images/pikau/information-theory-activity.jpg b/static/images/pikau/information-theory-activity.jpg new file mode 100644 index 0000000..2d03021 Binary files /dev/null and b/static/images/pikau/information-theory-activity.jpg differ diff --git a/static/images/pikau/use-modify-create-diagram.svg b/static/images/pikau/use-modify-create-diagram.svg new file mode 100644 index 0000000..8a053e7 --- /dev/null +++ b/static/images/pikau/use-modify-create-diagram.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/js/lite.render.js b/static/js/lite.render.js index fc8a76b..bd0fcac 100644 --- a/static/js/lite.render.js +++ b/static/js/lite.render.js @@ -1,5 +1,5 @@ /* -Viz.js 2.0.0-pre.8 (Graphviz 2.40.1, Emscripten 1.37.36) +Viz.js 2.0.0 (Graphviz 2.40.1, Emscripten 1.37.36) */ (function(global) { var Module = function(Module) { diff --git a/templates/files/file_detail.html b/templates/files/file_detail.html index 801c986..f654f47 100644 --- a/templates/files/file_detail.html +++ b/templates/files/file_detail.html @@ -9,7 +9,7 @@ {% endblock breadcrumbs %} {% block page_heading %} - File: {{ file.filename }} + {{ file.name }}
{% with text='Update file' %} {% url 'files:file_update' file.slug as update_url %} @@ -18,12 +18,40 @@
{% endblock page_heading %} -{% block content %} -

{{ file.location }}

+{% block content_container %} +
+ -

{{ file.licence.name }}

+

{{ file.description }}

+
+ +
+

Preview

+ {% if file.direct_link %} + {{ file.preview_html }} + {% else %} + No preview available. + {% endif %} +
-

{{ file.description }}

+
+

File Usage

-

Slug: {{ file.slug }}

-{% endblock content %} +

Pīkau courses: {{ file.pikau_courses.count }}

+
+ {% for pikau_course in file.pikau_courses.all %} + {% include "pikau/snippets/pikau_course_card.html" %} + {% endfor %} +
+
+{% endblock content_container %} diff --git a/templates/files/file_list.html b/templates/files/file_list.html index 344fb53..24b7748 100644 --- a/templates/files/file_list.html +++ b/templates/files/file_list.html @@ -24,14 +24,15 @@ {% block content %}

{% blocktrans trimmed %} - This page lists files used within Kia Takatū ā-Matihiko. + This page lists files used within Kia Takatū ā-Matihiko. + For retrieving licence information for images from Wikipedia and Wikimedia Commons use the Attribution Generator. {% endblocktrans %}

{% if unknown_licences %} {% endif %} diff --git a/templates/files/previews/external-image.html b/templates/files/previews/external-image.html new file mode 100644 index 0000000..b7cf68c --- /dev/null +++ b/templates/files/previews/external-image.html @@ -0,0 +1 @@ + diff --git a/templates/files/previews/internal-image.html b/templates/files/previews/internal-image.html new file mode 100644 index 0000000..6bbaf22 --- /dev/null +++ b/templates/files/previews/internal-image.html @@ -0,0 +1,3 @@ +{% load static %} + + diff --git a/templates/files/previews/preview.html b/templates/files/previews/preview.html new file mode 100644 index 0000000..711ca43 --- /dev/null +++ b/templates/files/previews/preview.html @@ -0,0 +1,3 @@ +
+ {{ record.preview_html }} +
diff --git a/templates/files/previews/type-icon.html b/templates/files/previews/type-icon.html new file mode 100644 index 0000000..66a2ebe --- /dev/null +++ b/templates/files/previews/type-icon.html @@ -0,0 +1,8 @@ +{% load static %} + +{% if record.media_type == "Image" %} + +{% elif record.media_type == "Video" %} + +{% endif %} +{{ record.media_type }} diff --git a/templates/files/previews/vimeo.html b/templates/files/previews/vimeo.html new file mode 100644 index 0000000..e2b49e5 --- /dev/null +++ b/templates/files/previews/vimeo.html @@ -0,0 +1 @@ +
diff --git a/templates/files/previews/youtube.html b/templates/files/previews/youtube.html new file mode 100644 index 0000000..dc0a4ec --- /dev/null +++ b/templates/files/previews/youtube.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/templates/pikau/glossaryterm_detail.html b/templates/pikau/glossaryterm_detail.html index 3bebb24..395ccd3 100644 --- a/templates/pikau/glossaryterm_detail.html +++ b/templates/pikau/glossaryterm_detail.html @@ -25,7 +25,7 @@

Pikau Courses: {{ glossaryterm.pikau_courses.count }}

- {% for pikau_course in level.pikau_courses.all %} + {% for pikau_course in glossaryterm.pikau_courses.all %} {% include "pikau/snippets/pikau_course_card.html" %} {% endfor %}
diff --git a/templates/pikau/pikaucourse_content.html b/templates/pikau/pikaucourse_content.html index 88c9a42..d6ca2ac 100644 --- a/templates/pikau/pikaucourse_content.html +++ b/templates/pikau/pikaucourse_content.html @@ -30,9 +30,11 @@

{{ pikau_course.name }}

{% endif %}
- - Start the course 🡢 - + {% if pikau_course.content.all %} + + Start the course 🡢 + + {% endif %}
diff --git a/templates/pikau/pikaucourse_detail.html b/templates/pikau/pikaucourse_detail.html index d5d2a7b..fc44b8c 100644 --- a/templates/pikau/pikaucourse_detail.html +++ b/templates/pikau/pikaucourse_detail.html @@ -3,6 +3,7 @@ {% load static %} {% load render_html_field %} {% load django_bootstrap_breadcrumbs %} +{% load render_table from django_tables2 %} {% block breadcrumbs %} {% breadcrumb "Home" "/" %} @@ -35,7 +36,6 @@

{{ pikau_course.name }}

{% endblock custom_page_heading %} {% block content %} -

Development Information

@@ -184,6 +184,12 @@

Trailer Video

View

{% endif %} + {% if pikau_course.files %} +

Files

+ + {% render_table table %} + {% endif %} + {% if pikau_course.resources %}

Resources

TODO RENDERING

diff --git a/tests/files/FileTestDataGenerator.py b/tests/files/FileTestDataGenerator.py index 586a649..b687543 100644 --- a/tests/files/FileTestDataGenerator.py +++ b/tests/files/FileTestDataGenerator.py @@ -27,9 +27,10 @@ def create_file(self, number, licence=None): """ file_object = File( slug="file-{}".format(number), - filename="File {}".format(number), + name="File {}".format(number), + filename="file-{}.ext".format(number), description="Description for file {}".format(number), - location="https://www.example.com", + location="https://www.example.com/{}".format(number), ) file_object.save() if licence: @@ -46,6 +47,7 @@ def create_licence(self, number): File object. """ licence = Licence( + slug="licence-{}".format(number), name="Licence {}".format(number), url="https://www.example.com/licence-{}".format(number), ) diff --git a/tests/files/models/test_file.py b/tests/files/models/test_file.py index 48981b6..7b6c58c 100644 --- a/tests/files/models/test_file.py +++ b/tests/files/models/test_file.py @@ -1,6 +1,9 @@ from tests.BaseTestWithDB import BaseTestWithDB from tests.files.FileTestDataGenerator import FileTestDataGenerator +from files.models import File +from django.db import IntegrityError + class FileModelTest(BaseTestWithDB): @@ -14,3 +17,34 @@ def test_file_str(self): obj.__str__(), "File 1" ) + + def test_file_repr(self): + obj = self.test_data.create_file(1) + self.assertEqual( + obj.__repr__(), + "File: {}".format(obj.slug) + ) + + def test_file_model_one_file(self): + file = self.test_data.create_file(1) + query_result = File.objects.get(slug="file-1") + self.assertEqual(query_result, file) + + def test_file_model_two_files(self): + self.test_data.create_file(1) + self.test_data.create_file(2) + self.assertQuerysetEqual( + File.objects.all(), + [ + "File: file-1", + "File: file-2" + ], + ordered=False + ) + + def test_file_model_uniqueness(self): + self.test_data.create_file(1) + self.assertRaises( + IntegrityError, + lambda: self.test_data.create_file(1) + ) diff --git a/tests/files/models/test_licence_model.py b/tests/files/models/test_licence_model.py new file mode 100644 index 0000000..95bc60e --- /dev/null +++ b/tests/files/models/test_licence_model.py @@ -0,0 +1,56 @@ +from tests.BaseTestWithDB import BaseTestWithDB +from tests.files.FileTestDataGenerator import FileTestDataGenerator + +from files.models import Licence +from django.db import IntegrityError + + +class LicenceModelTest(BaseTestWithDB): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.test_data = FileTestDataGenerator() + + def test_licence_str(self): + obj = self.test_data.create_licence(1) + self.assertEqual( + obj.__str__(), + "Licence 1" + ) + + def test_licence_model_one_licence(self): + licence = self.test_data.create_licence(1) + query_result = Licence.objects.get(name="Licence 1") + self.assertEqual(query_result, licence) + + def test_licence_model_two_licences(self): + self.test_data.create_licence(1) + self.test_data.create_licence(2) + self.assertQuerysetEqual( + Licence.objects.all(), + [ + "", + "", + ], + ordered=False + ) + + def test_licence_model_uniqueness(self): + self.test_data.create_licence(1) + self.assertRaises( + IntegrityError, + lambda: self.test_data.create_licence(1) + ) + + def test_licence_model_ordering(self): + self.test_data.create_licence(3) + self.test_data.create_licence(1) + self.test_data.create_licence(2) + self.assertQuerysetEqual( + Licence.objects.all(), + [ + "", + "", + "", + ], + ) diff --git a/tests/pikau/PikauTestDataGenerator.py b/tests/pikau/PikauTestDataGenerator.py index 9e97112..9004475 100644 --- a/tests/pikau/PikauTestDataGenerator.py +++ b/tests/pikau/PikauTestDataGenerator.py @@ -5,6 +5,14 @@ from pikau.models import ( PikauCourse, + GlossaryTerm, + Goal, + Tag, + Topic, + Level, + ProgressOutcome, + Milestone, + PikauUnit, ) @@ -44,3 +52,142 @@ def create_pikau_course(self, number): ) pikau_course.save() return pikau_course + + def create_glossary_term(self, number): + """Create GlossaryTerm object. + + Args: + number: Identifier of the glossary term (int). + + Returns: + GlossaryTerm object. + """ + glossary_term = GlossaryTerm( + slug="glossary-term-{}".format(number), + term="Glossary Term {}".format(number), + definition="

Description for glossary term {}.

".format(number), + ) + glossary_term.save() + return glossary_term + + def create_goal(self, number): + """Create Goal object. + + Args: + number: Identifier of the goal (int). + + Returns: + Goal object. + """ + goal = Goal( + slug="goal-{}".format(number), + description="

Description for goal {}.

".format(number), + ) + goal.save() + return goal + + def create_tag(self, number): + """Create Tag object. + + Args: + number: Identifier of the tag (int). + + Returns: + Tag object. + """ + tag = Tag( + slug="tag-{}".format(number), + name="tag-{}-name".format(number), + description="

Description for tag {}.

".format(number), + ) + tag.save() + return tag + + def create_topic(self, number): + """Create Topic object. + + Args: + number: Identifier of the topic (int). + + Returns: + Topic object. + """ + topic = Topic( + slug="topic-{}".format(number), + name="topic-{}-name".format(number), + ) + topic.save() + return topic + + def create_level(self, number): + """Create Level object. + + Args: + number: Identifier of the level (int). + + Returns: + Level object. + """ + level = Level( + slug="level-{}".format(number), + name="level-{}-name".format(number), + ) + level.save() + return level + + def create_progress_outcome(self, number): + """Create Progress Outcome object. + + Args: + number: Identifier of the progress outcome (int). + + Returns: + Progress Outcome object. + """ + progress_outcome = ProgressOutcome( + slug="progress-outcome-{}".format(number), + name="progress-outcome-{}-name".format(number), + abbreviation="pr-out-{}".format(number), + description="Description for progress outcome {}.".format(number), + exemplars="progress-outcome-{}".format(number), + ) + progress_outcome.save() + return progress_outcome + + def create_milestone(self, number, date): + """Create Milestone object. + + Args: + number: Identifier of the milestone (int). + + Returns: + Milestone object. + """ + milestone = Milestone( + name="milestone-{}".format(number), + date=date, + ) + milestone.save() + return milestone + + def create_pikau_unit(self, pikau_course, number, module_name=False): + """Create PikauUnit object. + + Args: + number: Identifier of the pikau unit (int). + + Returns: + PikauUnit object. + """ + pikau_unit = PikauUnit( + slug="pikau-unit-{}".format(number), + number=number, + pikau_course=pikau_course, + name="pikau-unit-{}-name".format(number), + content="

Content for piaku unit {}.

".format(number), + ) + if module_name: + pikau_unit.module_name = "pikau-unit-{}-module-name".format(number) + + pikau_unit.save() + return pikau_unit diff --git a/tests/pikau/models/test_glossary_model.py b/tests/pikau/models/test_glossary_model.py new file mode 100644 index 0000000..023377d --- /dev/null +++ b/tests/pikau/models/test_glossary_model.py @@ -0,0 +1,43 @@ +from tests.BaseTestWithDB import BaseTestWithDB +from tests.pikau.PikauTestDataGenerator import PikauTestDataGenerator + +from pikau.models import GlossaryTerm +from django.db import IntegrityError + + +class GlossaryModelTest(BaseTestWithDB): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.test_data = PikauTestDataGenerator() + + def test_glossary_model_one_glossary_term(self): + glossary_term = self.test_data.create_glossary_term(1) + query_result = GlossaryTerm.objects.get(slug="glossary-term-1") + self.assertEqual(query_result, glossary_term) + + def test_glossary_model_two_glossary_terms(self): + self.test_data.create_glossary_term(1) + self.test_data.create_glossary_term(2) + self.assertQuerysetEqual( + GlossaryTerm.objects.all(), + [ + "", + "" + ], + ordered=False + ) + + def test_glossary_model_uniqueness(self): + self.test_data.create_glossary_term(1) + self.assertRaises( + IntegrityError, + lambda: self.test_data.create_glossary_term(1) + ) + + def test_glossary_model_str(self): + glossary_term = self.test_data.create_glossary_term(1) + self.assertEqual( + glossary_term.__str__(), + "Glossary Term 1" + ) diff --git a/tests/pikau/models/test_goal_model.py b/tests/pikau/models/test_goal_model.py new file mode 100644 index 0000000..a2f3113 --- /dev/null +++ b/tests/pikau/models/test_goal_model.py @@ -0,0 +1,43 @@ +from tests.BaseTestWithDB import BaseTestWithDB +from tests.pikau.PikauTestDataGenerator import PikauTestDataGenerator + +from pikau.models import Goal +from django.db import IntegrityError + + +class GoalModelTest(BaseTestWithDB): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.test_data = PikauTestDataGenerator() + + def test_goal_model_one_goal(self): + goal = self.test_data.create_goal(1) + query_result = Goal.objects.get(slug="goal-1") + self.assertEqual(query_result, goal) + + def test_goal_model_two_goals(self): + self.test_data.create_goal(1) + self.test_data.create_goal(2) + self.assertQuerysetEqual( + Goal.objects.all(), + [ + "Description for goal 1.

>", + "Description for goal 2.

>" + ], + ordered=False + ) + + def test_goal_model_uniqueness(self): + self.test_data.create_goal(1) + self.assertRaises( + IntegrityError, + lambda: self.test_data.create_goal(1) + ) + + def test_goal_model_str(self): + goal = self.test_data.create_goal(1) + self.assertEqual( + goal.__str__(), + "

Description for goal 1.

" + ) diff --git a/tests/pikau/models/test_level_model.py b/tests/pikau/models/test_level_model.py new file mode 100644 index 0000000..5bd6abd --- /dev/null +++ b/tests/pikau/models/test_level_model.py @@ -0,0 +1,43 @@ +from tests.BaseTestWithDB import BaseTestWithDB +from tests.pikau.PikauTestDataGenerator import PikauTestDataGenerator + +from pikau.models import Level +from django.db import IntegrityError + + +class LevelModelTest(BaseTestWithDB): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.test_data = PikauTestDataGenerator() + + def test_level_model_one_level(self): + level = self.test_data.create_level(1) + query_result = Level.objects.get(slug="level-1") + self.assertEqual(query_result, level) + + def test_level_model_two_levels(self): + self.test_data.create_level(1) + self.test_data.create_level(2) + self.assertQuerysetEqual( + Level.objects.all(), + [ + "", + "", + ], + ordered=False + ) + + def test_level_model_uniqueness(self): + self.test_data.create_level(1) + self.assertRaises( + IntegrityError, + lambda: self.test_data.create_level(1) + ) + + def test_level_model_str(self): + level = self.test_data.create_level(1) + self.assertEqual( + level.__str__(), + "level-1-name" + ) diff --git a/tests/pikau/models/test_milestone_model.py b/tests/pikau/models/test_milestone_model.py new file mode 100644 index 0000000..1ffc6e1 --- /dev/null +++ b/tests/pikau/models/test_milestone_model.py @@ -0,0 +1,76 @@ +from tests.BaseTestWithDB import BaseTestWithDB +from tests.pikau.PikauTestDataGenerator import PikauTestDataGenerator + +from pikau.models import Milestone + +from django.db import IntegrityError +from django.template import defaultfilters + +import datetime as dt +from datetime import datetime + + +class MilestoneModelTest(BaseTestWithDB): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.test_data = PikauTestDataGenerator() + self.current_time = datetime.now() + + def test_milestone_model_one_milestone(self): + milestone = self.test_data.create_milestone(1, self.current_time) + query_result = Milestone.objects.get(name="milestone-1") + self.assertEqual(query_result, milestone) + + def test_milestone_model_two_milestones(self): + date_1 = self.current_time + date_2 = self.current_time + dt.timedelta(days=30) + self.test_data.create_milestone(1, date_1) + self.test_data.create_milestone(2, date_2) + self.assertQuerysetEqual( + Milestone.objects.all(), + [ + "".format(defaultfilters.date(date_1)), + "".format(defaultfilters.date(date_2)), + ], + ordered=False + ) + + def test_milestone_model_uniqueness(self): + date = self.current_time + self.test_data.create_milestone(1, date) + self.assertRaises( + IntegrityError, + lambda: self.test_data.create_milestone(1, date), + ) + + def test_milestone_model_str(self): + date = self.current_time + milestone = self.test_data.create_milestone(1, date) + self.assertEqual( + milestone.__str__(), + "milestone-1 - {}".format(defaultfilters.date(date)) + ) + + def test_milestone_model_is_upcoming_future_date(self): + future_date = self.current_time + dt.timedelta(days=30) + milestone = self.test_data.create_milestone(1, future_date.date()) + self.assertIs(milestone.is_upcoming, True) + + def test_milestone_model_is_upcoming_past_date(self): + past_date = self.current_time - dt.timedelta(days=30) + milestone = self.test_data.create_milestone(1, past_date.date()) + self.assertIs(milestone.is_upcoming, False) + + def test_milestone_model_ordering(self): + date_1 = self.current_time + date_2 = self.current_time + dt.timedelta(days=30) + self.test_data.create_milestone(1, date_2) + self.test_data.create_milestone(2, date_1) + self.assertQuerysetEqual( + Milestone.objects.all(), + [ + "".format(defaultfilters.date(date_1)), + "".format(defaultfilters.date(date_2)), + ], + ) diff --git a/tests/pikau/models/test_pikau_course.py b/tests/pikau/models/test_pikau_course.py index 6a62370..5b086bf 100644 --- a/tests/pikau/models/test_pikau_course.py +++ b/tests/pikau/models/test_pikau_course.py @@ -1,6 +1,9 @@ from tests.BaseTestWithDB import BaseTestWithDB from tests.pikau.PikauTestDataGenerator import PikauTestDataGenerator +import datetime as dt +from datetime import datetime + class PikauCourseModelTest(BaseTestWithDB): @@ -14,3 +17,60 @@ def test_pikau_course_str(self): pikau_course.__str__(), "Pikau Course 1" ) + + def test_pikau_course_is_overdue_past_milestone_date(self): + pikau_course = self.test_data.create_pikau_course(1) + milestone_date = datetime.now() - dt.timedelta(days=30) + pikau_course.milestone = self.test_data.create_milestone(1, milestone_date.date()) + self.assertEqual( + pikau_course.is_overdue_milestone, + True + ) + + def test_pikau_course_is_overdue_future_milestone_date(self): + pikau_course = self.test_data.create_pikau_course(1) + milestone_date = datetime.now() + dt.timedelta(days=30) + pikau_course.milestone = self.test_data.create_milestone(1, milestone_date.date()) + self.assertEqual( + pikau_course.is_overdue_milestone, + False + ) + + def test_pikau_course_is_overdue_current_milestone_date(self): + pikau_course = self.test_data.create_pikau_course(1) + milestone_date = datetime.now() + pikau_course.milestone = self.test_data.create_milestone(1, milestone_date.date()) + self.assertEqual( + pikau_course.is_overdue_milestone, + False + ) + + def test_pikau_course_is_overdue_course_complete_past_milestone_date(self): + pikau_course = self.test_data.create_pikau_course(1) + milestone_date = datetime.now() - dt.timedelta(days=30) + pikau_course.milestone = self.test_data.create_milestone(1, milestone_date.date()) + pikau_course.status = 7 + self.assertEqual( + pikau_course.is_overdue_milestone, + False + ) + + def test_pikau_course_is_overdue_course_complete_future_milestone_date(self): + pikau_course = self.test_data.create_pikau_course(1) + milestone_date = datetime.now() + dt.timedelta(days=30) + pikau_course.milestone = self.test_data.create_milestone(1, milestone_date.date()) + pikau_course.status = 7 + self.assertEqual( + pikau_course.is_overdue_milestone, + False + ) + + def test_pikau_course_is_overdue_course_complete_current_milestone_date(self): + pikau_course = self.test_data.create_pikau_course(1) + milestone_date = datetime.now() + pikau_course.milestone = self.test_data.create_milestone(1, milestone_date.date()) + pikau_course.status = 7 + self.assertEqual( + pikau_course.is_overdue_milestone, + False + ) diff --git a/tests/pikau/models/test_pikau_unit_model.py b/tests/pikau/models/test_pikau_unit_model.py new file mode 100644 index 0000000..cf99b23 --- /dev/null +++ b/tests/pikau/models/test_pikau_unit_model.py @@ -0,0 +1,82 @@ +from tests.BaseTestWithDB import BaseTestWithDB +from tests.pikau.PikauTestDataGenerator import PikauTestDataGenerator + +from pikau.models import PikauUnit +from django.db import IntegrityError + + +class PikauUnitModelTest(BaseTestWithDB): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.test_data = PikauTestDataGenerator() + + def test_pikau_unit_model_one_unit(self): + pikau_course = self.test_data.create_pikau_course("1") + pikau_unit = self.test_data.create_pikau_unit(pikau_course, 1) + query_result = PikauUnit.objects.get(slug="pikau-unit-1") + self.assertEqual(query_result, pikau_unit) + + def test_pikau_unit_model_two_units_with_module_name(self): + pikau_course = self.test_data.create_pikau_course("1") + self.test_data.create_pikau_unit(pikau_course, 1, True) + self.test_data.create_pikau_unit(pikau_course, 2, True) + self.assertQuerysetEqual( + PikauUnit.objects.all(), + [ + "", + "", + ], + ordered=False + ) + + def test_pikau_unit_model_two_units_without_module_name(self): + pikau_course = self.test_data.create_pikau_course("1") + self.test_data.create_pikau_unit(pikau_course, 1) + self.test_data.create_pikau_unit(pikau_course, 2) + self.assertQuerysetEqual( + PikauUnit.objects.all(), + [ + "", + "", + ], + ordered=False + ) + + def test_pikau_unit_model_uniqueness(self): + pikau_course = self.test_data.create_pikau_course("1") + self.test_data.create_pikau_unit(pikau_course, 1) + self.assertRaises( + IntegrityError, + lambda: self.test_data.create_pikau_unit(pikau_course, 1) + ) + + def test_pikau_unit_model_str_with_module(self): + pikau_course = self.test_data.create_pikau_course("1") + pikau_unit = self.test_data.create_pikau_unit(pikau_course, 1, True) + self.assertEqual( + pikau_unit.__str__(), + "Pikau Course 1: pikau-unit-1-module-name - pikau-unit-1-name" + ) + + def test_pikau_unit_model_str_without_module(self): + pikau_course = self.test_data.create_pikau_course("1") + pikau_unit = self.test_data.create_pikau_unit(pikau_course, 1) + self.assertEqual( + pikau_unit.__str__(), + "Pikau Course 1: pikau-unit-1-name" + ) + + def test_pikau_unit_model_ordering(self): + pikau_course = self.test_data.create_pikau_course("1") + self.test_data.create_pikau_unit(pikau_course, 1) + self.test_data.create_pikau_unit(pikau_course, 2) + self.test_data.create_pikau_unit(pikau_course, 3) + self.assertQuerysetEqual( + PikauUnit.objects.all(), + [ + "", + "", + "", + ] + ) diff --git a/tests/pikau/models/test_progress_outcome_model.py b/tests/pikau/models/test_progress_outcome_model.py new file mode 100644 index 0000000..91e4508 --- /dev/null +++ b/tests/pikau/models/test_progress_outcome_model.py @@ -0,0 +1,59 @@ +from tests.BaseTestWithDB import BaseTestWithDB +from tests.pikau.PikauTestDataGenerator import PikauTestDataGenerator + +from pikau.models import ProgressOutcome +from django.db import IntegrityError + + +class ProgressOutcomeModelTest(BaseTestWithDB): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.test_data = PikauTestDataGenerator() + + def test_progress_outcome_model_one_progress_outcome(self): + progress_outcome = self.test_data.create_progress_outcome(1) + query_result = ProgressOutcome.objects.get(slug="progress-outcome-1") + self.assertEqual(query_result, progress_outcome) + + def test_progress_outcome_model_two_progress_outcomes(self): + self.test_data.create_progress_outcome(1) + self.test_data.create_progress_outcome(2) + self.assertQuerysetEqual( + ProgressOutcome.objects.all(), + [ + "", + "", + ], + ordered=False + ) + + def test_progress_outcome_model_uniqueness(self): + self.test_data.create_progress_outcome(1) + self.assertRaises( + IntegrityError, + lambda: self.test_data.create_progress_outcome(1), + ) + + def test_progress_outcome_model_str(self): + progress_outcome = self.test_data.create_progress_outcome(1) + self.assertEqual( + progress_outcome.__str__(), + "progress-outcome-1-name" + ) + + # SQLite does not enforce max_length. This test may be used in the future + # to validate max_length. + # + # def test_progress_outcome_model_abbreviation_max_length(self): + # abbreviation = "a" * 11 # max length for abbreviation is 10. + # self.assertRaises( + # IntegrityError, + # lambda: ProgressOutcome( + # slug="progress-outcome-1", + # name="progress-outcome-1-name", + # abbreviation=abbreviation, + # description="Description for progress outcome 1.", + # exemplars="progress-outcome-1", + # ), + # ) diff --git a/tests/pikau/models/test_tag_model.py b/tests/pikau/models/test_tag_model.py new file mode 100644 index 0000000..f438b61 --- /dev/null +++ b/tests/pikau/models/test_tag_model.py @@ -0,0 +1,43 @@ +from tests.BaseTestWithDB import BaseTestWithDB +from tests.pikau.PikauTestDataGenerator import PikauTestDataGenerator + +from pikau.models import Tag +from django.db import IntegrityError + + +class TagModelTest(BaseTestWithDB): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.test_data = PikauTestDataGenerator() + + def test_tag_model_one_tag(self): + tag = self.test_data.create_tag(1) + query_result = Tag.objects.get(slug="tag-1") + self.assertEqual(query_result, tag) + + def test_tag_model_two_tags(self): + self.test_data.create_tag(1) + self.test_data.create_tag(2) + self.assertQuerysetEqual( + Tag.objects.all(), + [ + "", + "", + ], + ordered=False + ) + + def test_tag_model_uniqueness(self): + self.test_data.create_tag(1) + self.assertRaises( + IntegrityError, + lambda: self.test_data.create_tag(1) + ) + + def test_tag_model_str(self): + tag = self.test_data.create_tag(1) + self.assertEqual( + tag.__str__(), + "tag-1-name" + ) diff --git a/tests/pikau/models/test_topic_model.py b/tests/pikau/models/test_topic_model.py new file mode 100644 index 0000000..7361dbb --- /dev/null +++ b/tests/pikau/models/test_topic_model.py @@ -0,0 +1,43 @@ +from tests.BaseTestWithDB import BaseTestWithDB +from tests.pikau.PikauTestDataGenerator import PikauTestDataGenerator + +from pikau.models import Topic +from django.db import IntegrityError + + +class TopicModelTest(BaseTestWithDB): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.test_data = PikauTestDataGenerator() + + def test_topic_model_one_topic(self): + topic = self.test_data.create_topic(1) + query_result = Topic.objects.get(slug="topic-1") + self.assertEqual(query_result, topic) + + def test_topic_model_two_topics(self): + self.test_data.create_topic(1) + self.test_data.create_topic(2) + self.assertQuerysetEqual( + Topic.objects.all(), + [ + "", + "", + ], + ordered=False + ) + + def test_topic_model_uniqueness(self): + self.test_data.create_topic(1) + self.assertRaises( + IntegrityError, + lambda: self.test_data.create_topic(1) + ) + + def test_topic_model_str(self): + tag = self.test_data.create_topic(1) + self.assertEqual( + tag.__str__(), + "topic-1-name" + ) diff --git a/tests/pikau/views/__init__.py b/tests/pikau/views/__init__.py new file mode 100644 index 0000000..25229bc --- /dev/null +++ b/tests/pikau/views/__init__.py @@ -0,0 +1 @@ +"""Module for tests of the views in the pikau application.""" diff --git a/tests/pikau/views/test_documentation_view.py b/tests/pikau/views/test_documentation_view.py new file mode 100644 index 0000000..68471e8 --- /dev/null +++ b/tests/pikau/views/test_documentation_view.py @@ -0,0 +1,111 @@ +from tests.BaseTestWithDB import BaseTestWithDB +from tests.pikau.PikauTestDataGenerator import PikauTestDataGenerator +from django.urls import reverse +from http import HTTPStatus + +from pikau.models import ( + ProgressOutcome, + Tag, + STATUS_CHOICES, + READINESS_LEVELS, +) + + +class DocumentationViewTest(BaseTestWithDB): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.test_data = PikauTestDataGenerator() + self.language = "en" + + def test_documentation_view(self): + response = self.client.get(reverse("pikau:docs")) + self.assertEqual(HTTPStatus.OK, response.status_code) + self.assertContains(response, "Pīkau Documentation") + + def test_documentation_view_context_status_stages(self): + response = self.client.get(reverse("pikau:docs")) + self.assertEqual(HTTPStatus.OK, response.status_code) + self.assertEqual(response.context["status_stages"], STATUS_CHOICES) + + def test_documentation_view_context_topics(self): + self.test_data.create_topic(1) + self.test_data.create_topic(2) + + response = self.client.get(reverse("pikau:docs")) + self.assertEqual(HTTPStatus.OK, response.status_code) + self.assertQuerysetEqual( + response.context["topics"], + [ + "", + "", + ] + ) + + def test_documentation_view_context_levels(self): + self.test_data.create_level(1) + self.test_data.create_level(2) + + response = self.client.get(reverse("pikau:docs")) + self.assertEqual(HTTPStatus.OK, response.status_code) + self.assertQuerysetEqual( + response.context["levels"], + [ + "", + "", + ] + ) + + def test_documentation_view_context_progress_outcomes(self): + self.test_data.create_progress_outcome(1) + self.test_data.create_progress_outcome(2) + + response = self.client.get(reverse("pikau:docs")) + self.assertEqual(HTTPStatus.OK, response.status_code) + self.assertQuerysetEqual( + ProgressOutcome.objects.all(), + [ + "", + "", + ], + ordered=False + ) + + def test_documentation_view_context_srt_tags(self): + tag_1 = Tag( + slug="srt-tag-1", + name="srt-tag-1-name", + description="

Description for tag 1.

", + ) + tag_1.save() + + tag_2 = Tag( + slug="srt-tag-2", + name="srt-tag-2-name", + description="

Description for tag 2.

", + ) + tag_2.save() + + response = self.client.get(reverse("pikau:docs")) + self.assertEqual(HTTPStatus.OK, response.status_code) + self.assertQuerysetEqual( + response.context["srt_tags"], + [ + "", + "", + ] + ) + + def test_documentation_view_context_readiness_levels(self): + response = self.client.get(reverse("pikau:docs")) + self.assertEqual(HTTPStatus.OK, response.status_code) + self.assertEqual(response.context["readiness_levels"], READINESS_LEVELS) + + def test_documentation_view_with_no_data(self): + response = self.client.get(reverse("pikau:docs")) + self.assertEqual(HTTPStatus.OK, response.status_code) + self.assertEqual(response.context["status_stages"], STATUS_CHOICES) + self.assertQuerysetEqual(response.context["topics"], []) + self.assertQuerysetEqual(response.context["levels"], []) + self.assertQuerysetEqual(response.context["progress_outcomes"], []) + self.assertEqual(response.context["readiness_levels"], READINESS_LEVELS) diff --git a/tests/pikau/views/test_glossary_view.py b/tests/pikau/views/test_glossary_view.py new file mode 100644 index 0000000..c608f3d --- /dev/null +++ b/tests/pikau/views/test_glossary_view.py @@ -0,0 +1,67 @@ +from tests.BaseTestWithDB import BaseTestWithDB +from tests.pikau.PikauTestDataGenerator import PikauTestDataGenerator +from django.urls import reverse + + +class GlossaryViewTest(BaseTestWithDB): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.language = "en" + self.test_data = PikauTestDataGenerator() + + def test_pikau_glossary_view_with_no_definitions(self): + url = reverse("pikau:glossaryterm_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(len(response.context["glossaryterm_list"]), 0) + + def test_pikau_glossary_view_with_one_definition(self): + self.test_data.create_glossary_term(1) + + url = reverse("pikau:glossaryterm_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(len(response.context["glossaryterm_list"]), 1) + self.assertQuerysetEqual( + response.context["glossaryterm_list"], + [""] + ) + + def test_pikau_glossary_view_with_two_definitions(self): + self.test_data.create_glossary_term(1) + self.test_data.create_glossary_term(2) + + url = reverse("pikau:glossaryterm_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(len(response.context["glossaryterm_list"]), 2) + self.assertQuerysetEqual( + response.context["glossaryterm_list"], + [ + "", + "", + ], + ordered=False + ) + + def test_pikau_glossary_view_order(self): + self.test_data.create_glossary_term(3) + self.test_data.create_glossary_term(2) + self.test_data.create_glossary_term(1) + + url = reverse("pikau:glossaryterm_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(len(response.context["glossaryterm_list"]), 3) + self.assertQuerysetEqual( + response.context["glossaryterm_list"], + [ + "", + "", + "", + ], + ordered=False + ) + + # TODO: Test remaining glossary related views. diff --git a/tests/pikau/views/test_goal_view.py b/tests/pikau/views/test_goal_view.py new file mode 100644 index 0000000..bd2e708 --- /dev/null +++ b/tests/pikau/views/test_goal_view.py @@ -0,0 +1,63 @@ +from tests.BaseTestWithDB import BaseTestWithDB +from tests.pikau.PikauTestDataGenerator import PikauTestDataGenerator +from django.urls import reverse + + +class GoalViewTest(BaseTestWithDB): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.language = "en" + self.test_data = PikauTestDataGenerator() + + def test_pikau_goal_view_with_no_goals(self): + url = reverse("pikau:goal_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(len(response.context["goals"]), 0) + + def test_pikau_goal_view_with_one_goal(self): + self.test_data.create_goal(1) + + url = reverse("pikau:goal_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(len(response.context["goals"]), 1) + self.assertQuerysetEqual( + response.context["goals"], + ["Description for goal 1.

>"] + ) + + def test_pikau_goal_view_with_two_goals(self): + self.test_data.create_goal(1) + self.test_data.create_goal(2) + + url = reverse("pikau:goal_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(len(response.context["goals"]), 2) + self.assertQuerysetEqual( + response.context["goals"], + [ + "Description for goal 1.

>", + "Description for goal 2.

>", + ] + ) + + def test_pikau_goal_view_order(self): + self.test_data.create_goal(3) + self.test_data.create_goal(2) + self.test_data.create_goal(1) + + url = reverse("pikau:goal_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(len(response.context["goals"]), 3) + self.assertQuerysetEqual( + response.context["goals"], + [ + "Description for goal 1.

>", + "Description for goal 2.

>", + "Description for goal 3.

>", + ] + ) diff --git a/tests/pikau/views/test_index_view.py b/tests/pikau/views/test_index_view.py new file mode 100644 index 0000000..b099fb9 --- /dev/null +++ b/tests/pikau/views/test_index_view.py @@ -0,0 +1,15 @@ +from tests.BaseTestWithDB import BaseTestWithDB +from django.urls import reverse +from http import HTTPStatus + + +class IndexViewTest(BaseTestWithDB): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.language = "en" + + def test_index_view(self): + response = self.client.get(reverse("pikau:index")) + self.assertEqual(HTTPStatus.OK, response.status_code) + self.assertContains(response, "Pīkau Content") diff --git a/tests/pikau/views/test_level_view.py b/tests/pikau/views/test_level_view.py new file mode 100644 index 0000000..f0444c3 --- /dev/null +++ b/tests/pikau/views/test_level_view.py @@ -0,0 +1,78 @@ +from tests.BaseTestWithDB import BaseTestWithDB +from tests.pikau.PikauTestDataGenerator import PikauTestDataGenerator +from django.urls import reverse + + +class LevelViewTest(BaseTestWithDB): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.language = "en" + self.test_data = PikauTestDataGenerator() + + def test_pikau_level_view_with_valid_slug(self): + self.test_data.create_level(1) + + url = reverse("pikau:level", kwargs={"slug": "level-1"}) + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(url, "/pikau/levels/level-1/") + + def test_pikau_level_view_with_invalid_slug(self): + self.test_data.create_level(1) + + url = reverse("pikau:level", kwargs={"slug": "level-5"}) + response = self.client.get(url) + self.assertEqual(404, response.status_code) + + def test_pikau_level_list_view_with_no_levels(self): + url = reverse("pikau:level_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(len(response.context["levels"]), 0) + + def test_pikau_level_list_view_with_one_level(self): + self.test_data.create_level(1) + + url = reverse("pikau:level_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(len(response.context["levels"]), 1) + self.assertQuerysetEqual( + response.context["levels"], + [""] + ) + + def test_pikau_level_list_view_with_two_levels(self): + self.test_data.create_level(1) + self.test_data.create_level(2) + + url = reverse("pikau:level_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(len(response.context["levels"]), 2) + self.assertQuerysetEqual( + response.context["levels"], + [ + "", + "", + ] + ) + + def test_pikau_level_list_view_order(self): + self.test_data.create_level(3) + self.test_data.create_level(2) + self.test_data.create_level(1) + + url = reverse("pikau:level_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(len(response.context["levels"]), 3) + self.assertQuerysetEqual( + response.context["levels"], + [ + "", + "", + "", + ] + ) diff --git a/tests/pikau/views/test_milestone_view.py b/tests/pikau/views/test_milestone_view.py new file mode 100644 index 0000000..07c9496 --- /dev/null +++ b/tests/pikau/views/test_milestone_view.py @@ -0,0 +1,119 @@ +from tests.BaseTestWithDB import BaseTestWithDB +from tests.pikau.PikauTestDataGenerator import PikauTestDataGenerator +from django.urls import reverse +from django.template import defaultfilters + +import datetime as dt +from datetime import datetime + + +class MilestoneViewTest(BaseTestWithDB): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.language = "en" + self.test_data = PikauTestDataGenerator() + self.current_time = datetime.now() + + def test_pikau_milestone_list_view_with_no_milestones(self): + url = reverse("pikau:milestone_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(len(response.context["milestones"]), 0) + + def test_pikau_milestone_list_view_with_one_milestone(self): + self.test_data.create_milestone(1, self.current_time) + + url = reverse("pikau:milestone_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(len(response.context["milestones"]), 1) + self.assertQuerysetEqual( + response.context["milestones"], + ["".format(defaultfilters.date(self.current_time))] + ) + + def test_pikau_milestone_list_view_order(self): + date_1 = self.current_time + date_2 = self.current_time + dt.timedelta(days=30) + self.test_data.create_milestone(1, date_2) + self.test_data.create_milestone(2, date_1) + + url = reverse("pikau:milestone_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertQuerysetEqual( + response.context["milestones"], + [ + "".format(defaultfilters.date(date_1)), + "".format(defaultfilters.date(date_2)), + ], + ) + + def test_pikau_milestone_list_view_with_two_milestones(self): + self.test_data.create_milestone(1, self.current_time) + + url = reverse("pikau:milestone_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(len(response.context["milestones"]), 1) + self.assertQuerysetEqual( + response.context["milestones"], + ["".format(defaultfilters.date(self.current_time))] + ) + + def test_pikau_milestone_list_view_milestone_status_course_count(self): + # checks how many courses are on a certain status for a certain milestone + pikau_course = self.test_data.create_pikau_course(1) + milestone = self.test_data.create_milestone(1, self.current_time) + pikau_course.milestone = milestone + pikau_course.status = 3 + pikau_course.save() + + url = reverse("pikau:milestone_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + + milestones = response.context["milestones"] + milestone_1 = milestones[0] + milestone_1_status_3_course_count = milestone_1.status[3] + milestone_1_status_7_course_count = milestone_1.status[7] + self.assertEqual(milestone_1_status_3_course_count, 1) + self.assertEqual(milestone_1_status_7_course_count, 0) + + def test_pikau_milestone_list_view_status_stages(self): + url = reverse("pikau:milestone_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + + status_stages = [ + (1, 'Stage 1', 'Conceptualising'), + (2, 'Stage 2', 'Developing'), + (3, 'Stage 3', 'Reviewing\nAcademic'), + (4, 'Stage 4', 'Reviewing\nLanguage'), + (5, 'Stage 5', 'Reviewing\nTechnical'), + (6, 'Stage 6', 'Completed'), + (7, 'Stage 7', 'Completed\nPublished to iQualify'), + ] + self.assertEqual(response.context["status_stages"], status_stages) + + def test_milestone_view_no_milestones(self): + url = reverse("pikau:milestone", kwargs={"pk": 1}) + response = self.client.get(url) + self.assertEqual(404, response.status_code) + + def test_milestone_view_with_valid_pk(self): + self.test_data.create_milestone(1, self.current_time) + url = reverse("pikau:milestone", kwargs={"pk": 1}) + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual( + url, + "/pikau/milestones/1/" + ) + + def test_milestone_view_with_invalid_pk(self): + self.test_data.create_milestone(1, self.current_time) + url = reverse("pikau:milestone", kwargs={"pk": 5}) + response = self.client.get(url) + self.assertEqual(404, response.status_code) diff --git a/tests/pikau/views/test_pathways_view.py b/tests/pikau/views/test_pathways_view.py new file mode 100644 index 0000000..49c59d8 --- /dev/null +++ b/tests/pikau/views/test_pathways_view.py @@ -0,0 +1,22 @@ +from tests.BaseTestWithDB import BaseTestWithDB +from django.urls import reverse + +from pikau.models import READINESS_LEVELS + + +class PathwaysViewTest(BaseTestWithDB): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.language = "en" + + def test_pathways_view(self): + response = self.client.get(reverse("pikau:pathways")) + self.assertEqual(200, response.status_code) + + def test_pathways_view_context_readiness_levels(self): + response = self.client.get(reverse("pikau:pathways")) + self.assertEqual(200, response.status_code) + self.assertEqual(response.context["readiness_levels"], READINESS_LEVELS) + + # TODO: test pathways notation - utils test. diff --git a/tests/pikau/views/test_pikau_course_view.py b/tests/pikau/views/test_pikau_course_view.py new file mode 100644 index 0000000..bd93bdf --- /dev/null +++ b/tests/pikau/views/test_pikau_course_view.py @@ -0,0 +1,84 @@ +from tests.BaseTestWithDB import BaseTestWithDB +from tests.pikau.PikauTestDataGenerator import PikauTestDataGenerator +from django.urls import reverse + + +class PikauCourseViewTest(BaseTestWithDB): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.language = "en" + self.test_data = PikauTestDataGenerator() + + def test_pikau_course_list_view_with_no_courses(self): + url = reverse("pikau:pikau_course_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(len(response.context["pikau_courses"]), 0) + + def test_pikau_course_list_view_with_one_course(self): + self.test_data.create_pikau_course(1) + + url = reverse("pikau:pikau_course_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(len(response.context["pikau_courses"]), 1) + self.assertQuerysetEqual( + response.context["pikau_courses"], + [""] + ) + + def test_pikau_course_list_view_with_two_courses(self): + self.test_data.create_pikau_course(1) + self.test_data.create_pikau_course(2) + + url = reverse("pikau:pikau_course_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(len(response.context["pikau_courses"]), 2) + self.assertQuerysetEqual( + response.context["pikau_courses"], + [ + "", + "", + ] + ) + + def test_pikau_course_view_with_valid_slug(self): + self.test_data.create_pikau_course(1) + + url = reverse("pikau:pikau_course", kwargs={"slug": "pikau-course-1"}) + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(url, "/pikau/pikau-courses/pikau-course-1/") + + def test_pikau_course_view_with_invalid_slug(self): + self.test_data.create_pikau_course(1) + + url = reverse("pikau:pikau_course", kwargs={"slug": "pikau-course-5"}) + response = self.client.get(url) + self.assertEqual(404, response.status_code) + + def test_pikau_course_content_view_with_valid_slug(self): + pikau_course_1 = self.test_data.create_pikau_course(1) + self.test_data.create_pikau_unit(pikau_course_1, 1) + + url = reverse("pikau:pikau_content", kwargs={"slug": "pikau-course-1"}) + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertContains(response, "Pikau Course 1") + + def test_pikau_course_content_view_with_invalid_slug(self): + pikau_course_1 = self.test_data.create_pikau_course(1) + self.test_data.create_pikau_unit(pikau_course_1, 1) + + url = reverse("pikau:pikau_content", kwargs={"slug": "pikau-course-5"}) + response = self.client.get(url) + self.assertEqual(404, response.status_code) + + def test_pikau_course_content_view_with_no_pikau_units(self): + self.test_data.create_pikau_course(1) + + url = reverse("pikau:pikau_content", kwargs={"slug": "pikau-course-1"}) + response = self.client.get(url) + self.assertEqual(200, response.status_code) diff --git a/tests/pikau/views/test_pikau_unit_view.py b/tests/pikau/views/test_pikau_unit_view.py new file mode 100644 index 0000000..088e0f2 --- /dev/null +++ b/tests/pikau/views/test_pikau_unit_view.py @@ -0,0 +1,117 @@ +from tests.BaseTestWithDB import BaseTestWithDB +from tests.pikau.PikauTestDataGenerator import PikauTestDataGenerator +from django.urls import reverse + + +class PikauUnitViewTest(BaseTestWithDB): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.language = "en" + self.test_data = PikauTestDataGenerator() + + def test_pikau_unit_view_with_valid_slug(self): + pikau_course = self.test_data.create_pikau_course(1) + self.test_data.create_pikau_unit(pikau_course, 1) + + kwargs = { + "course_slug": "pikau-course-1", + "unit_slug": "pikau-unit-1" + } + url = reverse("pikau:pikau_unit", kwargs=kwargs) + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual( + url, + "/pikau/pikau-courses/pikau-course-1/content/pikau-unit-1/" + ) + + def test_pikau_unit_view_with_invalid_unit_slug(self): + pikau_course = self.test_data.create_pikau_course(1) + self.test_data.create_pikau_unit(pikau_course, 1) + + kwargs = { + "course_slug": "pikau-course-1", + "unit_slug": "pikau-unit-2" + } + url = reverse("pikau:pikau_unit", kwargs=kwargs) + response = self.client.get(url) + self.assertEqual(404, response.status_code) + + def test_pikau_unit_view_with_invalid_course_slug(self): + pikau_course = self.test_data.create_pikau_course(1) + self.test_data.create_pikau_unit(pikau_course, 1) + + kwargs = { + "course_slug": "pikau-course-2", + "unit_slug": "pikau-unit-1" + } + url = reverse("pikau:pikau_unit", kwargs=kwargs) + response = self.client.get(url) + self.assertEqual(404, response.status_code) + + def test_pikau_unit_view_context_with_previous_unit(self): + pikau_course = self.test_data.create_pikau_course(1) + previous_pikau_unit = self.test_data.create_pikau_unit(pikau_course, 1) + self.test_data.create_pikau_unit(pikau_course, 2) + + kwargs = { + "course_slug": "pikau-course-1", + "unit_slug": "pikau-unit-2" + } + url = reverse("pikau:pikau_unit", kwargs=kwargs) + response = self.client.get(url) + self.assertEqual(200, response.status_code) + previous_unit = response.context["previous_unit"] + self.assertEqual(previous_unit, previous_pikau_unit) + + def test_pikau_unit_view_context_with_next_unit(self): + pikau_course = self.test_data.create_pikau_course(1) + self.test_data.create_pikau_unit(pikau_course, 1) + next_pikau_unit = self.test_data.create_pikau_unit(pikau_course, 2) + + kwargs = { + "course_slug": "pikau-course-1", + "unit_slug": "pikau-unit-1" + } + url = reverse("pikau:pikau_unit", kwargs=kwargs) + response = self.client.get(url) + self.assertEqual(200, response.status_code) + next_unit = response.context["next_unit"] + self.assertEqual(next_unit, next_pikau_unit) + + def test_pikau_unit_view_context_with_previous_and_next_unit(self): + pikau_course = self.test_data.create_pikau_course(1) + previous_pikau_unit = self.test_data.create_pikau_unit(pikau_course, 1) + self.test_data.create_pikau_unit(pikau_course, 2) + next_pikau_unit = self.test_data.create_pikau_unit(pikau_course, 3) + + kwargs = { + "course_slug": "pikau-course-1", + "unit_slug": "pikau-unit-2" + } + url = reverse("pikau:pikau_unit", kwargs=kwargs) + response = self.client.get(url) + self.assertEqual(200, response.status_code) + + previous_unit = response.context["previous_unit"] + self.assertEqual(previous_unit, previous_pikau_unit) + next_unit = response.context["next_unit"] + self.assertEqual(next_unit, next_pikau_unit) + + def test_pikau_unit_view_context_with_no_previous_or_next_units(self): + pikau_course = self.test_data.create_pikau_course(1) + self.test_data.create_pikau_unit(pikau_course, 1) + + kwargs = { + "course_slug": "pikau-course-1", + "unit_slug": "pikau-unit-1" + } + url = reverse("pikau:pikau_unit", kwargs=kwargs) + response = self.client.get(url) + self.assertEqual(200, response.status_code) + + previous_unit = response.context["previous_unit"] + self.assertEqual(previous_unit, None) + next_unit = response.context["next_unit"] + self.assertEqual(next_unit, None) diff --git a/tests/pikau/views/test_progress_outcome_view.py b/tests/pikau/views/test_progress_outcome_view.py new file mode 100644 index 0000000..0cde5f6 --- /dev/null +++ b/tests/pikau/views/test_progress_outcome_view.py @@ -0,0 +1,37 @@ +from tests.BaseTestWithDB import BaseTestWithDB +from tests.pikau.PikauTestDataGenerator import PikauTestDataGenerator +from django.urls import reverse + + +class ProgressOutcomeViewTest(BaseTestWithDB): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.language = "en" + self.test_data = PikauTestDataGenerator() + + def test_pikau_progress_outcome_view_with_valid_slug(self): + self.test_data.create_progress_outcome(1) + + kwargs = { + "slug": "progress-outcome-1", + } + url = reverse("pikau:progress_outcome", kwargs=kwargs) + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual( + url, + "/pikau/progress-outcomes/progress-outcome-1/" + ) + + def test_pikau_progress_outcome_view_with_invalid_slug(self): + self.test_data.create_progress_outcome(1) + + kwargs = { + "slug": "progress-outcome-5", + } + url = reverse("pikau:progress_outcome", kwargs=kwargs) + response = self.client.get(url) + self.assertEqual(404, response.status_code) + + # TODO: Add tests for heatmap. diff --git a/tests/pikau/views/test_readiness_level_view.py b/tests/pikau/views/test_readiness_level_view.py new file mode 100644 index 0000000..6b33d32 --- /dev/null +++ b/tests/pikau/views/test_readiness_level_view.py @@ -0,0 +1,176 @@ +from tests.BaseTestWithDB import BaseTestWithDB +from tests.pikau.PikauTestDataGenerator import PikauTestDataGenerator +from django.urls import reverse + + +class ReadinessLevelViewTest(BaseTestWithDB): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.language = "en" + self.test_data = PikauTestDataGenerator() + + def test_readiness_level_list_view_with_no_courses(self): + url = reverse("pikau:readiness_level_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + + readiness_levels = response.context["readiness_levels"] + for level in range(1, 6): + level_data = readiness_levels[level] + num_of_courses_on_level = level_data["count"] + self.assertEqual(num_of_courses_on_level, 0) + + def test_readiness_level_list_view_with_one_course_for_all_levels(self): + # create 1 course under each readiness level + for level in range(1, 6): + pikau_course = self.test_data.create_pikau_course(level) + pikau_course.readiness_level = level + pikau_course.save() + + url = reverse("pikau:readiness_level_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + + readiness_levels = response.context["readiness_levels"] + level_data = readiness_levels[level] + num_of_courses_on_level = level_data["count"] + self.assertEqual(num_of_courses_on_level, 1) + + def test_readiness_level_list_view_multiple_courses_one_level(self): + pikau_course_1 = self.test_data.create_pikau_course(1) + pikau_course_1.readiness_level = 4 + pikau_course_1.save() + + pikau_course_2 = self.test_data.create_pikau_course(2) + pikau_course_2.readiness_level = 4 + pikau_course_2.save() + + pikau_course_3 = self.test_data.create_pikau_course(3) + pikau_course_3.readiness_level = 4 + pikau_course_3.save() + + url = reverse("pikau:readiness_level_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + + readiness_levels = response.context["readiness_levels"] + level_data = readiness_levels[4] + num_of_courses_on_level = level_data["count"] + self.assertEqual(num_of_courses_on_level, 3) + + def test_readiness_level_list_view_multiple_courses_multiple_levels(self): + pikau_course_1 = self.test_data.create_pikau_course(1) + pikau_course_1.readiness_level = 2 + pikau_course_1.save() + + pikau_course_2 = self.test_data.create_pikau_course(2) + pikau_course_2.readiness_level = 5 + pikau_course_2.save() + + pikau_course_3 = self.test_data.create_pikau_course(3) + pikau_course_3.readiness_level = 1 + pikau_course_3.save() + + pikau_course_4 = self.test_data.create_pikau_course(4) + pikau_course_4.readiness_level = 1 + pikau_course_4.save() + + pikau_course_5 = self.test_data.create_pikau_course(5) + pikau_course_5.readiness_level = 2 + pikau_course_5.save() + + url = reverse("pikau:readiness_level_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + + readiness_levels = response.context["readiness_levels"] + level_1_data = readiness_levels[1] + level_2_data = readiness_levels[2] + level_5_data = readiness_levels[5] + num_of_courses_on_level_1 = level_1_data["count"] + num_of_courses_on_level_2 = level_2_data["count"] + num_of_courses_on_level_5 = level_5_data["count"] + + self.assertEqual(num_of_courses_on_level_1, 2) + self.assertEqual(num_of_courses_on_level_2, 2) + self.assertEqual(num_of_courses_on_level_5, 1) + + def test_readiness_level_view_no_courses(self): + url = reverse("pikau:readiness_level", kwargs={"level_number": 2}) + response = self.client.get(url) + self.assertEqual(200, response.status_code) + readiness_level = response.context["readiness_level"] + pikau_courses = readiness_level["pikau_courses"] + self.assertQuerysetEqual(pikau_courses, []) + + def test_readiness_level_view_one_course(self): + pikau_course = self.test_data.create_pikau_course(1) + pikau_course.readiness_level = 5 + pikau_course.save() + + url = reverse("pikau:readiness_level", kwargs={"level_number": 5}) + response = self.client.get(url) + self.assertEqual(200, response.status_code) + readiness_level = response.context["readiness_level"] + pikau_courses = readiness_level["pikau_courses"] + self.assertQuerysetEqual( + pikau_courses, + [""] + ) + + def test_readiness_level_view_two_courses(self): + pikau_course_1 = self.test_data.create_pikau_course(1) + pikau_course_1.readiness_level = 3 + pikau_course_1.save() + + pikau_course_2 = self.test_data.create_pikau_course(2) + pikau_course_2.readiness_level = 3 + pikau_course_2.save() + + url = reverse("pikau:readiness_level", kwargs={"level_number": 3}) + response = self.client.get(url) + self.assertEqual(200, response.status_code) + readiness_level = response.context["readiness_level"] + pikau_courses = readiness_level["pikau_courses"] + self.assertQuerysetEqual( + pikau_courses, + [ + "", + "", + ], + ordered=False + ) + + def test_readiness_level_view_three_courses(self): + pikau_course_1 = self.test_data.create_pikau_course(1) + pikau_course_1.readiness_level = 4 + pikau_course_1.save() + + pikau_course_2 = self.test_data.create_pikau_course(2) + pikau_course_2.readiness_level = 4 + pikau_course_2.save() + + pikau_course_3 = self.test_data.create_pikau_course(3) + pikau_course_3.readiness_level = 4 + pikau_course_3.save() + + url = reverse("pikau:readiness_level", kwargs={"level_number": 4}) + response = self.client.get(url) + self.assertEqual(200, response.status_code) + readiness_level = response.context["readiness_level"] + pikau_courses = readiness_level["pikau_courses"] + self.assertQuerysetEqual( + pikau_courses, + [ + "", + "", + "", + ], + ordered=False + ) + + def test_readiness_level_view_with_invalid_level(self): + url = reverse("pikau:readiness_level", kwargs={"level_number": 6}) + response = self.client.get(url) + self.assertEqual(404, response.status_code) diff --git a/tests/pikau/views/test_tag_view.py b/tests/pikau/views/test_tag_view.py new file mode 100644 index 0000000..01a485d --- /dev/null +++ b/tests/pikau/views/test_tag_view.py @@ -0,0 +1,78 @@ +from tests.BaseTestWithDB import BaseTestWithDB +from tests.pikau.PikauTestDataGenerator import PikauTestDataGenerator +from django.urls import reverse + + +class TagViewTest(BaseTestWithDB): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.language = "en" + self.test_data = PikauTestDataGenerator() + + def test_pikau_tag_view_with_valid_slug(self): + self.test_data.create_tag(1) + + url = reverse("pikau:tag", kwargs={"slug": "tag-1"}) + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(url, "/pikau/tags/tag-1/") + + def test_pikau_tag_view_with_invalid_slug(self): + self.test_data.create_tag(1) + + url = reverse("pikau:tag", kwargs={"slug": "tag-5"}) + response = self.client.get(url) + self.assertEqual(404, response.status_code) + + def test_pikau_tag_list_view_with_no_tags(self): + url = reverse("pikau:tag_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(len(response.context["tags"]), 0) + + def test_pikau_tag_list_view_with_one_tag(self): + self.test_data.create_tag(1) + + url = reverse("pikau:tag_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(len(response.context["tags"]), 1) + self.assertQuerysetEqual( + response.context["tags"], + [""] + ) + + def test_pikau_tag_list_view_with_two_tags(self): + self.test_data.create_tag(1) + self.test_data.create_tag(2) + + url = reverse("pikau:tag_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(len(response.context["tags"]), 2) + self.assertQuerysetEqual( + response.context["tags"], + [ + "", + "", + ] + ) + + def test_pikau_tag_list_view_order(self): + self.test_data.create_tag(3) + self.test_data.create_tag(2) + self.test_data.create_tag(1) + + url = reverse("pikau:tag_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(len(response.context["tags"]), 3) + self.assertQuerysetEqual( + response.context["tags"], + [ + "", + "", + "", + ] + ) diff --git a/tests/pikau/views/test_topic_view.py b/tests/pikau/views/test_topic_view.py new file mode 100644 index 0000000..269fc05 --- /dev/null +++ b/tests/pikau/views/test_topic_view.py @@ -0,0 +1,78 @@ +from tests.BaseTestWithDB import BaseTestWithDB +from tests.pikau.PikauTestDataGenerator import PikauTestDataGenerator +from django.urls import reverse + + +class TopicViewTest(BaseTestWithDB): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.language = "en" + self.test_data = PikauTestDataGenerator() + + def test_pikau_topic_view_with_valid_slug(self): + self.test_data.create_topic(1) + + url = reverse("pikau:topic", kwargs={"slug": "topic-1"}) + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(url, "/pikau/topics/view/topic-1/") + + def test_pikau_topic_view_with_invalid_slug(self): + self.test_data.create_topic(1) + + url = reverse("pikau:topic", kwargs={"slug": "topic-5"}) + response = self.client.get(url) + self.assertEqual(404, response.status_code) + + def test_pikau_topic_list_view_with_no_topics(self): + url = reverse("pikau:topic_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(len(response.context["topics"]), 0) + + def test_pikau_topic_list_view_with_one_topic(self): + self.test_data.create_topic(1) + + url = reverse("pikau:topic_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(len(response.context["topics"]), 1) + self.assertQuerysetEqual( + response.context["topics"], + [""] + ) + + def test_pikau_topic_list_view_with_two_topics(self): + self.test_data.create_topic(1) + self.test_data.create_topic(2) + + url = reverse("pikau:topic_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(len(response.context["topics"]), 2) + self.assertQuerysetEqual( + response.context["topics"], + [ + "", + "", + ] + ) + + def test_pikau_topic_list_view_order(self): + self.test_data.create_topic(3) + self.test_data.create_topic(2) + self.test_data.create_topic(1) + + url = reverse("pikau:topic_list") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(len(response.context["topics"]), 3) + self.assertQuerysetEqual( + response.context["topics"], + [ + "", + "", + "", + ] + ) diff --git a/utils/BaseLoader.py b/utils/BaseLoader.py index de87d9e..d76f44c 100644 --- a/utils/BaseLoader.py +++ b/utils/BaseLoader.py @@ -74,6 +74,7 @@ def convert_md_file(self, md_file_path, config_file_path, heading_required=True, raise CouldNotFindMarkdownFileError(md_file_path, config_file_path) custom_processors = self.converter.processor_defaults() + custom_processors.remove("scratch") if remove_title: custom_processors.add("remove-title") self.converter.update_processors(custom_processors)