:-3px;left:0}.simditor .simditor-toolbar .toolbar-menu ul{min-width:160px;list-style:none;margin:0;padding:10px 1px}.simditor .simditor-toolbar .toolbar-menu ul>li .menu-item{display:block;font-size:16px;line-height:2em;padding:0 10px;text-decoration:none;color:#666}.simditor .simditor-toolbar .toolbar-menu ul>li .menu-item:hover{background:#f6f6f6}.simditor .simditor-toolbar .toolbar-menu ul>li .menu-item.menu-item-h1{font-size:24px;color:#333}.simditor .simditor-toolbar .toolbar-menu ul>li .menu-item.menu-item-h2{font-size:22px;color:#333}.simditor .simditor-toolbar .toolbar-menu ul>li .menu-item.menu-item-h3{font-size:20px;color:#333}.simditor .simditor-toolbar .toolbar-menu ul>li .menu-item.menu-item-h4{font-size:18px;color:#333}.simditor .simditor-toolbar .toolbar-menu ul>li .menu-item.menu-item-h5{font-size:16px;color:#333}.simditor .simditor-toolbar .toolbar-menu ul>li .separator{display:block;border-top:1px solid #ccc;height:0;line-height:0;font-size:0;margin:6px 0}.simditor .simditor-toolbar .toolbar-menu.toolbar-menu-color{width:96px}.simditor .simditor-toolbar .toolbar-menu.toolbar-menu-color .color-list{height:40px;margin:10px 6px 6px 10px;padding:0;min-width:0}.simditor .simditor-toolbar .toolbar-menu.toolbar-menu-color .color-list li{float:left;margin:0 4px 4px 0}.simditor .simditor-toolbar .toolbar-menu.toolbar-menu-color .color-list li .font-color{display:block;width:16px;height:16px;background:#dfdfdf;border-radius:2px}.simditor .simditor-toolbar .toolbar-menu.toolbar-menu-color .color-list li .font-color:hover{opacity:.8}.simditor .simditor-toolbar .toolbar-menu.toolbar-menu-color .color-list li .font-color.font-color-default{background:#333}.simditor .simditor-toolbar .toolbar-menu.toolbar-menu-color .color-list li .font-color-1{background:#e33737}.simditor .simditor-toolbar .toolbar-menu.toolbar-menu-color .color-list li .font-color-2{background:#e28b41}.simditor .simditor-toolbar .toolbar-menu.toolbar-menu-color .color-list li .font-color-3{background:#c8a732}.simditor .simditor-toolbar .toolbar-menu.toolbar-menu-color .color-list li .font-color-4{background:#209361}.simditor .simditor-toolbar .toolbar-menu.toolbar-menu-color .color-list li .font-color-5{background:#418caf}.simditor .simditor-toolbar .toolbar-menu.toolbar-menu-color .color-list li .font-color-6{background:#aa8773}.simditor .simditor-toolbar .toolbar-menu.toolbar-menu-color .color-list li .font-color-7{background:#999}.simditor .simditor-toolbar .toolbar-menu.toolbar-menu-table .menu-create-table{background:#fff;padding:1px}.simditor .simditor-toolbar .toolbar-menu.toolbar-menu-table .menu-create-table table{border:0;border-collapse:collapse;border-spacing:0;table-layout:fixed}.simditor .simditor-toolbar .toolbar-menu.toolbar-menu-table .menu-create-table table td{padding:0;cursor:pointer} 7
+.simditor .simditor-toolbar .toolbar-menu.toolbar-menu-table .menu-create-table table td:before{width:16px;height:16px;border:1px solid #fff;background:#f3f3f3;display:block;content:""}.simditor .simditor-toolbar .toolbar-menu.toolbar-menu-table .menu-create-table table td.selected:before{background:#cfcfcf}.simditor .simditor-toolbar .toolbar-menu.toolbar-menu-table .menu-edit-table{display:none}.simditor .simditor-toolbar .toolbar-menu.toolbar-menu-table .menu-edit-table ul li{white-space:nowrap}.simditor .simditor-toolbar .toolbar-menu.toolbar-menu-image .menu-item-upload-image{position:relative;overflow:hidden}.simditor .simditor-toolbar .toolbar-menu.toolbar-menu-image .menu-item-upload-image input[type=file]{position:absolute;right:0;top:0;opacity:0;font-size:100px;cursor:pointer}.simditor .simditor-toolbar .toolbar-menu.toolbar-menu-alignment{width:100%}.simditor .simditor-toolbar .toolbar-menu.toolbar-menu-alignment ul{min-width:100%}.simditor .simditor-toolbar .toolbar-menu.toolbar-menu-alignment .menu-item{text-align:center}.simditor .simditor-popover{display:none;padding:5px 8px 0;background:#fff;box-shadow:0 1px 4px rgba(0,0,0,0.4);border-radius:2px;position:absolute;z-index:2}.simditor .simditor-popover .settings-field{margin:0 0 5px 0;font-size:12px;height:25px;line-height:25px}.simditor .simditor-popover .settings-field label{display:inline-block;margin:0 5px 0 0}.simditor .simditor-popover .settings-field input[type=text]{display:inline-block;width:200px;box-sizing:border-box;font-size:12px}.simditor .simditor-popover .settings-field input[type=text].image-size{width:83px}.simditor .simditor-popover .settings-field .times{display:inline-block;width:26px;font-size:12px;text-align:center}.simditor .simditor-popover.link-popover .btn-unlink,.simditor .simditor-popover.image-popover .btn-upload,.simditor .simditor-popover.image-popover .btn-restore{display:inline-block;margin:0 0 0 5px;color:#333;font-size:14px;outline:0}.simditor .simditor-popover.link-popover .btn-unlink span,.simditor .simditor-popover.image-popover .btn-upload span,.simditor .simditor-popover.image-popover .btn-restore span{opacity:.6}.simditor .simditor-popover.link-popover .btn-unlink:hover span,.simditor .simditor-popover.image-popover .btn-upload:hover span,.simditor .simditor-popover.image-popover .btn-restore:hover span{opacity:1}.simditor .simditor-popover.image-popover .btn-upload{position:relative;display:inline-block;overflow:hidden;vertical-align:middle}.simditor .simditor-popover.image-popover .btn-upload input[type=file]{position:absolute;right:0;top:0;opacity:0;height:100%;width:28px}.simditor.simditor-mobile .simditor-wrapper.toolbar-floating .simditor-toolbar{position:absolute;top:0;z-index:10;box-shadow:0 0 6px rgba(0,0,0,0.1)}.simditor .simditor-body,.editor-style{font-size:16px;font-family:arial,sans-serif;line-height:1.6;color:#333;outline:0;word-wrap:break-word}.simditor .simditor-body>:first-child,.editor-style>:first-child{margin-top:0!important}.simditor .simditor-body a,.editor-style a{color:#4298ba;text-decoration:none;word-break:break-all}.simditor .simditor-body a:visited,.editor-style a:visited{color:#4298ba}.simditor .simditor-body a:hover,.editor-style a:hover{color:#0f769f}.simditor .simditor-body a:active,.editor-style a:active{color:#9e792e}.simditor .simditor-body a:hover,.simditor .simditor-body a:active,.editor-style a:hover,.editor-style a:active{outline:0}.simditor .simditor-body h1,.simditor .simditor-body h2,.simditor .simditor-body h3,.simditor .simditor-body h4,.simditor .simditor-body h5,.simditor .simditor-body h6,.editor-style h1,.editor-style h2,.editor-style h3,.editor-style h4,.editor-style h5,.editor-style h6{font-weight:normal;margin:40px 0 20px;color:#000}.simditor .simditor-body h1,.editor-style h1{font-size:24px}.simditor .simditor-body h2,.editor-style h2{font-size:22px}.simditor .simditor-body h3,.editor-style h3{font-size:20px}.simditor .simditor-body h4,.editor-style h4{font-size:18px}.simditor .simditor-body h5,.editor-style h5{font-size:16px}.simditor .simditor-body h6,.editor-style h6{font-size:16px}.simditor .simditor-body p,.simditor .simditor-body div,.editor-style p,.editor-style div{word-wrap:break-word;margin:0 0 15px 0;color:#333;word-wrap:break-word}.simditor .simditor-body b,.simditor .simditor-body strong,.editor-style b,.editor-style strong{font-weight:bold}.simditor .simditor-body i,.simditor .simditor-body em,.editor-style i,.editor-style em{font-style:italic}.simditor .simditor-body u,.editor-style u{text-decoration:underline}.simditor .simditor-body strike,.simditor .simditor-body del,.editor-style strike,.editor-style del{text-decoration:line-through}.simditor .simditor-body ul,.simditor .simditor-body ol,.editor-style ul,.editor-style ol{list-style:disc outside none;margin:15px 0;padding:0 0 0 40px;line-height:1.6}.simditor .simditor-body ul ul,.simditor .simditor-body ul ol,.simditor .simditor-body ol ul,.simditor .simditor-body ol ol,.editor-style ul ul,.editor-style ul ol,.editor-style ol ul,.editor-style ol ol{padding-left:30px}
8
+.simditor .simditor-body ul ul,.simditor .simditor-body ol ul,.editor-style ul ul,.editor-style ol ul{list-style:circle outside none}.simditor .simditor-body ul ul ul,.simditor .simditor-body ol ul ul,.editor-style ul ul ul,.editor-style ol ul ul{list-style:square outside none}.simditor .simditor-body ol,.editor-style ol{list-style:decimal}.simditor .simditor-body blockquote,.editor-style blockquote{border-left:6px solid #ddd;padding:5px 0 5px 10px;margin:15px 0 15px 15px}.simditor .simditor-body blockquote>:first-child,.editor-style blockquote>:first-child{margin-top:0}.simditor .simditor-body code,.editor-style code{display:inline-block;padding:0 4px;margin:0 5px;background:#eee;border-radius:3px;font-size:13px;font-family:'monaco','Consolas',"Liberation Mono",Courier,monospace}.simditor .simditor-body pre,.editor-style pre{padding:10px 5px 10px 10px;margin:15px 0;display:block;line-height:18px;background:#f0f0f0;border-radius:3px;font-size:13px;font-family:'monaco','Consolas',"Liberation Mono",Courier,monospace;white-space:pre;word-wrap:normal;overflow-x:auto}.simditor .simditor-body pre code,.editor-style pre code{display:block;padding:0;margin:0;background:0;border-radius:0}.simditor .simditor-body hr,.editor-style hr{display:block;height:0;border:0;border-top:1px solid #ccc;margin:15px 0;padding:0}.simditor .simditor-body table,.editor-style table{width:100%;table-layout:fixed;border-collapse:collapse;border-spacing:0;margin:15px 0}.simditor .simditor-body table thead,.editor-style table thead{background-color:#f9f9f9}.simditor .simditor-body table td,.simditor .simditor-body table th,.editor-style table td,.editor-style table th{min-width:40px;height:30px;border:1px solid #ccc;vertical-align:top;padding:2px 4px;text-align:left;box-sizing:border-box}.simditor .simditor-body table td.active,.simditor .simditor-body table th.active,.editor-style table td.active,.editor-style table th.active{background-color:#ffe}.simditor .simditor-body img,.editor-style img{margin:0 5px;vertical-align:middle}

+ 4 - 0
simditor/static/simditor/styles/simditor.scss

@@ -0,0 +1,4 @@
1
+@charset "UTF-8";
2
+
3
+@import 'fonticon';
4
+@import 'editor';

+ 3 - 0
simditor/templates/simditor/widget.html

@@ -0,0 +1,3 @@
1
+<div class="django-simditor-widget" data-field-id="{{id}}" style="display: inline-block;">
2
+    <textarea{{ final_attrs|safe }} data-processed="0" data-config='{{config|safe}}' data-id="{{id}}" data-type="simditortype">{{ value }}</textarea>
3
+</div>

+ 39 - 0
simditor/urls.py

@@ -0,0 +1,39 @@
1
+"""simditor urls."""
2
+from __future__ import absolute_import
3
+
4
+import django
5
+
6
+from django.conf import settings
7
+from django.conf.urls import url, static
8
+from django.contrib.admin.views.decorators import staff_member_required
9
+
10
+from . import views
11
+
12
+if django.VERSION >= (2, 0):
13
+    # pylint disable=C0103
14
+    from django.urls import path
15
+    urlpatterns = [
16
+        path('upload/', staff_member_required(views.UPLOAD),
17
+             name='simditor_upload'),
18
+    ]
19
+elif django.VERSION >= (1, 8):
20
+    # pylint disable=C0103
21
+    urlpatterns = [
22
+        url(r'^upload/', staff_member_required(views.UPLOAD),
23
+            name='simditor_upload'),
24
+    ]
25
+else:
26
+    from django.conf.urls import patterns    # pylint disable=C0411
27
+
28
+    # pylint disable=C0103
29
+    urlpatterns = patterns(
30
+        '',
31
+        url(r'^upload/', staff_member_required(views.UPLOAD),
32
+            name='simditor_upload'),
33
+    )
34
+
35
+if settings.DEBUG:
36
+    urlpatterns += static.static(settings.MEDIA_URL,
37
+                                 document_root=settings.MEDIA_ROOT)
38
+    urlpatterns += static.static(settings.STATIC_URL,
39
+                                 document_root=settings.STATIC_ROOT)

+ 46 - 0
simditor/utils.py

@@ -0,0 +1,46 @@
1
+"""simditor utils."""
2
+from __future__ import absolute_import
3
+
4
+import os.path
5
+import random
6
+
7
+import string
8
+
9
+from django.core.files.storage import default_storage
10
+from django.template.defaultfilters import slugify
11
+
12
+
13
+class NotAnImageException(Exception):
14
+    pass
15
+
16
+
17
+def get_random_string():
18
+    """Get random string."""
19
+    return ''.join(random.sample(string.ascii_lowercase * 6, 6))
20
+
21
+
22
+def get_slugified_name(filename):
23
+    """get_slugified_name."""
24
+    slugified = slugify(filename)
25
+    return slugified or get_random_string()
26
+
27
+
28
+def slugify_filename(filename):
29
+    """ Slugify filename """
30
+    name, ext = os.path.splitext(filename)
31
+    slugified = get_slugified_name(name)
32
+    return slugified + ext
33
+
34
+
35
+def get_media_url(path):
36
+    """
37
+    Determine system file's media URL.
38
+    """
39
+    return default_storage.url(path)
40
+
41
+
42
+def is_valid_image_extension(file_path):
43
+    """is_valid_image_extension."""
44
+    valid_extensions = ['.jpeg', '.jpg', '.gif', '.png']
45
+    _, extension = os.path.splitext(file_path)
46
+    return extension.lower() in valid_extensions

+ 84 - 0
simditor/views.py

@@ -0,0 +1,84 @@
1
+"""simditor views."""
2
+from __future__ import absolute_import
3
+
4
+import os
5
+from datetime import datetime
6
+
7
+from django.conf import settings
8
+from django.core.files.storage import default_storage
9
+
10
+from django.http import JsonResponse
11
+
12
+from django.views import generic
13
+from django.views.decorators.csrf import csrf_exempt
14
+
15
+from . import utils, image_processing
16
+
17
+
18
+def get_upload_filename(upload_name):
19
+    # Generate date based path to put uploaded file.
20
+    date_path = datetime.now().strftime('%Y/%m/%d')
21
+
22
+    # Complete upload path (upload_path + date_path).
23
+    upload_path = os.path.join(settings.SIMDITOR_UPLOAD_PATH, date_path)
24
+
25
+    if getattr(settings, 'SIMDITOR_UPLOAD_SLUGIFY_FILENAME', True):
26
+        upload_name = utils.slugify_filename(upload_name)
27
+
28
+    return default_storage.get_available_name(os.path.join(upload_path, upload_name))
29
+
30
+
31
+def upload_handler(request):
32
+    files = request.FILES
33
+
34
+    upload_config = settings.SIMDITOR_CONFIGS.get(
35
+        'upload', {'fileKey': 'upload'})
36
+    filekey = upload_config.get('fileKey', 'upload')
37
+
38
+    uploaded_file = files.get(filekey)
39
+
40
+    if not uploaded_file:
41
+        retdata = {'file_path': '', 'success': False,
42
+                   'msg': '图片上传失败,无法获取到图片对象!'}
43
+        return JsonResponse(retdata)
44
+
45
+    image_size = upload_config.get('image_size')
46
+    if image_size and uploaded_file.size > image_size:
47
+        retdata = {'file_path': '', 'success': False,
48
+                   'msg': '上传失败,已超出图片最大限制!'}
49
+        return JsonResponse(retdata)
50
+
51
+    backend = image_processing.get_backend()
52
+
53
+    if not getattr(settings, 'SIMDITOR_ALLOW_NONIMAGE_FILES', True):
54
+        try:
55
+            backend.image_verify(uploaded_file)
56
+        except utils.NotAnImageException:
57
+            retdata = {'file_path': '', 'success': False,
58
+                       'msg': '图片格式错误!'}
59
+            return JsonResponse(retdata)
60
+
61
+    filename = get_upload_filename(uploaded_file.name)
62
+    saved_path = default_storage.save(filename, uploaded_file)
63
+
64
+    url = utils.get_media_url(saved_path)
65
+
66
+    is_api = settings.SIMDITOR_CONFIGS.get('is_api', False)
67
+    url = request.META.get('HTTP_ORIGIN') + url if is_api else url
68
+
69
+    retdata = {'file_path': url, 'success': True, 'msg': '上传成功!'}
70
+
71
+    return JsonResponse(retdata)
72
+
73
+
74
+class ImageUploadView(generic.View):
75
+    """ImageUploadView."""
76
+
77
+    http_method_names = ['post']
78
+
79
+    def post(self, request, **kwargs):
80
+        """Post."""
81
+        return upload_handler(request)
82
+
83
+
84
+UPLOAD = csrf_exempt(ImageUploadView.as_view())

+ 170 - 0
simditor/widgets.py

@@ -0,0 +1,170 @@
1
+"""simditor widgets."""
2
+from __future__ import absolute_import
3
+
4
+from django import forms
5
+from django.conf import settings
6
+from django.core.exceptions import ImproperlyConfigured
7
+from django.core.serializers.json import DjangoJSONEncoder
8
+
9
+from django.template.loader import render_to_string
10
+from django.utils.encoding import force_text
11
+from django.utils.safestring import mark_safe
12
+from django.utils.html import conditional_escape
13
+from django.utils.functional import Promise
14
+
15
+try:
16
+    # Django >=2.1
17
+    from django.forms.widgets import get_default_renderer
18
+    IS_NEW_WIDGET = True
19
+except ImportError:
20
+    IS_NEW_WIDGET = False
21
+
22
+try:
23
+    # Django >=1.7
24
+    from django.forms.utils import flatatt
25
+except ImportError:
26
+    # Django <1.7
27
+    from django.forms.util import flatatt        # pylint disable=E0611, E0401
28
+
29
+
30
+class LazyEncoder(DjangoJSONEncoder):
31
+    """LazyEncoder."""
32
+
33
+    # pylint disable=E0202
34
+    def default(self, obj):
35
+        if isinstance(obj, Promise):
36
+            return force_text(obj)
37
+        return super(LazyEncoder, self).default(obj)
38
+
39
+
40
+JSON_ENCODE = LazyEncoder().encode
41
+
42
+
43
+FULL_TOOLBAR = [
44
+    'title', 'bold', 'italic', 'underline', 'strikethrough', 'fontScale',
45
+    'color', '|', 'ol', 'ul', 'blockquote', 'code', 'table', '|', 'link',
46
+    'image', 'hr', '|', 'indent', 'outdent', 'alignment', 'checklist',
47
+    'markdown', 'fullscreen'
48
+]
49
+
50
+DEFAULT_TOOLBAR = [
51
+    'title', 'bold', 'italic', 'underline', 'strikethrough', 'fontScale',
52
+    'color', '|', 'ol', 'ul', 'blockquote', 'table', '|', 'link',
53
+    'image', 'hr', '|', 'indent', 'outdent', 'alignment'
54
+]
55
+
56
+DEFAULT_CONFIG = {
57
+    'toolbar': DEFAULT_TOOLBAR,
58
+    'cleanPaste': True,
59
+    'tabIndent': True,
60
+    'pasteImage': True,
61
+    'upload': {
62
+        'url': '/',
63
+        'fileKey': 'file'
64
+    }
65
+}
66
+
67
+
68
+class SimditorWidget(forms.Textarea):
69
+    """
70
+    Widget providing Simditor for Rich Text Editing.abs
71
+    Supports direct image uploads and embed.
72
+    """
73
+    class Media:
74
+        """Media."""
75
+
76
+        css_list = [
77
+            'simditor/styles/simditor.min.css'
78
+        ]
79
+
80
+        if 'emoji' in settings.SIMDITOR_TOOLBAR:
81
+            css_list.append('simditor/styles/simditor-emoji.css')
82
+
83
+        if 'fullscreen' in settings.SIMDITOR_TOOLBAR:
84
+            css_list.append('simditor/styles/simditor-fullscreen.min.css')
85
+
86
+        if 'checklist' in settings.SIMDITOR_TOOLBAR:
87
+            css_list.append('simditor/styles/simditor-checklist.min.css')
88
+
89
+        if 'markdown' in settings.SIMDITOR_TOOLBAR:
90
+            css_list.append('simditor/styles/simditor-markdown.min.css')
91
+
92
+        css = {'all': tuple(settings.STATIC_URL + url for url in css_list)}
93
+
94
+        jquery_list = ['simditor/scripts/jquery.min.js',
95
+                       'simditor/scripts/module.min.js',
96
+                       'simditor/scripts/hotkeys.min.js',
97
+                       'simditor/scripts/uploader.min.js',
98
+                       'simditor/scripts/simditor.min.js']
99
+
100
+        if 'fullscreen' in settings.SIMDITOR_TOOLBAR:
101
+            jquery_list.append('simditor/scripts/simditor-fullscreen.min.js')
102
+
103
+        if 'checklist' in settings.SIMDITOR_TOOLBAR:
104
+            jquery_list.append('simditor/scripts/simditor-checklist.min.js')
105
+
106
+        if 'markdown' in settings.SIMDITOR_TOOLBAR:
107
+            jquery_list.append('simditor/scripts/marked.min.js')
108
+            jquery_list.append('simditor/scripts/to-markdown.min.js')
109
+            jquery_list.append('simditor/scripts/simditor-markdown.min.js')
110
+
111
+        if 'image' in settings.SIMDITOR_TOOLBAR:
112
+            jquery_list.append('simditor/scripts/simditor-dropzone.min.js')
113
+
114
+        if 'emoji' in settings.SIMDITOR_TOOLBAR:
115
+            jquery_list.append('simditor/scripts/simditor-emoji.js')
116
+
117
+        js = tuple(settings.STATIC_URL + url for url in jquery_list)
118
+
119
+        try:
120
+
121
+            js += (settings.STATIC_URL + 'simditor/simditor-init.js',)
122
+        except AttributeError:
123
+            raise ImproperlyConfigured("django-simditor requires \
124
+                     SIMDITOR_MEDIA_PREFIX setting. This setting specifies a \
125
+                    URL prefix to the ckeditor JS and CSS media (not \
126
+                    uploaded media). Make sure to use a trailing slash: \
127
+                    SIMDITOR_MEDIA_PREFIX = '/media/simditor/'")
128
+
129
+    def __init__(self, *args, **kwargs):
130
+        super(SimditorWidget, self).__init__(*args, **kwargs)
131
+        # Setup config from defaults.
132
+        self.config = DEFAULT_CONFIG.copy()
133
+
134
+        # Try to get valid config from settings.
135
+        configs = getattr(settings, 'SIMDITOR_CONFIGS', None)
136
+        if configs:
137
+            if isinstance(configs, dict):
138
+                self.config.update(configs)
139
+            else:
140
+                raise ImproperlyConfigured(
141
+                    'SIMDITOR_CONFIGS setting must be a dictionary type.')
142
+
143
+    def build_attrs(self, base_attrs, extra_attrs=None, **kwargs):
144
+        """
145
+        Helper function for building an attribute dictionary.
146
+        This is combination of the same method from Django<=1.10 and Django1.11
147
+        """
148
+        attrs = dict(base_attrs, **kwargs)
149
+        if extra_attrs:
150
+            attrs.update(extra_attrs)
151
+        return attrs
152
+
153
+    def render(self, name, value, attrs=None, renderer=None):
154
+        if value is None:
155
+            value = ''
156
+        final_attrs = self.build_attrs(self.attrs, attrs, name=name)
157
+
158
+        params = ('simditor/widget.html', {
159
+            'final_attrs': flatatt(final_attrs),
160
+            'value': conditional_escape(force_text(value)),
161
+            'id': final_attrs['id'],
162
+            'config': JSON_ENCODE(self.config)
163
+        })
164
+
165
+        if renderer is None and IS_NEW_WIDGET:
166
+            renderer = get_default_renderer()
167
+
168
+        data = renderer.render(*params) if IS_NEW_WIDGET else render_to_string(*params)
169
+
170
+        return mark_safe(data)

Pierakstīties - Gogs: Go Git Service

Pierakstīties

Aizmirsi paroli?
kodo - Gogs: Go Git Service

説明なし

Brightcells: 212e3994d1 MP verify file 8 年 前
..
JQ8Eskhrjp.txt 25cce5e3b1 Add qrcodejump verify file 9 年 前
MP_verify_vEq7hPudfncXpZ57.txt 212e3994d1 MP verify file 8 年 前