Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow content edit and translation #162

Merged
merged 1 commit into from
Jun 3, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion dips/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
from django.contrib.auth.forms import UserChangeForm
from django.utils.translation import gettext_lazy as _
from django import forms
from .models import User, DublinCore, Setting
from modeltranslation.forms import TranslationModelForm

from .models import Content, User, DublinCore, Setting


class DeleteByDublinCoreForm(forms.ModelForm):
Expand Down Expand Up @@ -165,3 +167,9 @@ class DublinCoreSettingsForm(SettingsForm):
"in the Collection and Folder view pages."
),
)


class ContentForm(TranslationModelForm):
class Meta:
model = Content
fields = ("content",)
29 changes: 29 additions & 0 deletions dips/migrations/0009_content.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 2.1.7 on 2019-05-31 23:22

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [("dips", "0008_alter_dip_objectszip")]

operations = [
migrations.CreateModel(
name="Content",
fields=[
(
"key",
models.CharField(max_length=50, primary_key=True, serialize=False),
),
("content", models.TextField(blank=True, verbose_name="content")),
(
"content_en",
models.TextField(blank=True, null=True, verbose_name="content"),
),
(
"content_fr",
models.TextField(blank=True, null=True, verbose_name="content"),
),
],
)
]
38 changes: 38 additions & 0 deletions dips/migrations/0010_initial_content.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from django.db import migrations

from dips.models import Content


def add(apps, schema_editor):
home_en = (
"## Digital Archives Access Interface\n\n"
"You can search digital files by using the search "
"bar below or you can browse our collections. \n"
"If you need help, please use our FAQ page in the menu above."
)
home_fr = (
"## Interface d'accès aux archives numériques\n\n"
"Pour effectuer une recherche dans les fichiers, utiliser la barre de "
"recherche ci-dessous ou naviguer dans nos collections. \n"
"Consulter la FAQ si vous avez besoin d'aide."
)
data = [
("01_home", {"en": home_en, "fr": home_fr}),
("02_login", {"en": "", "fr": ""}),
("03_faq", {"en": "", "fr": ""}),
]
for key, content in data:
Content.objects.create(
key=key, content_en=content["en"], content_fr=content["fr"]
)


def remove(apps, schema_editor):
Content.objects.all().delete()


class Migration(migrations.Migration):

dependencies = [("dips", "0009_content")]

operations = [migrations.RunPython(add, remove)]
15 changes: 15 additions & 0 deletions dips/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -437,3 +437,18 @@ class Setting(models.Model):

def __str__(self):
return self.name


class Content(models.Model):
key = models.CharField(max_length=50, primary_key=True)
content = models.TextField(_("content"), blank=True)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jraddaoui , wouldn't be a good idea to have the model mapped out after the concept of a "Page"? E.g. a model where you can also fit the title of the page or in the future things like its permalink? Or you're thinking on something more generic?

Copy link
Collaborator Author

@jraddaoui jraddaoui Jun 3, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point @sevein! I didn't go that way because some of the contents are added as part of a page, where other elements already exist. There are some follow-ups in #96 about those "static content" sections.

cole marked this conversation as resolved.
Show resolved Hide resolved

# Key label relation for display in the form.
# They are prefixed with a number to maintain order.
LABELS = {"01_home": _("Home"), "02_login": _("Login"), "03_faq": _("FAQ")}
cole marked this conversation as resolved.
Show resolved Hide resolved

def __str__(self):
return self.content

def get_label(self):
return self.LABELS[self.key]
16 changes: 15 additions & 1 deletion dips/templatetags/custom_tags.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import os

from django import template
import markdown

import os
from dips.models import Content

register = template.Library()

Expand Down Expand Up @@ -43,3 +46,14 @@ def render_label_with_class(field, class_attr):
def basename(path):
"""Returns the filename from a path."""
return os.path.basename(path)


@register.simple_tag
def render_content(key):
try:
content = Content.objects.get(key=key)
return markdown.markdown(str(content))
# It's not clear what may be raised from Markdown,
# this will also catch other model exceptions.
except Exception:
return ""
30 changes: 30 additions & 0 deletions dips/tests/test_migrations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from django.contrib.auth.models import Group
cole marked this conversation as resolved.
Show resolved Hide resolved
from django.db.migrations.executor import MigrationExecutor
from django.db import connection
from django.test import TransactionTestCase

from dips.models import Content


class TestMigrations(TransactionTestCase):
"""Uses TransactionTestCase to perform rollbacks."""

def setUp(self):
self.executor = MigrationExecutor(connection)

def test_rollbacks(self):
"""Checks that migration rollbacks run correctly.

Perform all rollbacks in order in the same test to maintain DB status.
"""
# Initial counts
self.assertEqual(Group.objects.count(), 3)
self.assertEqual(Content.objects.count(), 3)

# Content removal
self.executor.migrate([("dips", "0009_content")])
self.assertEqual(Content.objects.count(), 0)

# Groups removal
self.executor.migrate([("dips", "0001_initial")])
self.assertEqual(Group.objects.count(), 0)
8 changes: 8 additions & 0 deletions dips/tests/test_user_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,14 @@ class UserAccessTests(TestCase):
("basic", 302),
("viewer", 302),
],
"content": [
("unauth", 302),
("admin", 200),
("manager", 302),
("editor", 302),
("basic", 302),
("viewer", 302),
],
}

@patch("elasticsearch_dsl.DocType.save")
Expand Down
81 changes: 80 additions & 1 deletion dips/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
from django.test import TestCase
from unittest.mock import patch

from dips.models import User
from dips.models import Content, User


class ViewsTests(TestCase):
def setUp(self):
self.user = User.objects.create_superuser("admin", "[email protected]", "admin")
self.client.login(username="admin", password="admin")
self.faq = Content.objects.get(key="03_faq")

@patch("elasticsearch_dsl.Search.execute")
@patch("elasticsearch_dsl.Search.count", return_value=0)
Expand All @@ -26,3 +27,81 @@ def test_search_wrong_dates(self, mock_msg_error, mock_es_count, mock_es_exec):
self.assertEqual(response.context["filters"], expected_filters)
# But the errors should be added to the messages
self.assertEqual(mock_msg_error.call_count, 2)

def test_content_get_en(self):
response = self.client.get("/content/", HTTP_ACCEPT_LANGUAGE="en")
self.assertEqual(len(response.context["formset"]), 3)
self.assertContains(response, "## Digital Archives Access Interface")

def test_content_get_fr(self):
response = self.client.get("/content/", HTTP_ACCEPT_LANGUAGE="fr")
self.assertEqual(len(response.context["formset"]), 3)
self.assertContains(
response, "## Interface d'accès aux archives numériques"
)

def test_content_post_en(self):
data = {
"form-TOTAL_FORMS": ["3"],
"form-INITIAL_FORMS": ["3"],
"form-MIN_NUM_FORMS": ["0"],
"form-MAX_NUM_FORMS": ["1000"],
"form-0-content": ["New English content"],
"form-0-key": ["01_home"],
"form-1-content": [""],
"form-1-key": ["02_login"],
"form-2-content": [""],
"form-2-key": ["03_faq"],
}
self.client.post("/content/", data, HTTP_ACCEPT_LANGUAGE="en")
content = Content.objects.get(key="01_home")
self.assertEqual(content.content_en, "New English content")

def test_content_post_fr(self):
data = {
"form-TOTAL_FORMS": ["3"],
"form-INITIAL_FORMS": ["3"],
"form-MIN_NUM_FORMS": ["0"],
"form-MAX_NUM_FORMS": ["1000"],
"form-0-content": ["New French content"],
"form-0-key": ["01_home"],
"form-1-content": [""],
"form-1-key": ["02_login"],
"form-2-content": [""],
"form-2-key": ["03_faq"],
}
self.client.post("/content/", data, HTTP_ACCEPT_LANGUAGE="fr")
content = Content.objects.get(key="01_home")
self.assertEqual(content.content_fr, "New French content")

def test_faq_deleted(self):
self.faq.delete()
response = self.client.get("/faq/", HTTP_ACCEPT_LANGUAGE="en")
self.assertEqual(response.status_code, 200)

def test_faq_markdown(self):
self.faq.content_en = "## Header"
self.faq.save()
response = self.client.get("/faq/", HTTP_ACCEPT_LANGUAGE="en")
self.assertContains(response, "<h2>Header</h2>")

def test_faq_html(self):
self.faq.content_en = "<h2>Header</h2>"
self.faq.save()
response = self.client.get("/faq/", HTTP_ACCEPT_LANGUAGE="en")
self.assertContains(response, "<h2>Header</h2>")

def test_faq_langs(self):
self.faq.content_en = "English content"
self.faq.save()
# English
response = self.client.get("/faq/", HTTP_ACCEPT_LANGUAGE="en")
self.assertContains(response, "English content")
# Fallback
response = self.client.get("/faq/", HTTP_ACCEPT_LANGUAGE="fr")
self.assertContains(response, "English content")
# French
self.faq.content_fr = "French content"
self.faq.save()
response = self.client.get("/faq/", HTTP_ACCEPT_LANGUAGE="fr")
self.assertContains(response, "French content")
8 changes: 8 additions & 0 deletions dips/translation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from modeltranslation.translator import register, TranslationOptions

from .models import Content


@register(Content)
class ContentTranslationOptions(TranslationOptions):
fields = ("content",)
23 changes: 21 additions & 2 deletions dips/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,20 @@
from django.contrib import messages
from django.contrib.auth.models import Group
from django.contrib.auth.decorators import login_required
from django.forms import modelform_factory
from django.forms import modelform_factory, modelformset_factory
from django.http import Http404, HttpResponse, StreamingHttpResponse

from django.shortcuts import render, redirect, get_object_or_404
from django.utils.translation import gettext as _

from .helpers import get_sort_params, get_page_from_search
from .models import User, Collection, DIP, DigitalFile, DublinCore
from .models import User, Collection, DIP, DigitalFile, DublinCore, Content
from .forms import (
DeleteByDublinCoreForm,
UserCreationForm,
UserChangeForm,
DublinCoreSettingsForm,
ContentForm,
)
from .tasks import extract_mets, parse_mets, save_import_error
from search.helpers import (
Expand Down Expand Up @@ -727,3 +728,21 @@ def settings(request):
return redirect("settings")

return render(request, "settings.html", {"form": form})


@login_required(login_url="/login/")
def content(request):
if not request.user.is_superuser:
return redirect("home")

ContentFormSet = modelformset_factory(Content, form=ContentForm, extra=0)
formset = ContentFormSet(
request.POST or None, queryset=Content.objects.all().order_by("key")
)

if request.method == "POST" and formset.is_valid():
formset.save()
# Redirect to not add validation classes
return redirect("content")

return render(request, "content.html", {"formset": formset})
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ Django==2.1.7
django_celery_results==1.0.1
django-cleanup==2.1.0
django_compressor==2.2
django-modeltranslation==0.13.1
django-npm==1.0.0
djangorestframework==3.9.2
django-widget-tweaks==1.4.2
Expand All @@ -14,6 +15,7 @@ jsonfield2==3.0.1
kombu==4.3.0
libsass==0.14.5
lxml==4.2.4
Markdown==3.1.1
nodeenv==1.3.3
pytz==2018.5
redis==2.10.6
Expand Down
1 change: 1 addition & 0 deletions scope/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"django_celery_results",
"widget_tweaks",
"compressor",
"modeltranslation",
"dips.apps.DipsConfig",
"search.apps.SearchConfig",
"django_cleanup", # deletes FileFields when objects are deleted
Expand Down
1 change: 1 addition & 0 deletions scope/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
url(r"^new_user/", views.new_user, name="new_user"),
url(r"^users/", views.users, name="users"),
url(r"^settings/", views.settings, name="settings"),
url(r"^content/", views.content, name="content"),
url(r"^api/v1/", include("dips.api_urls")),
url(r"^i18n/", include("django.conf.urls.i18n")),
]
Loading