1# Copyright (c) 2008 Joost Cassee 2# Licensed under the terms of the MIT License (see LICENSE.txt) 3 4""" 5This TinyMCE widget was copied and extended from this code by John D'Agostino: 6http://code.djangoproject.com/wiki/CustomWidgetsTinyMCE 7""" 8from collections import OrderedDict 9import json 10from pathlib import Path 11import warnings 12 13from django import forms 14from django.conf import settings 15from django.contrib.admin import widgets as admin_widgets 16from django.forms.utils import flatatt 17from django.urls import reverse 18from django.utils.html import escape 19from django.utils.safestring import mark_safe 20from django.utils.translation import get_language, gettext as _, to_locale 21 22import tinymce.settings 23 24 25class TinyMCE(forms.Textarea): 26 """ 27 TinyMCE widget. Set settings.TINYMCE_JS_URL to set the location of the 28 javascript file. Default is "STATIC_URL + 'tinymce/tinymce.min.js'". 29 You can customize the configuration with the mce_attrs argument to the 30 constructor. 31 32 In addition to the standard configuration you can set the 33 'content_language' parameter. It takes the value of the 'language' 34 parameter by default. 35 36 In addition to the default settings from settings.TINYMCE_DEFAULT_CONFIG, 37 this widget sets the 'language', 'directionality' and 38 'spellchecker_languages' parameters by default. The first is derived from 39 the current Django language, the others from the 'content_language' 40 parameter. 41 """ 42 43 def __init__(self, content_language=None, attrs=None, mce_attrs=None): 44 super().__init__(attrs) 45 mce_attrs = mce_attrs or {} 46 self.mce_attrs = mce_attrs 47 if "mode" not in self.mce_attrs: 48 self.mce_attrs["mode"] = "exact" 49 self.mce_attrs["strict_loading_mode"] = 1 50 self.content_language = content_language 51 52 def use_required_attribute(self, *args): 53 # The html required attribute may disturb client-side browser validation. 54 return False 55 56 def get_mce_config(self, attrs): 57 mce_config = tinymce.settings.DEFAULT_CONFIG.copy() 58 if "language" not in mce_config: 59 mce_config["language"] = get_language_from_django() 60 if mce_config["language"] == "en_US": 61 del mce_config["language"] 62 else: 63 mce_config["language"] = match_language_with_tinymce(mce_config["language"]) 64 mce_config.update( 65 get_language_config(self.content_language or mce_config.get("language", "en_US")) 66 ) 67 if tinymce.settings.USE_FILEBROWSER: 68 mce_config["file_picker_callback"] = "djangoFileBrowser" 69 mce_config.update(self.mce_attrs) 70 if mce_config["mode"] == "exact": 71 mce_config["elements"] = attrs["id"] 72 return mce_config 73 74 def render(self, name, value, attrs=None, renderer=None): 75 if value is None: 76 value = "" 77 final_attrs = self.build_attrs(self.attrs, attrs) 78 final_attrs["name"] = name 79 if final_attrs.get("class", None) is None: 80 final_attrs["class"] = "tinymce" 81 else: 82 final_attrs["class"] = " ".join(final_attrs["class"].split(" ") + ["tinymce"]) 83 assert "id" in final_attrs, "TinyMCE widget attributes must contain 'id'" 84 mce_config = self.get_mce_config(final_attrs) 85 mce_json = json.dumps(mce_config) 86 if tinymce.settings.USE_COMPRESSOR: 87 compressor_config = { 88 "plugins": mce_config.get("plugins", ""), 89 "themes": mce_config.get("theme", "advanced"), 90 "languages": mce_config.get("language", ""), 91 "diskcache": True, 92 "debug": False, 93 } 94 final_attrs["data-mce-gz-conf"] = json.dumps(compressor_config) 95 final_attrs["data-mce-conf"] = mce_json 96 html = [f"<textarea{flatatt(final_attrs)}>{escape(value)}</textarea>"] 97 return mark_safe("\n".join(html)) 98 99 def _media(self): 100 css = None 101 if tinymce.settings.USE_COMPRESSOR: 102 js = [reverse("tinymce-compressor")] 103 else: 104 js = [tinymce.settings.JS_URL] 105 if tinymce.settings.USE_FILEBROWSER: 106 js.append(reverse("tinymce-filebrowser")) 107 if tinymce.settings.USE_EXTRA_MEDIA: 108 if "js" in tinymce.settings.USE_EXTRA_MEDIA: 109 js += tinymce.settings.USE_EXTRA_MEDIA["js"] 110 111 if "css" in tinymce.settings.USE_EXTRA_MEDIA: 112 css = tinymce.settings.USE_EXTRA_MEDIA["css"] 113 js.append("django_tinymce/init_tinymce.js") 114 return forms.Media(css=css, js=js) 115 116 media = property(_media) 117 118 119class AdminTinyMCE(TinyMCE, admin_widgets.AdminTextareaWidget): 120 pass 121 122 123def get_language_from_django(): 124 language = get_language() 125 language = to_locale(language) if language is not None else "en_US" 126 return language 127 128 129def match_language_with_tinymce(lang): 130 """ 131 Language codes in TinyMCE are inconsistent. E.g. Hebrew is he_IL.js, while 132 Danish is da.js. So we apply some heuristic to find a language code 133 with an existing TinyMCE translation file. 134 """ 135 if lang.startswith("en"): 136 return lang 137 # Read tinymce langs from tinymce/static/tinymce/langs/ 138 tiny_lang_dir = Path(__file__).parent / "static" / "tinymce" / "langs" 139 tiny_langs = [file_.stem for file_ in tiny_lang_dir.iterdir() if file_.suffix == ".js"] 140 if lang in tiny_langs: 141 return lang 142 if lang[:2] in tiny_langs: 143 return lang[:2] 144 two_letter_map = {lg[:2]: lg for lg in tiny_langs} 145 if lang[:2] in two_letter_map: 146 return two_letter_map[lang[:2]] 147 warnings.warn(f"No TinyMCE language found for '{lang}', defaulting to 'en_US'", RuntimeWarning) 148 return "en_US" 149 150 151def get_language_config(content_language): 152 content_language = content_language[:2] 153 154 config = {} 155 lang_names = OrderedDict() 156 for lang, name in settings.LANGUAGES: 157 if lang[:2] not in lang_names: 158 lang_names[lang[:2]] = [] 159 lang_names[lang[:2]].append(_(name)) 160 sp_langs = [] 161 for lang, names in lang_names.items(): 162 if lang == content_language: 163 default = "+" 164 else: 165 default = "" 166 sp_langs.append(f'{default}{" / ".join(names)}={lang}') 167 168 config["spellchecker_languages"] = ",".join(sp_langs) 169 170 if content_language in settings.LANGUAGES_BIDI: 171 config["directionality"] = "rtl" 172 else: 173 config["directionality"] = "ltr" 174 175 if tinymce.settings.USE_SPELLCHECKER: 176 config["spellchecker_rpc_url"] = reverse("tinymce-spellcheck") 177 178 return config 179