diff --git a/.coveragerc b/.coveragerc index dfc5eb0..5401677 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,6 +2,7 @@ branch = True source = config + files pikau static templates diff --git a/CHANGELOG.md b/CHANGELOG.md index 423a444..0e92862 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## 0.4.0 (Pre-release) + +- Add files application for tracking files and their licences. (fixes #34) + - Display warning if any files have unknown licence. (fixes #36) + - Allow filtering of files by licence type. + - Allow users to add and update files. +- Allow users to add and update pīkau glossary entries. +- Begin process of replacing manual HTML tables with tables from django-tables2. +- Dependency updates: + - Add django-tables2 2.0.0a2. + - Add django-filter 1.1.0. + - Update django from 2.0.4 to 2.0.5. + - Update django-allauth from 0.35.0 to 0.36.0. + - Update django-anymail from 2.0 to 2.2. + - Update django-debug-toolbar from 1.8 to 1.9.1. + - Update psycopg2 from 2.7.3.1 to 2.7.4. + - Update gunicorn from 19.7.1 to 19.8.1. + - Update python-markdown-math from 0.3 to 0.5. + ## 0.3.0 (Pre-release) - Add milestone list and detail pages, with table showing milestone statuses. (fixes #9) diff --git a/config/__init__.py b/config/__init__.py index 9535603..31e00d6 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -1,3 +1,3 @@ """Module for Django system configuration.""" -__version__ = "0.3.0" +__version__ = "0.4.0" diff --git a/config/settings/base.py b/config/settings/base.py index d685df6..184ee27 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -29,11 +29,14 @@ "allauth", "allauth.account", "allauth.socialaccount", + "django_tables2", + "django_filters", ] # Apps specific for this project go here. LOCAL_APPS = [ "pikau.apps.PikauConfig", + "files.apps.FilesConfig", ] # See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps @@ -218,5 +221,7 @@ # OTHER SETTINGS # ------------------------------------------------------------------------------ PIKAU_CONTENT_BASE_PATH = os.path.join(BASE_DIR, "pikau/content") +FILES_CONTENT_BASE_PATH = os.path.join(BASE_DIR, "files/content") CUSTOM_VERTO_TEMPLATES = os.path.join(BASE_DIR, "utils/custom_converter_templates/") BREADCRUMBS_TEMPLATE = "django_bootstrap_breadcrumbs/bootstrap4.html" +DJANGO_TABLES2_TEMPLATE = "tables/table.html" diff --git a/config/urls.py b/config/urls.py index bdd5a1c..81a9570 100644 --- a/config/urls.py +++ b/config/urls.py @@ -26,6 +26,7 @@ path("faq/", views.FAQView.as_view(), name="faq"), path("contact/", views.ContactView.as_view(), name="contact"), path("pikau/", include("pikau.urls")), + path("files/", include("files.urls")), path("accounts/", include("allauth.urls")), path("admin/", admin.site.urls), ] diff --git a/files/__init__.py b/files/__init__.py new file mode 100644 index 0000000..3ecb6e1 --- /dev/null +++ b/files/__init__.py @@ -0,0 +1 @@ +"""Module for the files application.""" diff --git a/files/admin.py b/files/admin.py new file mode 100644 index 0000000..c0d78a2 --- /dev/null +++ b/files/admin.py @@ -0,0 +1,11 @@ +"""Admin configuration for the files application.""" + +from django.contrib import admin +from files.models import ( + File, + Licence, +) + + +admin.site.register(File) +admin.site.register(Licence) diff --git a/files/apps.py b/files/apps.py new file mode 100644 index 0000000..472caa4 --- /dev/null +++ b/files/apps.py @@ -0,0 +1,9 @@ +"""Application configuration for the files application.""" + +from django.apps import AppConfig + + +class FilesConfig(AppConfig): + """Configuration object for the files application.""" + + name = "files" diff --git a/files/content/licences.yaml b/files/content/licences.yaml new file mode 100644 index 0000000..ee092c3 --- /dev/null +++ b/files/content/licences.yaml @@ -0,0 +1,6 @@ +- name: "Unknown" + url: "https://creativecommons.org/choose/" +- name: "Creative Commons (BY-SA 4.0)" + url: "https://creativecommons.org/licenses/by-sa/4.0/" +- name: "Creative Commons (BY-NC-SA 4.0)" + url: "https://creativecommons.org/licenses/by-nc-sa/4.0/" diff --git a/files/filters.py b/files/filters.py new file mode 100644 index 0000000..e3ed661 --- /dev/null +++ b/files/filters.py @@ -0,0 +1,14 @@ +"""Filters for the files application.""" + +import django_filters +from files.models import File + + +class FileFilter(django_filters.FilterSet): + """File filter for the files table.""" + + class Meta: + """Meta attributes for FileFilter class.""" + + model = File + fields = ["licence"] diff --git a/files/forms.py b/files/forms.py new file mode 100644 index 0000000..7c9add9 --- /dev/null +++ b/files/forms.py @@ -0,0 +1,37 @@ +"""Forms for files application.""" + +from django.forms import ModelForm +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Layout, Submit, Field +from files.models import ( + File +) + + +class FileForm(ModelForm): + """Form for pages relating to actions of files.""" + + def __init__(self, *args, **kwargs): + """Set helper for form layout.""" + super().__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.layout = Layout( + Field("filename", css_class="slug-source"), + "description", + "location", + "licence", + Field("slug", css_class="slug-input"), + Submit("submit", "Submit"), + ) + + class Meta: + """Meta attributes of GlossaryForm.""" + + model = File + fields = ( + "filename", + "description", + "location", + "licence", + "slug", + ) diff --git a/files/management/__init__.py b/files/management/__init__.py new file mode 100644 index 0000000..c1c0b44 --- /dev/null +++ b/files/management/__init__.py @@ -0,0 +1 @@ +"""Module for the management of the files application.""" diff --git a/files/management/commands/_LicenceLoader.py b/files/management/commands/_LicenceLoader.py new file mode 100644 index 0000000..14e2dfe --- /dev/null +++ b/files/management/commands/_LicenceLoader.py @@ -0,0 +1,26 @@ +"""Custom loader for loading licences.""" + +from django.db import transaction +from files.models import Licence +from utils.BaseLoader import BaseLoader + + +class LicenceLoader(BaseLoader): + """Custom loader for loading licences.""" + + @transaction.atomic + def load(self): + """Load the licences into the database.""" + licences = self.load_yaml_file("licences.yaml") + + for licence_data in licences: + defaults = { + "url": licence_data["url"], + } + licence, created = Licence.objects.update_or_create( + name=licence_data["name"], + defaults=defaults, + ) + self.log_object_creation(created, licence) + + self.log("All licences loaded!\n") diff --git a/files/management/commands/__init__.py b/files/management/commands/__init__.py new file mode 100644 index 0000000..2fd23cd --- /dev/null +++ b/files/management/commands/__init__.py @@ -0,0 +1 @@ +"""Module for the custom commands for the files appliation.""" diff --git a/files/management/commands/loadlicences.py b/files/management/commands/loadlicences.py new file mode 100644 index 0000000..1733050 --- /dev/null +++ b/files/management/commands/loadlicences.py @@ -0,0 +1,16 @@ +"""Module for the custom Django loadlicences command.""" + +from django.conf import settings +from django.core import management +from files.management.commands._LicenceLoader import LicenceLoader + + +class Command(management.base.BaseCommand): + """Required command class for the custom Django loadlicences command.""" + + help = "Loads licences into the website" + + def handle(self, *args, **options): + """Automatically called when the loadlicences command is given.""" + base_path = settings.FILES_CONTENT_BASE_PATH + LicenceLoader(base_path).load() diff --git a/files/migrations/0001_initial.py b/files/migrations/0001_initial.py new file mode 100644 index 0000000..2db3918 --- /dev/null +++ b/files/migrations/0001_initial.py @@ -0,0 +1,42 @@ +# Generated by Django 2.0.5 on 2018-05-21 04:19 + +from django.db import migrations, models +import django.db.models.deletion +import files.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='File', + 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)), + ('description', models.TextField(blank=True)), + ('location', models.URLField()), + ], + ), + migrations.CreateModel( + name='Licence', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200, unique=True)), + ('url', models.URLField()), + ], + options={ + 'ordering': ('name',), + }, + ), + migrations.AddField( + model_name='file', + name='licence', + field=models.ForeignKey(default=files.models.default_licence, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='files', to='files.Licence'), + ), + ] diff --git a/files/migrations/__init__.py b/files/migrations/__init__.py new file mode 100644 index 0000000..b9e1c84 --- /dev/null +++ b/files/migrations/__init__.py @@ -0,0 +1 @@ +"""Migrations for the files application.""" diff --git a/files/models.py b/files/models.py new file mode 100644 index 0000000..eb0a2a7 --- /dev/null +++ b/files/models.py @@ -0,0 +1,86 @@ +"""Models for the files application.""" + +from django.db import models +from django.core.exceptions import ObjectDoesNotExist +from django.urls import reverse + + +def default_licence(): + """Return default licence object. + + Returns: + Licence 'Unknown' if available, otherwise None. + """ + try: + default = Licence.objects.get(name="Unknown").pk + except ObjectDoesNotExist: + default = None + return default + + +class Licence(models.Model): + """Model for licence.""" + + name = models.CharField(max_length=200, unique=True) + url = models.URLField() + + class Meta: + """Set consistent ordering of licences.""" + + ordering = ("name", ) + + def get_absolute_url(self): + """Return the URL for a licence. + + Returns: + URL as string. + """ + return self.url + + def __str__(self): + """Text representation of Licence object. + + Returns: + String describing licence. + """ + return self.name + + +class File(models.Model): + """Model for file.""" + + slug = models.SlugField(unique=True) + filename = models.CharField(max_length=200) + description = models.TextField(blank=True) + location = models.URLField() + licence = models.ForeignKey( + Licence, + on_delete=models.CASCADE, + related_name="files", + default=default_licence, + null=True, + ) + + def get_absolute_url(self): + """Return the URL for a file. + + Returns: + URL as string. + """ + return reverse("files:file_detail", args=[self.slug]) + + def __str__(self): + """Text representation of File object. + + Returns: + String describing file. + """ + return self.filename + + def __repr__(self): + """Text representation of File object for developers. + + Returns: + String describing file. + """ + return "File: {}".format(self.slug) diff --git a/files/tables.py b/files/tables.py new file mode 100644 index 0000000..53bb4ad --- /dev/null +++ b/files/tables.py @@ -0,0 +1,20 @@ +"""Tables for the files application.""" + +import django_tables2 as tables +from files.models import ( + File, +) + + +class FileTable(tables.Table): + """Table to display all files.""" + + filename = tables.LinkColumn() + licence = tables.RelatedLinkColumn() + + class Meta: + """Meta attributes for FileTable class.""" + + model = File + fields = ("filename", "licence") + order_by = "filename" diff --git a/files/urls.py b/files/urls.py new file mode 100644 index 0000000..a8a8b20 --- /dev/null +++ b/files/urls.py @@ -0,0 +1,47 @@ +"""URL routing for the files application. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/2.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" + +from django.urls import path +from . import views + +app_name = "files" + +urlpatterns = [ + # eg: /files/ + path( + "", + views.FileList.as_view(), + name="file_list" + ), + # eg: /files/file/view/file-1/ + path( + "file/view//", + views.FileDetailView.as_view(), + name="file_detail" + ), + # eg: /files/file/create/ + path( + "file/create/", + views.FileCreateView.as_view(), + name="file_create" + ), + # eg: /files/file/update/file-1/ + path( + "file/update//", + views.FileUpdateView.as_view(), + name="file_update" + ), +] diff --git a/files/views.py b/files/views.py new file mode 100644 index 0000000..939f81e --- /dev/null +++ b/files/views.py @@ -0,0 +1,65 @@ +"""Views for the files application.""" + +from django_tables2 import SingleTableMixin +from django_filters.views import FilterView +from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.messages.views import SuccessMessageMixin +from django.urls import reverse_lazy +from django.views.generic import ( + DetailView, + CreateView, + UpdateView, +) +from files.tables import FileTable +from files.filters import FileFilter +from files.models import ( + File, +) +from files.forms import ( + FileForm, +) + + +class FileList(LoginRequiredMixin, SingleTableMixin, FilterView): + """View for the file list page.""" + + template_name = "files/file_list.html" + model = File + table_class = FileTable + filterset_class = FileFilter + + def get_context_data(self, **kwargs): + """Provide the context data for the view. + + Returns: + Dictionary of context data. + """ + context = super(FileList, self).get_context_data(**kwargs) + context["unknown_licences"] = File.objects.filter(licence__name="Unknown").count() + return context + + +class FileDetailView(LoginRequiredMixin, DetailView): + """View for a file.""" + + model = File + + +class FileCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): + """View for creating a glossary definition.""" + + model = File + form_class = FileForm + template_name = "files/file_form_create.html" + success_message = "File created!" + success_url = reverse_lazy("files:file_list") + + +class FileUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): + """View for updating a glossary definition.""" + + model = File + form_class = FileForm + template_name = "files/file_form_update.html" + success_message = "File updated!" + success_url = reverse_lazy("files:file_list") diff --git a/pikau/forms.py b/pikau/forms.py new file mode 100644 index 0000000..58481e5 --- /dev/null +++ b/pikau/forms.py @@ -0,0 +1,29 @@ +"""Forms for pikau application.""" + +from django.forms import ModelForm +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Layout, Submit, Field +from pikau.models import ( + GlossaryTerm +) + + +class GlossaryForm(ModelForm): + """Form for pages relating to actions of glossary terms.""" + + def __init__(self, *args, **kwargs): + """Set helper for form layout.""" + super().__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.layout = Layout( + Field("term", css_class="slug-source"), + "description", + Field("slug", css_class="slug-input"), + Submit("submit", "Submit"), + ) + + class Meta: + """Meta attributes of GlossaryForm.""" + + model = GlossaryTerm + fields = ("term", "description", "slug") diff --git a/pikau/migrations/0027_auto_20180522_1652.py b/pikau/migrations/0027_auto_20180522_1652.py new file mode 100644 index 0000000..98c0b27 --- /dev/null +++ b/pikau/migrations/0027_auto_20180522_1652.py @@ -0,0 +1,45 @@ +# Generated by Django 2.0.5 on 2018-05-22 04:52 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('pikau', '0026_auto_20180516_1331'), + ] + + operations = [ + migrations.AlterField( + model_name='glossaryterm', + name='slug', + field=models.SlugField(help_text='A unique readable identifier', unique=True), + ), + migrations.AlterField( + model_name='pikaucourse', + name='level', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='pikau_courses', to='pikau.Level'), + ), + migrations.AlterField( + model_name='pikaucourse', + name='manager', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='pikau_courses', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='pikaucourse', + name='milestone', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='pikau_courses', to='pikau.Milestone'), + ), + migrations.AlterField( + model_name='pikaucourse', + name='topic', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='pikau_courses', to='pikau.Topic'), + ), + migrations.AlterField( + model_name='pikauunit', + name='pikau_course', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='content', to='pikau.PikauCourse'), + ), + ] diff --git a/pikau/mixins.py b/pikau/mixins.py new file mode 100644 index 0000000..8d7a367 --- /dev/null +++ b/pikau/mixins.py @@ -0,0 +1,18 @@ +"""Mixins for pikau application.""" + +from django.contrib import messages + + +class SuccessMessageDeleteMixin(object): + """Allow success message for delete view.""" + + def delete(self, request, *args, **kwargs): + """Set success string in message.""" + messages.success(self.request, self.success_message) + return super().delete(request, *args, **kwargs) + + +class TopicActionMixin(object): + """Topic mixin.""" + + fields = ("name", "slug") diff --git a/pikau/models.py b/pikau/models.py index d53063c..49da107 100644 --- a/pikau/models.py +++ b/pikau/models.py @@ -46,10 +46,21 @@ class GlossaryTerm(models.Model): """Model for glossary term.""" # Auto-incrementing 'id' field is automatically set by Django - slug = models.SlugField(unique=True) + slug = models.SlugField( + unique=True, + help_text="A unique readable identifier", + ) term = models.CharField(max_length=200, unique=True) description = models.TextField() + def get_absolute_url(self): + """Return the canonical URL for a glossary term. + + Returns: + URL as string. + """ + return reverse("pikau:glossaryterm_detail", args=[self.slug]) + def __str__(self): """Text representation of GlossaryTerm object. @@ -124,6 +135,14 @@ class Topic(models.Model): slug = models.SlugField(unique=True) name = models.CharField(max_length=100, unique=True) + def get_absolute_url(self): + """Return the canonical URL for a topic. + + Returns: + URL as string. + """ + return reverse("pikau:topic", args=[self.slug]) + def __str__(self): """Text representation of Topic object. @@ -183,14 +202,14 @@ class PikauCourse(models.Model): ) topic = models.ForeignKey( Topic, - on_delete=models.CASCADE, + on_delete=models.PROTECT, related_name="pikau_courses", blank=True, null=True, ) level = models.ForeignKey( Level, - on_delete=models.CASCADE, + on_delete=models.PROTECT, related_name="pikau_courses", blank=True, null=True, @@ -234,14 +253,14 @@ class PikauCourse(models.Model): __previous_status = None manager = models.ForeignKey( User, - on_delete=models.CASCADE, + on_delete=models.SET_NULL, related_name="pikau_courses", blank=True, null=True, ) milestone = models.ForeignKey( Milestone, - on_delete=models.CASCADE, + on_delete=models.PROTECT, related_name="pikau_courses", blank=True, null=True, @@ -276,7 +295,7 @@ class PikauUnit(models.Model): number = models.PositiveSmallIntegerField() pikau_course = models.ForeignKey( PikauCourse, - on_delete=models.CASCADE, + on_delete=models.PROTECT, related_name="content" ) name = models.CharField(max_length=200) diff --git a/pikau/tables.py b/pikau/tables.py new file mode 100644 index 0000000..2ea73bc --- /dev/null +++ b/pikau/tables.py @@ -0,0 +1,21 @@ +"""Tables for the pikau application.""" + +import django_tables2 as tables +from pikau.models import ( + GlossaryTerm, +) + + +class GlossaryTermTable(tables.Table): + """Table to display all glossary terms.""" + + term = tables.LinkColumn() + slug = tables.TemplateColumn(template_code="{{ record.slug }}") + + class Meta: + """Meta attributes for GlossaryTermTable class.""" + + model = GlossaryTerm + fields = ("term", "slug", "description") + order_by = "term" + attrs = {"class": "table table-hover"} diff --git a/pikau/urls.py b/pikau/urls.py index 221e23d..e7eff59 100644 --- a/pikau/urls.py +++ b/pikau/urls.py @@ -35,37 +35,61 @@ # eg: /pikau/glossary/ path( "glossary/", - views.GlossaryList.as_view(), - name="glossary" + views.GlossaryListView.as_view(), + name="glossaryterm_list" + ), + # eg: /pikau/glossary/view/slug-1/ + path( + "glossary/view//", + views.GlossaryDetailView.as_view(), + name="glossaryterm_detail" + ), + # eg: /pikau/glossary/create/ + path( + "glossary/create/", + views.GlossaryCreateView.as_view(), + name="glossaryterm_create" + ), + # eg: /pikau/glossary/update/term-1/ + path( + "glossary/update//", + views.GlossaryUpdateView.as_view(), + name="glossaryterm_update" + ), + # eg: /pikau/glossary/delete/term-1/ + path( + "glossary/delete//", + views.GlossaryDeleteView.as_view(), + name="glossaryterm_delete" ), # eg: /pikau/goals/ path( "goals/", - views.GoalList.as_view(), + views.GoalListView.as_view(), name="goal_list" ), # eg: /pikau/levels/ path( "levels/", - views.LevelList.as_view(), + views.LevelListView.as_view(), name="level_list" ), # eg: /pikau/levels/level-1/ path( "levels//", - views.LevelDetail.as_view(), + views.LevelDetailView.as_view(), name="level" ), # eg: /pikau/milestones/ path( "milestones/", - views.MilestoneList.as_view(), + views.MilestoneListView.as_view(), name="milestone_list" ), # eg: /pikau/milestones/1/ path( "milestones//", - views.MilestoneDetail.as_view(), + views.MilestoneDetailView.as_view(), name="milestone" ), # eg: /pikau/pathways/ @@ -77,73 +101,91 @@ # eg: /pikau/pikau-courses/ path( "pikau-courses/", - views.PikauCourseList.as_view(), + views.PikauCourseListView.as_view(), name="pikau_course_list" ), # eg: /pikau/pikau-courses/pikau-1/ path( "pikau-courses//", - views.PikauCourseDetail.as_view(), + views.PikauCourseDetailView.as_view(), name="pikau_course" ), # eg: /pikau/pikau-courses/pikau-1/content/ path( "pikau-courses//content/", - views.PikauCourseContent.as_view(), + views.PikauCourseContentView.as_view(), name="pikau_content" ), # eg: /pikau/pikau-courses/pikau-1/content/unit-1/ path( "pikau-courses//content//", - views.PikauUnitDetail.as_view(), + views.PikauUnitDetailView.as_view(), name="pikau_unit" ), # eg: /pikau/progress-outcomes/ path( "progress-outcomes/", - views.ProgressOutcomeList.as_view(), + views.ProgressOutcomeListView.as_view(), name="progress_outcome_list" ), # eg: /pikau/progress-outcomes/progress-outcome-1/ path( "progress-outcomes//", - views.ProgressOutcomeDetail.as_view(), + views.ProgressOutcomeDetailView.as_view(), name="progress_outcome" ), # eg: /pikau/readiness-levels/ path( "readiness-levels/", - views.ReadinessLevelList.as_view(), + views.ReadinessLevelListView.as_view(), name="readiness_level_list" ), # eg: /pikau/readiness-levels/1/ path( "readiness-levels//", - views.ReadinessLevelDetail.as_view(), + views.ReadinessLevelDetailView.as_view(), name="readiness_level" ), # eg: /pikau/tags/ path( "tags/", - views.TagList.as_view(), + views.TagListView.as_view(), name="tag_list" ), # eg: /pikau/tags/tag-1/ path( "tags//", - views.TagDetail.as_view(), + views.TagDetailView.as_view(), name="tag" ), # eg: /pikau/topics/ path( "topics/", - views.TopicList.as_view(), + views.TopicListView.as_view(), name="topic_list" ), # eg: /pikau/topics/topic-1/ path( - "topics//", - views.TopicDetail.as_view(), + "topics/view//", + views.TopicDetailView.as_view(), name="topic" ), + # eg: /pikau/topics/create/ + path( + "topics/create/", + views.TopicCreateView.as_view(), + name="topic_create" + ), + # eg: /pikau/topics/update/topic-1/ + path( + "topics/update//", + views.TopicUpdateView.as_view(), + name="topic_update" + ), + # eg: /pikau/topics/delete/topic-1/ + path( + "topics/delete//", + views.TopicDeleteView.as_view(), + name="topic_delete" + ), ] diff --git a/pikau/views.py b/pikau/views.py index 1145dff..358384e 100644 --- a/pikau/views.py +++ b/pikau/views.py @@ -1,12 +1,22 @@ """Views for the pikau application.""" from re import sub -from django.views import generic +from django.views.generic import ( + TemplateView, + ListView, + DetailView, + CreateView, + UpdateView, + DeleteView, +) from django.db.models import F from django.shortcuts import get_object_or_404 from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.messages.views import SuccessMessageMixin from django.core.exceptions import ObjectDoesNotExist +from django.urls import reverse_lazy from django.http import Http404 +from django_tables2 import SingleTableView from pikau.models import ( GlossaryTerm, Goal, @@ -21,17 +31,25 @@ READINESS_LEVELS, ) from pikau.utils import pathways +from pikau import tables +from pikau.mixins import ( + SuccessMessageDeleteMixin, + TopicActionMixin, +) +from pikau.forms import ( + GlossaryForm, +) NUMBER_OF_FLAME_STAGES = 7 -class IndexView(LoginRequiredMixin, generic.TemplateView): +class IndexView(LoginRequiredMixin, TemplateView): """View for the pikau homepage that renders from a template.""" template_name = "pikau/index.html" -class DocumentationView(LoginRequiredMixin, generic.TemplateView): +class DocumentationView(LoginRequiredMixin, TemplateView): """View for the pikau documentation that renders from a template.""" template_name = "pikau/documentation.html" @@ -52,16 +70,49 @@ def get_context_data(self, **kwargs): return context -class GlossaryList(LoginRequiredMixin, generic.ListView): +class GlossaryListView(LoginRequiredMixin, SingleTableView): """View for the glossary list page.""" - template_name = "pikau/glossary.html" - context_object_name = "glossary_terms" model = GlossaryTerm - ordering = "term" + table_class = tables.GlossaryTermTable + + +class GlossaryDetailView(LoginRequiredMixin, DetailView): + """View for a glossary term.""" + + model = GlossaryTerm + + +class GlossaryCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): + """View for creating a glossary definition.""" + + model = GlossaryTerm + form_class = GlossaryForm + template_name = "pikau/glossaryterm_form_create.html" + success_message = "Glossary definition created!" + success_url = reverse_lazy("pikau:glossaryterm_list") + + +class GlossaryUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): + """View for updating a glossary definition.""" + + model = GlossaryTerm + form_class = GlossaryForm + template_name = "pikau/glossaryterm_form_update.html" + success_message = "Glossary definition updated!" + success_url = reverse_lazy("pikau:glossaryterm_list") + +class GlossaryDeleteView(LoginRequiredMixin, SuccessMessageDeleteMixin, DeleteView): + """View for deleting a glossary definition.""" -class GoalList(LoginRequiredMixin, generic.ListView): + model = GlossaryTerm + template_name = "pikau/glossaryterm_form_delete.html" + success_message = "Glossary definition deleted!" + success_url = reverse_lazy("pikau:glossaryterm_list") + + +class GoalListView(LoginRequiredMixin, ListView): """View for the goal list page.""" context_object_name = "goals" @@ -69,7 +120,7 @@ class GoalList(LoginRequiredMixin, generic.ListView): ordering = "slug" -class LevelList(LoginRequiredMixin, generic.ListView): +class LevelListView(LoginRequiredMixin, ListView): """View for the level list page.""" context_object_name = "levels" @@ -77,14 +128,14 @@ class LevelList(LoginRequiredMixin, generic.ListView): ordering = "name" -class LevelDetail(LoginRequiredMixin, generic.DetailView): +class LevelDetailView(LoginRequiredMixin, DetailView): """View for a level.""" context_object_name = "level" model = Level -class MilestoneList(LoginRequiredMixin, generic.ListView): +class MilestoneListView(LoginRequiredMixin, ListView): """View for the level list page.""" context_object_name = "milestones" @@ -103,7 +154,7 @@ def get_context_data(self, **kwargs): Returns: Dictionary of context data. """ - context = super(MilestoneList, self).get_context_data(**kwargs) + context = super(MilestoneListView, self).get_context_data(**kwargs) line_broken_status_stages = [] for status_num, status_name in STATUS_CHOICES: line_broken_status_stages.append( @@ -120,14 +171,14 @@ def get_context_data(self, **kwargs): return context -class MilestoneDetail(LoginRequiredMixin, generic.DetailView): +class MilestoneDetailView(LoginRequiredMixin, DetailView): """View for a level.""" context_object_name = "milestone" model = Milestone -class PathwaysView(LoginRequiredMixin, generic.TemplateView): +class PathwaysView(LoginRequiredMixin, TemplateView): """View for the pikau pathway that renders from a template.""" template_name = "pikau/pathways.html" @@ -144,7 +195,7 @@ def get_context_data(self, **kwargs): return context -class PikauCourseList(LoginRequiredMixin, generic.ListView): +class PikauCourseListView(LoginRequiredMixin, ListView): """View for the pīkau course list page.""" context_object_name = "pikau_courses" @@ -162,14 +213,14 @@ def get_queryset(self): ) -class PikauCourseDetail(LoginRequiredMixin, generic.DetailView): +class PikauCourseDetailView(LoginRequiredMixin, DetailView): """View for a pīkau course.""" context_object_name = "pikau_course" model = PikauCourse -class PikauCourseContent(LoginRequiredMixin, generic.DetailView): +class PikauCourseContentView(LoginRequiredMixin, DetailView): """View for a pīkau course's content.""" context_object_name = "pikau_course" @@ -177,7 +228,7 @@ class PikauCourseContent(LoginRequiredMixin, generic.DetailView): template_name = "pikau/pikaucourse_content.html" -class PikauUnitDetail(LoginRequiredMixin, generic.DetailView): +class PikauUnitDetailView(LoginRequiredMixin, DetailView): """View for a pīkau unit.""" context_object_name = "pikau_unit" @@ -201,7 +252,7 @@ def get_context_data(self, **kwargs): Returns: Dictionary of context data. """ - context = super(PikauUnitDetail, self).get_context_data(**kwargs) + context = super(PikauUnitDetailView, self).get_context_data(**kwargs) try: context["previous_unit"] = PikauUnit.objects.get( pikau_course=self.object.pikau_course, @@ -219,7 +270,7 @@ def get_context_data(self, **kwargs): return context -class ProgressOutcomeList(LoginRequiredMixin, generic.ListView): +class ProgressOutcomeListView(LoginRequiredMixin, ListView): """View for the progress outcome list page.""" context_object_name = "progress_outcomes" @@ -238,7 +289,7 @@ def get_context_data(self, **kwargs): Returns: Dictionary of context data. """ - context = super(ProgressOutcomeList, self).get_context_data(**kwargs) + context = super(ProgressOutcomeListView, self).get_context_data(**kwargs) topics = Topic.objects.order_by("name") context["topics"] = topics max_count = NUMBER_OF_FLAME_STAGES @@ -277,14 +328,14 @@ def get_context_data(self, **kwargs): return context -class ProgressOutcomeDetail(LoginRequiredMixin, generic.DetailView): +class ProgressOutcomeDetailView(LoginRequiredMixin, DetailView): """View for a progress outcome.""" context_object_name = "progress_outcome" model = ProgressOutcome -class ReadinessLevelList(LoginRequiredMixin, generic.TemplateView): +class ReadinessLevelListView(LoginRequiredMixin, TemplateView): """View for the readiness level list page.""" template_name = "pikau/readiness_level_list.html" @@ -303,7 +354,7 @@ def get_context_data(self, **kwargs): return context -class ReadinessLevelDetail(LoginRequiredMixin, generic.TemplateView): +class ReadinessLevelDetailView(LoginRequiredMixin, TemplateView): """View for a readiness level.""" template_name = "pikau/readiness_level_detail.html" @@ -325,7 +376,7 @@ def get_context_data(self, **kwargs): return context -class TagList(LoginRequiredMixin, generic.ListView): +class TagListView(LoginRequiredMixin, ListView): """View for the tag list page.""" context_object_name = "tags" @@ -333,14 +384,14 @@ class TagList(LoginRequiredMixin, generic.ListView): ordering = "name" -class TagDetail(LoginRequiredMixin, generic.DetailView): +class TagDetailView(LoginRequiredMixin, DetailView): """View for a tag.""" context_object_name = "tag" model = Tag -class TopicList(LoginRequiredMixin, generic.ListView): +class TopicListView(LoginRequiredMixin, ListView): """View for the topic list page.""" context_object_name = "topics" @@ -348,8 +399,33 @@ class TopicList(LoginRequiredMixin, generic.ListView): ordering = "name" -class TopicDetail(LoginRequiredMixin, generic.DetailView): +class TopicDetailView(LoginRequiredMixin, DetailView): """View for a topic.""" context_object_name = "topic" model = Topic + + +class TopicCreateView(LoginRequiredMixin, SuccessMessageMixin, TopicActionMixin, CreateView): + """View for creating a topic.""" + + model = Topic + template_name = "pikau/topic_form_create.html" + success_message = "Topic created!" + + +class TopicUpdateView(LoginRequiredMixin, SuccessMessageMixin, TopicActionMixin, UpdateView): + """View for updating a topic.""" + + model = Topic + template_name = "pikau/topic_form_update.html" + success_message = "Topic updated!" + + +class TopicDeleteView(LoginRequiredMixin, SuccessMessageDeleteMixin, TopicActionMixin, DeleteView): + """View for deleting a topic.""" + + model = Topic + template_name = "pikau/topic_form_delete.html" + success_message = "Topic deleted!" + success_url = reverse_lazy("pikau:topic_list") diff --git a/release.sh b/release.sh index 0ff90fe..2e9378d 100755 --- a/release.sh +++ b/release.sh @@ -1,4 +1,5 @@ #!/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 loadfiles --settings=config.settings.production python manage.py loadpikau --settings=config.settings.production diff --git a/requirements/base.txt b/requirements/base.txt index dc4fd48..6cabc75 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,21 +1,25 @@ # Base dependencies go here # Django -django==2.0.4 +django==2.0.5 whitenoise==3.3.1 django-heroku==0.3.1 django-environ==0.4.4 django-bootstrap-breadcrumbs==0.9.1 +# File 'templates/tables/table.hmtl' is based off their 'bootstrap4' +# template so should be updated when original is updated in package. +django-tables2==2.0.0a2 +django-filter==1.1.0 # Content Loading verto==0.7.4 -python-markdown-math==0.3 +python-markdown-math==0.5 PyYAML==3.12 # Password storage argon2-cffi==18.1.0 # Users -django-allauth==0.35.0 +django-allauth==0.36.0 django-crispy-forms==1.7.2 -django-anymail==2.0 +django-anymail==2.2 diff --git a/requirements/local.txt b/requirements/local.txt index db41787..c0254a6 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -3,4 +3,4 @@ -r testing.txt # Debugging Tools -django-debug-toolbar==1.8 +django-debug-toolbar==1.9.1 diff --git a/requirements/production.txt b/requirements/production.txt index eddb40e..d5d3c75 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -3,7 +3,7 @@ -r base.txt # Python-PostgreSQL Database Adapter -psycopg2==2.7.3.1 +psycopg2==2.7.4 -gunicorn==19.7.1 +gunicorn==19.8.1 # WSGI Handler diff --git a/static/css/website.css b/static/css/website.css index 248c419..a6e06fd 100644 --- a/static/css/website.css +++ b/static/css/website.css @@ -122,3 +122,12 @@ footer > .container { .permalink { margin: 4rem 0; } +.asteriskField { + color: #dc3545; +} +.slug-input { + font-family: monospace; +} +.form-inline label { + margin-right: 0.5rem; +} diff --git a/static/images/icons/icons8/check-file.png b/static/images/icons/icons8/check-file.png new file mode 100644 index 0000000..fba6a03 Binary files /dev/null and b/static/images/icons/icons8/check-file.png differ diff --git a/static/js/speakingurl.min.js b/static/js/speakingurl.min.js new file mode 100644 index 0000000..c621c22 --- /dev/null +++ b/static/js/speakingurl.min.js @@ -0,0 +1,7 @@ +/** + * speakingurl + * @version v14.0.1 + * @link http://pid.github.io/speakingurl/ + * @license BSD + * @author + */!function(a){"use strict";var e={"À":"A","Á":"A","Â":"A","Ã":"A","Ä":"Ae","Å":"A","Æ":"AE","Ç":"C","È":"E","É":"E","Ê":"E","Ë":"E","Ì":"I","Í":"I","Î":"I","Ï":"I","Ð":"D","Ñ":"N","Ò":"O","Ó":"O","Ô":"O","Õ":"O","Ö":"Oe","Ő":"O","Ø":"O","Ù":"U","Ú":"U","Û":"U","Ü":"Ue","Ű":"U","Ý":"Y","Þ":"TH","ß":"ss","à":"a","á":"a","â":"a","ã":"a","ä":"ae","å":"a","æ":"ae","ç":"c","è":"e","é":"e","ê":"e","ë":"e","ì":"i","í":"i","î":"i","ï":"i","ð":"d","ñ":"n","ò":"o","ó":"o","ô":"o","õ":"o","ö":"oe","ő":"o","ø":"o","ù":"u","ú":"u","û":"u","ü":"ue","ű":"u","ý":"y","þ":"th","ÿ":"y","ẞ":"SS","ا":"a","أ":"a","إ":"i","آ":"aa","ؤ":"u","ئ":"e","ء":"a","ب":"b","ت":"t","ث":"th","ج":"j","ح":"h","خ":"kh","د":"d","ذ":"th","ر":"r","ز":"z","س":"s","ش":"sh","ص":"s","ض":"dh","ط":"t","ظ":"z","ع":"a","غ":"gh","ف":"f","ق":"q","ك":"k","ل":"l","م":"m","ن":"n","ه":"h","و":"w","ي":"y","ى":"a","ة":"h","ﻻ":"la","ﻷ":"laa","ﻹ":"lai","ﻵ":"laa","گ":"g","چ":"ch","پ":"p","ژ":"zh","ک":"k","ی":"y","َ":"a","ً":"an","ِ":"e","ٍ":"en","ُ":"u","ٌ":"on","ْ":"","٠":"0","١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9","۰":"0","۱":"1","۲":"2","۳":"3","۴":"4","۵":"5","۶":"6","۷":"7","۸":"8","۹":"9","က":"k","ခ":"kh","ဂ":"g","ဃ":"ga","င":"ng","စ":"s","ဆ":"sa","ဇ":"z","စျ":"za","ည":"ny","ဋ":"t","ဌ":"ta","ဍ":"d","ဎ":"da","ဏ":"na","တ":"t","ထ":"ta","ဒ":"d","ဓ":"da","န":"n","ပ":"p","ဖ":"pa","ဗ":"b","ဘ":"ba","မ":"m","ယ":"y","ရ":"ya","လ":"l","ဝ":"w","သ":"th","ဟ":"h","ဠ":"la","အ":"a","ြ":"y","ျ":"ya","ွ":"w","ြွ":"yw","ျွ":"ywa","ှ":"h","ဧ":"e","၏":"-e","ဣ":"i","ဤ":"-i","ဉ":"u","ဦ":"-u","ဩ":"aw","သြော":"aw","ဪ":"aw","၀":"0","၁":"1","၂":"2","၃":"3","၄":"4","၅":"5","၆":"6","၇":"7","၈":"8","၉":"9","္":"","့":"","း":"","č":"c","ď":"d","ě":"e","ň":"n","ř":"r","š":"s","ť":"t","ů":"u","ž":"z","Č":"C","Ď":"D","Ě":"E","Ň":"N","Ř":"R","Š":"S","Ť":"T","Ů":"U","Ž":"Z","ހ":"h","ށ":"sh","ނ":"n","ރ":"r","ބ":"b","ޅ":"lh","ކ":"k","އ":"a","ވ":"v","މ":"m","ފ":"f","ދ":"dh","ތ":"th","ލ":"l","ގ":"g","ޏ":"gn","ސ":"s","ޑ":"d","ޒ":"z","ޓ":"t","ޔ":"y","ޕ":"p","ޖ":"j","ޗ":"ch","ޘ":"tt","ޙ":"hh","ޚ":"kh","ޛ":"th","ޜ":"z","ޝ":"sh","ޞ":"s","ޟ":"d","ޠ":"t","ޡ":"z","ޢ":"a","ޣ":"gh","ޤ":"q","ޥ":"w","ަ":"a","ާ":"aa","ި":"i","ީ":"ee","ު":"u","ޫ":"oo","ެ":"e","ޭ":"ey","ޮ":"o","ޯ":"oa","ް":"","ა":"a","ბ":"b","გ":"g","დ":"d","ე":"e","ვ":"v","ზ":"z","თ":"t","ი":"i","კ":"k","ლ":"l","მ":"m","ნ":"n","ო":"o","პ":"p","ჟ":"zh","რ":"r","ს":"s","ტ":"t","უ":"u","ფ":"p","ქ":"k","ღ":"gh","ყ":"q","შ":"sh","ჩ":"ch","ც":"ts","ძ":"dz","წ":"ts","ჭ":"ch","ხ":"kh","ჯ":"j","ჰ":"h","α":"a","β":"v","γ":"g","δ":"d","ε":"e","ζ":"z","η":"i","θ":"th","ι":"i","κ":"k","λ":"l","μ":"m","ν":"n","ξ":"ks","ο":"o","π":"p","ρ":"r","σ":"s","τ":"t","υ":"y","φ":"f","χ":"x","ψ":"ps","ω":"o","ά":"a","έ":"e","ί":"i","ό":"o","ύ":"y","ή":"i","ώ":"o","ς":"s","ϊ":"i","ΰ":"y","ϋ":"y","ΐ":"i","Α":"A","Β":"B","Γ":"G","Δ":"D","Ε":"E","Ζ":"Z","Η":"I","Θ":"TH","Ι":"I","Κ":"K","Λ":"L","Μ":"M","Ν":"N","Ξ":"KS","Ο":"O","Π":"P","Ρ":"R","Σ":"S","Τ":"T","Υ":"Y","Φ":"F","Χ":"X","Ψ":"PS","Ω":"O","Ά":"A","Έ":"E","Ί":"I","Ό":"O","Ύ":"Y","Ή":"I","Ώ":"O","Ϊ":"I","Ϋ":"Y","ā":"a","ē":"e","ģ":"g","ī":"i","ķ":"k","ļ":"l","ņ":"n","ū":"u","Ā":"A","Ē":"E","Ģ":"G","Ī":"I","Ķ":"k","Ļ":"L","Ņ":"N","Ū":"U","Ќ":"Kj","ќ":"kj","Љ":"Lj","љ":"lj","Њ":"Nj","њ":"nj","Тс":"Ts","тс":"ts","ą":"a","ć":"c","ę":"e","ł":"l","ń":"n","ś":"s","ź":"z","ż":"z","Ą":"A","Ć":"C","Ę":"E","Ł":"L","Ń":"N","Ś":"S","Ź":"Z","Ż":"Z","Є":"Ye","І":"I","Ї":"Yi","Ґ":"G","є":"ye","і":"i","ї":"yi","ґ":"g","ă":"a","Ă":"A","ș":"s","Ș":"S","ț":"t","Ț":"T","ţ":"t","Ţ":"T","а":"a","б":"b","в":"v","г":"g","д":"d","е":"e","ё":"yo","ж":"zh","з":"z","и":"i","й":"i","к":"k","л":"l","м":"m","н":"n","о":"o","п":"p","р":"r","с":"s","т":"t","у":"u","ф":"f","х":"kh","ц":"c","ч":"ch","ш":"sh","щ":"sh","ъ":"","ы":"y","ь":"","э":"e","ю":"yu","я":"ya","А":"A","Б":"B","В":"V","Г":"G","Д":"D","Е":"E","Ё":"Yo","Ж":"Zh","З":"Z","И":"I","Й":"I","К":"K","Л":"L","М":"M","Н":"N","О":"O","П":"P","Р":"R","С":"S","Т":"T","У":"U","Ф":"F","Х":"Kh","Ц":"C","Ч":"Ch","Ш":"Sh","Щ":"Sh","Ъ":"","Ы":"Y","Ь":"","Э":"E","Ю":"Yu","Я":"Ya","ђ":"dj","ј":"j","ћ":"c","џ":"dz","Ђ":"Dj","Ј":"j","Ћ":"C","Џ":"Dz","ľ":"l","ĺ":"l","ŕ":"r","Ľ":"L","Ĺ":"L","Ŕ":"R","ş":"s","Ş":"S","ı":"i","İ":"I","ğ":"g","Ğ":"G","ả":"a","Ả":"A","ẳ":"a","Ẳ":"A","ẩ":"a","Ẩ":"A","đ":"d","Đ":"D","ẹ":"e","Ẹ":"E","ẽ":"e","Ẽ":"E","ẻ":"e","Ẻ":"E","ế":"e","Ế":"E","ề":"e","Ề":"E","ệ":"e","Ệ":"E","ễ":"e","Ễ":"E","ể":"e","Ể":"E","ỏ":"o","ọ":"o","Ọ":"o","ố":"o","Ố":"O","ồ":"o","Ồ":"O","ổ":"o","Ổ":"O","ộ":"o","Ộ":"O","ỗ":"o","Ỗ":"O","ơ":"o","Ơ":"O","ớ":"o","Ớ":"O","ờ":"o","Ờ":"O","ợ":"o","Ợ":"O","ỡ":"o","Ỡ":"O","Ở":"o","ở":"o","ị":"i","Ị":"I","ĩ":"i","Ĩ":"I","ỉ":"i","Ỉ":"i","ủ":"u","Ủ":"U","ụ":"u","Ụ":"U","ũ":"u","Ũ":"U","ư":"u","Ư":"U","ứ":"u","Ứ":"U","ừ":"u","Ừ":"U","ự":"u","Ự":"U","ữ":"u","Ữ":"U","ử":"u","Ử":"ư","ỷ":"y","Ỷ":"y","ỳ":"y","Ỳ":"Y","ỵ":"y","Ỵ":"Y","ỹ":"y","Ỹ":"Y","ạ":"a","Ạ":"A","ấ":"a","Ấ":"A","ầ":"a","Ầ":"A","ậ":"a","Ậ":"A","ẫ":"a","Ẫ":"A","ắ":"a","Ắ":"A","ằ":"a","Ằ":"A","ặ":"a","Ặ":"A","ẵ":"a","Ẵ":"A","⓪":"0","①":"1","②":"2","③":"3","④":"4","⑤":"5","⑥":"6","⑦":"7","⑧":"8","⑨":"9","⑩":"10","⑪":"11","⑫":"12","⑬":"13","⑭":"14","⑮":"15","⑯":"16","⑰":"17","⑱":"18","⑲":"18","⑳":"18","⓵":"1","⓶":"2","⓷":"3","⓸":"4","⓹":"5","⓺":"6","⓻":"7","⓼":"8","⓽":"9","⓾":"10","⓿":"0","⓫":"11","⓬":"12","⓭":"13","⓮":"14","⓯":"15","⓰":"16","⓱":"17","⓲":"18","⓳":"19","⓴":"20","Ⓐ":"A","Ⓑ":"B","Ⓒ":"C","Ⓓ":"D","Ⓔ":"E","Ⓕ":"F","Ⓖ":"G","Ⓗ":"H","Ⓘ":"I","Ⓙ":"J","Ⓚ":"K","Ⓛ":"L","Ⓜ":"M","Ⓝ":"N","Ⓞ":"O","Ⓟ":"P","Ⓠ":"Q","Ⓡ":"R","Ⓢ":"S","Ⓣ":"T","Ⓤ":"U","Ⓥ":"V","Ⓦ":"W","Ⓧ":"X","Ⓨ":"Y","Ⓩ":"Z","ⓐ":"a","ⓑ":"b","ⓒ":"c","ⓓ":"d","ⓔ":"e","ⓕ":"f","ⓖ":"g","ⓗ":"h","ⓘ":"i","ⓙ":"j","ⓚ":"k","ⓛ":"l","ⓜ":"m","ⓝ":"n","ⓞ":"o","ⓟ":"p","ⓠ":"q","ⓡ":"r","ⓢ":"s","ⓣ":"t","ⓤ":"u","ⓦ":"v","ⓥ":"w","ⓧ":"x","ⓨ":"y","ⓩ":"z","“":'"',"”":'"',"‘":"'","’":"'","∂":"d","ƒ":"f","™":"(TM)","©":"(C)","œ":"oe","Œ":"OE","®":"(R)","†":"+","℠":"(SM)","…":"...","˚":"o","º":"o","ª":"a","•":"*","၊":",","။":".",$:"USD","€":"EUR","₢":"BRN","₣":"FRF","£":"GBP","₤":"ITL","₦":"NGN","₧":"ESP","₩":"KRW","₪":"ILS","₫":"VND","₭":"LAK","₮":"MNT","₯":"GRD","₱":"ARS","₲":"PYG","₳":"ARA","₴":"UAH","₵":"GHS","¢":"cent","¥":"CNY","元":"CNY","円":"YEN","﷼":"IRR","₠":"EWE","฿":"THB","₨":"INR","₹":"INR","₰":"PF","₺":"TRY","؋":"AFN","₼":"AZN","лв":"BGN","៛":"KHR","₡":"CRC","₸":"KZT","ден":"MKD","zł":"PLN","₽":"RUB","₾":"GEL"},n=["်","ް"],t={"ာ":"a","ါ":"a","ေ":"e","ဲ":"e","ိ":"i","ီ":"i","ို":"o","ု":"u","ူ":"u","ေါင်":"aung","ော":"aw","ော်":"aw","ေါ":"aw","ေါ်":"aw","်":"်","က်":"et","ိုက်":"aik","ောက်":"auk","င်":"in","ိုင်":"aing","ောင်":"aung","စ်":"it","ည်":"i","တ်":"at","ိတ်":"eik","ုတ်":"ok","ွတ်":"ut","ေတ်":"it","ဒ်":"d","ိုဒ်":"ok","ုဒ်":"ait","န်":"an","ာန်":"an","ိန်":"ein","ုန်":"on","ွန်":"un","ပ်":"at","ိပ်":"eik","ုပ်":"ok","ွပ်":"ut","န်ုပ်":"nub","မ်":"an","ိမ်":"ein","ုမ်":"on","ွမ်":"un","ယ်":"e","ိုလ်":"ol","ဉ်":"in","ံ":"an","ိံ":"ein","ုံ":"on","ައް":"ah","ަށް":"ah"},i={en:{},az:{"ç":"c","ə":"e","ğ":"g","ı":"i","ö":"o","ş":"s","ü":"u","Ç":"C","Ə":"E","Ğ":"G","İ":"I","Ö":"O","Ş":"S","Ü":"U"},cs:{"č":"c","ď":"d","ě":"e","ň":"n","ř":"r","š":"s","ť":"t","ů":"u","ž":"z","Č":"C","Ď":"D","Ě":"E","Ň":"N","Ř":"R","Š":"S","Ť":"T","Ů":"U","Ž":"Z"},fi:{"ä":"a","Ä":"A","ö":"o","Ö":"O"},hu:{"ä":"a","Ä":"A","ö":"o","Ö":"O","ü":"u","Ü":"U","ű":"u","Ű":"U"},lt:{"ą":"a","č":"c","ę":"e","ė":"e","į":"i","š":"s","ų":"u","ū":"u","ž":"z","Ą":"A","Č":"C","Ę":"E","Ė":"E","Į":"I","Š":"S","Ų":"U","Ū":"U"},lv:{"ā":"a","č":"c","ē":"e","ģ":"g","ī":"i","ķ":"k","ļ":"l","ņ":"n","š":"s","ū":"u","ž":"z","Ā":"A","Č":"C","Ē":"E","Ģ":"G","Ī":"i","Ķ":"k","Ļ":"L","Ņ":"N","Š":"S","Ū":"u","Ž":"Z"},pl:{"ą":"a","ć":"c","ę":"e","ł":"l","ń":"n","ó":"o","ś":"s","ź":"z","ż":"z","Ą":"A","Ć":"C","Ę":"e","Ł":"L","Ń":"N","Ó":"O","Ś":"S","Ź":"Z","Ż":"Z"},sv:{"ä":"a","Ä":"A","ö":"o","Ö":"O"},sk:{"ä":"a","Ä":"A"},sr:{"љ":"lj","њ":"nj","Љ":"Lj","Њ":"Nj","đ":"dj","Đ":"Dj"},tr:{"Ü":"U","Ö":"O","ü":"u","ö":"o"}},o={ar:{"∆":"delta","∞":"la-nihaya","♥":"hob","&":"wa","|":"aw","<":"aqal-men",">":"akbar-men","∑":"majmou","¤":"omla"},az:{},ca:{"∆":"delta","∞":"infinit","♥":"amor","&":"i","|":"o","<":"menys que",">":"mes que","∑":"suma dels","¤":"moneda"},cs:{"∆":"delta","∞":"nekonecno","♥":"laska","&":"a","|":"nebo","<":"mensi nez",">":"vetsi nez","∑":"soucet","¤":"mena"},de:{"∆":"delta","∞":"unendlich","♥":"Liebe","&":"und","|":"oder","<":"kleiner als",">":"groesser als","∑":"Summe von","¤":"Waehrung"},dv:{"∆":"delta","∞":"kolunulaa","♥":"loabi","&":"aai","|":"noonee","<":"ah vure kuda",">":"ah vure bodu","∑":"jumula","¤":"faisaa"},en:{"∆":"delta","∞":"infinity","♥":"love","&":"and","|":"or","<":"less than",">":"greater than","∑":"sum","¤":"currency"},es:{"∆":"delta","∞":"infinito","♥":"amor","&":"y","|":"u","<":"menos que",">":"mas que","∑":"suma de los","¤":"moneda"},fa:{"∆":"delta","∞":"bi-nahayat","♥":"eshgh","&":"va","|":"ya","<":"kamtar-az",">":"bishtar-az","∑":"majmooe","¤":"vahed"},fi:{"∆":"delta","∞":"aarettomyys","♥":"rakkaus","&":"ja","|":"tai","<":"pienempi kuin",">":"suurempi kuin","∑":"summa","¤":"valuutta"},fr:{"∆":"delta","∞":"infiniment","♥":"Amour","&":"et","|":"ou","<":"moins que",">":"superieure a","∑":"somme des","¤":"monnaie"},ge:{"∆":"delta","∞":"usasruloba","♥":"siqvaruli","&":"da","|":"an","<":"naklebi",">":"meti","∑":"jami","¤":"valuta"},gr:{},hu:{"∆":"delta","∞":"vegtelen","♥":"szerelem","&":"es","|":"vagy","<":"kisebb mint",">":"nagyobb mint","∑":"szumma","¤":"penznem"},it:{"∆":"delta","∞":"infinito","♥":"amore","&":"e","|":"o","<":"minore di",">":"maggiore di","∑":"somma","¤":"moneta"},lt:{"∆":"delta","∞":"begalybe","♥":"meile","&":"ir","|":"ar","<":"maziau nei",">":"daugiau nei","∑":"suma","¤":"valiuta"},lv:{"∆":"delta","∞":"bezgaliba","♥":"milestiba","&":"un","|":"vai","<":"mazak neka",">":"lielaks neka","∑":"summa","¤":"valuta"},my:{"∆":"kwahkhyaet","∞":"asaonasme","♥":"akhyait","&":"nhin","|":"tho","<":"ngethaw",">":"kyithaw","∑":"paungld","¤":"ngwekye"},mk:{},nl:{"∆":"delta","∞":"oneindig","♥":"liefde","&":"en","|":"of","<":"kleiner dan",">":"groter dan","∑":"som","¤":"valuta"},pl:{"∆":"delta","∞":"nieskonczonosc","♥":"milosc","&":"i","|":"lub","<":"mniejsze niz",">":"wieksze niz","∑":"suma","¤":"waluta"},pt:{"∆":"delta","∞":"infinito","♥":"amor","&":"e","|":"ou","<":"menor que",">":"maior que","∑":"soma","¤":"moeda"},ro:{"∆":"delta","∞":"infinit","♥":"dragoste","&":"si","|":"sau","<":"mai mic ca",">":"mai mare ca","∑":"suma","¤":"valuta"},ru:{"∆":"delta","∞":"beskonechno","♥":"lubov","&":"i","|":"ili","<":"menshe",">":"bolshe","∑":"summa","¤":"valjuta"},sk:{"∆":"delta","∞":"nekonecno","♥":"laska","&":"a","|":"alebo","<":"menej ako",">":"viac ako","∑":"sucet","¤":"mena"},sr:{},tr:{"∆":"delta","∞":"sonsuzluk","♥":"ask","&":"ve","|":"veya","<":"kucuktur",">":"buyuktur","∑":"toplam","¤":"para birimi"},uk:{"∆":"delta","∞":"bezkinechnist","♥":"lubov","&":"i","|":"abo","<":"menshe",">":"bilshe","∑":"suma","¤":"valjuta"},vn:{"∆":"delta","∞":"vo cuc","♥":"yeu","&":"va","|":"hoac","<":"nho hon",">":"lon hon","∑":"tong","¤":"tien te"}},u=[";","?",":","@","&","=","+","$",",","/"].join(""),s=[";","?",":","@","&","=","+","$",","].join(""),l=[".","!","~","*","'","(",")"].join(""),r=function(a,r){var m,d,g,k,y,f,p,z,b,A,v,E,O,j,S="-",w="",U="",C=!0,N={},R="";if("string"!=typeof a)return"";if("string"==typeof r&&(S=r),p=o.en,z=i.en,"object"==typeof r){m=r.maintainCase||!1,N=r.custom&&"object"==typeof r.custom?r.custom:N,g=+r.truncate>1&&r.truncate||!1,k=r.uric||!1,y=r.uricNoSlash||!1,f=r.mark||!1,C=!1!==r.symbols&&!1!==r.lang,S=r.separator||S,k&&(R+=u),y&&(R+=s),f&&(R+=l),p=r.lang&&o[r.lang]&&C?o[r.lang]:C?o.en:{},z=r.lang&&i[r.lang]?i[r.lang]:!1===r.lang||!0===r.lang?{}:i.en,r.titleCase&&"number"==typeof r.titleCase.length&&Array.prototype.toString.call(r.titleCase)?(r.titleCase.forEach(function(a){N[a+""]=a+""}),d=!0):d=!!r.titleCase,r.custom&&"number"==typeof r.custom.length&&Array.prototype.toString.call(r.custom)&&r.custom.forEach(function(a){N[a+""]=a+""}),Object.keys(N).forEach(function(e){var n;n=e.length>1?new RegExp("\\b"+h(e)+"\\b","gi"):new RegExp(h(e),"gi"),a=a.replace(n,N[e])});for(v in N)R+=v}for(R=h(R+=S),O=!1,j=!1,A=0,E=(a=a.replace(/(^\s+|\s+$)/g,"")).length;A=0?(U+=v,v=""):!0===j?(v=t[U]+e[v],U=""):v=O&&e[v].match(/[A-Za-z0-9]/)?" "+e[v]:e[v],O=!1,j=!1):v in t?(U+=v,v="",A===E-1&&(v=t[U]),j=!0):!p[v]||k&&-1!==u.indexOf(v)||y&&-1!==s.indexOf(v)?(!0===j?(v=t[U]+v,U="",j=!1):O&&(/[A-Za-z0-9]/.test(v)||w.substr(-1).match(/A-Za-z0-9]/))&&(v=" "+v),O=!1):(v=O||w.substr(-1).match(/[A-Za-z0-9]/)?S+p[v]:p[v],v+=void 0!==a[A+1]&&a[A+1].match(/[A-Za-z0-9]/)?S:"",O=!0),w+=v.replace(new RegExp("[^\\w\\s"+R+"_-]","g"),S);return d&&(w=w.replace(/(\w)(\S*)/g,function(a,e,n){var t=e.toUpperCase()+(null!==n?n:"");return Object.keys(N).indexOf(t.toLowerCase())<0?t:t.toLowerCase()})),w=w.replace(/\s+/g,S).replace(new RegExp("\\"+S+"+","g"),S).replace(new RegExp("(^\\"+S+"+|\\"+S+"+$)","g"),""),g&&w.length>g&&(b=w.charAt(g)===S,w=w.slice(0,g),b||(w=w.slice(0,w.lastIndexOf(S)))),m||d||(w=w.toLowerCase()),w},m=function(a){return function(e){return r(e,a)}},h=function(a){return a.replace(/[-\\^$*+?.()|[\]{}\/]/g,"\\$&")},c=function(a,e){for(var n in e)if(e[n]===a)return!0};if("undefined"!=typeof module&&module.exports)module.exports=r,module.exports.createSlug=m;else if("undefined"!=typeof define&&define.amd)define([],function(){return r});else try{if(a.getSlug||a.createSlug)throw"speakingurl: globals exists /(getSlug|createSlug)/";a.getSlug=r,a.createSlug=m}catch(a){}}(this); \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 2165100..8ff9dc9 100644 --- a/templates/base.html +++ b/templates/base.html @@ -36,6 +36,9 @@ {% trans "Pīkau" %} + + {% trans "Files" %} +