diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index af9d190b..56bf4c9c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -72,7 +72,7 @@ Ready to contribute? Here's how to set up `django-baton` for local development. ``` - Start the js app in watch mode:: + Start both the django testapp and the js app (the last one in watch mode):: ``` $ cd baton/static/baton/app diff --git a/README.md b/README.md index 51b57d0d..157b8686 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ Login with user `demo` and password `demo` --- **Last changes** +Baton 4.2.1 integrates the computer vision in the `BatonAiImageField`, fixes some minor styling issues and includes some PR. + Baton 4.2.0 introduces the use of computer vision to generate alt attributes for images. Baton 4.0.* introduces a bunch of new AI functionalities! @@ -28,10 +30,14 @@ Baton 4.0.* introduces a bunch of new AI functionalities! - automatic translations with django-modeltranslation - text summarization - text corrections +- image vision - image generation It also introduces themes, and makes it easier to customize the application, there is no need to recompile the js app unless you wanto to change primary and secondary colors or you need heavy customization. +> New! +> Take a look at the new `django-baton-themes` repo: [django-baton-themes](https://github.com/otto-torino/django-baton-themes) + --- ![Screenshot](docs/images/baton-ai.gif) @@ -84,7 +90,7 @@ Everything is styled through CSS and when required, JS is used. - Optional display of changelist filters in a modal - Optional use of changelist filters as a form (combine some filters at once and perform the search action) - Customization available by editing css vars and/or recompiling the js app provided -- IT translations provided +- IT ad FA translations provided Baton is based on the following frontend technologies: @@ -178,6 +184,7 @@ BATON = { }, 'BATON_CLIENT_ID': 'xxxxxxxxxxxxxxxxxxxx', 'BATON_CLIENT_SECRET': 'xxxxxxxxxxxxxxxxxx', + 'IMAGE_PREVIEW_WIDTH': 200, 'AI': { "MODELS": "myapp.foo.bar", # alternative to the below for lines, a function which returns the models dictionary "IMAGES_MODEL": AIModels.BATON_DALL_E_3, @@ -239,12 +246,13 @@ Default value is `True`. - `FORCE_THEME`: You can force the light or dark theme, and the theme toggle disappears from the user area. Defaults to `None` - `BATON_CLIENT_ID`: The client ID of your baton subscription (unleashes AI functionalities). Defaults to `None` - `BATON_CLIENT_SECRET`: The client secret of your baton subscription (unleashes AI functionalities). Defaults to `None` +- `IMAGE_PREVIEW_WIDTH`: The default image width in pixels of the preview shown to set the subject location of the `BatonAiImageField`. Defaults to `200` `AI`, `MENU` and `SEARCH_FIELD` configurations in detail: ### AI -Django Baton can provide you AI assistance in the admin interface: translations, summarizations, corrections and image generation. You can choose which model to use for each functionality, please note that different models have different prices, see [Baton site](https://www.baton.sqrt64.it). +Django Baton can provide you AI assistance in the admin interface: translations, summarizations, corrections, image generation and image vision. You can choose which model to use for each functionality, please note that different models have different prices, see [Baton site](https://www.baton.sqrt64.it). Django Baton supports native fields (input, textarea) and ckeditor (django-ckeditor package) by default, but provides hooks you can use to add support to any other wysiwyg editor, read more in the [AI](#baton-ai) section. @@ -521,29 +529,6 @@ In this modal you can edit the `words` and `useBulletedList` parameters and perf All default fields and CKEDITOR fields are supported, see AI Hooks section below if you need to support other wysiwyg editors. -### Image vision - -In your `ModelAdmin` classes you can define which images can be described in order to generate an alt text, look at the following example: - -``` python -class MyModelAdmin(admin.ModelAdmin): - # ... - baton_vision_fields = { - "image": [{ - "target": "image_alt", - "chars": 80, - "language": "en", - }], - } -``` - -You have to specify the target field name. You can also optionally specify the follwing parameters: - -- `chars`: max number of characters used in the alt description (approximate, it will not be followed strictly, default is 100) -- `language`: the language of the summary, default is your default language - -With this configuration, one (the number of targets) button will appear near the `image` field, clicking it the calculated image alt text will be inserted in the `image_alt` field. - ### Image Generation Baton provides a new model field and a new image widget which can be used to generate images from text. The image field can be used as a normal image field, but also a new button will appear near it. @@ -565,6 +550,57 @@ There is also another way to add the AI image generation functionality to a norm ``` +Baton also integrates the functionality of [django-subject-imagefield](https://github.com/otto-torino/django-subject-imagefield/), so you can specify a `subject_location` field that will store the percentage coordinated of the subject of the image, and in editing mode a point will appear on the image preview in order to let you change this position: + +``` python +from baton.fields import BatonAiImageField + +class MyModel(models.Model): + image = BatonAiImageField(verbose_name=_("immagine"), upload_to="news/", subject_location_field='subject_location') + subject_location = models.CharField(max_length=7, default="50,50") +``` + +You can configure the width of the preview image through the settings `IMAGE_PREVIEW_WIDTH` which by default equals `200`. + +Check the `django-subject-imagefield` documentation for more details and properties. + +### Image vision +There are two ways to activate image vision functionality in Baton, both allow to generate an alt text for the image through the AI. + +The first way is to just use the `BatonAiImageField` and define the `alt_field` attribute (an optionally `alt_chars`, `alt_language`) + +``` python +from baton.fields import BatonAiImageField + +class MyModel(models.Model): + image = BatonAiImageField(verbose_name=_("immagine"), upload_to="news/", alt_field="image_alt", alt_chars=20, alt_language="en") + image_alt = models.CharField(max_length=40, blank=True) +``` + +This method will work only when images are inside inlines. + +The second method consists in defining in the `ModelAdmin` classes which images can be described in order to generate an alt text, look at the following example: + +``` python +class MyModelAdmin(admin.ModelAdmin): + # ... + baton_vision_fields = { + "#id_image": [{ # key must be a selector (useful for inlines) + "target": "image_alt", # target should be the name of a field of the same model + "chars": 80, + "language": "en", + }], + } +``` + +You have to specify the target field name. You can also optionally specify the follwing parameters: + +- `chars`: max number of characters used in the alt description (approximate, it will not be followed strictly, default is 100) +- `language`: the language of the summary, default is your default language + +With this configuration, one (the number of targets) button will appear near the `image` field, clicking it the calculated image alt text will be inserted in the `image_alt` field. +Even this methos should work for inline images. + ### Stats widget Baton provides a new widget which can be used to display stats about AI usage. Just include it in your admin index template: @@ -1122,6 +1158,9 @@ You can override all the css variables, just create a `baton/css/root.css` file You can also create themes directly from the admin site, just surf to `/admin/baton/batontheme/`. There can be only one active theme, if present, the saved content is used instead of the `root.css` file. So just copy the content of that file in the field and change the colors you want. Be aware that the theme content is considered safe and injected into the page as is, so be carefull. +> New! +> You may find ready to use themes and ideas [here](https://github.com/otto-torino/django-baton-themes). + If you need heavy customization or you need to customize the `primary` and `secondary` colors, you can edit and recompile the JS app which resides in `baton/static/baton/app`. ![Customization](docs/images/customization.png) @@ -1141,7 +1180,7 @@ So: If you want to test your live changes, just start the webpack dev server: $ cd django-baton/baton/static/baton/app/ - $ npm run dev + $ npm run dev:baton And inside the `base_site.html` template, make these changes: @@ -1169,7 +1208,7 @@ Switch the baton js path in `base_site.html` -Start the js app in watch mode +Start both the django testapp and the js app (the last one in watch mode): $ cd baton/static/baton/app $ npm install @@ -1197,6 +1236,8 @@ Read [CONTRIBUTING.md](CONTRIBUTING.md) ## Screenshots +Actually the following screenshots are not always up to date, better to visit the [demo site](https://django-baton.sqrt64.it/) + ![Screenshot](docs/screenshots/mobile_mix.jpg) ![Screenshot](docs/screenshots/mobile_mix2.png) diff --git a/baton/config.py b/baton/config.py index 86786992..c8b41f44 100644 --- a/baton/config.py +++ b/baton/config.py @@ -11,6 +11,7 @@ 'SUPPORT_HREF': 'https://github.com/otto-torino/django-baton/issues', 'COPYRIGHT': 'copyright © 2022 Otto srl', # noqa 'POWERED_BY': 'Otto srl', + 'IMAGE_PREVIEW_WIDTH': 200, 'CONFIRM_UNSAVED_CHANGES': True, 'SHOW_MULTIPART_UPLOADING': True, 'ENABLE_IMAGES_PREVIEW': True, diff --git a/baton/fields.py b/baton/fields.py index 73f373e0..d77e3a1b 100644 --- a/baton/fields.py +++ b/baton/fields.py @@ -1,11 +1,88 @@ from django.db import models + +from django.db.models.fields.files import ImageFieldFile +from django.utils.translation import gettext_lazy as _ +from django.conf import settings +from django.utils.translation import get_language + +from .forms import BatonAiImageFormField from .widgets import BatonAiImageInput +class BatonAiImageFieldFile(ImageFieldFile): + @property + def subject_perc_position(self): + if self.field.subject_location_field and getattr( + self.instance, self.field.subject_location_field): + (cX, cY) = getattr(self.instance, + self.field.subject_location_field).split(',') + + return { + 'x': int(cX), + 'y': int(cY) + } + return None + + @property + def subject_position(self): + perc = self.subject_perc_position + if perc: + return { + 'x': (self.width or 0) * perc.get('x', 0) // 100, + 'y': (self.height or 0) * perc.get('y', 0) // 100 + } + return None + + @property + def sorl(self): + """ shortcut property to use with sorl-thumbnmail crop featur e""" + position = self.subject_perc_position + if position: + x = position.get('x') + y = position.get('y') + # also need -0 + return '%s%% %s%%' % (x, y) + return '50% 50%' + class BatonAiImageField(models.ImageField): + attr_class = BatonAiImageFieldFile + + def __init__(self, + verbose_name=None, + name=None, + width_field=None, + height_field=None, + subject_location_field=None, + alt_field=None, + alt_chars=20, + alt_language=get_language(), + **kwargs): + self.width_field, self.height_field = width_field, height_field + self.subject_location_field = subject_location_field + self.alt_field = alt_field + self.alt_chars = alt_chars + self.alt_language = alt_language + super().__init__(verbose_name, name, **kwargs) + + def deconstruct(self): + name, path, args, kwargs = super().deconstruct() + if self.alt_field: + kwargs['alt_field'] = self.alt_field + kwargs['alt_chars'] = self.alt_chars + kwargs['alt_language'] = self.alt_language + if self.subject_location_field: + kwargs['subject_location_field'] = self.subject_location_field + return name, path, args, kwargs + def formfield(self, **kwargs): d = kwargs # override widget d.update({ 'widget': BatonAiImageInput, + 'form_class': BatonAiImageFormField, + 'alt_field': self.alt_field, + 'alt_chars': self.alt_chars, + 'alt_language': self.alt_language, + 'subject_location_field': self.subject_location_field, + 'help_text':_('Drag the circle or click on the image to set the image subject') if self.subject_location_field else d.get('help_text', ''), }) - return super(BatonAiImageField, self).formfield(**d) + return super().formfield(**d) diff --git a/baton/forms.py b/baton/forms.py new file mode 100644 index 00000000..feb7d9b0 --- /dev/null +++ b/baton/forms.py @@ -0,0 +1,23 @@ +from django.forms.fields import ImageField +from .widgets import BatonAiImageInput +from .config import get_config + + +class BatonAiImageFormField(ImageField): + widget = BatonAiImageInput + + def __init__(self, subject_location_field=None, alt_field=None, alt_chars=20, alt_language='en', *args, **kwargs): + self.subject_location_field = subject_location_field + self.alt_field = alt_field + self.alt_chars = alt_chars + self.alt_language = alt_language + return super().__init__(*args, **kwargs) + + def widget_attrs(self, widget): + attrs = super().widget_attrs(widget) + attrs['subject_location_field'] = self.subject_location_field + attrs['alt_field'] = self.alt_field + attrs['alt_chars'] = self.alt_chars + attrs['alt_language'] = self.alt_language + attrs['preview_width'] = get_config('IMAGE_PREVIEW_WIDTH') + return attrs diff --git a/baton/locale/fa/LC_MESSAGES/django.mo b/baton/locale/fa/LC_MESSAGES/django.mo new file mode 100644 index 00000000..8fbb9802 Binary files /dev/null and b/baton/locale/fa/LC_MESSAGES/django.mo differ diff --git a/baton/locale/fa/LC_MESSAGES/django.po b/baton/locale/fa/LC_MESSAGES/django.po new file mode 100644 index 00000000..ebfa48ec --- /dev/null +++ b/baton/locale/fa/LC_MESSAGES/django.po @@ -0,0 +1,116 @@ +# baton translations +# Copyright (C) 2024 Otto srl +# This file is distributed under the same license as the django-baton package. +# Stefano Contini , 2024. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: 0.1.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-12-11 10:47+0100\n" +"PO-Revision-Date: 2024-09-12 17:00+0100\n" +"Last-Translator: Your Name \n" +"Language-Team: fa \n" +"Language: fa\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: config.py:9 +msgid "Site administration" +msgstr "مدیریت سایت" + +#: config.py:10 +msgid "Menu" +msgstr "منو" + +#: fields.py:75 +msgid "Drag the circle or click on the image to set the image subject" +msgstr "دایره را بکشید یا بر روی تصویر کلیک کنید تا موضوع تصویر تنظیم شود" + +#: models.py:5 +msgid "id" +msgstr "" + +#: models.py:6 +msgid "name" +msgstr "" + +#: models.py:7 +#, fuzzy +#| msgid "Theme" +msgid "theme" +msgstr "قالب" + +#: models.py:8 +#, fuzzy +#| msgid "Active" +msgid "active" +msgstr "فعال" + +#: models.py:20 +#, fuzzy +#| msgid "Light theme" +msgid "admin theme" +msgstr "تم روشن" + +#: models.py:21 +#, fuzzy +#| msgid "Light theme" +msgid "admin themes" +msgstr "تم روشن" + +#: templates/admin/base_site.html:6 +msgid "Django site admin" +msgstr "مدیر سایت جنگو" + +#: templates/admin/base_site.html:34 +msgid "Django administration" +msgstr "مدیریت جنگو" + +#: templates/admin/filer/folder/directory_listing.html:4 +msgid "Folders" +msgstr "پوشه‌ها" + +#: templates/admin/filter.html:2 templates/baton/filters/dropdown_filter.html:6 +#: templates/baton/filters/input_filter.html:4 +#: templates/baton/filters/multiple_choice_filter.html:2 +#, python-format +msgid " By %(filter_title)s " +msgstr " بر اساس %(filter_title)s " + +#: templates/baton/ai_stats.html:13 +msgid "Something went wrong" +msgstr "مشکلی پیش آمد" + +#: templates/baton/filters/input_filter.html:18 +#: templates/baton/filters/input_filter.html:31 +msgid "type and press enter..." +msgstr "تایپ کنید و اینتر را بزنید..." + +#: templates/baton/filters/input_filter.html:22 +msgid "Reset" +msgstr "بازنشانی" + +#: templates/baton/footer.html:7 +msgid "Support" +msgstr "پشتیبانی" + +#: templates/baton/footer.html:17 +#, python-format +msgid "Developed by %(powered_by)s" +msgstr "توسعه داده شده توسط %(powered_by)s" + +#~ msgid "Name" +#~ msgstr "نام" + +#~ msgid "Baton" +#~ msgstr "باتون" + +#~ msgid "Filter" +#~ msgstr "فیلتر" + +#~ msgid " Search contents... " +#~ msgstr " جستجوی محتوا... " diff --git a/baton/locale/it/LC_MESSAGES/django.mo b/baton/locale/it/LC_MESSAGES/django.mo index a5129937..449675a1 100644 Binary files a/baton/locale/it/LC_MESSAGES/django.mo and b/baton/locale/it/LC_MESSAGES/django.mo differ diff --git a/baton/locale/it/LC_MESSAGES/django.po b/baton/locale/it/LC_MESSAGES/django.po index fe1542e3..e3481389 100644 --- a/baton/locale/it/LC_MESSAGES/django.po +++ b/baton/locale/it/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.1.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-05-20 14:33+0200\n" +"POT-Creation-Date: 2024-12-11 10:47+0100\n" "PO-Revision-Date: 2017-02-12 17:00+0100\n" "Last-Translator: Stefano Contini \n" "Language-Team: it \n" @@ -26,23 +26,43 @@ msgstr "Amministrazione sito" msgid "Menu" msgstr "Menu" +#: fields.py:75 +msgid "Drag the circle or click on the image to set the image subject" +msgstr "Trascina il cerchio o clicca sull'immagine per impostare la posizione del soggetto" + #: models.py:5 -msgid "Name" -msgstr "Nome" +msgid "id" +msgstr "" #: models.py:6 -msgid "Theme" -msgstr "Tema" +msgid "name" +msgstr "" #: models.py:7 -msgid "Active" +#, fuzzy +#| msgid "Theme" +msgid "theme" +msgstr "Tema" + +#: models.py:8 +#, fuzzy +#| msgid "Active" +msgid "active" msgstr "Attivo" -#: templates/admin/base_site.html:5 +#: models.py:20 +msgid "admin theme" +msgstr "" + +#: models.py:21 +msgid "admin themes" +msgstr "" + +#: templates/admin/base_site.html:6 msgid "Django site admin" msgstr "Amministrazione sito django" -#: templates/admin/base_site.html:27 +#: templates/admin/base_site.html:34 msgid "Django administration" msgstr "Amministrazione django" @@ -79,6 +99,9 @@ msgstr "Supporto" msgid "Developed by %(powered_by)s" msgstr "Sviluppato da %(powered_by)s" +#~ msgid "Name" +#~ msgstr "Nome" + #~ msgid "close" #~ msgstr "chiudi" diff --git a/baton/migrations/0001_initial.py b/baton/migrations/0001_initial.py index 59bc2e7d..5d3e7af4 100644 --- a/baton/migrations/0001_initial.py +++ b/baton/migrations/0001_initial.py @@ -13,18 +13,14 @@ class Migration(migrations.Migration): migrations.CreateModel( name="BatonTheme", fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("name", models.CharField(max_length=255, verbose_name="Name")), - ("theme", models.TextField(verbose_name="Theme")), - ("active", models.BooleanField(default=False, verbose_name="Active")), + ("id", models.AutoField(primary_key=True, serialize=False, verbose_name="id")), + ("name", models.CharField(max_length=255, verbose_name="name")), + ("theme", models.TextField(verbose_name="theme")), + ("active", models.BooleanField(default=False, verbose_name="active")), ], + options={ + 'verbose_name': 'admin theme', + 'verbose_name_plural': 'admin themes' + } ), ] diff --git a/baton/static/admin/css/rtl.css b/baton/static/admin/css/rtl.css new file mode 100644 index 00000000..9079f1ea --- /dev/null +++ b/baton/static/admin/css/rtl.css @@ -0,0 +1,60 @@ +@font-face { + font-family: 'Yekan'; + src: url('../../app/dist/7a71a48e0bbd982d07192d15f.ttf') format('truetype'); + font-weight: normal; + font-style: normal; + text-rendering: optimizeLegibility; + font-display: auto; +} + +*, body, h1, h2, h3, h4, h5, h6, p, a, input, input::placeholder, select, option { + font-family: "Yekan"; +} +.sidebar-menu ul li a i::before{ + margin-left: 12px; +} + +.sidebar-menu ul span.has-children i::before{ + margin-left: 12px; +} +.sidebar-menu .active:not(.with-active) a { + margin-left: 0!important; +} +.sidebar-menu ul span.has-children::after, .sidebar-menu ul a.has-children::after{ + float: left!important; + rotate: 180deg; + } + +.change-list .changelist-filter-toggler.with-actions, .change-list .changelist-filter-toggler { + position: static!important; + float: left!important; +} + +.change-form #content form.baton-fixed-submit-row .submit-row{ + left: 0!important; +} + +#content form .submit-row input[type=submit] { + margin: 0 10px; +} + +.dashboard .actionlist li::before { + right: -18px; +} + +.dashboard .actionlist { + border-left: none!important; + border-right: 5px solid var(--bs-baton-dashboard-action-list-border-color); + list-style-type: none; + margin-right: .5rem; + margin-top: 1.5rem; + padding-right: 0; +} + +.change-form #content form label{ + text-align: right; +} + +#login-form { + direction: ltr; +} \ No newline at end of file diff --git a/baton/static/baton/app/dist/baton.min.js b/baton/static/baton/app/dist/baton.min.js index aee06c8f..288cbc3f 100644 --- a/baton/static/baton/app/dist/baton.min.js +++ b/baton/static/baton/app/dist/baton.min.js @@ -1,2 +1,2 @@ /*! For license information please see baton.min.js.LICENSE.txt */ -(()=>{var t={888:(t,e,o)=>{"use strict";var n=o(414),r=o.n(n),a=o(72),i=o.n(a),c=o(668);i()(c.A,{insert:"head",singleton:!1}),c.A.locals;var l=o(305),s=o(692),m=o.n(s);function f(t,e){for(var o=0;o",{class:"navbar-toggler navbar-toggler-right","data-bs-toggle":"collapse"}).html('').click((function(){return m()(document.body).addClass("menu-open")}))),m()("#user-tools").contents().filter((function(){return 3===this.nodeType})).remove();var e=m()("
",{class:"dropdown"}).appendTo(m()("#user-tools")),o=m()("
",{class:"dropdown-menu dropdown-menu-right"}).appendTo(e);if(m()("#user-tools strong").addClass("dropdown-toggle btn btn-default").attr("data-bs-toggle","dropdown").prependTo(e),m()("#user-tools > a").addClass("dropdown-item").appendTo(o),m()("#logout-form").length&&(m()("#logout-form button").css("display","none"),m()("",{class:"dropdown-item","data-item":"logout"}).html(m()("#logout-form button").html()).on("click",(function(){m()("#logout-form").submit()})).appendTo(o)),!t.forceTheme){var n=this,r=m()("html").attr("data-bs-theme"),a=m()("",{class:"dropdown-item dropdown-item-theme"}).html("dark"===r?this.t.get("lightTheme"):this.t.get("darkTheme")).css("cursor","pointer").click((function(){var t=m()("html").attr("data-bs-theme");m()("hmtl").attr("data-theme","dark"===t?"light":"dark"),m()("html").attr("data-bs-theme","dark"===t?"light":"dark"),m()(this).html("dark"===t?n.t.get("darkTheme"):n.t.get("lightTheme")),localStorage.setItem("baton-theme","dark"===t?"light":"dark")}));0===o.find(".dropdown-item-theme").length&&o.append(a)}}},g=function(t){m()("#footer").appendTo("#content"),t.remove&&m()("#footer").remove()},b={init:function(t,e){this.Dispatcher=e,this.t=new p(m()("html").attr("lang")),this.collapsableUserArea=t.collapsableUserArea,this.menuTitle=t.menuTitle,this.searchField=t.searchField,this.appListUrl=t.api.app_list,this.gravatarUrl=t.api.gravatar,this.gravatarDefaultImg=t.gravatarDefaultImg,this.gravatarEnabled=t.gravatarEnabled,this.alwaysCollapsed=m()("#header").hasClass("menu-always-collapsed"),this.fixNodes(),this.brandingClone=m()("#branding").clone(),this.manageBrandingUserTools(),this.addThemeToggle(t),this.searchTimeout=null,this.manageSearchField(),this.fetchData(),this.setHeight();var o=this;m()(window).on("resize",(function(){o.setHeight(),o.manageBrandingUserTools(),o.addThemeToggle(t)}))},fixNodes:function(){var t=m()("
",{class:"container-fluid"});m()("#footer").before(t);var e=m()("
",{class:"row"}).appendTo(t);this.menu=m()("