├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── changes2.0.md ├── frontend ├── babel.config.json ├── images │ └── icons.svg ├── package.json ├── src │ ├── admin_popup_response.js │ ├── components │ │ ├── AbstractBlock.vue │ │ ├── AddBlockHere.vue │ │ ├── App.vue │ │ ├── BlockHeader.vue │ │ ├── BlockList.vue │ │ ├── BlockOptions.vue │ │ └── StreamBlock.vue │ ├── streamfield_widget.js │ ├── style.sass │ └── utils.js └── webpack.config.js ├── setup.py ├── streamfield ├── __init__.py ├── admin.py ├── apps.py ├── base.py ├── fields.py ├── forms.py ├── locale │ ├── en │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fr │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── it │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── ru │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── models.py ├── settings.py ├── static │ └── streamfield │ │ ├── admin_popup_response.js │ │ ├── streamfield_widget.css │ │ └── streamfield_widget.js ├── templates │ └── streamfield │ │ ├── admin │ │ ├── abstract_block_template.html │ │ ├── change_form.html │ │ ├── change_form_render_template.html │ │ ├── fields │ │ │ ├── default.html │ │ │ ├── file_browse_widget.html │ │ │ ├── select.html │ │ │ └── stream_field_widget.html │ │ └── streamfield_popup_response.html │ │ ├── default_block_tmpl.html │ │ ├── streamfield_admin_help.html │ │ ├── streamfield_texts.js │ │ └── streamfield_widget.html ├── templatetags │ ├── __init__.py │ └── streamfield_tags.py ├── tests.py ├── urls.py └── views.py └── test_project ├── manage.py ├── pages ├── __init__.py ├── admin.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── templates │ └── pages │ │ ├── index.html │ │ └── page_detail.html ├── tests.py └── views.py ├── streamblocks ├── __init__.py ├── admin.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── templates │ └── streamblocks │ │ ├── admin │ │ ├── fields │ │ │ └── textarea.html │ │ └── richtext.html │ │ ├── column.html │ │ └── richtext.html ├── tests.py └── views.py └── test_project ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .DS_Store 3 | build/* 4 | dist/* 5 | django_streamfield.egg-info/* 6 | test_project/db.sqlite3 7 | package-lock.json 8 | frontend/node_modules/* 9 | streamfield/static/streamfield/index.html 10 | streamfield/static/streamfield/streamfield_widget.js.LICENSE.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | django-streamfield 2 | ---------------- 3 | 4 | Copyright (c) 2019 Lapshinov Yury (y.raagin@gmail.com) 5 | 6 | Redistribution and use in source and binary forms, with or without modification, 7 | are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, 10 | this list of conditions and the following disclaimer. 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation and/or 13 | other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS 16 | OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 17 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 18 | THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 19 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 20 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | recursive-include streamfield/static * 4 | recursive-include streamfield/templates * 5 | recursive-include streamfield/locale * -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django StreamField 2 | 3 | This is a simple realisation of StreamField's idea of Wagtail CMS for plain Django admin or with Grappelli skin. 4 | Stable version: 2.4.0 5 | Django <= 5.2 6 | 7 | [Major changes (1.4.5 > 2)](changes2.0.md) 8 | 9 | ## Highlights 10 | You can build your page with different kind of blocks. 11 | Sort them and sort the lists inside the blocks. 12 | 13 | **The blocks here are regular instances of Django models.** For editing content inside the blocks, it use native popup mechanism in Django admin interface. 14 | This allow you to use other field's widgets inside the blocks as is. 15 | For example, if you want to use in your blocks FileBrowseField 16 | from django-filebrowser, it will perfectly working 17 | without any additional settings. 18 | 19 | Module also working with [Grappelli Interface](https://github.com/sehmaschine/django-grappelli) (Optional) 20 | 21 | ![django-streamfield demo screenshot](https://raagin.ru/media/uploads/django-streamfield-2.jpg) 22 | 23 | ## Contents 24 | 25 | - [Installation](#installation) 26 | - [How to use](#how-to-use) 27 | - [Admin](#admin) 28 | - [Custom admin class for block's models](#custom-admin-class-for-blocks-models) 29 | - [Custom templates for render block models in admin](#custom-templates-for-render-block-models-in-admin) 30 | - [Override how to render block's fields in admin](#override-how-to-render-blocks-fields-in-admin) 31 | - [Override list of blocks for your StreamField in admin.py](#override-list-of-blocks-for-your-streamfield-in-adminpy) 32 | - [Block options](#block-options) 33 | - [Special cases](#special-cases) 34 | - [Complex Blocks](#complex-blocks) 35 | - [Blocks without data in database. Only templates](#blocks-without-data-in-database-only-templates) 36 | - [Add extra context to blocks](#add-extra-context-to-blocks) 37 | - [Get field data as list](#get-field-data-as-list) 38 | - [Cache for reduce the number of database requests](#cache-for-reduce-the-number-of-database-requests) 39 | - [Create a copy](#create-a-copy) 40 | - [Add block programarly](#add-block-programarly) 41 | - [Set size of block's popup window](#set-size-of-blocks-popup-window) 42 | - [Settings](#settings) 43 | - [Migrations](#migrations) 44 | 45 | ## Installation 46 | 47 | Requirements: `django>=3.1` 48 | 49 | `pip install django-streamfield` 50 | 51 | ## How to use 52 | - Create streamblocks app with your models 53 | - Add streamfield and streamblocks to INSTALLED_APPS 54 | - Add streamfield.urls 55 | - Create templates for streamblocks 56 | - Add StreamField to your model 57 | - Use it in templates 58 | 59 | **1. Create new app called `streamblocks` and put there some models** 60 | 61 | ...that you want to use as blocks in your StreamField. 62 | Add them to the list `STREAMBLOCKS_MODELS`. 63 | For example: 64 | 65 | ```python 66 | # streamblocks/models.py 67 | 68 | # one object 69 | class RichText(models.Model): 70 | text = models.TextField(blank=True, null=True) 71 | 72 | def __str__(self): 73 | # This text will be added to block title name. 74 | # For better navigation when block is collapsed. 75 | return self.text[:30] 76 | 77 | class Meta: 78 | # This will use as name of block in admin 79 | # See also STREAMFIELD_BLOCK_TITLE in settings 80 | verbose_name="Text" 81 | 82 | # list of objects 83 | class ImageWithText(models.Model): 84 | image = models.ImageField(upload_to="folder/") 85 | text = models.TextField(null=True, blank=True) 86 | 87 | # StreamField option for list of objects 88 | as_list = True 89 | 90 | def __str__(self): 91 | # This text will be added to block title name. 92 | # For better navigation when block is collapsed. 93 | return self.text[:30] 94 | 95 | class Meta: 96 | verbose_name="Image with text" 97 | verbose_name_plural="Images with text" 98 | 99 | # Register blocks for StreamField as list of models 100 | STREAMBLOCKS_MODELS = [ 101 | RichText, 102 | ImageWithText 103 | ] 104 | ``` 105 | > Important!: Don't use 'as_list', 'options', 'extra_options' as models field names, because they are used by streamfield. 106 | 107 | **2. Add apps to settings.py and make migrations** 108 | 109 | Add to INSTALLED_APPS 110 | 111 | ```python 112 | INSTALLED_APPS = [ 113 | ... 114 | 'streamblocks', 115 | 'streamfield', 116 | ... 117 | ``` 118 | Run `python manage.py makemigrations` and `python manage.py migrate` 119 | 120 | **3. Add streamfield.urls to main urls.py** 121 | ```python 122 | urlpatterns += [ 123 | path('streamfield/', include('streamfield.urls')) 124 | ] 125 | ``` 126 | 127 | **4. Create templates for each block model, named as lowercase names of the models:** 128 | 129 | 1. streamblocks/templates/streamblocks/richtext.html 130 | 2. streamblocks/templates/streamblocks/imagewithtext.html 131 | 132 | And use `block_content` as context. 133 | 134 | > Note: block_content will be single object 135 | if no 'as_list' property in your model, 136 | and will be a list of objects if there is. 137 | 138 | ```html 139 | 140 |
141 | {{ block_content.text|safe }} 142 |
143 | ``` 144 | ```html 145 | 146 | 154 | ``` 155 | 156 | > Note: You may use also `block_template` option. For specify a block template file. 157 | 158 | ```python 159 | class RichText(models.Model): 160 | ... 161 | block_template = "streamblocks/richtext.html" 162 | ... 163 | ``` 164 | > Note: If you need unique string in block template, use `block_model` and `block_unique_id` 165 | 166 | *Full list of variables in template context:* 167 | - `block_model` (lowercase of modelname - "richtext") 168 | - `block_unique_id` (unique string) 169 | - `block_content` (block data from db) 170 | - `as_list` (boolean) 171 | - `options` ([block options](#block-options)) 172 | 173 | > Note: For unique identifier inside the lists you may use a combination of `block_unique_id` and `block.id` of subblock. 174 | 175 | **5. Add StreamField to your model in your application** 176 | 177 | And add the models that you want to use in this stream as model_list 178 | ```python 179 | # models.py 180 | from streamfield.fields import StreamField 181 | from streamblocks.models import RichText, ImageWithText 182 | 183 | class Page(models.Model): 184 | stream = StreamField( 185 | model_list=[ 186 | RichText, 187 | ImageWithText 188 | ], 189 | verbose_name="Page blocks" 190 | ) 191 | ``` 192 | **6. Use it in template** 193 | If you have your `page` in context, 194 | you can get content by field's cached property page.stream.render 195 | ```html 196 | ... 197 |
198 | {{ page.stream.render }} 199 |
200 | ... 201 | ``` 202 | 203 | Or, if you need extra context in blocks, you may use template tag: 204 | ```html 205 | {% load streamfield_tags %} 206 | ... 207 |
208 | {% stream_render page.stream request=request %} 209 |
210 | ... 211 | ``` 212 | Third way it's to use list. [See bellow](#get-field-data-as-list) 213 | 214 | 215 | ## Admin 216 | ### Custom admin class for block's models 217 | Models will automaticaly register in admin. 218 | If you want provide custom admin class, 219 | first unregister models and register again, using `StreamBlocksAdmin` class. 220 | 221 | ```python 222 | # streamblocks/admin.py 223 | 224 | from django.contrib import admin 225 | from streamfield.admin import StreamBlocksAdmin 226 | 227 | from streamblocks.models import RichText 228 | 229 | admin.site.unregister(RichText) 230 | @admin.register(RichText) 231 | class RichTextBlockAdmin(StreamBlocksAdmin, admin.ModelAdmin): 232 | pass 233 | ``` 234 | 235 | ### Custom templates for render block models in admin 236 | If you need to customize admin templates for block models wich you are using, you need to put templates named as 237 | described in section 4 (above). but put it inside "admin" folder. 238 | 239 | For example for RichText block it will be: 240 | 241 | `streamblocks/templates/streamblocks/admin/richtext.html` 242 | 243 | As context use "form" and/or "object" (Not working for abstract blocks): 244 | ```html 245 | {{ form.text.value }} 246 | {{ object }} 247 | ``` 248 | 249 | The default admin template is: `streamfield/admin/change_form_render_template.html` 250 | You can extend it 251 | ```html 252 | {% extends "streamfield/admin/change_form_render_template.html" %} 253 | {% block streamblock_form %} 254 | {{ block.super }} 255 | Original object is: {{ object }} 256 | {% endblock streamblock_form %} 257 | ``` 258 | 259 | 260 | You may also specify custom template as option: 261 | ```python 262 | class RichText(models.Model): 263 | ... 264 | custom_admin_template = "streamblocks/admin/richtext.html" 265 | ... 266 | ``` 267 | 268 | ### Override how to render block's fields in admin 269 | Create custom template for field with name as generated by `django.utils.text.camel_case_to_spaces` from field widget name, and put it inside `.../streamblocks/admin/fields/` folder. 270 | 271 | For example for TextField widget (Textarea) of RichText block, it will be: 272 | `streamblocks/templates/streamblocks/admin/fields/textarea.html` 273 | 274 | And `MyCustomWidget`: 275 | `streamblocks/templates/streamblocks/admin/fields/my_custom_widget.html` 276 | 277 | As context use "field": 278 | ```html 279 | {{ field.value|default:""|safe }} 280 | ``` 281 | 282 | ### Override list of blocks for your StreamField in admin.py 283 | Typicaly you set the blocks in your models as `model_list` attribute of StreamField field. 284 | But if you want to change the blocks, for example depending on the object, you can do it in the admin site 285 | of your model. Suppose you want to use only `RichText` on page with id=1. 286 | 287 | ```python 288 | # admin.py 289 | from streamfield.fields import StreamFieldWidget 290 | from streamblocks.models import RichText 291 | from .models import Page 292 | 293 | @admin.register(Page) 294 | class PageAdmin(models.Admin): 295 | 296 | def get_form(self, request, obj=None, **kwargs): 297 | form = super().get_form(request, obj, **kwargs) 298 | if obj and obj.id == 1: 299 | form.base_fields['stream'].widget = StreamFieldWidget(attrs={ 300 | 'model_list': [ RichText ] 301 | }) 302 | return form 303 | ``` 304 | Be careful with already existing blocks in db. If you remove them from admin, it produce error. 305 | 306 | ## Block options 307 | You may use `options` property in your streamblocks models to add some additional options to your block. 308 | This is useful with `as_list` property when you need to add some options to whole block not separatly to each object of this list. 309 | 310 | For example: 311 | ```python 312 | # streamblocks/models.py 313 | 314 | # list of objects as slider 315 | class Slide(models.Model): 316 | image = models.ImageField(upload_to="folder/") 317 | text = models.TextField(null=True, blank=True) 318 | 319 | # StreamField option for list of objects 320 | as_list = True 321 | 322 | options = { 323 | 'autoplay': { 324 | 'label': 'Autoplay slider', 325 | 'type': 'checkbox', 326 | 'default': False 327 | }, 328 | 'width': { 329 | 'label': 'Slider size', 330 | 'type': 'select', 331 | 'default': 'wide', 332 | 'options': [ 333 | {'value': 'wide', 'name': 'Wide slider'}, 334 | {'value': 'narrow', 'name': 'Narrow slider'}, 335 | ] 336 | }, 337 | 'class_name': { 338 | 'label': 'Class Name', 339 | 'type': 'text', 340 | 'default': '' 341 | } 342 | } 343 | 344 | class Meta: 345 | verbose_name="Slide" 346 | verbose_name_plural="Slider" 347 | ``` 348 | In block template you can use this options as `options.autoplay` 349 | In page admin you will see it on the bottom of this block. 350 | > Note: Now only "checkbox", "text" and "select" type is working. 351 | You may apply options for all blocks with `STREAMFIELD_BLOCK_OPTIONS` (See [Settings](#settings)) 352 | 353 | If you want to add block options to options, which was set in django settings, you may use `extra_options`. 354 | ```python 355 | class Slide(models.Model): 356 | ... 357 | extra_options = { 358 | "autoplay": { 359 | "label": "Autoplay", 360 | "type": "checkbox", 361 | "default": False 362 | } 363 | } 364 | ... 365 | ``` 366 | 367 | If you want to switch off options, which set in django settings, for current block. Set `options={}` 368 | 369 | ## Special cases 370 | ### Complex Blocks 371 | You may use StreamField as part of blocks and create with that way complex structure 372 | and use `{{ block_content..render }}` 373 | 374 | ### Blocks without data in database. Only templates. 375 | You may use it for widgets or separators or for whatever you want... 376 | Just make the block model `abstract`. 377 | ```python 378 | class EmptyBlock(models.Model): 379 | class Meta: 380 | abstract = True 381 | verbose_name='Empty space' 382 | ``` 383 | and use `streamblocks/templates/streamblocks/emptyblock.html` for your content. 384 | For admin `streamblocks/templates/streamblocks/admin/emptyblock.html` 385 | > Note: Don't forget to register a block in STREAMBLOCKS_MODELS 386 | 387 | ### Add extra context to blocks 388 | Supose, you need to add some data to blocks from global context. 389 | Instead of using render property in template `{{ page.stream.render }}`, 390 | you need to use template tag `stream_render` from `streamfield_tags` with keywords arguments. 391 | 392 | For example, if you have in page template `request` and `page` objects and want to use it in blocks: 393 | ```html 394 | {% load streamfield_tags %} 395 | ... 396 |
397 | {% stream_render page.stream request=request page=page %} 398 |
399 | ... 400 | ``` 401 | 402 | ### Get field data as list 403 | If you have special case, you can get data as list. 404 | 405 | ```python 406 | # views.py 407 | stream_list = page.stream.as_list() 408 | # You will get list of dictionaries 409 | # print(stream_list) 410 | [{ 411 | 'data': { 412 | 'block_model': '.....', 413 | 'block_unique_id': '....', 414 | 'block_content': [...], 415 | 'as_list': True, 416 | 'options': {} 417 | }, 418 | 'template': '....' 419 | }, 420 | ... 421 | ] 422 | ``` 423 | 424 | ```html 425 | 426 | {% for b in page.stream.as_list %} 427 | {% include b.template with block_content=b.data.block_content %} 428 | {% endfor %} 429 | ``` 430 | 431 | 432 | 433 | ### Cache for reduce the number of database requests 434 | There is two ways of caching: 435 | - Simple cache view with django cache 436 | - Create additional field, for example: 'stream_rendered' 437 | and render to this field html in save method 438 | 439 | ```python 440 | def save(self, *args, **kwargs): 441 | self.stream_rendered = self.stream.render 442 | super().save(*args, **kwargs) 443 | ``` 444 | ...and use this field in your html 445 | 446 | ### Create a copy 447 | StreamObject have a method 'copy', which create a copies of all the instances that used in this field. 448 | For example, if you have object 'page' with the field 'stream', and you need a copy of this object. You can do this: 449 | 450 | ```python 451 | page.pk = None 452 | page.stream = page.stream.copy() 453 | page.save() 454 | ``` 455 | > Note: If you will not use `page.stream.copy()` instances will be the same as in the original object 456 | 457 | ### Add block programarly 458 | ```python 459 | r = RichText(text='

Lorem ipsum

') 460 | im1 = ImageWithText.objects.create(image='...') 461 | im2 = ImageWithText.objects.create(image='...') 462 | page.stream.add(r) 463 | page.stream.add([im1, im2]) 464 | page.save() 465 | ``` 466 | > Note: If you create a new instance of page in shell, before using `add` method, you need to call instance form db. Because field `stream` should be wrapped in StreamObject 467 | ```python 468 | page = Page() 469 | page.save() 470 | page.refresh_from_db() 471 | ``` 472 | 473 | ### Set size of block's popup window 474 | Add `popup_size` attribute to StreamField 475 | ```python 476 | ... 477 | stream = StreamField( 478 | model_list=[...], 479 | popup_size=(1000, 500) # default value. Width: 1000px, Height: 500px 480 | ) 481 | ... 482 | ``` 483 | 484 | 485 | ## Settings 486 | ```python 487 | # settings.py 488 | ``` 489 | 490 | ### STREAMFIELD_SHOW_ADMIN_HELP_TEXT 491 | If you want to show "Help" link in admin. 492 | Set: 493 | ```python 494 | STREAMFIELD_SHOW_ADMIN_HELP_TEXT = True 495 | ``` 496 | 497 | ### STREAMFIELD_ADMIN_HELP_TEXT 498 | You can setup custom help text in settings 499 | ```python 500 | STREAMFIELD_ADMIN_HELP_TEXT = '

Text

' 501 | ``` 502 | 503 | ### STREAMFIELD_DELETE_BLOCKS_FROM_DB 504 | If you want to keep streamblock's instances in db, when you removing it from StreamField. Set: 505 | ```python 506 | STREAMFIELD_DELETE_BLOCKS_FROM_DB = False 507 | ``` 508 | It was default behavior in previous releases. 509 | > Note: If you delete entire object which contain StreamField, streamblock's instances will not be deleted. You should care about it by yourself. 510 | 511 | ### STREAMFIELD_BLOCK_TITLE (> 2.0.1) 512 | The default block name uses the verbose_name from the model. Plus the name for each object is taken from `__str__` method. For "as_list" blocks, from the first block. You can use STREAMFIELD_BLOCK_TITLE to change it to another method or property. If you want disable this, set to False. If some blocks will not have setuped method, they will be ignored. 513 | 514 | ### STREAMFIELD_BLOCK_OPTIONS 515 | 516 | You may use `STREAMFIELD_BLOCK_OPTIONS` in settings.py to add some options to all blocks. 517 | 518 | For example: 519 | ```python 520 | STREAMFIELD_BLOCK_OPTIONS = { 521 | "margins": { 522 | "label": "Margins", 523 | "type": "checkbox", 524 | "default": True 525 | } 526 | } 527 | ``` 528 | In block template use `{{ options.margins }}` 529 | 530 | > Note: Now only "checkbox", "text", and "select" type is working. 531 | 532 | ## Migrations 533 | If you add new options to Block with already existed data, you need to migrate options for adding default values to stored json. 534 | Create empty migration and use `migrate_stream_options` function from `streamfield.base`. 535 | At the moment this only works with unique streamblocks class names. 536 | 537 | Example: 538 | ```python 539 | # migration 540 | from django.db import migrations 541 | from streamfield.base import migrate_stream_options 542 | 543 | def migrate_options(apps, schema_editor): 544 | Page = apps.get_model("main", "Page") 545 | for page in Page.objects.all(): 546 | page.stream = migrate_stream_options(page.stream) 547 | page.save() 548 | 549 | class Migration(migrations.Migration): 550 | 551 | dependencies = [ 552 | '...' 553 | ] 554 | 555 | operations = [ 556 | migrations.RunPython(migrate_options), 557 | ] 558 | ``` 559 | 560 | 561 | -------------------------------------------------------------------------------- /changes2.0.md: -------------------------------------------------------------------------------- 1 | # django-streamfield. Changes 2.0 2 | ## Major changes 3 | 1. Removed string escaping in the database. Now StreamField is stored in the database as a native JSONField, since version 3.1 Django supports JSON in all databases. When resaving the object, escaping in the new version will be automatically removed. 4 | 2. Added new frontend features: You can open/collapse blocks by one (click on the block header) or all together. You can add new block between the others blocks (put cursor between the blocks and wait for plus button). 5 | 3. For better blocks navigation you can add name of the block by using `__str__` method in block definition code. Or you can change the method name in [settings](https://github.com/raagin/django-streamfield#streamfield_block_title-v201) 6 | 4. The collapsed state of the blocks is stored in the database. 7 | 5. For development. Webpack 5 is used to build frontend part. JS scripts is divided into components. Vue updated to version 3. SASS is used for styling. 8 | 6. JS libraries are join to one bundle including streamfield. 9 | 10 | ## Minor changes 11 | 1. StreamBlocksAdminMixin now using for StreamBlocksAdmin class (#21) 12 | 2. Icons changed from png to svg 13 | 3. STREAMFIELD_SHOW_ADMIN_HELP_TEXT bug fixed (#27) 14 | 4. STREAMFIELD_SHOW_ADMIN_HELP_TEXT now is False by default. And you can add your own text by using STREAMFIELD_ADMIN_HELP_TEXT in settings. 15 | 5. Removed STREAMFIELD_SHOW_ADMIN_COLLAPSE from settings. 16 | 6. Fixed migrate_stream_options method. 17 | -------------------------------------------------------------------------------- /frontend/babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false, 7 | "targets": { 8 | "browsers": [ 9 | "> 1%", 10 | "last 2 versions", 11 | "not dead", 12 | "ie > 11" 13 | ] 14 | } 15 | } 16 | ] 17 | ] 18 | } -------------------------------------------------------------------------------- /frontend/images/icons.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "streamfield_widget.js", 3 | "version": "2.4.0", 4 | "description": "", 5 | "private": true, 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "dev": "webpack --mode development --watch", 9 | "build": "webpack --mode production --progress" 10 | }, 11 | "author": "Yury Lapshinov ", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@babel/preset-env": "^7.20.2", 15 | "@vue/compiler-sfc": "^3.2.47", 16 | "axios": "^1.3.4", 17 | "babel-loader": "^9.1.2", 18 | "clean-webpack-plugin": "^4.0.0", 19 | "css-loader": "^6.7.3", 20 | "css-minimizer-webpack-plugin": "^4.2.2", 21 | "file-loader": "^6.2.0", 22 | "html-webpack-plugin": "^5.5.0", 23 | "mini-css-extract-plugin": "^2.7.5", 24 | "sass": "^1.60.0", 25 | "sass-loader": "^13.2.2", 26 | "sortablejs": "^1.15.0", 27 | "style-loader": "^3.3.2", 28 | "terser-webpack-plugin": "^5.3.7", 29 | "vue-loader": "^17.0.1", 30 | "vuedraggable": "^4.1.0", 31 | "webpack": "^5.77.0", 32 | "webpack-cli": "^5.0.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /frontend/src/admin_popup_response.js: -------------------------------------------------------------------------------- 1 | /*global opener */ 2 | (function() { 3 | 'use strict'; 4 | var initData = JSON.parse(document.getElementById('django-admin-popup-response-constants').dataset.popupResponse); 5 | switch(initData.action) { 6 | case 'change': 7 | opener.streamapps[initData.app_id].updateBlock(initData.block_id, initData.instance_id); 8 | window.close(); 9 | break; 10 | case 'delete': 11 | // opener.console.log("delete", initData); 12 | break; 13 | default: 14 | opener.streamapps[initData.app_id].updateBlock(initData.block_id, initData.instance_id); 15 | window.close(); 16 | break; 17 | } 18 | })(); 19 | -------------------------------------------------------------------------------- /frontend/src/components/AbstractBlock.vue: -------------------------------------------------------------------------------- 1 | 32 | -------------------------------------------------------------------------------- /frontend/src/components/AddBlockHere.vue: -------------------------------------------------------------------------------- 1 | 35 | -------------------------------------------------------------------------------- /frontend/src/components/App.vue: -------------------------------------------------------------------------------- 1 | 198 | 230 | -------------------------------------------------------------------------------- /frontend/src/components/BlockHeader.vue: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /frontend/src/components/BlockList.vue: -------------------------------------------------------------------------------- 1 | 18 | 25 | -------------------------------------------------------------------------------- /frontend/src/components/BlockOptions.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /frontend/src/components/StreamBlock.vue: -------------------------------------------------------------------------------- 1 | 83 | -------------------------------------------------------------------------------- /frontend/src/streamfield_widget.js: -------------------------------------------------------------------------------- 1 | import "@/style.sass" 2 | import axios from 'axios' 3 | import { createApp } from 'vue' 4 | import App from '@/components/App.vue' 5 | 6 | (function(){ 7 | function initApps(node) { 8 | let app_nodes = node.querySelectorAll(".streamfield_app:not([id*='__prefix__'])"); 9 | for (let i = 0; i < app_nodes.length; i++) { 10 | let app_node = app_nodes[i]; 11 | let app = createApp(App, {app_node}).mount(app_node.querySelector('.mount-node')); 12 | window.streamapps[app_node.id] = app; 13 | } 14 | } 15 | function onReady() { 16 | window.ax = axios.create({ 17 | headers: {"X-CSRFToken": document.querySelector('[name=csrfmiddlewaretoken]').value} 18 | }); 19 | window.streamapps = {}; 20 | initApps(document) 21 | }; 22 | window.addEventListener('DOMContentLoaded', function(event) { 23 | onReady(); 24 | }); 25 | document.addEventListener("formset:added", function(event) { 26 | initApps(event.target) 27 | }); 28 | })(); -------------------------------------------------------------------------------- /frontend/src/style.sass: -------------------------------------------------------------------------------- 1 | .streamfield_app 2 | max-width: 758px 3 | width: 98% 4 | margin-top: 5px 5 | float: left 6 | 7 | textarea 8 | display: none 9 | 10 | p 11 | margin-bottom: 0.5em 12 | 13 | a 14 | &.grp-change-related, 15 | &.grp-add-another 16 | background-image: none 17 | 18 | .collapse-handlers 19 | text-align: right 20 | margin-bottom: 5px 21 | 22 | .collapse-handler 23 | display: inline-block 24 | margin: 5px 0 25 | font-weight: bold 26 | text-decoration: underline 27 | 28 | .streamfield-models 29 | clear: both 30 | margin-bottom: 10px 31 | 32 | .stream-model-block 33 | position: relative 34 | color: var(--body-fg) 35 | 36 | &__inner 37 | box-shadow: 0 0 5px 2px rgba(0,0,0,0.1) 38 | 39 | &.collapsed 40 | .stream-model-block__content, 41 | .stream-block__options 42 | display: none 43 | 44 | .add_here 45 | min-height: 12px 46 | 47 | &.show_add_block 48 | &:before, 49 | &:after 50 | top: 5px 51 | 52 | .stream-blocks-list 53 | padding: 10px 0 54 | margin: 0 10px 5px 55 | 56 | .stream-blocks-list 57 | padding: 15px 0 10px 58 | margin: 0 10px 10px 59 | position: relative 60 | z-index: 2 61 | 62 | li 63 | list-style: none 64 | 65 | .streamblock__block__title 66 | position: relative 67 | 68 | .streamblock__block__model_title 69 | font-weight: bold 70 | margin-left: 0 71 | font-size: 1.15em 72 | user-select: none 73 | position: relative 74 | 75 | .streamblock__block__subtitle 76 | font-weight: normal 77 | font-size: 1em 78 | opacity: 0.6 79 | position: relative 80 | margin: 0 81 | 82 | &-click 83 | position: absolute 84 | display: block 85 | top: 0 86 | left: 0 87 | right: 0 88 | bottom: 0 89 | cursor: pointer 90 | z-index: 1 91 | 92 | &:hover 93 | background: rgba(255, 255, 255, 0.1) 94 | 95 | .add_here 96 | position: relative 97 | min-height: 20px 98 | width: 100% 99 | cursor: pointer 100 | 101 | &.show, 102 | &.show_add_block 103 | z-index: 10 104 | 105 | &:before 106 | display: block 107 | z-index: 2 108 | 109 | &.show_add_block 110 | &:before 111 | top: 10px 112 | transform: rotate(45deg) 113 | 114 | &:after 115 | content: "" 116 | position: absolute 117 | top: 10px 118 | bottom: 0 119 | left: 0 120 | right: 0 121 | border: 1px dashed var(--border-color) 122 | border-radius: 5px 123 | background: rgba(255,255,255,0.1) 124 | 125 | &:before 126 | content: "" 127 | position: absolute 128 | width: 30px 129 | height: 30px 130 | border-radius: 15px 131 | top: 50% 132 | left: 50% 133 | margin-top: -15px 134 | margin-left: -15px 135 | background: var(--secondary) 136 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16.729' height='16.728'%3E%3Cg fill='none' stroke='%23fff' stroke-linecap='round' stroke-width='2'%3E%3Cpath d='M8.364 2v12.728M14.728 8.364H2'/%3E%3C/g%3E%3C/svg%3E") 137 | background-position: 50% 50% 138 | background-repeat: no-repeat 139 | background-size: 18px 140 | display: none 141 | font-size: 28px 142 | line-height: 30px 143 | color: white 144 | text-align: center 145 | transition: transform .2s ease-out 146 | 147 | &:last-child .add_here 148 | display: none 149 | 150 | &__content 151 | padding: 20px 152 | background: var(--darkened-bg) 153 | 154 | .model-field-content:after 155 | content: "." 156 | display: block 157 | height: 0 158 | clear: both 159 | visibility: hidden 160 | 161 | img 162 | border: 1px solid #b6b6b6 163 | 164 | 165 | &.no-subblocks 166 | padding: 20px 167 | 168 | &.abstract-block 169 | a 170 | img 171 | border: 3px solid #a6d1e0 172 | 173 | &:hover 174 | img 175 | border-color: #78adbf 176 | 177 | .stream-block__options 178 | padding: 7px 179 | text-align: right 180 | background: var(--selected-bg) 181 | 182 | .stream-block__option 183 | display: inline-block 184 | margin: 4px 185 | 186 | span 187 | margin: 0 0 0 5px 188 | 189 | & + * 190 | margin: 0 0 0 3px 191 | 192 | .block-fields 193 | &__label 194 | font-style: italic 195 | color: #999 196 | display: block 197 | 198 | & > div 199 | margin-bottom: 0.4em 200 | 201 | h1 202 | color: var(--body-fg) 203 | 204 | h2 205 | padding: 0 206 | text-shadow: none 207 | border: none 208 | background-image: none 209 | font-size: 1.3em 210 | color: var(--body-fg) 211 | 212 | h3 213 | padding: 0 214 | text-shadow: none 215 | border: none 216 | background-image: none 217 | font-size: 1.1em 218 | margin-bottom: 0.5em 219 | color: var(--body-fg) 220 | 221 | h4 222 | font-weight: bold 223 | color: var(--body-fg) 224 | 225 | blockquote 226 | font-size: 1.1em 227 | font-style: italic 228 | font-weight: bold 229 | margin-bottom: 0.4em 230 | 231 | table td, 232 | table th 233 | border-bottom: 1px solid #CCC 234 | 235 | ul li 236 | margin-left: 20px 237 | list-style: disc 238 | 239 | ol li 240 | margin-left: 20px 241 | list-style: decimal 242 | 243 | b 244 | font-weight: bold 245 | 246 | svg, 247 | img 248 | max-width: 150px 249 | max-height: 150px 250 | 251 | .stream-model-subblock 252 | background: var(--selected-bg) // #e0e7eb 253 | padding: 10px 254 | display: block 255 | border-radius: 5px 256 | margin-bottom: 10px 257 | position: relative 258 | 259 | .stream-btn 260 | background: var(--button-bg) 261 | color: white !important 262 | padding: 3px 7px 263 | display: inline-block 264 | border-radius: 2px 265 | margin-top: 5px 266 | font-weight: bold 267 | user-select: none 268 | text-decoration: none 269 | 270 | &:hover 271 | background: var(--button-hover-bg) 272 | 273 | .add-another 274 | margin-left: 10px 275 | 276 | /* add new block */ 277 | .stream-insert-new-block 278 | text-align: right 279 | font-size: 1.1em 280 | margin-top: 10px 281 | 282 | li 283 | list-style: none 284 | 285 | .stream-blocks-list 286 | text-align: right 287 | 288 | .add-new-block-button 289 | padding: 5px 290 | font-weight: bold 291 | text-decoration: underline 292 | cursor: pointer 293 | color: var(--link-fg) 294 | user-select: none 295 | 296 | /* Help text */ 297 | .stream-help-text 298 | margin-bottom: 5px 299 | 300 | &__title 301 | padding: 5px 302 | text-align: right 303 | font-weight: bold 304 | text-decoration: underline 305 | cursor: pointer 306 | 307 | &__content 308 | padding: 10px 309 | background: #d4e6d7 310 | font-size: 15px 311 | line-height: 1.2em 312 | border-radius: 5px 313 | color: #333 314 | 315 | ul li 316 | list-style: disc 317 | margin: .5em 20px 318 | 319 | .flip-list-move 320 | transition: transform 0.5s 321 | 322 | .no-move 323 | transition: transform 0s 324 | 325 | /* Handle */ 326 | .streamblock__block-handle, 327 | .stream-model-subblock-handle 328 | position: absolute 329 | right: 2px 330 | top: 1px 331 | z-index: 2 332 | 333 | .streamblock__block-handle .block-move, 334 | .stream-model-subblock .subblock-move, 335 | .streamblock__block-handle .block-delete, 336 | .stream-model-subblock .subblock-delete 337 | position: relative 338 | width: 24px 339 | height: 24px 340 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='175'%3E%3Cdefs%3E%3CclipPath id='a'%3E%3Cpath d='M0 0h24v175H0z'/%3E%3C/clipPath%3E%3C/defs%3E%3Cg clip-path='url(%23a)' fill='none' stroke-linecap='round' stroke-width='1.5'%3E%3Cpath stroke='%23000' d='m7.5 16.5 9 9M6.5 109h11'/%3E%3Cpath stroke='%23309bbf' d='M6.5 153h11'/%3E%3Cpath stroke='%23bf3030' d='m7.5 60.5 9 9'/%3E%3Cpath stroke='%23000' d='m16.5 16.5-9 9'/%3E%3Cpath stroke='%23bf3030' d='m16.5 60.5-9 9'/%3E%3Cpath d='m9.03 106.546 3.113-3.113 3.1 3.1' stroke='%23000' stroke-linejoin='round'/%3E%3Cpath d='m9.03 150.546 3.113-3.113 3.1 3.1' stroke='%23309bbf' stroke-linejoin='round'/%3E%3Cpath d='m9.03 111.432 3.113 3.113 3.1-3.1' stroke='%23000' stroke-linejoin='round'/%3E%3Cpath d='m9.03 155.432 3.113 3.113 3.1-3.1' stroke='%23309bbf' stroke-linejoin='round'/%3E%3C/g%3E%3C/svg%3E") 341 | background-repeat: no-repeat 342 | display: block 343 | margin-left: 0 344 | float: left 345 | cursor: pointer 346 | 347 | .streamblock__block-handle .block-delete, 348 | .stream-model-subblock .subblock-delete 349 | background-position: 0 -9px 350 | 351 | .streamblock__block-handle .block-delete:hover, 352 | .stream-model-subblock .subblock-delete:hover 353 | background-position: 0 -53px 354 | 355 | .streamblock__block-handle .block-move, 356 | .stream-model-subblock .subblock-move 357 | background-position: 0 -97px 358 | 359 | .streamblock__block-handle .block-move:hover, 360 | .stream-model-subblock .subblock-move:hover 361 | background-position: 0 -141px 362 | 363 | // grp 364 | :root 365 | --primary: #a9c8d5 366 | --primary-fg: #000 367 | --secondary: #309bbf 368 | --button-bg: #78adbf 369 | --button-hover-bg: #309bbf 370 | --selected-bg: #ececec 371 | --darkened-bg: #f5f5f5 372 | --body-fg: #333 373 | --link-fg: #309bbf 374 | --border-color: #AAA 375 | 376 | /* for django skin */ 377 | #content-main 378 | .streamfield_app 379 | .stream-model-block 380 | .streamblock__block__title 381 | background: var(--primary) 382 | color: var(--primary-fg) 383 | margin: 0 384 | line-height: 20px 385 | font-weight: normal 386 | padding: 7px 50px 7px 10px 387 | display: flex 388 | align-items: center 389 | 390 | .streamblock__block__model_title 391 | font-size: 16px 392 | 393 | .streamblock__block__subtitle 394 | font-size: 15px 395 | 396 | .stream-insert-new-block 397 | font-size: 15px 398 | .add-new-block-button 399 | font-weight: normal 400 | 401 | .stream-btn 402 | font-weight: normal 403 | font-size: 14px 404 | 405 | .stream-help-text ul 406 | margin-left: 0 407 | 408 | .collapse-handlers 409 | .collapse-handler 410 | color: var(--link-fg) 411 | font-weight: normal 412 | font-size: 14px 413 | 414 | .streamblock__block-handle, 415 | .stream-model-subblock-handle 416 | top: 5px 417 | 418 | .grp-button 419 | text-decoration: none 420 | 421 | -------------------------------------------------------------------------------- /frontend/src/utils.js: -------------------------------------------------------------------------------- 1 | const isArray = Array.isArray; 2 | const isEmpty = obj => [Object, Array].includes((obj || {}).constructor) && !Object.entries((obj || {})).length; 3 | const find = function(arr, obj) { 4 | return arr.find((elem) => { 5 | return obj && Object.keys(obj).every(key => elem[key] === obj[key]); 6 | }) || arr[0]; 7 | } 8 | 9 | export {isArray, isEmpty, find} -------------------------------------------------------------------------------- /frontend/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 3 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); 4 | const {VueLoaderPlugin} = require("vue-loader"); 5 | const {CleanWebpackPlugin} = require("clean-webpack-plugin"); 6 | const TerserPlugin = require('terser-webpack-plugin'); 7 | 8 | 9 | const src = path.resolve(__dirname, 'src'); 10 | const dist = path.resolve(__dirname, '../streamfield/static/streamfield/'); 11 | 12 | 13 | module.exports = (env, argv) => { 14 | const IS_PRODUCTION = argv.mode === 'production'; 15 | 16 | const config = { 17 | entry: { 18 | streamfield_widget: './src/streamfield_widget.js', 19 | admin_popup_response: './src/admin_popup_response.js' 20 | }, 21 | output: { 22 | path: dist, 23 | filename: "[name].js", 24 | }, 25 | 26 | resolve: { 27 | alias: { 28 | "@": src 29 | } 30 | }, 31 | mode: argv.mode, 32 | devServer: { 33 | static: dist, 34 | // to be able to visit dev server from phones and other computers in your network 35 | allowedHosts: 'all' 36 | }, 37 | plugins: [ 38 | new VueLoaderPlugin(), 39 | new CleanWebpackPlugin(), 40 | ], 41 | module: { 42 | rules: [{ 43 | test: /\.vue$/, 44 | loader: "vue-loader", 45 | exclude: /node_modules/ 46 | }, 47 | { 48 | test: /\.sass$/, 49 | exclude: /node_modules/, 50 | use: [ 51 | IS_PRODUCTION ? MiniCssExtractPlugin.loader : "style-loader", 52 | { 53 | loader: "css-loader" 54 | }, 55 | { 56 | loader: 'sass-loader', 57 | options: { 58 | sassOptions: { 59 | indentedSyntax: true 60 | } 61 | } 62 | } 63 | ] 64 | }, 65 | { 66 | test: /\.js$/, 67 | loader: "babel-loader", 68 | exclude: /node_modules/ 69 | }] 70 | }, 71 | optimization: { 72 | minimizer: [ 73 | // extend default plugins 74 | // `...`, 75 | new TerserPlugin({ 76 | extractComments: false, 77 | terserOptions: { 78 | format: { 79 | comments: false, 80 | }, 81 | }, 82 | }), 83 | // HTML and JS are minified by default if config.mode === production. 84 | // But for CSS we need to add this: 85 | new CssMinimizerPlugin() 86 | ] 87 | } 88 | }; 89 | 90 | 91 | if (IS_PRODUCTION) { 92 | // put all CSS files to a single 93 | config.plugins.push(new MiniCssExtractPlugin({ 94 | filename: "streamfield_widget.css" 95 | })); 96 | 97 | } 98 | 99 | return config; 100 | } -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="django-streamfield", 8 | version="2.4.0", 9 | author="Yury Lapshinov", 10 | author_email="y.raagin@gmail.com", 11 | description="StreamField for native Django Admin or with Grappelli", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/raagin/django-streamfield", 15 | packages=setuptools.find_packages(exclude=['test_project', 'frontend']), 16 | include_package_data=True, 17 | zip_safe=False, 18 | python_requires=">=3.10", 19 | classifiers=[ 20 | 'Development Status :: 5 - Production/Stable', 21 | 'License :: OSI Approved :: BSD License', 22 | 'Operating System :: OS Independent', 23 | 'Programming Language :: Python', 24 | 'Programming Language :: Python :: 3.10', 25 | "Programming Language :: Python :: 3.11", 26 | "Programming Language :: Python :: 3.12", 27 | "Framework :: Django", 28 | "Framework :: Django :: 4.2", 29 | "Framework :: Django :: 5.0", 30 | "Framework :: Django :: 5.1", 31 | "Framework :: Django :: 5.2" 32 | ], 33 | ) -------------------------------------------------------------------------------- /streamfield/__init__.py: -------------------------------------------------------------------------------- 1 | name = "streamfield" 2 | VERSION = "2.4.0" -------------------------------------------------------------------------------- /streamfield/admin.py: -------------------------------------------------------------------------------- 1 | import json 2 | from django.contrib import admin 3 | from django.contrib.admin.options import TO_FIELD_VAR 4 | from django.template.response import TemplateResponse 5 | from .base import get_streamblocks_models 6 | 7 | 8 | class StreamBlocksAdminMixin: 9 | change_form_template = 'streamfield/admin/change_form.html' 10 | popup_response_template = 'streamfield/admin/streamfield_popup_response.html' 11 | 12 | def response_add(self, request, obj, post_url_continue=None): 13 | if "block_id" in request.POST: 14 | opts = obj._meta 15 | to_field = request.POST.get(TO_FIELD_VAR) 16 | attr = str(to_field) if to_field else opts.pk.attname 17 | value = obj.serializable_value(attr) 18 | popup_response_data = json.dumps({ 19 | 'app_id': request.POST.get("app_id"), 20 | 'block_id': request.POST.get("block_id"), 21 | 'instance_id': str(value), 22 | }) 23 | return TemplateResponse(request, self.popup_response_template, { 24 | 'popup_response_data': popup_response_data, 25 | }) 26 | return super().response_add(request, obj, post_url_continue) 27 | 28 | def response_change(self, request, obj): 29 | if "block_id" in request.POST: 30 | opts = obj._meta 31 | to_field = request.POST.get(TO_FIELD_VAR) 32 | attr = str(to_field) if to_field else opts.pk.attname 33 | value = request.resolver_match.kwargs['object_id'] 34 | new_value = obj.serializable_value(attr) 35 | popup_response_data = json.dumps({ 36 | 'action': 'change', 37 | 'app_id': request.POST.get("app_id"), 38 | 'block_id': request.POST.get("block_id"), 39 | 'instance_id': request.POST.get("instance_id"), 40 | }) 41 | return TemplateResponse(request, self.popup_response_template, { 42 | 'popup_response_data': popup_response_data, 43 | }) 44 | 45 | return super().response_change(request, obj) 46 | 47 | def response_delete(self, request, obj_display, obj_id): 48 | if "block_id" in request.POST: 49 | popup_response_data = json.dumps({ 50 | 'action': 'delete', 51 | 'value': str(obj_id), 52 | 'app_id': request.POST.get("app_id"), 53 | 'block_id': request.POST.get("block_id"), 54 | 'instance_id': request.POST.get("instance_id"), 55 | }) 56 | return TemplateResponse(request, self.popup_response_template, { 57 | 'popup_response_data': popup_response_data, 58 | }) 59 | 60 | return super().response_delete(request, obj_display, obj_id) 61 | 62 | 63 | class StreamBlocksAdmin(StreamBlocksAdminMixin, admin.ModelAdmin): 64 | pass 65 | 66 | # if user defined admin for his blocks, then do not autoregiser block models 67 | for model in get_streamblocks_models(): 68 | if not model._meta.abstract and \ 69 | not admin.site.is_registered(model): 70 | admin.site.register(model, StreamBlocksAdmin) 71 | -------------------------------------------------------------------------------- /streamfield/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | class StreamfieldConfig(AppConfig): 4 | name = 'streamfield' -------------------------------------------------------------------------------- /streamfield/base.py: -------------------------------------------------------------------------------- 1 | import json 2 | from copy import deepcopy 3 | from uuid import uuid4 4 | 5 | from django.apps import apps 6 | from django.template import loader 7 | from django.utils.functional import cached_property 8 | from django.utils.html import format_html_join 9 | from django.utils.safestring import mark_safe 10 | 11 | from .forms import get_form_class 12 | from .settings import BLOCK_OPTIONS, ADMIN_HELP_TEXT 13 | 14 | __all__ = ( 15 | 'StreamObject', 16 | 'get_streamblocks_models', 17 | 'migrate_stream_options' 18 | ) 19 | 20 | class StreamObject: 21 | """ 22 | The instance contains raw data from db and rendered html 23 | 24 | # Example: 25 | # streamblocks/models.py 26 | 27 | # one value per model 28 | class RichText(models.Model): 29 | text = models.TextField(blank=True, null=True, verbose_name='Текстовое поле') 30 | 31 | # list of values per model 32 | class NumberInText(models.Model): 33 | big_number = models.CharField(max_length=32) 34 | small = models.CharField(max_length=32, null=True, blank=True) 35 | text = models.TextField(null=True, blank=True) 36 | 37 | as_list = True 38 | 39 | # data in db 40 | value = [ 41 | { 42 | "unique_id": "lsupu", 43 | "model_name": "NumberInText", 44 | "id": [1,2,3], 45 | "options":{"margins":true} 46 | }, 47 | { 48 | "unique_id": "vlbh7j", 49 | "model_name": "RichText", 50 | "id": 1, 51 | "options": {"margins":true} 52 | } 53 | ] 54 | """ 55 | 56 | def __init__(self, value, model_list): 57 | self.value = value 58 | self.model_list = model_list 59 | self.model_list_names = [m.__name__ for m in model_list] 60 | 61 | def __str__(self): 62 | if isinstance(self.value, str): 63 | return self.value or "[]" 64 | else: 65 | return json.dumps(self.value) 66 | 67 | def __repr__(self): 68 | return "<%s: %s>" % (self.__class__.__name__, self or "None") 69 | 70 | def __bool__(self): 71 | return bool(self.value) 72 | 73 | def _iterate_over_models(self, callback, tmpl_ctx=None): 74 | # iterate over models and apply callback function 75 | data = [] 76 | for m in self.from_json(): 77 | model_str = m['model_name'] 78 | 79 | if model_str in self.model_list_names: 80 | idx = self.model_list_names.index(model_str) 81 | model_class = self.model_list[idx] 82 | as_list = hasattr(model_class, 'as_list') and model_class.as_list 83 | content = {} 84 | 85 | # if block is not abstract model and have content in database 86 | if not model_class._meta.abstract: 87 | if as_list: 88 | unordered_items = model_class.objects.filter(pk__in=m['id']) 89 | # set id as key and reorder queryset same as ids order 90 | unordered_items_dict = {i.id: i for i in unordered_items} 91 | content = [unordered_items_dict[i] for i in m['id']] 92 | elif m['id'] != -1: 93 | content = model_class.objects.get(pk=m['id']) 94 | 95 | ctx = dict( 96 | block_model=model_str.lower(), 97 | block_unique_id=m['unique_id'], 98 | block_content=content, 99 | as_list=as_list, 100 | options=m['options'] 101 | ) 102 | 103 | # add tmpl_ctx if exists. tmpl_ctx: additional context from templates 104 | if tmpl_ctx: 105 | ctx.update(tmpl_ctx) 106 | res = callback(model_class, model_str, content, ctx) 107 | data.append(res) 108 | 109 | return data 110 | 111 | def _render(self, tmpl_ctx=None): 112 | data = self._iterate_over_models(_get_render_data, tmpl_ctx) 113 | return mark_safe("".join(data)) 114 | 115 | @cached_property 116 | def render(self): 117 | return self._render() 118 | 119 | def as_list(self): 120 | return self._iterate_over_models(_get_data_list) 121 | 122 | # only for complex blocks 123 | def render_admin(self): 124 | data = self._iterate_over_models(_get_render_admin_data) 125 | return mark_safe("".join(data)) 126 | 127 | # for backward compatibility 128 | def from_json(self): 129 | if isinstance(self.value, str): 130 | return json.loads(self.value) 131 | else: 132 | return self.value 133 | 134 | def copy(self): 135 | return StreamObject( 136 | value=self._iterate_over_models(_copy), 137 | model_list=self.model_list 138 | ) 139 | 140 | def to_json(self): 141 | return json.dumps(self.value) 142 | 143 | def add(self, block): 144 | if isinstance(block, list): 145 | model_class = block[0].__class__ 146 | if hasattr(model_class, 'as_list') and model_class.as_list: 147 | id = [] 148 | for b in block: 149 | if b.__class__ != model_class: 150 | raise ValueError("Not all list objects have the same model class") 151 | else: 152 | id.append(b.id) 153 | else: 154 | raise ValueError("Block is list. but model hasn't as_list property") 155 | else: 156 | model_class = block.__class__ 157 | id = block.id 158 | 159 | if model_class not in self.model_list: 160 | raise ValueError("Model class not in StreamField model_list") 161 | 162 | options = _get_default_options(model_class) 163 | self.value.append({ 164 | "id": id, 165 | "options": options, 166 | "unique_id": uuid4().hex[:6], 167 | "model_name": model_class.__name__ 168 | }) 169 | 170 | @cached_property 171 | def help_text(self): 172 | return ADMIN_HELP_TEXT 173 | 174 | 175 | 176 | def _get_block_tmpl(model_class, model_str): 177 | if hasattr(model_class, 'block_template'): 178 | return model_class.block_template 179 | else: 180 | return 'streamblocks/%s.html' % model_str.lower() 181 | 182 | 183 | def _get_render_data(model_class, model_str, content, ctx): 184 | block_tmpl = _get_block_tmpl(model_class, model_str) 185 | try: 186 | t = loader.get_template(block_tmpl) 187 | except loader.TemplateDoesNotExist: 188 | ctx.update(dict( 189 | block_tmpl=block_tmpl, 190 | model_str=model_str 191 | )) 192 | t = loader.get_template('streamfield/default_block_tmpl.html') 193 | return t.render(ctx) 194 | 195 | # only for complex blocks 196 | def _get_render_admin_data(model_class, model_str, content, ctx): 197 | t = loader.select_template([ 198 | 'streamblocks/admin/%s.html' % model_str.lower(), 199 | 'streamfield/admin/change_form_render_template.html' 200 | ]) 201 | objs = content if isinstance(content, list) else [content] 202 | return format_html_join( 203 | '\n', "{}", 204 | ( 205 | (t.render({ 206 | 'form': get_form_class(model_class)(instance=obj) 207 | }), 208 | ) for obj in objs) 209 | ) 210 | 211 | def _get_data_list(model_class, model_str, content, ctx): 212 | return { 213 | 'data': ctx, 214 | 'template': _get_block_tmpl(model_class, model_str) 215 | } 216 | 217 | def _check_subblocks(obj): 218 | # check if have subblocks 219 | changed = False 220 | for f in obj._meta.fields: 221 | # isinstance(f, StreamField) 222 | if hasattr(f, 'model_list'): 223 | changed = True 224 | value = getattr(obj, f.name) 225 | # value is StreamObject 226 | setattr(obj, f.name, value.copy()) 227 | if changed: 228 | obj.save() 229 | 230 | def _get_default_options(model_class): 231 | options = model_class.options if hasattr(model_class, "options") else BLOCK_OPTIONS 232 | if hasattr(model_class, "extra_options"): 233 | options = deepcopy(options) 234 | options.update(model_class.extra_options) 235 | options = { k: v['default'] for k, v in options.items() if bool(v.get('default')) } 236 | return options 237 | 238 | def _copy(model_class, model_str, content, ctx): 239 | as_list = ctx['as_list'] 240 | resp = dict( 241 | unique_id=uuid4().hex[:6], 242 | model_name=model_str, 243 | options=ctx['options'] 244 | ) 245 | if not model_class._meta.abstract: 246 | if as_list: 247 | id = [] 248 | for obj in content: 249 | obj.pk = None 250 | obj.save() 251 | id.append(obj.pk) 252 | _check_subblocks(obj) 253 | else: 254 | content.pk = None 255 | content.save() 256 | id = content.pk 257 | _check_subblocks(content) 258 | resp['id'] = id 259 | return resp 260 | 261 | def get_streamblocks_models(): 262 | streamblock_models = [] 263 | 264 | for app_config in apps.get_app_configs(): 265 | module = app_config.models_module 266 | for m in getattr(module, "STREAMBLOCKS_MODELS", []): 267 | if m not in streamblock_models: 268 | streamblock_models.append(m) 269 | 270 | return streamblock_models 271 | 272 | def get_model_by_string(model_str): 273 | try: 274 | return [m for m in get_streamblocks_models() if m.__name__ == model_str][0] 275 | except IndexError: 276 | return None 277 | 278 | def migrate_stream_options(stream_obj): 279 | """ 280 | Receive StreamObject from StreamField in your model 281 | Return updated StreamObject, with new default options. 282 | At the moment this only works with unique streamblocks class names. 283 | """ 284 | stream_dict = stream_obj.from_json() 285 | for b in stream_dict: 286 | model_class = get_model_by_string(b['model_name']) 287 | options = _get_default_options(model_class) 288 | options.update(b['options']) 289 | b['options'] = options 290 | return StreamObject( 291 | stream_dict, 292 | stream_obj.model_list 293 | ) -------------------------------------------------------------------------------- /streamfield/fields.py: -------------------------------------------------------------------------------- 1 | import json 2 | from copy import deepcopy 3 | from django import forms 4 | from django.db.models import JSONField 5 | from django.forms.widgets import Widget 6 | from django.contrib.admin.options import FORMFIELD_FOR_DBFIELD_DEFAULTS 7 | from django.urls import reverse_lazy 8 | 9 | from .base import StreamObject 10 | from .settings import ( 11 | BLOCK_OPTIONS, 12 | SHOW_ADMIN_HELP_TEXT, 13 | DELETE_BLOCKS_FROM_DB, 14 | BASE_ADMIN_URL 15 | ) 16 | 17 | # widget 18 | class StreamFieldWidget(Widget): 19 | template_name = 'streamfield/streamfield_widget.html' 20 | 21 | def __init__(self, attrs=None): 22 | self.model_list = attrs.pop('model_list', []) 23 | 24 | model_list_info = {} 25 | for block in self.model_list: 26 | as_list = hasattr(block, "as_list") and block.as_list 27 | 28 | options = block.options if hasattr(block, "options") else BLOCK_OPTIONS 29 | if hasattr(block, "extra_options"): 30 | options = deepcopy(options) 31 | options.update(block.extra_options) 32 | 33 | model_doc = block._meta.verbose_name_plural if as_list else block._meta.verbose_name 34 | model_list_info[block.__name__] = { 35 | 'model_doc': str(model_doc), 36 | 'abstract': block._meta.abstract, 37 | 'as_list': as_list, 38 | 'options': options 39 | } 40 | 41 | attrs["model_list_info"] = json.dumps(model_list_info) 42 | attrs['show_admin_help_text'] = SHOW_ADMIN_HELP_TEXT 43 | attrs['delete_blocks_from_db'] = DELETE_BLOCKS_FROM_DB 44 | attrs['base_admin_url'] = BASE_ADMIN_URL 45 | super().__init__(attrs) 46 | 47 | def format_value(self, value): 48 | if value and not isinstance(value, StreamObject): 49 | value = StreamObject(value, self.model_list) 50 | return value 51 | 52 | class Media: 53 | css = {'all': ('streamfield/streamfield_widget.css',)} 54 | js = (reverse_lazy("streamfield-texts"), 'streamfield/streamfield_widget.js',) 55 | 56 | 57 | # form field 58 | class StreamFormField(forms.JSONField): 59 | def prepare_value(self, value): 60 | if isinstance(value, StreamObject): 61 | value = value.value 62 | return super().prepare_value(value) 63 | 64 | # main field 65 | class StreamField(JSONField): 66 | description = "StreamField" 67 | 68 | def __init__(self, *args, **kwargs): 69 | self.model_list = kwargs.pop('model_list', []) 70 | self.popup_size = kwargs.pop('popup_size', (1000, 500)) 71 | kwargs['blank'] = True 72 | kwargs['default'] = list 73 | super().__init__(*args, **kwargs) 74 | 75 | def from_db_value(self, value, expression, connection): 76 | return self.to_python(value) 77 | 78 | def to_python(self, value): 79 | if isinstance(value, StreamObject): 80 | return value 81 | if not value: 82 | return StreamObject([], self.model_list) 83 | # for backward compatibility 84 | while isinstance(value, str): 85 | value = json.loads(value) 86 | return StreamObject(value, self.model_list) 87 | 88 | def validate(self, value, model_instance): 89 | super().validate(value.value, model_instance) 90 | 91 | def get_prep_value(self, value): 92 | if isinstance(value, StreamObject): 93 | value = value.value 94 | return value 95 | 96 | def get_db_prep_value(self, value, connection, prepared=False): 97 | if value and isinstance(value, StreamObject): 98 | value = value.value 99 | return super().get_db_prep_value(value, connection, prepared) 100 | 101 | def value_to_string(self, obj): 102 | value = self.value_from_object(obj) 103 | return value.value 104 | 105 | def formfield(self, **kwargs): 106 | attrs = {} 107 | attrs["model_list"] = self.model_list 108 | attrs["data-popup_size"] = list(self.popup_size) 109 | return super().formfield(**{ 110 | 'widget': StreamFieldWidget(attrs=attrs), 111 | 'form_class': StreamFormField 112 | }) 113 | 114 | FORMFIELD_FOR_DBFIELD_DEFAULTS[StreamField] = {'widget': StreamFieldWidget} 115 | -------------------------------------------------------------------------------- /streamfield/forms.py: -------------------------------------------------------------------------------- 1 | from django.forms import ModelForm 2 | 3 | def get_form_class(model, base=ModelForm): 4 | model_ = model 5 | 6 | class Meta: 7 | model = model_ 8 | fields = '__all__' 9 | 10 | attrs = dict( 11 | Meta = Meta, 12 | ) 13 | return type(str(model_.__name__ + 'Form'), (base, ), attrs ) -------------------------------------------------------------------------------- /streamfield/locale/en/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raagin/django-streamfield/a5851a4cd4f36eff23a7c0b70237f0c0d9eb57a3/streamfield/locale/en/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /streamfield/locale/en/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2024-10-23 17:38-0400\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: templates/streamfield/admin/change_form.html 22 | msgid "Please correct the error below." 23 | msgstr "" 24 | 25 | #: templates/streamfield/admin/change_form.html 26 | msgid "Please correct the errors below." 27 | msgstr "" 28 | 29 | #: templates/streamfield/admin/fields/file_browse_widget.html 30 | msgid "File not found" 31 | msgstr "" 32 | 33 | #: templates/streamfield/admin/streamfield_popup_response.html 34 | msgid "Popup closing..." 35 | msgstr "" 36 | 37 | #: templates/streamfield/streamfield_admin_help.html 38 | msgid "" 39 | "\n" 40 | "
    \n" 41 | "
  • The blocks are moved by drag-and-drop.
  • \n" 42 | "
  • After moving, adding or deleting blocks you need to save the entire " 43 | "object.
  • \n" 44 | "
  • If you have deleted some blocks accidentally, you may get them back " 45 | "by reloading the page.\n" 46 | " All edited blocks will remain, in the order of the saved version.\n" 47 | " Newly added blocks will need to be recreated.
  • \n" 48 | "
\n" 49 | msgstr "" 50 | 51 | #: templates/streamfield/streamfield_texts.js 52 | msgid "Are you sure that you want to delete this block?" 53 | msgstr "" 54 | 55 | #: templates/streamfield/streamfield_texts.js 56 | msgid "Are you sure that you want to delete this subblock?" 57 | msgstr "" 58 | 59 | #: templates/streamfield/streamfield_texts.js 60 | msgid "Collapse all" 61 | msgstr "" 62 | 63 | #: templates/streamfield/streamfield_texts.js 64 | msgid "Open all" 65 | msgstr "" 66 | 67 | #: templates/streamfield/streamfield_texts.js 68 | msgid "Add one more" 69 | msgstr "" 70 | 71 | #: templates/streamfield/streamfield_texts.js 72 | msgid "Change" 73 | msgstr "" 74 | 75 | #: templates/streamfield/streamfield_texts.js 76 | msgid "Add content" 77 | msgstr "" 78 | 79 | #: templates/streamfield/streamfield_texts.js 80 | msgid "Help?" 81 | msgstr "" 82 | 83 | #: templates/streamfield/streamfield_texts.js 84 | msgid "Add new block" 85 | msgstr "" 86 | -------------------------------------------------------------------------------- /streamfield/locale/fr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raagin/django-streamfield/a5851a4cd4f36eff23a7c0b70237f0c0d9eb57a3/streamfield/locale/fr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /streamfield/locale/fr/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2024-10-23 17:38-0400\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 20 | 21 | #: templates/streamfield/admin/change_form.html 22 | msgid "Please correct the error below." 23 | msgstr "Veuillez corriger l’erreur ci-dessous." 24 | 25 | #: templates/streamfield/admin/change_form.html 26 | msgid "Please correct the errors below." 27 | msgstr "Veuillez corriger les erreurs ci-dessous." 28 | 29 | #: templates/streamfield/admin/fields/file_browse_widget.html 30 | msgid "File not found" 31 | msgstr "Fichier non trouvé" 32 | 33 | #: templates/streamfield/admin/streamfield_popup_response.html 34 | msgid "Popup closing..." 35 | msgstr "Fermeture de la modale…" 36 | 37 | #: templates/streamfield/streamfield_admin_help.html 38 | msgid "" 39 | "\n" 40 | "
    \n" 41 | "
  • The blocks are moved by drag-and-drop.
  • \n" 42 | "
  • After moving, adding or deleting blocks you need to save the entire " 43 | "object.
  • \n" 44 | "
  • If you have deleted some blocks accidentally, you may get them back " 45 | "by reloading the page.\n" 46 | " All edited blocks will remain, in the order of the saved version.\n" 47 | " Newly added blocks will need to be recreated.
  • \n" 48 | "
\n" 49 | msgstr "" 50 | "\n" 51 | "
    \n" 52 | "
  • Les blocs se déplacent par glisser-déposer.
  • \n" 53 | "
  • Après avoir déplacé, ajouté ou supprimé des blocks vous devez " 54 | "sauvegarder l’objet au complet.
  • \n" 55 | "
  • Si vous avez effacé des blocs par accident, vous pouvez les " 56 | "récupérer en rechargeant la page; les blocs modifiés seront toujours " 57 | "présents, mais dans l’ordre de la version précédente. Les blocs ajoutés " 58 | "devront être recréés.
  • \n" 59 | "
\n" 60 | 61 | #: templates/streamfield/streamfield_texts.js 62 | msgid "Are you sure that you want to delete this block?" 63 | msgstr "Voulez-vous vraiment supprimer ce bloc?" 64 | 65 | #: templates/streamfield/streamfield_texts.js 66 | msgid "Are you sure that you want to delete this subblock?" 67 | msgstr "Voulez-vous vraiment supprimer ce sous-bloc?" 68 | 69 | #: templates/streamfield/streamfield_texts.js 70 | msgid "Collapse all" 71 | msgstr "Tout réduire" 72 | 73 | #: templates/streamfield/streamfield_texts.js 74 | msgid "Open all" 75 | msgstr "Tout ouvrir" 76 | 77 | #: templates/streamfield/streamfield_texts.js 78 | msgid "Add one more" 79 | msgstr "Ajouter un autre" 80 | 81 | #: templates/streamfield/streamfield_texts.js 82 | msgid "Change" 83 | msgstr "Modifier" 84 | 85 | #: templates/streamfield/streamfield_texts.js 86 | msgid "Add content" 87 | msgstr "Ajouter du contenu" 88 | 89 | #: templates/streamfield/streamfield_texts.js 90 | msgid "Help?" 91 | msgstr "Aide?" 92 | 93 | #: templates/streamfield/streamfield_texts.js 94 | msgid "Add new block" 95 | msgstr "Ajouter un bloc" 96 | -------------------------------------------------------------------------------- /streamfield/locale/it/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raagin/django-streamfield/a5851a4cd4f36eff23a7c0b70237f0c0d9eb57a3/streamfield/locale/it/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /streamfield/locale/it/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2024-10-23 17:38-0400\n" 11 | "PO-Revision-Date: 2020-02-08 14:10+0100\n" 12 | "Last-Translator: \n" 13 | "Language-Team: \n" 14 | "Language: it_IT\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | "X-Generator: Poedit 2.3\n" 20 | 21 | #: templates/streamfield/admin/change_form.html 22 | msgid "Please correct the error below." 23 | msgstr "" 24 | 25 | #: templates/streamfield/admin/change_form.html 26 | msgid "Please correct the errors below." 27 | msgstr "" 28 | 29 | #: templates/streamfield/admin/fields/file_browse_widget.html 30 | msgid "File not found" 31 | msgstr "" 32 | 33 | #: templates/streamfield/admin/streamfield_popup_response.html 34 | msgid "Popup closing..." 35 | msgstr "" 36 | 37 | #: templates/streamfield/streamfield_admin_help.html 38 | msgid "" 39 | "\n" 40 | "
    \n" 41 | "
  • The blocks are moved by drag-and-drop.
  • \n" 42 | "
  • After moving, adding or deleting blocks you need to save the entire " 43 | "object.
  • \n" 44 | "
  • If you have deleted some blocks accidentally, you may get them back " 45 | "by reloading the page.\n" 46 | " All edited blocks will remain, in the order of the saved version.\n" 47 | " Newly added blocks will need to be recreated.
  • \n" 48 | "
\n" 49 | msgstr "" 50 | "\n" 51 | "
    \n" 52 | "
  • I blocchi si possono spostare tramite il drag-and-drop.
  • \n" 53 | "
  • Dopo lo spostamento, l'aggiunta o l'eliminazione di blocchi, è " 54 | "necessario salvare l'intero oggetto.
  • \n" 55 | "
  • Se si fossero cancellati accidentalmente dei blocchi, è possibile " 56 | "tornare indietro\n" 57 | " ricaricando la pagina. Così facendo, blocchi già esistenti, anche se " 58 | "modificati, verranno salvati.\n" 59 | "L'ordinamento sarà quello della versione precedente. Eventuali nuovi " 60 | "blocchi \n" 61 | " andranno invece persi.
  • \n" 62 | "
\n" 63 | 64 | #: templates/streamfield/streamfield_texts.js 65 | msgid "Are you sure that you want to delete this block?" 66 | msgstr "Sei sicuro di voler cancellare questo blocco?" 67 | 68 | #: templates/streamfield/streamfield_texts.js 69 | msgid "Are you sure that you want to delete this subblock?" 70 | msgstr "Sei sicuro di voler cancellare questo sotto blocco?" 71 | 72 | #: templates/streamfield/streamfield_texts.js 73 | msgid "Collapse all" 74 | msgstr "Crolla tutto" 75 | 76 | #: templates/streamfield/streamfield_texts.js 77 | msgid "Open all" 78 | msgstr "Apri tutto" 79 | 80 | #: templates/streamfield/streamfield_texts.js 81 | msgid "Add one more" 82 | msgstr "Aggiungine un altro" 83 | 84 | #: templates/streamfield/streamfield_texts.js 85 | msgid "Change" 86 | msgstr "Modifica" 87 | 88 | #: templates/streamfield/streamfield_texts.js 89 | msgid "Add content" 90 | msgstr "Aggiungi contenuto" 91 | 92 | #: templates/streamfield/streamfield_texts.js 93 | msgid "Help?" 94 | msgstr "Un aiutino?" 95 | 96 | #: templates/streamfield/streamfield_texts.js 97 | msgid "Add new block" 98 | msgstr "Aggiungi un nuovo blocco" 99 | -------------------------------------------------------------------------------- /streamfield/locale/ru/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raagin/django-streamfield/a5851a4cd4f36eff23a7c0b70237f0c0d9eb57a3/streamfield/locale/ru/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /streamfield/locale/ru/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2024-10-23 17:38-0400\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " 20 | "n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || " 21 | "(n%100>=11 && n%100<=14)? 2 : 3);\n" 22 | 23 | #: templates/streamfield/admin/change_form.html 24 | msgid "Please correct the error below." 25 | msgstr "" 26 | 27 | #: templates/streamfield/admin/change_form.html 28 | msgid "Please correct the errors below." 29 | msgstr "" 30 | 31 | #: templates/streamfield/admin/fields/file_browse_widget.html 32 | msgid "File not found" 33 | msgstr "" 34 | 35 | #: templates/streamfield/admin/streamfield_popup_response.html 36 | msgid "Popup closing..." 37 | msgstr "" 38 | 39 | #: templates/streamfield/streamfield_admin_help.html 40 | msgid "" 41 | "\n" 42 | "
    \n" 43 | "
  • The blocks are moved by drag-and-drop.
  • \n" 44 | "
  • After moving, adding or deleting blocks you need to save the entire " 45 | "object.
  • \n" 46 | "
  • If you have deleted some blocks accidentally, you may get them back " 47 | "by reloading the page.\n" 48 | " All edited blocks will remain, in the order of the saved version.\n" 49 | " Newly added blocks will need to be recreated.
  • \n" 50 | "
\n" 51 | msgstr "" 52 | "\n" 53 | "
    \n" 54 | "
  • Блоки перемещаются — перетаскиванием.
  • \n" 55 | "
  • При перемещении, добавлении или удалении блоков страницы, необходимо " 56 | "сохранение всей страницы.
  • \n" 57 | "
  • Если вы удалили какой-то блок случайно.
    Можно вернуться к " 58 | "сохраненной версии,\n" 59 | " перезагрузив страницу. При этом все отредактированные блоки сохранятся, " 60 | "но сортировка будет взята из сохраненной версии. Новые добавленные объекты " 61 | "так же не сохранятся.
  • \n" 62 | "
\n" 63 | 64 | #: templates/streamfield/streamfield_texts.js 65 | msgid "Are you sure that you want to delete this block?" 66 | msgstr "Вы уверены, что хотите удалить этот блок?" 67 | 68 | #: templates/streamfield/streamfield_texts.js 69 | msgid "Are you sure that you want to delete this subblock?" 70 | msgstr "Вы уверены, что хотите удалить этот подблок?" 71 | 72 | #: templates/streamfield/streamfield_texts.js 73 | msgid "Collapse all" 74 | msgstr "Закрыть все" 75 | 76 | #: templates/streamfield/streamfield_texts.js 77 | msgid "Open all" 78 | msgstr "Открыть все" 79 | 80 | #: templates/streamfield/streamfield_texts.js 81 | msgid "Add one more" 82 | msgstr "Добавить ещё" 83 | 84 | #: templates/streamfield/streamfield_texts.js 85 | msgid "Change" 86 | msgstr "Изменить" 87 | 88 | #: templates/streamfield/streamfield_texts.js 89 | msgid "Add content" 90 | msgstr "Добавить контент" 91 | 92 | #: templates/streamfield/streamfield_texts.js 93 | msgid "Help?" 94 | msgstr "Помощь?" 95 | 96 | #: templates/streamfield/streamfield_texts.js 97 | msgid "Add new block" 98 | msgstr "Добавить новый блок" 99 | -------------------------------------------------------------------------------- /streamfield/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /streamfield/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.urls import reverse_lazy 3 | from django.template.loader import render_to_string 4 | 5 | BLOCK_OPTIONS = getattr(settings, "STREAMFIELD_BLOCK_OPTIONS", {}) 6 | DELETE_BLOCKS_FROM_DB = getattr(settings, "STREAMFIELD_DELETE_BLOCKS_FROM_DB", True) 7 | BASE_ADMIN_URL = getattr(settings, "STREAMFIELD_BASE_ADMIN_URL", reverse_lazy('admin:index')) 8 | BLOCK_TITLE = getattr(settings, "STREAMFIELD_BLOCK_TITLE", '__str__') 9 | 10 | SHOW_ADMIN_HELP_TEXT = getattr(settings, "STREAMFIELD_SHOW_ADMIN_HELP_TEXT", False) 11 | if SHOW_ADMIN_HELP_TEXT: 12 | ADMIN_HELP_TEXT = getattr(settings, "STREAMFIELD_ADMIN_HELP_TEXT", render_to_string('streamfield/streamfield_admin_help.html')) 13 | else: 14 | ADMIN_HELP_TEXT = "" 15 | -------------------------------------------------------------------------------- /streamfield/static/streamfield/admin_popup_response.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";var e=JSON.parse(document.getElementById("django-admin-popup-response-constants").dataset.popupResponse);switch(e.action){case"change":default:opener.streamapps[e.app_id].updateBlock(e.block_id,e.instance_id),window.close();case"delete":}}(); -------------------------------------------------------------------------------- /streamfield/static/streamfield/streamfield_widget.css: -------------------------------------------------------------------------------- 1 | .streamfield_app{float:left;margin-top:5px;max-width:758px;width:98%}.streamfield_app textarea{display:none}.streamfield_app p{margin-bottom:.5em}.streamfield_app a.grp-add-another,.streamfield_app a.grp-change-related{background-image:none}.streamfield_app .collapse-handlers{margin-bottom:5px;text-align:right}.streamfield_app .collapse-handlers .collapse-handler{display:inline-block;font-weight:700;margin:5px 0;text-decoration:underline}.streamfield_app .streamfield-models{clear:both;margin-bottom:10px}.streamfield_app .streamfield-models .stream-model-block{color:var(--body-fg);position:relative}.streamfield_app .streamfield-models .stream-model-block__inner{box-shadow:0 0 5px 2px rgba(0,0,0,.1)}.streamfield_app .streamfield-models .stream-model-block.collapsed .stream-block__options,.streamfield_app .streamfield-models .stream-model-block.collapsed .stream-model-block__content{display:none}.streamfield_app .streamfield-models .stream-model-block.collapsed .add_here{min-height:12px}.streamfield_app .streamfield-models .stream-model-block.collapsed .add_here.show_add_block:after,.streamfield_app .streamfield-models .stream-model-block.collapsed .add_here.show_add_block:before{top:5px}.streamfield_app .streamfield-models .stream-model-block.collapsed .stream-blocks-list{margin:0 10px 5px;padding:10px 0}.streamfield_app .streamfield-models .stream-model-block .stream-blocks-list{margin:0 10px 10px;padding:15px 0 10px;position:relative;z-index:2}.streamfield_app .streamfield-models .stream-model-block .stream-blocks-list li{list-style:none}.streamfield_app .streamfield-models .stream-model-block .streamblock__block__title{position:relative}.streamfield_app .streamfield-models .stream-model-block .streamblock__block__title .streamblock__block__model_title{font-size:1.15em;font-weight:700;margin-left:0;position:relative;user-select:none}.streamfield_app .streamfield-models .stream-model-block .streamblock__block__title .streamblock__block__subtitle{font-size:1em;font-weight:400;margin:0;opacity:.6;position:relative}.streamfield_app .streamfield-models .stream-model-block .streamblock__block__title-click{bottom:0;cursor:pointer;display:block;left:0;position:absolute;right:0;top:0;z-index:1}.streamfield_app .streamfield-models .stream-model-block .streamblock__block__title-click:hover{background:hsla(0,0%,100%,.1)}.streamfield_app .streamfield-models .stream-model-block .add_here{cursor:pointer;min-height:20px;position:relative;width:100%}.streamfield_app .streamfield-models .stream-model-block .add_here.show,.streamfield_app .streamfield-models .stream-model-block .add_here.show_add_block{z-index:10}.streamfield_app .streamfield-models .stream-model-block .add_here.show:before,.streamfield_app .streamfield-models .stream-model-block .add_here.show_add_block:before{display:block;z-index:2}.streamfield_app .streamfield-models .stream-model-block .add_here.show_add_block:before{top:10px;transform:rotate(45deg)}.streamfield_app .streamfield-models .stream-model-block .add_here.show_add_block:after{background:hsla(0,0%,100%,.1);border:1px dashed var(--border-color);border-radius:5px;bottom:0;content:"";left:0;position:absolute;right:0;top:10px}.streamfield_app .streamfield-models .stream-model-block .add_here:before{background:var(--secondary);background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16.729' height='16.728'%3E%3Cpath d='M8.364 2v12.728m6.364-6.364H2' fill='none' stroke='%23fff' stroke-linecap='round' stroke-width='2'/%3E%3C/svg%3E");background-position:50% 50%;background-repeat:no-repeat;background-size:18px;border-radius:15px;color:#fff;content:"";display:none;font-size:28px;height:30px;left:50%;line-height:30px;margin-left:-15px;margin-top:-15px;position:absolute;text-align:center;top:50%;transition:transform .2s ease-out;width:30px}.streamfield_app .streamfield-models .stream-model-block:last-child .add_here{display:none}.streamfield_app .streamfield-models .stream-model-block__content{background:var(--darkened-bg);padding:20px}.streamfield_app .streamfield-models .stream-model-block__content .model-field-content:after{clear:both;content:".";display:block;height:0;visibility:hidden}.streamfield_app .streamfield-models .stream-model-block__content img{border:1px solid #b6b6b6}.streamfield_app .streamfield-models .stream-model-block__content.no-subblocks{padding:20px}.streamfield_app .streamfield-models .stream-model-block__content.abstract-block a img{border:3px solid #a6d1e0}.streamfield_app .streamfield-models .stream-model-block__content.abstract-block a:hover img{border-color:#78adbf}.streamfield_app .stream-block__options{background:var(--selected-bg);padding:7px;text-align:right}.streamfield_app .stream-block__options .stream-block__option{display:inline-block;margin:4px}.streamfield_app .stream-block__options .stream-block__option span{margin:0 0 0 5px}.streamfield_app .stream-block__options .stream-block__option span+*{margin:0 0 0 3px}.streamfield_app .block-fields__label{color:#999;display:block;font-style:italic}.streamfield_app .block-fields>div{margin-bottom:.4em}.streamfield_app .block-fields h1{color:var(--body-fg)}.streamfield_app .block-fields h2{font-size:1.3em}.streamfield_app .block-fields h2,.streamfield_app .block-fields h3{background-image:none;border:none;color:var(--body-fg);padding:0;text-shadow:none}.streamfield_app .block-fields h3{font-size:1.1em;margin-bottom:.5em}.streamfield_app .block-fields h4{color:var(--body-fg);font-weight:700}.streamfield_app .block-fields blockquote{font-size:1.1em;font-style:italic;font-weight:700;margin-bottom:.4em}.streamfield_app .block-fields table td,.streamfield_app .block-fields table th{border-bottom:1px solid #ccc}.streamfield_app .block-fields ul li{list-style:disc;margin-left:20px}.streamfield_app .block-fields ol li{list-style:decimal;margin-left:20px}.streamfield_app .block-fields b{font-weight:700}.streamfield_app .block-fields img,.streamfield_app .block-fields svg{max-height:150px;max-width:150px}.streamfield_app .stream-model-subblock{background:var(--selected-bg);border-radius:5px;display:block;margin-bottom:10px;padding:10px;position:relative}.streamfield_app .stream-btn{background:var(--button-bg);border-radius:2px;color:#fff!important;display:inline-block;font-weight:700;margin-top:5px;padding:3px 7px;text-decoration:none;user-select:none}.streamfield_app .stream-btn:hover{background:var(--button-hover-bg)}.streamfield_app .add-another{margin-left:10px}.streamfield_app .stream-insert-new-block{font-size:1.1em;margin-top:10px;text-align:right}.streamfield_app .stream-insert-new-block li{list-style:none}.streamfield_app .stream-blocks-list{text-align:right}.streamfield_app .add-new-block-button{color:var(--link-fg);cursor:pointer;font-weight:700;padding:5px;text-decoration:underline;user-select:none}.streamfield_app .stream-help-text{margin-bottom:5px}.streamfield_app .stream-help-text__title{cursor:pointer;font-weight:700;padding:5px;text-align:right;text-decoration:underline}.streamfield_app .stream-help-text__content{background:#d4e6d7;border-radius:5px;color:#333;font-size:15px;line-height:1.2em;padding:10px}.streamfield_app .stream-help-text__content ul li{list-style:disc;margin:.5em 20px}.streamfield_app .flip-list-move{transition:transform .5s}.streamfield_app .no-move{transition:transform 0s}.stream-model-subblock-handle,.streamblock__block-handle{position:absolute;right:2px;top:1px;z-index:2}.stream-model-subblock .subblock-delete,.stream-model-subblock .subblock-move,.streamblock__block-handle .block-delete,.streamblock__block-handle .block-move{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='175'%3E%3Cdefs%3E%3CclipPath id='a'%3E%3Cpath d='M0 0h24v175H0z'/%3E%3C/clipPath%3E%3C/defs%3E%3Cg clip-path='url(%23a)' fill='none' stroke-linecap='round' stroke-width='1.5'%3E%3Cpath stroke='%23000' d='m7.5 16.5 9 9M6.5 109h11'/%3E%3Cpath stroke='%23309bbf' d='M6.5 153h11'/%3E%3Cpath stroke='%23bf3030' d='m7.5 60.5 9 9'/%3E%3Cpath stroke='%23000' d='m16.5 16.5-9 9'/%3E%3Cpath stroke='%23bf3030' d='m16.5 60.5-9 9'/%3E%3Cpath d='m9.03 106.546 3.113-3.113 3.1 3.1' stroke='%23000' stroke-linejoin='round'/%3E%3Cpath d='m9.03 150.546 3.113-3.113 3.1 3.1' stroke='%23309bbf' stroke-linejoin='round'/%3E%3Cpath d='m9.03 111.432 3.113 3.113 3.1-3.1' stroke='%23000' stroke-linejoin='round'/%3E%3Cpath d='m9.03 155.432 3.113 3.113 3.1-3.1' stroke='%23309bbf' stroke-linejoin='round'/%3E%3C/g%3E%3C/svg%3E");background-repeat:no-repeat;cursor:pointer;display:block;float:left;height:24px;margin-left:0;position:relative;width:24px}.stream-model-subblock .subblock-delete,.streamblock__block-handle .block-delete{background-position:0 -9px}.stream-model-subblock .subblock-delete:hover,.streamblock__block-handle .block-delete:hover{background-position:0 -53px}.stream-model-subblock .subblock-move,.streamblock__block-handle .block-move{background-position:0 -97px}.stream-model-subblock .subblock-move:hover,.streamblock__block-handle .block-move:hover{background-position:0 -141px}:root{--primary:#a9c8d5;--primary-fg:#000;--secondary:#309bbf;--button-bg:#78adbf;--button-hover-bg:#309bbf;--selected-bg:#ececec;--darkened-bg:#f5f5f5;--body-fg:#333;--link-fg:#309bbf;--border-color:#aaa}#content-main .streamfield_app .stream-model-block .streamblock__block__title{align-items:center;background:var(--primary);color:var(--primary-fg);display:flex;font-weight:400;line-height:20px;margin:0;padding:7px 50px 7px 10px}#content-main .streamfield_app .stream-model-block .streamblock__block__title .streamblock__block__model_title{font-size:16px}#content-main .streamfield_app .stream-insert-new-block,#content-main .streamfield_app .stream-model-block .streamblock__block__title .streamblock__block__subtitle{font-size:15px}#content-main .streamfield_app .stream-insert-new-block .add-new-block-button{font-weight:400}#content-main .streamfield_app .stream-btn{font-size:14px;font-weight:400}#content-main .streamfield_app .stream-help-text ul{margin-left:0}#content-main .streamfield_app .collapse-handlers .collapse-handler{color:var(--link-fg);font-size:14px;font-weight:400}#content-main .stream-model-subblock-handle,#content-main .streamblock__block-handle{top:5px}.grp-button{text-decoration:none} -------------------------------------------------------------------------------- /streamfield/templates/streamfield/admin/abstract_block_template.html: -------------------------------------------------------------------------------- 1 |   -------------------------------------------------------------------------------- /streamfield/templates/streamfield/admin/change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %}{% load static i18n admin_modify admin_urls %} 2 | 3 | {% block content %} 4 |
{% csrf_token %}{% block form_top %}{% endblock %} 5 |
6 | 7 | {% if is_popup %}{% endif %} 8 | 9 | 10 | {% if request.GET.block_id and is_popup %}{% endif %} 11 | {% if request.GET.instance_id and is_popup %}{% endif %} 12 | {% if request.GET.app_id and is_popup %}{% endif %} 13 | 14 | 15 | 16 | 17 | 18 | {% if errors %} 19 |

{% if errors|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}

20 |
    {% for error in adminform.form.non_field_errors %}
  • {{ error }}
  • {% endfor %}
21 | {% endif %} 22 | 23 | 24 | {% block field_sets %} 25 | {% for fieldset in adminform %} 26 | {% include "admin/includes/fieldset.html" %} 27 | {% endfor %} 28 | {% endblock %} 29 | 30 | {% block after_field_sets %}{% endblock %} 31 | 32 | 33 | {% block inline_field_sets %} 34 | {% for inline_admin_formset in inline_admin_formsets %} 35 | {% include inline_admin_formset.opts.template %} 36 | {% endfor %} 37 | {% endblock %} 38 | 39 | {% block after_related_objects %}{% endblock %} 40 | 41 | 42 | {% block submit_buttons_bottom %}{% submit_row %}{% endblock %} 43 | 44 | 45 | {% prepopulated_fields_js %} 46 | 47 |
48 |
49 | {% endblock %} 50 | 51 | -------------------------------------------------------------------------------- /streamfield/templates/streamfield/admin/change_form_render_template.html: -------------------------------------------------------------------------------- 1 | {% load streamfield_tags %} 2 | {% block streamblock_form %} 3 |
4 | {% for f in form %} 5 | {% if f.value %} 6 |
7 | {{ f.label }}: 8 | {% format_field f %} 9 |
10 | {% endif %} 11 | {% endfor %} 12 |
13 | {% endblock streamblock_form %} -------------------------------------------------------------------------------- /streamfield/templates/streamfield/admin/fields/default.html: -------------------------------------------------------------------------------- 1 | {{ field.value|default:""|safe }} -------------------------------------------------------------------------------- /streamfield/templates/streamfield/admin/fields/file_browse_widget.html: -------------------------------------------------------------------------------- 1 | {% load i18n fb_versions %} 2 | {% with field.value as value %} 3 | 4 | {% if value and value.exists %} 5 | {% if value.filetype == "Image" %} 6 | {% version value.path 'admin_thumbnail' as thumbnail_version %} 7 | {% if thumbnail_version %} 8 |

9 | 10 | 11 | 12 |

13 | {% else %} 14 | 19 | {% endif %} 20 | {% else %} 21 |

{{ value }}

22 | {% endif %} 23 | {% else %} 24 |
  • {% trans "File not found" %}
25 | {% endif %} 26 | {% endwith %} -------------------------------------------------------------------------------- /streamfield/templates/streamfield/admin/fields/select.html: -------------------------------------------------------------------------------- 1 | {{ field.obj }} -------------------------------------------------------------------------------- /streamfield/templates/streamfield/admin/fields/stream_field_widget.html: -------------------------------------------------------------------------------- 1 |
{{ field.value.render_admin }}
-------------------------------------------------------------------------------- /streamfield/templates/streamfield/admin/streamfield_popup_response.html: -------------------------------------------------------------------------------- 1 | {% load i18n static %} 2 | 3 | {% trans 'Popup closing...' %} 4 | 5 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /streamfield/templates/streamfield/default_block_tmpl.html: -------------------------------------------------------------------------------- 1 |
2 | Add template "{{ block_tmpl }}" for block: "{{ model_str }}" with context variable block_content as model object or as model objects list. 3 |
-------------------------------------------------------------------------------- /streamfield/templates/streamfield/streamfield_admin_help.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %} 3 |
    4 |
  • The blocks are moved by drag-and-drop.
  • 5 |
  • After moving, adding or deleting blocks you need to save the entire object.
  • 6 |
  • If you have deleted some blocks accidentally, you may get them back by reloading the page. 7 | All edited blocks will remain, in the order of the saved version. 8 | Newly added blocks will need to be recreated.
  • 9 |
10 | {% endblocktrans %} 11 | -------------------------------------------------------------------------------- /streamfield/templates/streamfield/streamfield_texts.js: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | window.stream_texts = window.stream_texts || { 4 | 'deleteBlock': '{% trans "Are you sure that you want to delete this block?" %}', 5 | 'deleteInstance': '{% trans "Are you sure that you want to delete this subblock?" %}', 6 | 'Collapse all': '{% trans "Collapse all" %}', 7 | 'Open all': '{% trans "Open all" %}', 8 | 'AddOneMore': '{% trans "Add one more" %}', 9 | 'Change': '{% trans "Change" %}', 10 | 'AddContent': '{% trans "Add content" %}', 11 | 'Help?': '{% trans "Help?" %}', 12 | 'AddNewBlock': '{% trans "Add new block" %}' 13 | } 14 | -------------------------------------------------------------------------------- /streamfield/templates/streamfield/streamfield_widget.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | -------------------------------------------------------------------------------- /streamfield/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raagin/django-streamfield/a5851a4cd4f36eff23a7c0b70237f0c0d9eb57a3/streamfield/templatetags/__init__.py -------------------------------------------------------------------------------- /streamfield/templatetags/streamfield_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.utils.text import ( 3 | get_valid_filename, 4 | camel_case_to_spaces 5 | ) 6 | from django.utils.safestring import mark_safe 7 | from django.template import loader 8 | 9 | register = template.Library() 10 | 11 | 12 | @register.simple_tag 13 | def format_field(field): 14 | widget_name = get_widget_name(field) 15 | 16 | t = loader.select_template([ 17 | 'streamblocks/admin/fields/%s.html' % widget_name, 18 | 'streamfield/admin/fields/%s.html' % widget_name, 19 | 'streamfield/admin/fields/default.html' 20 | ]) 21 | 22 | if widget_name == 'select': 23 | 24 | # ForeignKey Field 25 | if hasattr(field.field, '_queryset'): 26 | for obj in field.field._queryset: 27 | if obj.pk == field.value(): 28 | field.obj = obj 29 | 30 | # CharField choices 31 | if hasattr(field.field, '_choices'): 32 | for obj in field.field._choices: 33 | if obj[0] == field.value(): 34 | field.obj = obj[1] 35 | 36 | 37 | return mark_safe(t.render(dict( 38 | field=field 39 | ))) 40 | 41 | def get_widget_name(field): 42 | return get_valid_filename( 43 | camel_case_to_spaces(field.field.widget.__class__.__name__) 44 | ) 45 | 46 | @register.simple_tag 47 | def stream_render(stream_obj, **kwargs): 48 | return stream_obj._render(kwargs) -------------------------------------------------------------------------------- /streamfield/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /streamfield/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | from django.contrib.auth.decorators import login_required 3 | from django.views.generic import TemplateView 4 | 5 | 6 | from . import views 7 | from .base import get_streamblocks_models 8 | 9 | admin_instance_urls = [] 10 | 11 | for model in get_streamblocks_models(): 12 | if not model._meta.abstract: 13 | block_path = path( 14 | 'admin-instance/%s/' % model.__name__.lower(), 15 | login_required(views.admin_instance(model)), 16 | name='admin-instance' 17 | ) 18 | else: 19 | block_path = path( 20 | 'abstract-block/%s/' % model.__name__.lower(), 21 | login_required(views.abstract_block_class(model).as_view()), 22 | name='abstract-block' 23 | ) 24 | 25 | admin_instance_urls.append(block_path) 26 | 27 | urlpatterns = [ 28 | path( 29 | 'admin-instance///delete/', 30 | login_required(views.delete_instance), 31 | name='admin-instance-delete' 32 | ), 33 | path( 34 | 'streamfield_texts.js', 35 | login_required(TemplateView.as_view(template_name="streamfield/streamfield_texts.js", content_type="text/javascript")), 36 | name='streamfield-texts', 37 | ), 38 | *admin_instance_urls 39 | ] 40 | -------------------------------------------------------------------------------- /streamfield/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.apps import apps 3 | from django.template import loader 4 | from django.http import JsonResponse 5 | from django.views.generic import DetailView, TemplateView 6 | from django.views.generic.detail import BaseDetailView 7 | from .settings import BLOCK_TITLE 8 | from .forms import get_form_class 9 | 10 | def admin_instance(model): 11 | def instance_view(request, pk): 12 | if hasattr(model, 'custom_admin_template'): 13 | tmpl = loader.get_template(model.custom_admin_template) 14 | else: 15 | tmpl = loader.select_template([ 16 | 'streamblocks/admin/%s.html' % model.__name__.lower(), 17 | 'streamfield/admin/change_form_render_template.html' 18 | ]) 19 | obj = model.objects.get(pk=pk) 20 | ctx = { 21 | 'form': get_form_class(model)(instance=obj), 22 | 'object': obj 23 | } 24 | block_title = getattr(obj, BLOCK_TITLE, '') if BLOCK_TITLE else '' 25 | block_title = block_title() if block_title and callable(block_title) else block_title 26 | return JsonResponse({ 27 | 'content': tmpl.render(ctx), 28 | 'title': block_title 29 | }) 30 | return instance_view 31 | 32 | 33 | def abstract_block_class(model, base=TemplateView): 34 | 35 | if hasattr(model, 'custom_admin_template'): 36 | tmpl_name = model.custom_admin_template 37 | else: 38 | tmpl = loader.select_template([ 39 | 'streamblocks/admin/%s.html' % model.__name__.lower(), 40 | 'streamfield/admin/abstract_block_template.html' 41 | ]) 42 | tmpl_name = tmpl.template.name 43 | 44 | attrs = dict( 45 | model = model, 46 | template_name = tmpl_name, 47 | ) 48 | 49 | return type(str(model.__name__ + 'TemplateView'), (base, ), attrs ) 50 | 51 | 52 | def delete_instance(request, model_name, pk): 53 | model_class = apps.get_model(app_label='streamblocks', model_name=model_name) 54 | obj = model_class.objects.filter(pk=pk).first() 55 | if request.method == 'DELETE' and obj: 56 | obj.delete() 57 | resp = {'success': True} 58 | else: 59 | resp = {'success': False} 60 | return JsonResponse(resp) -------------------------------------------------------------------------------- /test_project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == '__main__': 6 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 7 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_project.settings') 8 | try: 9 | from django.core.management import execute_from_command_line 10 | except ImportError as exc: 11 | raise ImportError( 12 | "Couldn't import Django. Are you sure it's installed and " 13 | "available on your PYTHONPATH environment variable? Did you " 14 | "forget to activate a virtual environment?" 15 | ) from exc 16 | execute_from_command_line(sys.argv) 17 | -------------------------------------------------------------------------------- /test_project/pages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raagin/django-streamfield/a5851a4cd4f36eff23a7c0b70237f0c0d9eb57a3/test_project/pages/__init__.py -------------------------------------------------------------------------------- /test_project/pages/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Page 4 | 5 | @admin.register(Page) 6 | class PageAdmin(admin.ModelAdmin): 7 | pass -------------------------------------------------------------------------------- /test_project/pages/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.4 on 2019-08-12 15:46 2 | 3 | from django.db import migrations, models 4 | import streamfield.fields 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Page', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('title', models.CharField(max_length=255)), 20 | ('stream', streamfield.fields.StreamField(blank=True, default=list, verbose_name='Page blocks')), 21 | ], 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /test_project/pages/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raagin/django-streamfield/a5851a4cd4f36eff23a7c0b70237f0c0d9eb57a3/test_project/pages/migrations/__init__.py -------------------------------------------------------------------------------- /test_project/pages/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.urls import reverse 3 | 4 | from streamfield.fields import StreamField 5 | from streamblocks.models import RichText, Column, Separator 6 | 7 | class Page(models.Model): 8 | title = models.CharField(max_length=255) 9 | stream = StreamField( 10 | model_list=[ 11 | RichText, 12 | Column, 13 | Separator 14 | ], 15 | verbose_name="Page blocks" 16 | ) 17 | 18 | def __str__(self): 19 | return self.title 20 | 21 | def get_absolute_url(self): 22 | return reverse('page', args=[self.pk]) -------------------------------------------------------------------------------- /test_project/pages/templates/pages/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Index 5 | 8 | 9 | 10 |

Index

11 |
12 | 17 |
18 | 19 | -------------------------------------------------------------------------------- /test_project/pages/templates/pages/page_detail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ page.title }} 5 | 28 | 29 | 30 |

{{ page.title }}

31 |
32 | {{ page.stream.render }} 33 |
34 | 35 | -------------------------------------------------------------------------------- /test_project/pages/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file demonstrates two different styles of tests (one doctest and one 3 | unittest). These will both pass when you run "manage.py test". 4 | 5 | Replace these with more appropriate tests for your application. 6 | """ 7 | 8 | from django.test import TestCase 9 | 10 | class SimpleTest(TestCase): 11 | def test_basic_addition(self): 12 | """ 13 | Tests that 1 + 1 always equals 2. 14 | """ 15 | self.failUnlessEqual(1 + 1, 2) 16 | 17 | __test__ = {"doctest": """ 18 | Another way to test that 1 + 1 is equal to 2. 19 | 20 | >>> 1 + 1 == 2 21 | True 22 | """} 23 | 24 | -------------------------------------------------------------------------------- /test_project/pages/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.views.generic.detail import DetailView 3 | from .models import Page 4 | 5 | class PageView(DetailView): 6 | model = Page 7 | 8 | def index(request): 9 | return render(request, 'pages/index.html', dict(pages=Page.objects.all())) 10 | -------------------------------------------------------------------------------- /test_project/streamblocks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raagin/django-streamfield/a5851a4cd4f36eff23a7c0b70237f0c0d9eb57a3/test_project/streamblocks/__init__.py -------------------------------------------------------------------------------- /test_project/streamblocks/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from streamfield.admin import StreamBlocksAdmin 3 | from streamblocks.models import RichText, Column 4 | 5 | class RichTextAdmin: 6 | # rich text admin class 7 | pass 8 | 9 | admin.site.unregister(RichText) 10 | @admin.register(RichText) 11 | class RichTextBlockAdmin(StreamBlocksAdmin, RichTextAdmin): 12 | pass 13 | 14 | admin.site.unregister(Column) 15 | @admin.register(Column) 16 | class ColumnBlockAdmin(StreamBlocksAdmin): 17 | pass 18 | 19 | -------------------------------------------------------------------------------- /test_project/streamblocks/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.4 on 2019-08-12 15:46 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Column', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('text', models.TextField(blank=True, null=True)), 19 | ], 20 | options={ 21 | 'verbose_name': 'Column', 22 | 'verbose_name_plural': 'Columns', 23 | }, 24 | ), 25 | migrations.CreateModel( 26 | name='RichText', 27 | fields=[ 28 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 29 | ('text', models.TextField(blank=True, null=True)), 30 | ], 31 | options={ 32 | 'verbose_name': 'Text', 33 | }, 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /test_project/streamblocks/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raagin/django-streamfield/a5851a4cd4f36eff23a7c0b70237f0c0d9eb57a3/test_project/streamblocks/migrations/__init__.py -------------------------------------------------------------------------------- /test_project/streamblocks/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | class RichText(models.Model): 4 | text = models.TextField(blank=True, null=True) 5 | 6 | options = { 7 | "gray_bgr": { 8 | "label": "Block on gray background", 9 | "type": "checkbox", 10 | "default": False 11 | } 12 | } 13 | 14 | class Meta: 15 | # This will use as name of block in admin 16 | verbose_name="Text" 17 | 18 | # list of objects 19 | class Column(models.Model): 20 | text = models.TextField(null=True, blank=True) 21 | 22 | # StreamField option for list of objects 23 | as_list = True 24 | 25 | class Meta: 26 | verbose_name="Column" 27 | verbose_name_plural="Columns" 28 | 29 | 30 | class Separator(models.Model): 31 | 32 | class Meta: 33 | abstract = True 34 | verbose_name="Separator" 35 | 36 | # Register blocks for StreamField as list of models 37 | STREAMBLOCKS_MODELS = [ 38 | RichText, 39 | Column, 40 | Separator 41 | ] 42 | -------------------------------------------------------------------------------- /test_project/streamblocks/templates/streamblocks/admin/fields/textarea.html: -------------------------------------------------------------------------------- 1 |
Overriding textarea! Italic font style added
{{ field.value|default:""|safe }}
-------------------------------------------------------------------------------- /test_project/streamblocks/templates/streamblocks/admin/richtext.html: -------------------------------------------------------------------------------- 1 |

{{ form.text.value }} (Overrided by custom template)

-------------------------------------------------------------------------------- /test_project/streamblocks/templates/streamblocks/column.html: -------------------------------------------------------------------------------- 1 |
2 | block_unique_id: {{ block_unique_id }}, block_model: {{ block_model }} 3 |
    4 | {% for block in block_content %} 5 |
  • 6 |

    7 | {{ block.id }}
    8 | {{ block.text }} 9 |

    10 |
  • 11 | {% endfor %} 12 |
13 |
-------------------------------------------------------------------------------- /test_project/streamblocks/templates/streamblocks/richtext.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | block_unique_id: {{ block_unique_id }}, block_model: {{ block_model }} 4 |
{{ block_content.text|safe }}
5 |
-------------------------------------------------------------------------------- /test_project/streamblocks/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file demonstrates two different styles of tests (one doctest and one 3 | unittest). These will both pass when you run "manage.py test". 4 | 5 | Replace these with more appropriate tests for your application. 6 | """ 7 | 8 | from django.test import TestCase 9 | 10 | class SimpleTest(TestCase): 11 | def test_basic_addition(self): 12 | """ 13 | Tests that 1 + 1 always equals 2. 14 | """ 15 | self.failUnlessEqual(1 + 1, 2) 16 | 17 | __test__ = {"doctest": """ 18 | Another way to test that 1 + 1 is equal to 2. 19 | 20 | >>> 1 + 1 == 2 21 | True 22 | """} 23 | 24 | -------------------------------------------------------------------------------- /test_project/streamblocks/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | -------------------------------------------------------------------------------- /test_project/test_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raagin/django-streamfield/a5851a4cd4f36eff23a7c0b70237f0c0d9eb57a3/test_project/test_project/__init__.py -------------------------------------------------------------------------------- /test_project/test_project/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for test_project project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.1.8. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.1/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = 'v%)sakq72iafbjw6ysh3#xjr*)jb=t2^xzigcks3sp&h3e@7yx' 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = [] 30 | 31 | 32 | STREAMFIELD_BLOCK_OPTIONS = { 33 | 'margin_bottom': { 34 | 'label': 'Bottom margin', 35 | 'default': '1', 36 | 'type': 'select', 37 | 'options': [ 38 | {'value': '', 'name': 'No'}, 39 | {'value': '1', 'name': 'x1'}, 40 | {'value': '2', 'name': 'x2'}, 41 | {'value': 'auto', 'name': 'Auto'}, 42 | ] 43 | } 44 | } 45 | 46 | 47 | # Application definition 48 | 49 | INSTALLED_APPS = [ 50 | 51 | 'pages', 52 | 53 | 'grappelli', 54 | 'streamblocks', 55 | 'streamfield', 56 | 57 | 'django.contrib.admin', 58 | 'django.contrib.auth', 59 | 'django.contrib.contenttypes', 60 | 'django.contrib.sessions', 61 | 'django.contrib.messages', 62 | 'django.contrib.staticfiles', 63 | ] 64 | 65 | MIDDLEWARE = [ 66 | 'django.middleware.security.SecurityMiddleware', 67 | 'django.contrib.sessions.middleware.SessionMiddleware', 68 | 'django.middleware.common.CommonMiddleware', 69 | 'django.middleware.csrf.CsrfViewMiddleware', 70 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 71 | 'django.contrib.messages.middleware.MessageMiddleware', 72 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 73 | ] 74 | 75 | ROOT_URLCONF = 'test_project.urls' 76 | 77 | TEMPLATES = [ 78 | { 79 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 80 | 'DIRS': [], 81 | 'APP_DIRS': True, 82 | 'OPTIONS': { 83 | 'context_processors': [ 84 | 'django.template.context_processors.debug', 85 | 'django.template.context_processors.request', 86 | 'django.contrib.auth.context_processors.auth', 87 | 'django.contrib.messages.context_processors.messages', 88 | ], 89 | }, 90 | }, 91 | ] 92 | 93 | WSGI_APPLICATION = 'test_project.wsgi.application' 94 | 95 | 96 | # Database 97 | # https://docs.djangoproject.com/en/2.1/ref/settings/#databases 98 | 99 | DATABASES = { 100 | 'default': { 101 | 'ENGINE': 'django.db.backends.sqlite3', 102 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 103 | } 104 | } 105 | 106 | 107 | # Password validation 108 | # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators 109 | 110 | AUTH_PASSWORD_VALIDATORS = [ 111 | { 112 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 113 | }, 114 | { 115 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 116 | }, 117 | { 118 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 119 | }, 120 | { 121 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 122 | }, 123 | ] 124 | 125 | 126 | # Internationalization 127 | # https://docs.djangoproject.com/en/2.1/topics/i18n/ 128 | 129 | LANGUAGE_CODE = 'en-us' 130 | 131 | TIME_ZONE = 'UTC' 132 | 133 | USE_I18N = True 134 | 135 | USE_L10N = True 136 | 137 | USE_TZ = True 138 | 139 | 140 | # Static files (CSS, JavaScript, Images) 141 | # https://docs.djangoproject.com/en/2.1/howto/static-files/ 142 | 143 | STATIC_URL = '/static/' 144 | -------------------------------------------------------------------------------- /test_project/test_project/urls.py: -------------------------------------------------------------------------------- 1 | """test_project URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | 19 | from pages import views 20 | 21 | urlpatterns = [ 22 | path('streamfield/', include('streamfield.urls')), 23 | path('grappelli/', include('grappelli.urls')), 24 | path('admin/', admin.site.urls), 25 | 26 | path('', views.index, name="index"), 27 | path('/', views.PageView.as_view(), name="page") 28 | ] 29 | -------------------------------------------------------------------------------- /test_project/test_project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for test_project project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_project.settings') 15 | 16 | application = get_wsgi_application() 17 | --------------------------------------------------------------------------------