Management dialog for language packs
Allows managing language packs of core OctoPrint and plugins.
This commit is contained in:
parent
5904d01bff
commit
52dd2ad7ac
5 changed files with 336 additions and 3 deletions
|
|
@ -331,7 +331,8 @@ class Server():
|
|||
|
||||
self._tornado_app = Application(server_routes)
|
||||
max_body_sizes = [
|
||||
("POST", r"/api/files/([^/]*)", s.getInt(["server", "uploads", "maxSize"]))
|
||||
("POST", r"/api/files/([^/]*)", s.getInt(["server", "uploads", "maxSize"])),
|
||||
("POST", r"/api/languages", 5 * 1024 * 1024)
|
||||
]
|
||||
|
||||
# allow plugins to extend allowed maximum body sizes
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ from . import users as api_users
|
|||
from . import log as api_logs
|
||||
from . import slicing as api_slicing
|
||||
from . import printer_profiles as api_printer_profiles
|
||||
from . import languages as api_languages
|
||||
|
||||
|
||||
VERSION = "0.1"
|
||||
|
|
|
|||
147
src/octoprint/server/api/languages.py
Normal file
147
src/octoprint/server/api/languages.py
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
__copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms of the AGPLv3 License"
|
||||
|
||||
import os
|
||||
import tarfile
|
||||
import zipfile
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from flask import request, jsonify, make_response
|
||||
|
||||
from octoprint.settings import settings
|
||||
|
||||
from octoprint.server import admin_permission
|
||||
from octoprint.server.api import api
|
||||
from octoprint.server.util.flask import restricted_access
|
||||
|
||||
from octoprint.plugin import plugin_manager
|
||||
|
||||
from flask.ext.babel import Locale
|
||||
|
||||
@api.route("/languages", methods=["GET"])
|
||||
@restricted_access
|
||||
@admin_permission.require(403)
|
||||
def getInstalledLanguagePacks():
|
||||
translation_folder = settings().getBaseFolder("translations")
|
||||
if not os.path.exists(translation_folder):
|
||||
return jsonify(language_packs=dict(_core=[]))
|
||||
|
||||
core_packs = []
|
||||
plugin_packs = defaultdict(lambda: dict(identifier=None, display=None, languages=[]))
|
||||
for folder in os.listdir(translation_folder):
|
||||
path = os.path.join(translation_folder, folder)
|
||||
|
||||
if not os.path.isdir(path):
|
||||
continue
|
||||
|
||||
def load_meta(path, locale):
|
||||
meta = dict()
|
||||
|
||||
meta_path = os.path.join(path, "meta.yaml")
|
||||
if os.path.isfile(meta_path):
|
||||
import yaml
|
||||
try:
|
||||
with open(meta_path) as f:
|
||||
meta = yaml.safe_load(f)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
import datetime
|
||||
if "last_update" in meta and isinstance(meta["last_update"], datetime.datetime):
|
||||
meta["last_update"] = (meta["last_update"] - datetime.datetime(1970,1,1)).total_seconds()
|
||||
|
||||
l = Locale.parse(locale)
|
||||
meta["locale"] = locale
|
||||
meta["locale_display"] = l.display_name
|
||||
meta["locale_english"] = l.english_name
|
||||
return meta
|
||||
|
||||
if folder == "_plugins":
|
||||
for plugin_folder in os.listdir(path):
|
||||
plugin_path = os.path.join(path, plugin_folder)
|
||||
if not os.path.isdir(plugin_path):
|
||||
continue
|
||||
|
||||
if not plugin_folder in plugin_manager().plugins:
|
||||
continue
|
||||
|
||||
plugin_info = plugin_manager().plugins[plugin_folder]
|
||||
|
||||
plugin_packs[plugin_folder]["identifier"] = plugin_folder
|
||||
plugin_packs[plugin_folder]["display"] = plugin_info.name
|
||||
|
||||
for language_folder in os.listdir(plugin_path):
|
||||
plugin_packs[plugin_folder]["languages"].append(load_meta(os.path.join(plugin_path, language_folder), language_folder))
|
||||
else:
|
||||
core_packs.append(load_meta(os.path.join(translation_folder, folder), folder))
|
||||
|
||||
result = dict(_core=dict(identifier="_core", display="Core", languages=core_packs))
|
||||
result.update(plugin_packs)
|
||||
return jsonify(language_packs=result)
|
||||
|
||||
@api.route("/languages", methods=["POST"])
|
||||
@restricted_access
|
||||
@admin_permission.require(403)
|
||||
def uploadLanguagePack():
|
||||
input_name = "file"
|
||||
input_upload_path = input_name + "." + settings().get(["server", "uploads", "pathSuffix"])
|
||||
if not input_upload_path in request.values:
|
||||
return make_response("No file included", 400)
|
||||
|
||||
upload_path = request.values[input_upload_path]
|
||||
|
||||
target_path = settings().getBaseFolder("translations")
|
||||
|
||||
if tarfile.is_tarfile(upload_path):
|
||||
_unpack_uploaded_tarball(upload_path, target_path)
|
||||
elif zipfile.is_zipfile(upload_path):
|
||||
_unpack_uploaded_zipfile(upload_path, target_path)
|
||||
else:
|
||||
return make_response("Neither zip file nor tarball included", 400)
|
||||
|
||||
return getInstalledLanguagePacks()
|
||||
|
||||
@api.route("/languages/<string:locale>/<string:pack>", methods=["DELETE"])
|
||||
@restricted_access
|
||||
@admin_permission.require(403)
|
||||
def deleteInstalledLanguagePack(locale, pack):
|
||||
|
||||
if pack == "_core":
|
||||
target_path = os.path.join(settings().getBaseFolder("translations"), locale)
|
||||
else:
|
||||
target_path = os.path.join(settings().getBaseFolder("translations"), "_plugins", pack, locale)
|
||||
|
||||
if os.path.isdir(target_path):
|
||||
import shutil
|
||||
shutil.rmtree(target_path)
|
||||
|
||||
return getInstalledLanguagePacks()
|
||||
|
||||
def _unpack_uploaded_zipfile(path, target):
|
||||
with zipfile.ZipFile(path, "r") as zip:
|
||||
# sanity check
|
||||
map(_validate_archive_name, zip.namelist())
|
||||
|
||||
# unpack everything
|
||||
zip.extractall(target)
|
||||
|
||||
def _unpack_uploaded_tarball(path, target):
|
||||
with tarfile.open(path, "r") as tar:
|
||||
# sanity check
|
||||
map(_validate_archive_name, tar.getmembers())
|
||||
|
||||
# unpack everything
|
||||
tar.extractall(target)
|
||||
|
||||
def _validate_archive_name(name):
|
||||
if name.startswith("/") or ".." in name:
|
||||
raise InvalidLanguagePack("Provided language pack contains invalid name {name}".format(**locals()))
|
||||
|
||||
|
||||
class InvalidLanguagePack(BaseException):
|
||||
pass
|
||||
|
|
@ -16,6 +16,37 @@ $(function() {
|
|||
self.appearance_defaultLanguage = ko.observable();
|
||||
|
||||
self.settingsDialog = undefined;
|
||||
self.translationManagerDialog = undefined;
|
||||
self.translationUploadElement = $("#settings_appearance_managelanguagesdialog_upload");
|
||||
self.translationUploadButton = $("#settings_appearance_managelanguagesdialog_upload_start");
|
||||
|
||||
self.translationUploadFilename = ko.observable();
|
||||
self.invalidTranslationArchive = ko.computed(function() {
|
||||
var name = self.translationUploadFilename();
|
||||
return name !== undefined && !(_.endsWith(name.toLocaleLowerCase(), ".zip") || _.endsWith(name.toLocaleLowerCase(), ".tar.gz") || _.endsWith(name.toLocaleLowerCase(), ".tgz") || _.endsWith(name.toLocaleLowerCase(), ".tar"));
|
||||
});
|
||||
self.enableTranslationUpload = ko.computed(function() {
|
||||
var name = self.translationUploadFilename();
|
||||
return name !== undefined && name.trim() != "" && !self.invalidTranslationArchive();
|
||||
});
|
||||
|
||||
self.translations = new ItemListHelper(
|
||||
"settings.translations",
|
||||
{
|
||||
"locale": function (a, b) {
|
||||
// sorts ascending
|
||||
if (a["locale"].toLocaleLowerCase() < b["locale"].toLocaleLowerCase()) return -1;
|
||||
if (a["locale"].toLocaleLowerCase() > b["locale"].toLocaleLowerCase()) return 1;
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
{
|
||||
},
|
||||
"locale",
|
||||
[],
|
||||
[],
|
||||
0
|
||||
);
|
||||
|
||||
self.appearance_available_colors = ko.observable([
|
||||
{key: "default", name: gettext("default")},
|
||||
|
|
@ -108,7 +139,7 @@ $(function() {
|
|||
self.scripts_gcode_afterPrintPaused = ko.observable(undefined);
|
||||
self.scripts_gcode_beforePrintResumed = ko.observable(undefined);
|
||||
self.scripts_gcode_afterPrinterConnected = ko.observable(undefined);
|
||||
|
||||
|
||||
self.temperature_profiles = ko.observableArray(undefined);
|
||||
self.temperature_cutoff = ko.observable(undefined);
|
||||
|
||||
|
|
@ -140,11 +171,43 @@ $(function() {
|
|||
|
||||
self.onStartup = function() {
|
||||
self.settingsDialog = $('#settings_dialog');
|
||||
self.translationManagerDialog = $('#settings_appearance_managelanguagesdialog');
|
||||
self.translationUploadElement = $("#settings_appearance_managelanguagesdialog_upload");
|
||||
self.translationUploadButton = $("#settings_appearance_managelanguagesdialog_upload_start");
|
||||
|
||||
self.translationUploadElement.fileupload({
|
||||
dataType: "json",
|
||||
maxNumberOfFiles: 1,
|
||||
autoUpload: false,
|
||||
add: function(e, data) {
|
||||
if (data.files.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.translationUploadFilename(data.files[0].name);
|
||||
|
||||
self.translationUploadButton.unbind("click");
|
||||
self.translationUploadButton.bind("click", function() {
|
||||
data.submit();
|
||||
return false;
|
||||
});
|
||||
},
|
||||
done: function(e, data) {
|
||||
self.translationUploadButton.unbind("click");
|
||||
self.translationUploadFilename(undefined);
|
||||
self.fromTranslationResponse(data.result);
|
||||
},
|
||||
fail: function(e, data) {
|
||||
self.translationUploadButton.unbind("click");
|
||||
self.translationUploadFilename(undefined);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
self.onAllBound = function(allViewModels) {
|
||||
self.settingsDialog.on('show', function(event) {
|
||||
if (event.target.id == "settings_dialog") {
|
||||
self.requestTranslationData();
|
||||
_.each(allViewModels, function(viewModel) {
|
||||
if (viewModel.hasOwnProperty("onSettingsShown")) {
|
||||
viewModel.onSettingsShown();
|
||||
|
|
@ -182,6 +245,11 @@ $(function() {
|
|||
return false;
|
||||
};
|
||||
|
||||
self.showTranslationManager = function() {
|
||||
self.translationManagerDialog.modal();
|
||||
return false;
|
||||
};
|
||||
|
||||
self.requestData = function(callback) {
|
||||
$.ajax({
|
||||
url: API_BASEURL + "settings",
|
||||
|
|
@ -194,6 +262,71 @@ $(function() {
|
|||
});
|
||||
};
|
||||
|
||||
self.requestTranslationData = function(callback) {
|
||||
$.ajax({
|
||||
url: API_BASEURL + "languages",
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: function(response) {
|
||||
self.fromTranslationResponse(response);
|
||||
if (callback) callback();
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
self.fromTranslationResponse = function(response) {
|
||||
var translationsByLocale = {};
|
||||
_.each(response.language_packs, function(item, key) {
|
||||
_.each(item.languages, function(pack) {
|
||||
var locale = pack.locale;
|
||||
if (!_.has(translationsByLocale, locale)) {
|
||||
translationsByLocale[locale] = {
|
||||
locale: locale,
|
||||
display: pack.locale_display,
|
||||
english: pack.locale_english,
|
||||
packs: []
|
||||
};
|
||||
}
|
||||
|
||||
translationsByLocale[locale]["packs"].push({
|
||||
identifier: key,
|
||||
display: item.display,
|
||||
pack: pack
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var translations = [];
|
||||
_.each(translationsByLocale, function(item) {
|
||||
item["packs"].sort(function(a, b) {
|
||||
if (a.identifier == "_core") return -1;
|
||||
if (b.identifier == "_core") return 1;
|
||||
|
||||
if (a.display < b.display) return -1;
|
||||
if (a.display > b.display) return 1;
|
||||
return 0;
|
||||
});
|
||||
translations.push(item);
|
||||
});
|
||||
|
||||
self.translations.updateItems(translations);
|
||||
};
|
||||
|
||||
self.languagePackDisplay = function(item) {
|
||||
return item.display + ((item.english != undefined) ? ' (' + item.english + ')' : '');
|
||||
};
|
||||
|
||||
self.deleteLanguagePack = function(locale, pack) {
|
||||
$.ajax({
|
||||
url: API_BASEURL + "languages/" + locale + "/" + pack,
|
||||
type: "DELETE",
|
||||
dataType: "json",
|
||||
success: function(response) {
|
||||
self.fromTranslationResponse(response);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
self.fromResponse = function(response) {
|
||||
if (self.settings === undefined) {
|
||||
self.settings = ko.mapping.fromJS(response);
|
||||
|
|
@ -263,7 +396,7 @@ $(function() {
|
|||
self.scripts_gcode_afterPrintPaused(response.scripts.gcode.afterPrintPaused);
|
||||
self.scripts_gcode_beforePrintResumed(response.scripts.gcode.beforePrintResumed);
|
||||
self.scripts_gcode_afterPrinterConnected(response.scripts.gcode.afterPrinterConnected);
|
||||
|
||||
|
||||
self.temperature_profiles(response.temperature.profiles);
|
||||
self.temperature_cutoff(response.temperature.cutoff);
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,12 @@
|
|||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group" title="">
|
||||
<label class="control-label" for="settings-appearanceLanguages">{{ _('Language Packs') }}</label>
|
||||
<div class="controls">
|
||||
<button class="btn" data-bind="click: showTranslationManager">{{ _('Manage...') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group" title="">
|
||||
<label class="control-label" for="settings-appearanceDefaultLanguage">{{ _('Default Language') }}</label>
|
||||
<div class="controls">
|
||||
|
|
@ -31,3 +37,48 @@
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="settings_appearance_managelanguagesdialog" class="modal hide fade">
|
||||
<div class="modal-header">
|
||||
<a href="#" class="close" data-dismiss="modal" aria-hidden="true">×</a>
|
||||
<h3>{{ _('Manage Language Packs...') }}</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="settings_appearance_managelanguagesdialog_list" data-bind="slimScrolledForeach: translations.paginatedItems" style="height: 300px">
|
||||
<div class="entry">
|
||||
<strong><a href="#" onclick="$(this).children('i').toggleClass('icon-caret-right icon-caret-down').parent().parent().next().slideToggle('fast')"><i class="icon-caret-down"></i> <span data-bind="text: $root.languagePackDisplay($data)"></span></a></strong>
|
||||
<div class="packs">
|
||||
<!-- ko foreach: $data.packs -->
|
||||
<div class="row-fluid">
|
||||
<div class="span8 offset1">
|
||||
<strong data-bind="text: display"></strong><br />
|
||||
<small data-bind="visible: pack.last_update" class="muted">{{ _('Last update:') }} <span data-bind="text: formatDate($data.pack.last_update)"></span></small>
|
||||
</div>
|
||||
<div class="span3">
|
||||
<button class="btn btn-block btn-small" data-bind="click: function() {$root.deleteLanguagePack($data.pack.locale, $data.identifier)}"><i class="icon-trash"></i> {{ _('Delete') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<form class="form-inline">
|
||||
<div class="control-group row-fluid" data-bind="css: {error: invalidTranslationArchive}">
|
||||
<div class="input-prepend span9">
|
||||
<span class="btn fileinput-button">
|
||||
<span>{{ _('Browse...') }}</span>
|
||||
<input id="settings_appearance_managelanguagesdialog_upload" type="file" name="file" data-url="{{ url_for("api.uploadLanguagePack") }}">
|
||||
</span>
|
||||
<span class="add-on add-on-limited text-left" data-bind="text: translationUploadFilename, attr: {title: translationUploadFilename}"></span>
|
||||
</div>
|
||||
<button id="settings_appearance_managelanguagesdialog_upload_start" class="btn btn-primary span3" data-bind="enable: enableTranslationUpload, css: {disabled: !enableTranslationUpload()}, click: function(){}">{{ _('Upload') }}</button>
|
||||
</div>
|
||||
<span class="help-block" data-bind="visible: invalidTranslationArchive">{{ _('This does not look like a valid language pack. Valid language packs should be either zip files or tarballs and have the extension ".zip", ".tar.gz", ".tgz" or ".tar"') }}</span>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" data-dismiss="modal" aria-hidden="true">{{ _('Close') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue