Skip to content

Commit

Permalink
SIO-2107 Add multiple language support for News
Browse files Browse the repository at this point in the history
Adding the possibility to create different title/content versions
of a News instance for each language supported by application.

Change-Id: If72cdae6c5d499114f7bde92dc0ea67ca0c08ae5
  • Loading branch information
olafik committed Jun 5, 2018
1 parent 970b6e8 commit beae347
Show file tree
Hide file tree
Showing 17 changed files with 497 additions and 51 deletions.
118 changes: 118 additions & 0 deletions oioioi/base/static/js/translation-formset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Constructs an object which will control behaviour of translations formset
// (a set of forms containing different translations of some content).
// You may want to override some of it's parameters - pass them in a dictionary
// as the first argument:
// - 'select': a jQuery-wrapped HTMLSelectElement that is used to choose a displayed translation,
// - 'formGetter': a function returning forms (as jQuery objects) for a given language,
// - 'requiredFieldsSelector': a CSS selector string, matching the inputs of required fields.
function TranslationFormset(parameters) {
for (var parameter in parameters) {
if (typeof(this[parameter]) === 'undefined') {
throw "Sorry, parameter '" + parameter + "' passed to TranslationFormset is not recognized.";
} else {
this[parameter] = parameters[parameter];
}
}

var tFormset = this;

this.select.change(function() {
if (!this.value) {
tFormset.hideTranslation(tFormset.currentLang);
} else {
tFormset.showTranslation(this.value);
}
});

// For each form in a formset, Django adds a checkbox indicating deletion of the
// form's instance. We hide the checkbox (along with it's parent-label) from
// the user and check/uncheck it in TranslationFormset's internal functions.
$('input[name*="DELETE"]').parent().hide();


// Find the forms that should be visible (translations already created by user) according
// to POST data and display them - with forms containing errors at the beginning.
var notDeletedForms = [];
var notDeletedFormsWithErrors = [];

this.forms().each(function(index, form) {
var $form = $(form);

var requiredFields = $form.find(tFormset.requiredFieldsSelector);
form.tf__requiredFields = requiredFields.get();

tFormset.computeFormDeletion($form);
requiredFields.change(tFormset.computeFormDeletion.bind(tFormset, $form));

if (!tFormset.formDeleted($form)) {
if ($form.find('.has-error').length > 0) {
notDeletedFormsWithErrors.push($form);
} else {
notDeletedForms.push($form);
}
}
});

// Forms for created translations, the ones containing errors at the beginning.
notDeletedForms = notDeletedFormsWithErrors.concat(notDeletedForms);

notDeletedForms.forEach(function(form) {
var lang = form.data('lang');
tFormset.addTranslation(lang);
});

// The form from the first column is always required.
this.formDeleted($('.first-column'), false);
}

$.extend(TranslationFormset.prototype, {
select: $('#translation-select'),
forms: function (lang) { return lang ? $('#form-' + lang) : $('[id^=form-]'); },
requiredFieldsSelector: '',
currentLang: '',

hideTranslation: function(lang) {
if (lang) {
this.forms(lang).css('display', 'none');
}
if (lang === this.currentLang) {
this.currentLang = '';
}
},
showTranslation: function(lang) {
var form = this.forms(lang);

// Hide previously displayed form - only one form per column.
this.hideTranslation(this.currentLang);

form.css('display', 'block');
this.select.val(lang);
this.currentLang = lang;
},
addTranslation: function(lang) {
// Mark new languages' form as not deleted.
this.formDeleted(this.forms(lang), false);

// If there's an empty column - display it with the new translation form.
if (this.currentLang === '') {
this.showTranslation(lang);
}
},
formDeleted: function(form, value) {
if (arguments.length < 2) {
return form.find('input[name*="DELETE"]').prop('checked');
} else {
return form.find('input[name*="DELETE"]').prop('checked', value);
}
},
computeFormDeletion: function(form) {
if (form.get(0).tf__requiredFields.every(this.fieldIsEmpty)) {
this.formDeleted(form, true);
} else if (this.formDeleted(form)) {
this.formDeleted(form, false);
}
},
fieldIsEmpty: function(field) {
return !field.value;
}
});
28 changes: 28 additions & 0 deletions oioioi/base/static/scss/_translation-formset.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#translation-formset {
padding-bottom: 50px;

.first-column {
float: left;
width: 50%;
padding-right: 15px;
}

.second-column {
float: right;
width: 50%;
padding-left: 15px;

select {
display: inline-block;
width: initial;
}

.translation-form {
display: none;

.delete-translation {
margin: 20px 0 10px;
}
}
}
}
1 change: 1 addition & 0 deletions oioioi/base/static/scss/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
@import "scss/markdown-editor";
@import "scss/spinner";
@import "scss/language-picker";
@import "scss/translation-formset";

body {
min-width: $oioioi-body-min-width;
Expand Down
60 changes: 60 additions & 0 deletions oioioi/base/templates/ingredients/translation-formset.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{% load i18n staticfiles compress %}
{% get_current_language as LANGUAGE_CODE %}

<div id="translation-formset" class="clearfix">
{{ formset.management_form }}

{% if formset.non_form_errors %}
<div class="form-group">
{% for error in formset.non_form_errors %}
<div class="alert alert-danger" role="alert">
<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
<span class="sr-only">{% trans "Error" %}:</span>
{{ error }}
</div>
{% endfor %}
</div>
{% endif %}

<div class="clearfix">
<div class="first-column">
<label>{% trans "Current language" %}:</label>
{{ LANGUAGE_CODE|language_name_translated }}
</div>
<div class="second-column">
<label for="translation-select">{% trans "Tranlation language" %}:</label>
<select id="translation-select" class="form-control">
<option value="">------</option>
{% for lang_short, lang_name in LANGUAGES %}
{% if lang_short != LANGUAGE_CODE %}
<option value="{{ lang_short }}">
{{ lang_short|language_name_translated }}
</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>

<div class="first-column">
{% for form in formset %}
{% if form.language.value == LANGUAGE_CODE %}
{% include "ingredients/form.html" %}
{% endif %}
{% endfor %}
</div>

<div class="second-column">
{% for form in formset %}
{% if form.language.value != LANGUAGE_CODE %}
<div id="form-{{ form.language.value }}" class="translation-form" data-lang="{{ form.language.value }}">
{% include "ingredients/form.html" %}
</div>
{% endif %}
{% endfor %}
</div>
</div>

{% compress js %}
<script type="text/javascript" src="{% static 'js/translation-formset.js' %}"></script>
{% endcompress %}
22 changes: 15 additions & 7 deletions oioioi/newsfeed/fixtures/newsfeed.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
[
{
"fields": {
"date": "2016-03-14T19:00:32.428Z",
"content": "This is a test",
"title": "Test news"
},
"model": "newsfeed.news",
"pk": 1
"fields": {
"date": "2018-04-23T20:54:17.274Z"
},
"model": "newsfeed.news",
"pk": 1
},
{
"model": "newsfeed.newslanguageversion",
"pk": 2,
"fields": {
"news": 1,
"language": "en",
"title": "Test news",
"content": "This is a test"
}
}
]
37 changes: 27 additions & 10 deletions oioioi/newsfeed/forms.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,37 @@
from django import forms
from django.conf import settings
from django.forms import modelformset_factory
from django.utils.translation import ugettext_lazy as _

from oioioi.newsfeed.models import News
from oioioi.newsfeed.models import NewsLanguageVersion


class NewsForm(forms.ModelForm):
class NewsLanguageVersionForm(forms.ModelForm):
class Meta(object):
model = News
fields = ['title', 'content']
model = NewsLanguageVersion
fields = ['language', 'title', 'content', ]

language = forms.ChoiceField(
label=_("Language"),
choices=settings.LANGUAGES,
widget=forms.HiddenInput(),
)

title = forms.CharField(
label=_("Title"),
max_length=255,
widget=forms.TextInput(attrs={'class': 'input-xxlarge'}))
label=_("Title"),
max_length=255,
widget=forms.TextInput(attrs={'class': 'input-xxlarge'}),
)

content = forms.CharField(
label=_("Content"),
widget=forms.Textarea(attrs={'class': 'input-xxlarge',
'rows': 10}))
label=_("Content"),
widget=forms.Textarea(attrs={'class': 'input-xxlarge',
'rows': 10}),
)


NewsLanguageVersionFormset = modelformset_factory(
NewsLanguageVersion, form=NewsLanguageVersionForm,
extra=len(settings.LANGUAGES), min_num=1, max_num=len(settings.LANGUAGES),
validate_min=True, validate_max=True, can_delete=True,
)
36 changes: 36 additions & 0 deletions oioioi/newsfeed/migrations/0002_newslanguageversion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.13 on 2018-04-25 19:02
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('newsfeed', '0001_initial'),
]

operations = [
migrations.CreateModel(
name='NewsLanguageVersion',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('language', models.CharField(max_length=6, verbose_name='language code')),
('title', models.CharField(max_length=255, verbose_name='title')),
('content', models.TextField(verbose_name='content')),
('news', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='versions', to='newsfeed.News')),
],
),
migrations.AlterField(
model_name='news',
name='content',
field=models.TextField(blank=True, null=True, verbose_name='content'),
),
migrations.AlterField(
model_name='news',
name='title',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='title'),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.conf import settings
from django.db import migrations


def move_news_data_to_newslanguageversion(apps, schema_editor):
News = apps.get_model('newsfeed', 'News')
NewsLanguageVersion = apps.get_model('newsfeed', 'NewsLanguageVersion')

for news in News.objects.all():
news_content = NewsLanguageVersion(news=news, language=settings.LANGUAGE_CODE,
title=news.title, content=news.content)
news_content.save()


def reverse_news_data_migration(apps, schema_editor):
News = apps.get_model('newsfeed', 'News')
NewsLanguageVersion = apps.get_model('newsfeed', 'NewsLanguageVersion')

for news in News.objects.all():
try:
default_news_content = news.versions.get(
language=settings.LANGUAGE_CODE)
except NewsLanguageVersion.DoesNotExist:
default_news_content = news.versions.first()

if default_news_content is not None:
news.title = default_news_content.title
news.content = default_news_content.content
news.save()


class Migration(migrations.Migration):

dependencies = [
('newsfeed', '0002_newslanguageversion'),
]

operations = [
migrations.RunPython(move_news_data_to_newslanguageversion,
reverse_news_data_migration)
]
22 changes: 22 additions & 0 deletions oioioi/newsfeed/migrations/0004_remove_news_content.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('newsfeed', '0003_news_data_to_newslanguageversion'),
]

operations = [
migrations.RemoveField(
model_name='news',
name='content',
),
migrations.RemoveField(
model_name='news',
name='title',
),
]
Loading

0 comments on commit beae347

Please sign in to comment.