From adb8937d78dfcd73cb0113057583b81bbbe2ff51 Mon Sep 17 00:00:00 2001 From: Igor Santos Date: Mon, 5 Jun 2023 13:23:52 -0300 Subject: [PATCH 1/6] feat: add pressure settings plugin with bonde widget --- contrib/bonde/models.py | 123 +++++++++++++++++- contrib/campaign/cms_plugins.py | 76 +++-------- contrib/campaign/forms.py | 13 ++ .../migrations/0008_auto_20230605_1417.py | 43 ++++++ contrib/campaign/models.py | 92 +------------ contrib/frontend/cms_plugins.py | 2 +- .../migrations/0010_alter_block_alignment.py | 18 +++ 7 files changed, 216 insertions(+), 151 deletions(-) create mode 100644 contrib/campaign/migrations/0008_auto_20230605_1417.py create mode 100644 contrib/frontend/migrations/0010_alter_block_alignment.py diff --git a/contrib/bonde/models.py b/contrib/bonde/models.py index 878b0c33..f047511b 100644 --- a/contrib/bonde/models.py +++ b/contrib/bonde/models.py @@ -6,6 +6,8 @@ # * Remove `managed = False` lines if you wish to allow Django to create, modify, and delete the table # Feel free to rename the models, but don't rename db_table values or field names. from django.db import models +from django.conf import settings +from django.contrib.sites.models import Site class User(models.Model): @@ -91,7 +93,9 @@ class Meta: class DnsHostedZone(models.Model): - community = models.ForeignKey(Community, on_delete=models.CASCADE, blank=True, null=True) + community = models.ForeignKey( + Community, on_delete=models.CASCADE, blank=True, null=True + ) domain_name = models.CharField(unique=True, max_length=100, blank=True, null=True) # comment = models.TextField(blank=True, null=True) created_at = models.DateTimeField() @@ -103,4 +107,119 @@ class DnsHostedZone(models.Model): class Meta: managed = False - db_table = 'dns_hosted_zones' \ No newline at end of file + db_table = "dns_hosted_zones" + + +class RequestManager(models.Manager): + + def __init__(self, lookup_field=None): + self.lookup_field = lookup_field + super(RequestManager, self).__init__() + + def on_site(self, request=None): + site = Site.objects.get(id=settings.SITE_ID) + if request: + site = request.current_site + + prefix = '' if not self.lookup_field else f'{self.lookup_field}__' + + params = { + f'{prefix}community__dnshostedzone__domain_name': site.domain + } + + return self.get_queryset().filter(**params) + + +class MobilizationStatus(models.TextChoices): + archived = "archived", "Arquivada" + active = "active", "Ativa" + + +class Mobilization(models.Model): + name = models.CharField(max_length=266, blank=True, null=True) + created_at = models.DateTimeField() + # user_id = models.IntegerField(blank=True, null=True) + # color_scheme = models.CharField(max_length=-1, blank=True, null=True) + # google_analytics_code = models.CharField(max_length=-1, blank=True, null=True) + # goal = models.TextField(blank=True, null=True) + # header_font = models.CharField(max_length=-1, blank=True, null=True) + # body_font = models.CharField(max_length=-1, blank=True, null=True) + # facebook_share_title = models.CharField(max_length=-1, blank=True, null=True) + # facebook_share_description = models.TextField(blank=True, null=True) + # facebook_share_image = models.CharField(max_length=-1, blank=True, null=True) + # slug = models.CharField(unique=True, max_length=-1, blank=True, null=True) + custom_domain = models.CharField(unique=True, max_length=255, blank=True, null=True) + # twitter_share_text = models.CharField(max_length=300, blank=True, null=True) + community = models.ForeignKey(Community, models.DO_NOTHING, blank=True, null=True) + # favicon = models.CharField(max_length=-1, blank=True, null=True) + # deleted_at = models.DateTimeField(blank=True, null=True) + status = models.CharField( + choices=MobilizationStatus.choices, max_length=30, blank=True, null=True + ) + # traefik_host_rule = models.CharField(max_length=-1, blank=True, null=True) + # traefik_backend_address = models.CharField(max_length=-1, blank=True, null=True) + language = models.CharField(max_length=5, blank=True, null=True) + updated_at = models.DateTimeField(blank=True, null=True) + # theme = models.ForeignKey('Themes', models.DO_NOTHING, blank=True, null=True) + + objects = RequestManager() + + class Meta: + managed = False + db_table = "mobilizations" + + +class Block(models.Model): + mobilization = models.ForeignKey( + Mobilization, models.DO_NOTHING, blank=True, null=True + ) + created_at = models.DateTimeField() + updated_at = models.DateTimeField() + # bg_class = models.CharField(max_length=-1, blank=True, null=True) + # position = models.IntegerField(blank=True, null=True) + # hidden = models.BooleanField(blank=True, null=True) + # bg_image = models.TextField(blank=True, null=True) + # name = models.CharField(max_length=-1, blank=True, null=True) + # menu_hidden = models.BooleanField(blank=True, null=True) + deleted_at = models.DateTimeField(blank=True, null=True) + + class Meta: + managed = False + db_table = "blocks" + + +class WidgetKind(models.TextChoices): + content = "content", "Conteúdo" + donation = "donation", "Doação" + draft = "draft", "Rascunho" + form = "form", "Formulário" + phone = "phone", "Pressão por telefone" + plip = "plip", "PLIP" + pressure = "pressure", "Pressão por email" + + +class Widget(models.Model): + block = models.ForeignKey(Block, models.DO_NOTHING, blank=True, null=True) + # settings = models.JSONField(blank=True, null=True) + kind = models.CharField( + max_length=50, choices=WidgetKind.choices, blank=True, null=True + ) + created_at = models.DateTimeField() + updated_at = models.DateTimeField() + # sm_size = models.IntegerField(blank=True, null=True) + # md_size = models.IntegerField(blank=True, null=True) + # lg_size = models.IntegerField(blank=True, null=True) + # mailchimp_segment_id = models.CharField(max_length=-1, blank=True, null=True) + # action_community = models.BooleanField(blank=True, null=True) + # exported_at = models.DateTimeField(blank=True, null=True) + # mailchimp_unique_segment_id = models.CharField(max_length=-1, blank=True, null=True) + # mailchimp_recurring_active_segment_id = models.CharField(max_length=-1, blank=True, null=True) + # mailchimp_recurring_inactive_segment_id = models.CharField(max_length=-1, blank=True, null=True) + # goal = models.DecimalField(max_digits=12, decimal_places=2, blank=True, null=True) + deleted_at = models.DateTimeField(blank=True, null=True) + + objects = RequestManager(lookup_field='block__mobilization') + + class Meta: + managed = False + db_table = "widgets" diff --git a/contrib/campaign/cms_plugins.py b/contrib/campaign/cms_plugins.py index 52b413a1..9029d6e7 100644 --- a/contrib/campaign/cms_plugins.py +++ b/contrib/campaign/cms_plugins.py @@ -1,74 +1,34 @@ +from typing import Any, Optional from django.db.models import Q from cms.plugin_base import CMSPluginBase from cms.plugin_pool import plugin_pool -from .models import ActionButton, Row, Column -from .forms import PressureForm +from contrib.bonde.models import Widget - -# @plugin_pool.register_plugin -# class BlockPlugin(CMSPluginBase): -# model = Block -# name = "Bloco" -# render_template = "campaign/plugins/block.html" -# allow_children = True -# child_classes = [ -# "PicturePlugin", -# "TextPlugin", -# "ActionButtonPlugin", -# "RowPlugin", -# ] - - -# @plugin_pool.register_plugin -# class GridBlockPlugin(CMSPluginBase): -# model = Block -# name = "Grid" -# render_template = "campaign/plugins/grid-block.html" -# allow_children = True -# child_classes = ["TextPlugin", "PressurePlugin"] - - -@plugin_pool.register_plugin -class ActionButtonPlugin(CMSPluginBase): - model = ActionButton - name = "Botão de ação" - render_template = "campaign/plugins/action-button.html" - allow_children = False +from .models import Pressure +from .forms import PressureForm, PressureSettingsForm @plugin_pool.register_plugin class PressurePlugin(CMSPluginBase): name = "Pressão" render_template = "campaign/plugins/pressure.html" + model = Pressure + form = PressureSettingsForm - def render(self, context, instance, placeholder): - context = super(PressurePlugin, self).render(context, instance, placeholder) - context.update({"form": PressureForm()}) - return context + def get_form(self, request, obj=None, change=False, **kwargs): + form = super(PressurePlugin, self).get_form(request, obj, change, **kwargs) + + qs = Widget.objects.on_site(request=request).filter(kind='pressure') + + choices = list(map(lambda x: (x.id, f'{x.block.mobilization.name} {x.kind} {x.id}'), qs)) + form.base_fields['widget'].widget.choices = choices -@plugin_pool.register_plugin -class RowPlugin(CMSPluginBase): - model = Row - name = "Linha" - render_template = "campaign/plugins/row.html" - allow_children = True - child_classes = ["ColumnPlugin", "PicturePlugin"] - - -# @plugin_pool.register_plugin -# class ColumnPlugin(CMSPluginBase): -# model = Column -# name = "Coluna" -# render_template = "campaign/plugins/column.html" -# allow_children = True -# parent_classes = ["RowPlugin"] - + return form -@plugin_pool.register_plugin -class FooterPlugin(CMSPluginBase): - name = "Rodapé" - render_template = "campaign/plugins/footer.html" - allow_children = False + def render(self, context, instance, placeholder): + context = super(PressurePlugin, self).render(context, instance, placeholder) + context.update({"form": PressureForm()}) + return context \ No newline at end of file diff --git a/contrib/campaign/forms.py b/contrib/campaign/forms.py index 93ad06a3..509616a1 100644 --- a/contrib/campaign/forms.py +++ b/contrib/campaign/forms.py @@ -4,6 +4,10 @@ from cms.forms.wizards import CreateCMSPageForm from djangocms_text_ckeditor.widgets import TextEditorWidget +from contrib.bonde.widgets import BondeWidget + +from .models import Pressure + class CreatePressureForm(CreateCMSPageForm): content = None @@ -147,3 +151,12 @@ def __init__(self, *args, **kwargs): if isinstance(visible.field.widget, forms.Textarea): visible.field.widget.attrs["class"] += " h-28" + + + +class PressureSettingsForm(forms.ModelForm): + widget = forms.IntegerField(widget=forms.Select) + + class Meta: + model = Pressure + fields = ['widget'] \ No newline at end of file diff --git a/contrib/campaign/migrations/0008_auto_20230605_1417.py b/contrib/campaign/migrations/0008_auto_20230605_1417.py new file mode 100644 index 00000000..c8fce21f --- /dev/null +++ b/contrib/campaign/migrations/0008_auto_20230605_1417.py @@ -0,0 +1,43 @@ +# Generated by Django 3.2 on 2023-06-05 14:17 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('cms', '0022_auto_20180620_1551'), + ('campaign', '0007_delete_block'), + ] + + operations = [ + migrations.CreateModel( + name='Pressure', + fields=[ + ('cmsplugin_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='campaign_pressure', serialize=False, to='cms.cmsplugin')), + ('widget', models.IntegerField(blank=True, null=True)), + ], + options={ + 'abstract': False, + }, + bases=('cms.cmsplugin',), + ), + migrations.RemoveField( + model_name='column', + name='cmsplugin_ptr', + ), + migrations.RemoveField( + model_name='row', + name='cmsplugin_ptr', + ), + migrations.DeleteModel( + name='ActionButton', + ), + migrations.DeleteModel( + name='Column', + ), + migrations.DeleteModel( + name='Row', + ), + ] diff --git a/contrib/campaign/models.py b/contrib/campaign/models.py index 26bb3526..c1bbb60b 100644 --- a/contrib/campaign/models.py +++ b/contrib/campaign/models.py @@ -3,93 +3,5 @@ from cms.models import CMSPlugin -class BlockBase(CMSPlugin): - title = models.CharField("título", max_length=80, blank=True) - slug = models.SlugField( - verbose_name="slug", - max_length=80, - blank=True, - help_text="a parte do título que é usada na URL", - ) - menu_title = models.CharField("título do menu", max_length=50, blank=True) - menu_hidden = models.BooleanField("esconder menu?", default=False) - - # Styles - background = models.CharField( - "background", max_length=200, default="white", blank=True - ) - - class Meta: - abstract = True - - def __str__(self): - return self.title - - def get_menu_title(self): - return self.menu_title or self.title - - def get_background(self): - if self.background.startswith("url"): - return f"bg-[{self.background}] bg-no-repeat bg-cover" - - return f"bg-[{self.background}]" - - -# class Block(BlockBase): -# pass - -class ActionButton(CMSPlugin): - title = models.CharField("título", max_length=80) - action_url = models.CharField( - "endereço da ação", max_length=80, help_text="slug do bloco usado na URL" - ) - bg_color = models.CharField( - "cor do fundo", max_length=100, default="blue", blank=True - ) - - def get_bg_color(self): - if self.bg_color.startswith("bg-"): - return self.bg_color - - return f"bg-{{self.bg_color}}-800 hover:bg-{{self.bg_color}}-900" - - -class RowStyles(models.TextChoices): - flex = ("flex", "Flex") - wrap = ("wrap", "Wrap") - - -class Row(CMSPlugin): - styled = models.CharField( - "Estilo da linha", - max_length=20, - choices=RowStyles.choices, - default=RowStyles.flex - ) - - def classnames(self, attrs=None): - if self.styled == RowStyles.flex: - return 'flex flex-row items-center gap-8' - elif self.styled == RowStyles.wrap: - return 'flex flex-wrap gap-8 justify-center' - - return '' - - -class ColumnStyles(models.TextChoices): - auto = ("auto", "Auto") - - -class Column(CMSPlugin): - styled = models.CharField( - "Estilho da coluna", - max_length=20, - choices=ColumnStyles.choices, - default=ColumnStyles.auto - ) - - def classnames(self, attrs=None): - if self.styled == ColumnStyles.auto: - return 'flex-auto' - - return '' \ No newline at end of file +class Pressure(CMSPlugin): + widget = models.IntegerField(null=True, blank=True) \ No newline at end of file diff --git a/contrib/frontend/cms_plugins.py b/contrib/frontend/cms_plugins.py index 2ebee6e1..35bd6d8e 100644 --- a/contrib/frontend/cms_plugins.py +++ b/contrib/frontend/cms_plugins.py @@ -73,7 +73,7 @@ class ColumnPlugin(CMSPluginBase): module = "Frontend" render_template = "frontend/plugins/column.html" allow_children = True - child_classes = ["PicturePlugin", "TextPlugin", "ButtonPlugin"] + child_classes = ["PicturePlugin", "TextPlugin", "ButtonPlugin", "PressurePlugin"] def render(self, context, instance, placeholder): context = super(ColumnPlugin, self).render(context, instance, placeholder) diff --git a/contrib/frontend/migrations/0010_alter_block_alignment.py b/contrib/frontend/migrations/0010_alter_block_alignment.py new file mode 100644 index 00000000..97fc487c --- /dev/null +++ b/contrib/frontend/migrations/0010_alter_block_alignment.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2 on 2023-06-05 14:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('frontend', '0009_auto_20230602_1924'), + ] + + operations = [ + migrations.AlterField( + model_name='block', + name='alignment', + field=models.CharField(blank=True, choices=[('grid justify-items-center', 'Centralizar')], max_length=30, null=True, verbose_name='alinhamento'), + ), + ] From 02ecbeef052cc5d071ac3e4e338126e4fe970f3e Mon Sep 17 00:00:00 2001 From: Igor Santos Date: Tue, 6 Jun 2023 15:43:23 -0300 Subject: [PATCH 2/6] feat: add custom change form pressure --- .../templates/bonde/widgets/bonde_widget.html | 3 + contrib/bonde/views.py | 5 +- contrib/bonde/widgets.py | 20 +++ contrib/campaign/cms_plugins.py | 52 ++++-- contrib/campaign/forms.py | 20 ++- .../migrations/0009_auto_20230605_1704.py | 85 +++++++++ contrib/campaign/models.py | 76 +++++++- .../templates/campaign/admin/change_form.html | 165 ++++++++++++++++++ 8 files changed, 408 insertions(+), 18 deletions(-) create mode 100644 contrib/bonde/templates/bonde/widgets/bonde_widget.html create mode 100644 contrib/bonde/widgets.py create mode 100644 contrib/campaign/migrations/0009_auto_20230605_1704.py create mode 100644 contrib/campaign/templates/campaign/admin/change_form.html diff --git a/contrib/bonde/templates/bonde/widgets/bonde_widget.html b/contrib/bonde/templates/bonde/widgets/bonde_widget.html new file mode 100644 index 00000000..fe8fd8ae --- /dev/null +++ b/contrib/bonde/templates/bonde/widgets/bonde_widget.html @@ -0,0 +1,3 @@ +{% for subwidget in widget.subwidgets %} + {% include subwidget.template_name with widget=subwidget %} +{% endfor %} \ No newline at end of file diff --git a/contrib/bonde/views.py b/contrib/bonde/views.py index 91ea44a2..32c99898 100644 --- a/contrib/bonde/views.py +++ b/contrib/bonde/views.py @@ -1,3 +1,6 @@ from django.shortcuts import render -# Create your views here. + +# def select_widget(request): +# if request.is_ajax(): +# pass \ No newline at end of file diff --git a/contrib/bonde/widgets.py b/contrib/bonde/widgets.py new file mode 100644 index 00000000..1b2b3f23 --- /dev/null +++ b/contrib/bonde/widgets.py @@ -0,0 +1,20 @@ +from django.forms import MultiWidget, Select + + +class BondeWidget(MultiWidget): + description = "A Bonde widget reference" + template_name = "bonde/widgets/bonde_widget.html" + + def __init__(self, *args, **kwargs): + kwargs["widgets"] = { + "mobilization_id": Select, + "widget_id": Select + } + + super(BondeWidget, self).__init__(*args, **kwargs) + + + def decompress(self, value): + if value: + return [value.mobilization_id, value.widget_id] + return [] \ No newline at end of file diff --git a/contrib/campaign/cms_plugins.py b/contrib/campaign/cms_plugins.py index 9029d6e7..dc420192 100644 --- a/contrib/campaign/cms_plugins.py +++ b/contrib/campaign/cms_plugins.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from django.contrib import admin from django.db.models import Q from cms.plugin_base import CMSPluginBase @@ -6,29 +6,59 @@ from contrib.bonde.models import Widget -from .models import Pressure +from .models import Pressure, TargetGroup from .forms import PressureForm, PressureSettingsForm +class TargetGroupInline(admin.StackedInline): + model = TargetGroup + + @plugin_pool.register_plugin class PressurePlugin(CMSPluginBase): name = "Pressão" render_template = "campaign/plugins/pressure.html" + change_form_template = "campaign/admin/change_form.html" model = Pressure form = PressureSettingsForm + inlines = [TargetGroupInline, ] + fieldsets = [ + ("Alvos", {"fields": ["is_group"], "classes": ["tab-active"]}), + ("Email", {"fields": ["disable_editing"]}), + ( + "Envio", + { + "fields": ["submissions_limit", "submissions_interval"], + }, + ), + ( + "Agradecimento", + { + "fields": [ + "email_subject", + "sender_name", + "sender_email", + "email_body", + ], + }, + ), + ("Pós-ação", {"fields": ["sharing", "whatsapp_text"]}), + ] + + # def get_form(self, request, obj=None, change=False, **kwargs): + # form = super(PressurePlugin, self).get_form(request, obj, change, **kwargs) + + # qs = Widget.objects.on_site(request=request).filter(kind="pressure") - def get_form(self, request, obj=None, change=False, **kwargs): - form = super(PressurePlugin, self).get_form(request, obj, change, **kwargs) - - qs = Widget.objects.on_site(request=request).filter(kind='pressure') - - choices = list(map(lambda x: (x.id, f'{x.block.mobilization.name} {x.kind} {x.id}'), qs)) + # choices = list( + # map(lambda x: (x.id, f"{x.block.mobilization.name} {x.kind} {x.id}"), qs) + # ) - form.base_fields['widget'].widget.choices = choices + # form.base_fields["widget"].widget.choices = choices - return form + # return form def render(self, context, instance, placeholder): context = super(PressurePlugin, self).render(context, instance, placeholder) context.update({"form": PressureForm()}) - return context \ No newline at end of file + return context diff --git a/contrib/campaign/forms.py b/contrib/campaign/forms.py index 509616a1..3804c330 100644 --- a/contrib/campaign/forms.py +++ b/contrib/campaign/forms.py @@ -6,7 +6,7 @@ from contrib.bonde.widgets import BondeWidget -from .models import Pressure +from .models import Pressure, SharingChoices class CreatePressureForm(CreateCMSPageForm): @@ -153,10 +153,22 @@ def __init__(self, *args, **kwargs): visible.field.widget.attrs["class"] += " h-28" - class PressureSettingsForm(forms.ModelForm): - widget = forms.IntegerField(widget=forms.Select) + # widget = forms.IntegerField(widget=forms.Select) + + is_group = forms.ChoiceField( + label="Tipo", + choices=((False, "Um grupo de alvos"), (True, "Mais de um grupo")), + widget=forms.RadioSelect + ) + + sharing = forms.MultipleChoiceField( + label="Opções de compartilhamento", + choices=SharingChoices.choices, + widget=forms.CheckboxSelectMultiple + ) class Meta: model = Pressure - fields = ['widget'] \ No newline at end of file + # fields = "__all__" + exclude = ["widget"] diff --git a/contrib/campaign/migrations/0009_auto_20230605_1704.py b/contrib/campaign/migrations/0009_auto_20230605_1704.py new file mode 100644 index 00000000..7b539e5f --- /dev/null +++ b/contrib/campaign/migrations/0009_auto_20230605_1704.py @@ -0,0 +1,85 @@ +# Generated by Django 3.2 on 2023-06-05 17:04 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('campaign', '0008_auto_20230605_1417'), + ] + + operations = [ + migrations.CreateModel( + name='Target', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, verbose_name='Nome do alvo')), + ('email', models.EmailField(blank=True, max_length=100, null=True, verbose_name='Email do alvo')), + ('phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='Telefone do alvo')), + ], + ), + migrations.AddField( + model_name='pressure', + name='disable_editing', + field=models.BooleanField(default=True, verbose_name='Desabilitar edição do e-mail e do assunto pelos ativistas?'), + ), + migrations.AddField( + model_name='pressure', + name='email_body', + field=models.TextField(default='', verbose_name='Corpo do e-mail de agradecimento'), + preserve_default=False, + ), + migrations.AddField( + model_name='pressure', + name='email_subject', + field=models.CharField(default='', max_length=120, verbose_name='Assunto do e-mail de agradecimento para quem vai pressionar'), + preserve_default=False, + ), + migrations.AddField( + model_name='pressure', + name='sender_email', + field=models.EmailField(default='', max_length=254, verbose_name='Email de resposta'), + preserve_default=False, + ), + migrations.AddField( + model_name='pressure', + name='sender_name', + field=models.CharField(default='', max_length=120, verbose_name='Remetente'), + preserve_default=False, + ), + migrations.AddField( + model_name='pressure', + name='sharing', + field=models.CharField(choices=[('whatsapp', 'Whatsapp'), ('twitter', 'Twitter'), ('facebook', 'Facebook')], default='', max_length=50, verbose_name='Opções de compartilhamento'), + preserve_default=False, + ), + migrations.AddField( + model_name='pressure', + name='submissions_interval', + field=models.IntegerField(blank=True, null=True, verbose_name='Intervalo de envio'), + ), + migrations.AddField( + model_name='pressure', + name='submissions_limit', + field=models.IntegerField(blank=True, null=True, verbose_name='Limite de envios únicos'), + ), + migrations.AddField( + model_name='pressure', + name='whatsapp_text', + field=models.TextField(default='', verbose_name='Mensagem para o whatsapp'), + preserve_default=False, + ), + migrations.CreateModel( + name='TargetGroup', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50, verbose_name='Nome do grupo')), + ('email_subject', models.CharField(blank=True, max_length=120, null=True, verbose_name='Assunto do e-mail para os alvos')), + ('email_body', models.TextField(blank=True, null=True, verbose_name='Corpo do e-mail para os alvos')), + ('pressure', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='campaign.pressure')), + ('targets', models.ManyToManyField(to='campaign.Target')), + ], + ), + ] diff --git a/contrib/campaign/models.py b/contrib/campaign/models.py index c1bbb60b..aa5f43fe 100644 --- a/contrib/campaign/models.py +++ b/contrib/campaign/models.py @@ -3,5 +3,77 @@ from cms.models import CMSPlugin -class Pressure(CMSPlugin): - widget = models.IntegerField(null=True, blank=True) \ No newline at end of file +class Thank(models.Model): + # Agradecimento + email_subject = models.CharField( + verbose_name="Assunto do e-mail de agradecimento para quem vai pressionar", + max_length=120, + ) + sender_name = models.CharField(verbose_name="Remetente", max_length=120) + sender_email = models.EmailField(verbose_name="Email de resposta") + email_body = models.TextField(verbose_name="Corpo do e-mail de agradecimento") + + class Meta: + abstract = True + + +class SharingChoices(models.TextChoices): + whatsapp = "whatsapp", "Whatsapp" + twitter = "twitter", "Twitter" + facebook = "facebook", "Facebook" + + +class PostAction(models.Model): + # Pós ação + sharing = models.CharField( + verbose_name="Opções de compartilhamento", + choices=SharingChoices.choices, + max_length=50, + ) + whatsapp_text = models.TextField(verbose_name="Mensagem para o whatsapp") + + class Meta: + abstract = True + + +class Target(models.Model): + name = models.CharField(verbose_name="Nome do alvo", max_length=100) + email = models.EmailField( + verbose_name="Email do alvo", max_length=100, null=True, blank=True + ) + phone = models.CharField( + verbose_name="Telefone do alvo", max_length=20, null=True, blank=True + ) + + +class Pressure(PostAction, Thank, CMSPlugin): + widget = models.IntegerField(null=True, blank=True) + + # Envio + submissions_limit = models.IntegerField( + verbose_name="Limite de envios únicos", null=True, blank=True + ) + submissions_interval = models.IntegerField( + verbose_name="Intervalo de envio", null=True, blank=True + ) + + disable_editing = models.BooleanField( + verbose_name="Desabilitar edição do e-mail e do assunto pelos ativistas?", + default=True, + ) + + +class TargetGroup(models.Model): + name = models.CharField(verbose_name="Nome do grupo", max_length=50) + targets = models.ManyToManyField(Target) + email_subject = models.CharField( + verbose_name="Assunto do e-mail para os alvos", + max_length=120, + null=True, + blank=True, + ) + email_body = models.TextField( + verbose_name="Corpo do e-mail para os alvos", null=True, blank=True + ) + + pressure = models.ForeignKey(Pressure, on_delete=models.CASCADE) diff --git a/contrib/campaign/templates/campaign/admin/change_form.html b/contrib/campaign/templates/campaign/admin/change_form.html new file mode 100644 index 00000000..a4d0c3e1 --- /dev/null +++ b/contrib/campaign/templates/campaign/admin/change_form.html @@ -0,0 +1,165 @@ +{% extends "admin/change_form.html" %} +{% load i18n admin_urls static admin_modify %} + +{% block extrastyle %} +{{ block.super }} + + + +{% endblock %} + +{% block content %} +
+
+ {% csrf_token %} + {% block form_top %}{% endblock %} + + {% if is_popup %} + + {% endif %} + {% if to_field %} + + {% endif %} +
+
+ + + +
+ +
+ {% for fieldset in adminform %} + + {{ fieldset.name }} + + {% endfor %} +
+ +
+ {% for fieldset in adminform %} + + {% endfor %} + + {% block admin_change_form_document_ready %} + {{ block.super }} + + + {% endblock %} + + {# JavaScript for prepopulated fields #} + {% prepopulated_fields_js %} +
+
+
+
+
+ + + {% block submit_buttons_bottom %}{% submit_row %}{% endblock %} +
+
+
+
+{% endblock %} \ No newline at end of file From bb9d644359d9e0d2630c3d7f70e20bfadb3e4545 Mon Sep 17 00:00:00 2001 From: Igor Santos Date: Wed, 7 Jun 2023 16:07:25 -0300 Subject: [PATCH 3/6] feat: implement InputArrayField --- .../templates/campaign/admin/change_form.html | 63 ++++------------- .../campaign/admin/includes/tab_panel.html | 30 ++++++++ tailwind/fields.py | 32 +++++++++ .../tailwind/widgets/input_array.html | 68 +++++++++++++++++++ .../templates/tailwind/widgets/radio.html | 21 ++++++ .../tailwind/widgets/radio_option.html | 8 +++ tailwind/widgets.py | 26 +++++++ 7 files changed, 197 insertions(+), 51 deletions(-) create mode 100644 contrib/campaign/templates/campaign/admin/includes/tab_panel.html create mode 100644 tailwind/fields.py create mode 100644 tailwind/templates/tailwind/widgets/input_array.html create mode 100644 tailwind/templates/tailwind/widgets/radio.html create mode 100644 tailwind/templates/tailwind/widgets/radio_option.html create mode 100644 tailwind/widgets.py diff --git a/contrib/campaign/templates/campaign/admin/change_form.html b/contrib/campaign/templates/campaign/admin/change_form.html index a4d0c3e1..edede0ec 100644 --- a/contrib/campaign/templates/campaign/admin/change_form.html +++ b/contrib/campaign/templates/campaign/admin/change_form.html @@ -24,7 +24,7 @@
- +
@@ -38,58 +38,19 @@
+
{% for fieldset in adminform %} - + {% include "campaign/admin/includes/tab_panel.html" %} {% endfor %} - +
+ + {% if errors %} +

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

+ {{ adminform.form.non_field_errors }} + {% endif %} + {% block admin_change_form_document_ready %} {{ block.super }} \ No newline at end of file diff --git a/tailwind/templates/tailwind/widgets/radio.html b/tailwind/templates/tailwind/widgets/radio.html new file mode 100644 index 00000000..2ae2add1 --- /dev/null +++ b/tailwind/templates/tailwind/widgets/radio.html @@ -0,0 +1,21 @@ +{% with id=widget.attrs.id %} +
+ {% for group, options, index in widget.optgroups %} + {% if group %} +
+ + {% endif %} + {% for option in options %} +
+ {% include option.template_name with widget=option %} +
+ {% endfor %} + {% if group %} +
+ {% endif %} + {% endfor %} +
+{% endwith %} \ No newline at end of file diff --git a/tailwind/templates/tailwind/widgets/radio_option.html b/tailwind/templates/tailwind/widgets/radio_option.html new file mode 100644 index 00000000..0e1d28e3 --- /dev/null +++ b/tailwind/templates/tailwind/widgets/radio_option.html @@ -0,0 +1,8 @@ +{% if widget.wrap_label %} + +{% endif %} \ No newline at end of file diff --git a/tailwind/widgets.py b/tailwind/widgets.py new file mode 100644 index 00000000..97dc5a0d --- /dev/null +++ b/tailwind/widgets.py @@ -0,0 +1,26 @@ +import json +from typing import Any, Optional +from django import forms + + +class RadioSelect(forms.RadioSelect): + template_name = "tailwind/widgets/radio.html" + option_template_name = "tailwind/widgets/radio_option.html" + + +class InputArrayWidget(forms.MultiWidget): + template_name = "tailwind/widgets/input_array.html" + + def get_context(self, name, value, attrs): + if type(value) == str: + value = json.loads(value) + + value = list(filter(lambda x: x, value)) + + return { + "widget": {"name": name, "value": value, "attrs": attrs}, + "input_array_limit": len(self.widgets), + } + + def decompress(self, value): + return json.loads(value) \ No newline at end of file From c124e51865dc3cf65d03b9b864f9706ada7dab41 Mon Sep 17 00:00:00 2001 From: Igor Santos Date: Wed, 7 Jun 2023 18:22:16 -0300 Subject: [PATCH 4/6] feat: mount pressure plugin form to settings --- contrib/campaign/cms_plugins.py | 95 ++++++++++--- contrib/campaign/cms_wizards.py | 106 +++++++------- contrib/campaign/forms.py | 133 ++++-------------- .../migrations/0010_auto_20230607_1917.py | 21 +++ .../migrations/0011_auto_20230607_2053.py | 36 +++++ .../migrations/0012_auto_20230607_2056.py | 25 ++++ contrib/campaign/models.py | 39 +++-- .../templates/campaign/admin/change_form.html | 9 +- .../campaign/admin/edit_inline/stacked.html | 29 ++++ .../campaign/admin/includes/tab_panel.html | 6 + .../templatetags/campaign_form_tags.py | 10 +- .../tailwind/widgets/checkbox_option.html | 1 + .../tailwind/widgets/checkbox_select.html | 1 + .../tailwind/widgets/input_option.html | 8 ++ .../tailwind/widgets/multiple_input.html | 21 +++ .../templates/tailwind/widgets/radio.html | 22 +-- .../tailwind/widgets/radio_option.html | 9 +- tailwind/widgets.py | 8 +- 18 files changed, 355 insertions(+), 224 deletions(-) create mode 100644 contrib/campaign/migrations/0010_auto_20230607_1917.py create mode 100644 contrib/campaign/migrations/0011_auto_20230607_2053.py create mode 100644 contrib/campaign/migrations/0012_auto_20230607_2056.py create mode 100644 contrib/campaign/templates/campaign/admin/edit_inline/stacked.html create mode 100644 tailwind/templates/tailwind/widgets/checkbox_option.html create mode 100644 tailwind/templates/tailwind/widgets/checkbox_select.html create mode 100644 tailwind/templates/tailwind/widgets/input_option.html create mode 100644 tailwind/templates/tailwind/widgets/multiple_input.html diff --git a/contrib/campaign/cms_plugins.py b/contrib/campaign/cms_plugins.py index dc420192..0c12fef3 100644 --- a/contrib/campaign/cms_plugins.py +++ b/contrib/campaign/cms_plugins.py @@ -1,5 +1,4 @@ from django.contrib import admin -from django.db.models import Q from cms.plugin_base import CMSPluginBase from cms.plugin_pool import plugin_pool @@ -11,7 +10,10 @@ class TargetGroupInline(admin.StackedInline): + fieldset = "Alvos" model = TargetGroup + template = "campaign/admin/edit_inline/stacked.html" + extra = 1 @plugin_pool.register_plugin @@ -20,30 +22,42 @@ class PressurePlugin(CMSPluginBase): render_template = "campaign/plugins/pressure.html" change_form_template = "campaign/admin/change_form.html" model = Pressure + # model = Dummy form = PressureSettingsForm - inlines = [TargetGroupInline, ] + inlines = [ + TargetGroupInline, + ] fieldsets = [ - ("Alvos", {"fields": ["is_group"], "classes": ["tab-active"]}), - ("Email", {"fields": ["disable_editing"]}), - ( - "Envio", - { - "fields": ["submissions_limit", "submissions_interval"], - }, - ), - ( - "Agradecimento", - { - "fields": [ - "email_subject", - "sender_name", - "sender_email", - "email_body", - ], - }, - ), - ("Pós-ação", {"fields": ["sharing", "whatsapp_text"]}), + ("Alvos", {"fields": [], "classes": ["tab-active"]}), + ("Email", {"fields": ["email_subject", "email_body", "disable_editing"]}), + ("Envio", {"fields": ["submissions_limit", "submissions_interval"]}), + ("Agradecimento", {"fields": ["thank_email_subject", "sender_name", "sender_email", "thank_email_body"]}), + ("Pós-ação", {"fields": ["sharing", "whatsapp_text"]}) ] + # fieldsets = [ + # ("Alvos", {"fields": ["is_group", "email_subject"], "classes": ["tab-active"]}), + # ("Email", {"fields": ["disable_editing"]}), + # ( + # "Envio", + # { + # "fields": ["submissions_limit", "submissions_interval"], + # }, + # ), + # ( + # "Agradecimento", + # { + # "fields": [ + # "email_subject", + # "sender_name", + # "sender_email", + # "email_body", + # ], + # }, + # ), + # ("Pós-ação", {"fields": ["sharing", "whatsapp_text"]}), + # ] + + # return super().save_form(request, form, change) # def get_form(self, request, obj=None, change=False, **kwargs): # form = super(PressurePlugin, self).get_form(request, obj, change, **kwargs) @@ -58,6 +72,43 @@ class PressurePlugin(CMSPluginBase): # return form + def render_change_form( + self, request, context, add=False, change=False, form_url="", obj=None + ): + sorted_inline_formsets = {} + inline_admin_formsets = context["inline_admin_formsets"] + formsets_to_remove = [] + + for inline_formset in inline_admin_formsets: + if hasattr(inline_formset.opts, "fieldset"): + fieldset = inline_formset.opts.fieldset + if fieldset in sorted_inline_formsets: + sorted_inline_formsets[fieldset].append(inline_formset) + else: + sorted_inline_formsets.update( + { + fieldset: [ + inline_formset, + ] + } + ) + formsets_to_remove.append(inline_formset) + + for inline_formset in formsets_to_remove: + inline_admin_formsets.remove(inline_formset) + + context.update( + { + "sorted_inline_formsets": sorted_inline_formsets, + "inline_admin_formsets": inline_admin_formsets, + } + ) + + # import ipdb;ipdb.set_trace() + return super(PressurePlugin, self).render_change_form( + request, context, add, change, form_url, obj + ) + def render(self, context, instance, placeholder): context = super(PressurePlugin, self).render(context, instance, placeholder) context.update({"form": PressureForm()}) diff --git a/contrib/campaign/cms_wizards.py b/contrib/campaign/cms_wizards.py index e0212a4d..2d33dde2 100644 --- a/contrib/campaign/cms_wizards.py +++ b/contrib/campaign/cms_wizards.py @@ -1,68 +1,68 @@ -from django import forms +# from django import forms -from cms.forms.wizards import CreateCMSPageForm -# from cms.admin.forms import AddPageForm -from cms.wizards.wizard_base import Wizard -from cms.wizards.wizard_pool import wizard_pool -from cms.models import Page -from cms.utils.page_permissions import user_can_add_page, user_can_add_subpage +# from cms.forms.wizards import CreateCMSPageForm +# # from cms.admin.forms import AddPageForm +# from cms.wizards.wizard_base import Wizard +# from cms.wizards.wizard_pool import wizard_pool +# from cms.models import Page +# from cms.utils.page_permissions import user_can_add_page, user_can_add_subpage -# from .forms.wizards import CreateCMSPageForm, CreateCMSSubPageForm -# from .wizards.wizard_base import Wizard -# from .wizards.wizard_pool import wizard_pool -from .forms import CreatePressureForm +# # from .forms.wizards import CreateCMSPageForm, CreateCMSSubPageForm +# # from .wizards.wizard_base import Wizard +# # from .wizards.wizard_pool import wizard_pool +# from .forms import CreatePressureForm -class CMSPressureWizard(Wizard): +# class CMSPressureWizard(Wizard): - def user_has_add_permission(self, user, page=None, **kwargs): - if page: - parent_page = page.get_parent_page() - else: - parent_page = None +# def user_has_add_permission(self, user, page=None, **kwargs): +# if page: +# parent_page = page.get_parent_page() +# else: +# parent_page = None - if page and page.get_parent_page(): - # User is adding a page which will be a right - # sibling to the current page. - has_perm = user_can_add_subpage(user, target=parent_page) - else: - has_perm = user_can_add_page(user) - return has_perm +# if page and page.get_parent_page(): +# # User is adding a page which will be a right +# # sibling to the current page. +# has_perm = user_can_add_subpage(user, target=parent_page) +# else: +# has_perm = user_can_add_page(user) +# return has_perm -class CMSDonationWizard(Wizard): +# class CMSDonationWizard(Wizard): - def user_has_add_permission(self, user, page=None, **kwargs): - if page: - parent_page = page.get_parent_page() - else: - parent_page = None +# def user_has_add_permission(self, user, page=None, **kwargs): +# if page: +# parent_page = page.get_parent_page() +# else: +# parent_page = None - if page and page.get_parent_page(): - # User is adding a page which will be a right - # sibling to the current page. - has_perm = user_can_add_subpage(user, target=parent_page) - else: - has_perm = user_can_add_page(user) - return has_perm +# if page and page.get_parent_page(): +# # User is adding a page which will be a right +# # sibling to the current page. +# has_perm = user_can_add_subpage(user, target=parent_page) +# else: +# has_perm = user_can_add_page(user) +# return has_perm -cms_pressure_wizard = CMSPressureWizard( - title="Campanha de Pressão", - weight=100, - form=CreatePressureForm, - model=Page, - description="Pressão por e-mail Explica tática" -) +# cms_pressure_wizard = CMSPressureWizard( +# title="Campanha de Pressão", +# weight=100, +# form=CreatePressureForm, +# model=Page, +# description="Pressão por e-mail Explica tática" +# ) -cms_donation_wizard = CMSDonationWizard( - title="Campanha de Doação", - weight=100, - form=CreatePressureForm, - model=Page, - description="Doação Explica tática" -) +# cms_donation_wizard = CMSDonationWizard( +# title="Campanha de Doação", +# weight=100, +# form=CreatePressureForm, +# model=Page, +# description="Doação Explica tática" +# ) -wizard_pool.register(cms_pressure_wizard) -wizard_pool.register(cms_donation_wizard) \ No newline at end of file +# wizard_pool.register(cms_pressure_wizard) +# wizard_pool.register(cms_donation_wizard) \ No newline at end of file diff --git a/contrib/campaign/forms.py b/contrib/campaign/forms.py index 3804c330..ed8c47d5 100644 --- a/contrib/campaign/forms.py +++ b/contrib/campaign/forms.py @@ -1,112 +1,18 @@ +from typing import Any, Dict from django import forms from django.db import transaction -from cms.forms.wizards import CreateCMSPageForm +# from cms.forms.wizards import CreateCMSPageForm from djangocms_text_ckeditor.widgets import TextEditorWidget +from tailwind.widgets import RadioSelect, CheckboxSelectMultiple +from tailwind.fields import InputArrayField + from contrib.bonde.widgets import BondeWidget from .models import Pressure, SharingChoices -class CreatePressureForm(CreateCMSPageForm): - content = None - storytelling = forms.CharField(label="Narrativa", widget=TextEditorWidget) - - def get_template(self): - return "campaign/modelo1.html" - - @transaction.atomic - def save(self, content=None, **kwargs): - from cms.api import add_plugin - - new_page = super(CreatePressureForm, self).save(**kwargs) - new_page.rescan_placeholders() - - slot = "content" - - placeholder = self.get_placeholder(new_page, slot=slot) - - # Trabalhando bloco principal - hero_block_plugin = add_plugin( - placeholder=placeholder, - plugin_type="BlockPlugin", - language=self.language_code, - title="Hero", - slug="hero", - background="#e40523", - ) - - # Trabalhando bloco de pressão - pressure_block_plugin = add_plugin( - placeholder=placeholder, - plugin_type="GridBlockPlugin", - language=self.language_code, - title="Action", - slug="action", - background="black", - ) - - row_plugin = add_plugin( - placeholder=placeholder, - language=self.language_code, - target=pressure_block_plugin, - plugin_type="RowPlugin", - ) - - col_left_plugin = add_plugin( - placeholder=placeholder, - language=self.language_code, - target=row_plugin, - plugin_type="ColumnPlugin", - ) - storytelling = self.cleaned_data.get("storytelling") - add_plugin( - placeholder=placeholder, - language=self.language_code, - target=col_left_plugin, - plugin_type="TextPlugin", - body=storytelling, - ) - - col_right_plugin = add_plugin( - placeholder=placeholder, - language=self.language_code, - target=row_plugin, - plugin_type="ColumnPlugin", - ) - add_plugin( - placeholder=placeholder, - language=self.language_code, - target=col_right_plugin, - plugin_type="PressurePlugin", - ) - - # Trabalhando bloco de assinatura - signature_block_plugin = add_plugin( - placeholder=placeholder, - plugin_type="BlockPlugin", - language=self.language_code, - title="Signature", - slug="signature", - ) - - # content = self.cleaned_data.get("storytelling") - # add_plugin( - # placeholder=placeholder, - # plugin_type="TextPlugin", - # language=self.language_code, - # target=hero_block_plugin, - # body=content - # ) - - return new_page - - # import ipdb;ipdb.set_trace() - # Criar página padrão para tipo de form - # pass - - class PressureForm(forms.Form): # People Fields email_address = forms.EmailField( @@ -156,19 +62,36 @@ def __init__(self, *args, **kwargs): class PressureSettingsForm(forms.ModelForm): # widget = forms.IntegerField(widget=forms.Select) - is_group = forms.ChoiceField( - label="Tipo", - choices=((False, "Um grupo de alvos"), (True, "Mais de um grupo")), - widget=forms.RadioSelect + email_subject = InputArrayField( + label="Assunto do e-mail para os alvos", + num_widgets=10 + ) + + disable_editing = forms.ChoiceField( + label="Desabilitar edição do e-mail e do assunto pelos ativistas?", + choices=( + (True, "Desabilitar"), + (False, "Habilitar") + ), + widget=RadioSelect, + initial=True ) sharing = forms.MultipleChoiceField( label="Opções de compartilhamento", choices=SharingChoices.choices, - widget=forms.CheckboxSelectMultiple + widget=CheckboxSelectMultiple ) class Meta: model = Pressure - # fields = "__all__" exclude = ["widget"] + + def clean(self): + cleaned_data = super(PressureSettingsForm, self).clean() + # import ipdb;ipdb.set_trace() + return cleaned_data + + def save(self, commit): + import ipdb;ipdb.set_trace() + return super(PressureSettingsForm, self).save(commit) \ No newline at end of file diff --git a/contrib/campaign/migrations/0010_auto_20230607_1917.py b/contrib/campaign/migrations/0010_auto_20230607_1917.py new file mode 100644 index 00000000..94ab9a7c --- /dev/null +++ b/contrib/campaign/migrations/0010_auto_20230607_1917.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2 on 2023-06-07 19:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('campaign', '0009_auto_20230605_1704'), + ] + + operations = [ + migrations.RemoveField( + model_name='targetgroup', + name='email_body', + ), + migrations.RemoveField( + model_name='targetgroup', + name='email_subject', + ), + ] diff --git a/contrib/campaign/migrations/0011_auto_20230607_2053.py b/contrib/campaign/migrations/0011_auto_20230607_2053.py new file mode 100644 index 00000000..cc5bf2a5 --- /dev/null +++ b/contrib/campaign/migrations/0011_auto_20230607_2053.py @@ -0,0 +1,36 @@ +# Generated by Django 3.2 on 2023-06-07 20:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('campaign', '0010_auto_20230607_1917'), + ] + + operations = [ + migrations.AlterModelOptions( + name='target', + options={'verbose_name': 'Alvo', 'verbose_name_plural': 'Alvos'}, + ), + migrations.AlterModelOptions( + name='targetgroup', + options={'verbose_name': 'Grupo de alvos', 'verbose_name_plural': 'Grupos de alvos'}, + ), + migrations.RenameField( + model_name='pressure', + old_name='email_body', + new_name='thank_email_body', + ), + migrations.RenameField( + model_name='pressure', + old_name='email_subject', + new_name='thank_email_subject', + ), + migrations.AlterField( + model_name='targetgroup', + name='targets', + field=models.ManyToManyField(to='campaign.Target', verbose_name='Alvos'), + ), + ] diff --git a/contrib/campaign/migrations/0012_auto_20230607_2056.py b/contrib/campaign/migrations/0012_auto_20230607_2056.py new file mode 100644 index 00000000..10b5144f --- /dev/null +++ b/contrib/campaign/migrations/0012_auto_20230607_2056.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2 on 2023-06-07 20:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('campaign', '0011_auto_20230607_2053'), + ] + + operations = [ + migrations.AddField( + model_name='pressure', + name='email_body', + field=models.TextField(default='', verbose_name='Corpo do e-mail para os alvos'), + preserve_default=False, + ), + migrations.AddField( + model_name='pressure', + name='email_subject', + field=models.JSONField(default='', verbose_name='Assunto do e-mail para os alvos'), + preserve_default=False, + ), + ] diff --git a/contrib/campaign/models.py b/contrib/campaign/models.py index aa5f43fe..a9c8e01a 100644 --- a/contrib/campaign/models.py +++ b/contrib/campaign/models.py @@ -5,13 +5,13 @@ class Thank(models.Model): # Agradecimento - email_subject = models.CharField( + thank_email_subject = models.CharField( verbose_name="Assunto do e-mail de agradecimento para quem vai pressionar", max_length=120, ) + thank_email_body = models.TextField(verbose_name="Corpo do e-mail de agradecimento") sender_name = models.CharField(verbose_name="Remetente", max_length=120) sender_email = models.EmailField(verbose_name="Email de resposta") - email_body = models.TextField(verbose_name="Corpo do e-mail de agradecimento") class Meta: abstract = True @@ -45,10 +45,21 @@ class Target(models.Model): verbose_name="Telefone do alvo", max_length=20, null=True, blank=True ) + class Meta: + verbose_name ="Alvo" + verbose_name_plural = "Alvos" + class Pressure(PostAction, Thank, CMSPlugin): widget = models.IntegerField(null=True, blank=True) + email_subject = models.JSONField( + verbose_name="Assunto do e-mail para os alvos" + ) + email_body = models.TextField( + verbose_name="Corpo do e-mail para os alvos" + ) + # Envio submissions_limit = models.IntegerField( verbose_name="Limite de envios únicos", null=True, blank=True @@ -62,18 +73,22 @@ class Pressure(PostAction, Thank, CMSPlugin): default=True, ) + def copy_relations(self, old_instance): + # https://docs.django-cms.org/en/latest/how_to/custom_plugins.html#handling-relations + self.targetgroup_set.delete() + + for item in old_instance.targetgroup_set.all(): + item.pk = None + item.plugin = self + item.save() + class TargetGroup(models.Model): name = models.CharField(verbose_name="Nome do grupo", max_length=50) - targets = models.ManyToManyField(Target) - email_subject = models.CharField( - verbose_name="Assunto do e-mail para os alvos", - max_length=120, - null=True, - blank=True, - ) - email_body = models.TextField( - verbose_name="Corpo do e-mail para os alvos", null=True, blank=True - ) + targets = models.ManyToManyField(Target, verbose_name="Alvos") pressure = models.ForeignKey(Pressure, on_delete=models.CASCADE) + + class Meta: + verbose_name = "Grupo de alvos" + verbose_name_plural = "Grupos de alvos" diff --git a/contrib/campaign/templates/campaign/admin/change_form.html b/contrib/campaign/templates/campaign/admin/change_form.html index edede0ec..54450855 100644 --- a/contrib/campaign/templates/campaign/admin/change_form.html +++ b/contrib/campaign/templates/campaign/admin/change_form.html @@ -40,7 +40,7 @@
{% for fieldset in adminform %} - {% include "campaign/admin/includes/tab_panel.html" %} + {% include "campaign/admin/includes/tab_panel.html" with sorted_inline_formsets=sorted_inline_formsets %} {% endfor %}
@@ -50,6 +50,13 @@

{{ adminform.form.non_field_errors }} {% endif %} + + {% block inline_field_sets %} + {% for inline_admin_formset in inline_admin_formsets %} + {% include inline_admin_formset.opts.template %} + {% endfor %} + {% endblock %} + {% block admin_change_form_document_ready %} {{ block.super }} diff --git a/contrib/campaign/templates/campaign/admin/edit_inline/stacked.html b/contrib/campaign/templates/campaign/admin/edit_inline/stacked.html new file mode 100644 index 00000000..190341b0 --- /dev/null +++ b/contrib/campaign/templates/campaign/admin/edit_inline/stacked.html @@ -0,0 +1,29 @@ +{% load i18n admin_urls %} +
+
+ +{{ inline_admin_formset.formset.management_form }} +{{ inline_admin_formset.formset.non_form_errors }} + +{% for inline_admin_form in inline_admin_formset %}
+

{{ inline_admin_formset.opts.verbose_name|capfirst }}: {% if inline_admin_form.original %}{{ inline_admin_form.original }}{% if inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %} {% if inline_admin_formset.has_change_permission %}{% translate "Change" %}{% else %}{% translate "View" %}{% endif %}{% endif %} +{% else %}#{{ forloop.counter }}{% endif %} + {% if inline_admin_form.show_url %}{% translate "View on site" %}{% endif %} + {% if inline_admin_formset.formset.can_delete and inline_admin_formset.has_delete_permission and inline_admin_form.original %}{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}{% endif %} +

+ {% if inline_admin_form.form.non_field_errors %}{{ inline_admin_form.form.non_field_errors }}{% endif %} + {% for fieldset in inline_admin_form %} + {% include "admin/includes/fieldset.html" %} + {% endfor %} + {% if inline_admin_form.needs_explicit_pk_field %}{{ inline_admin_form.pk_field.field }}{% endif %} + {% if inline_admin_form.fk_field %}{{ inline_admin_form.fk_field.field }}{% endif %} +
{% endfor %} +
+
\ No newline at end of file diff --git a/contrib/campaign/templates/campaign/admin/includes/tab_panel.html b/contrib/campaign/templates/campaign/admin/includes/tab_panel.html index a0f42108..2fd53145 100644 --- a/contrib/campaign/templates/campaign/admin/includes/tab_panel.html +++ b/contrib/campaign/templates/campaign/admin/includes/tab_panel.html @@ -1,3 +1,5 @@ +{% load campaign_form_tags %} + {% endfor %} {% endfor %} + + {% for inline_admin_formset in sorted_inline_formsets|get_dict_value:fieldset.name %} + {% include inline_admin_formset.opts.template %} + {% endfor %}
\ No newline at end of file diff --git a/contrib/campaign/templatetags/campaign_form_tags.py b/contrib/campaign/templatetags/campaign_form_tags.py index 5b1ebe69..5b0b6aeb 100644 --- a/contrib/campaign/templatetags/campaign_form_tags.py +++ b/contrib/campaign/templatetags/campaign_form_tags.py @@ -8,4 +8,12 @@ def add_class(field, class_name): return field.as_widget(attrs={ "class": " ".join((field.css_classes(), class_name)) - }) \ No newline at end of file + }) + + +@register.filter +def get_dict_value(data, key): + if key in data: + return data[key] + else: + return [] \ No newline at end of file diff --git a/tailwind/templates/tailwind/widgets/checkbox_option.html b/tailwind/templates/tailwind/widgets/checkbox_option.html new file mode 100644 index 00000000..ae3e5dcb --- /dev/null +++ b/tailwind/templates/tailwind/widgets/checkbox_option.html @@ -0,0 +1 @@ +{% include "tailwind/widgets/input_option.html" %} \ No newline at end of file diff --git a/tailwind/templates/tailwind/widgets/checkbox_select.html b/tailwind/templates/tailwind/widgets/checkbox_select.html new file mode 100644 index 00000000..25e7787a --- /dev/null +++ b/tailwind/templates/tailwind/widgets/checkbox_select.html @@ -0,0 +1 @@ +{% include "tailwind/widgets/multiple_input.html" %} \ No newline at end of file diff --git a/tailwind/templates/tailwind/widgets/input_option.html b/tailwind/templates/tailwind/widgets/input_option.html new file mode 100644 index 00000000..0e1d28e3 --- /dev/null +++ b/tailwind/templates/tailwind/widgets/input_option.html @@ -0,0 +1,8 @@ +{% if widget.wrap_label %} + +{% endif %} \ No newline at end of file diff --git a/tailwind/templates/tailwind/widgets/multiple_input.html b/tailwind/templates/tailwind/widgets/multiple_input.html new file mode 100644 index 00000000..2ae2add1 --- /dev/null +++ b/tailwind/templates/tailwind/widgets/multiple_input.html @@ -0,0 +1,21 @@ +{% with id=widget.attrs.id %} +
+ {% for group, options, index in widget.optgroups %} + {% if group %} +
+ + {% endif %} + {% for option in options %} +
+ {% include option.template_name with widget=option %} +
+ {% endfor %} + {% if group %} +
+ {% endif %} + {% endfor %} +
+{% endwith %} \ No newline at end of file diff --git a/tailwind/templates/tailwind/widgets/radio.html b/tailwind/templates/tailwind/widgets/radio.html index 2ae2add1..25e7787a 100644 --- a/tailwind/templates/tailwind/widgets/radio.html +++ b/tailwind/templates/tailwind/widgets/radio.html @@ -1,21 +1 @@ -{% with id=widget.attrs.id %} -
- {% for group, options, index in widget.optgroups %} - {% if group %} -
- - {% endif %} - {% for option in options %} -
- {% include option.template_name with widget=option %} -
- {% endfor %} - {% if group %} -
- {% endif %} - {% endfor %} -
-{% endwith %} \ No newline at end of file +{% include "tailwind/widgets/multiple_input.html" %} \ No newline at end of file diff --git a/tailwind/templates/tailwind/widgets/radio_option.html b/tailwind/templates/tailwind/widgets/radio_option.html index 0e1d28e3..ae3e5dcb 100644 --- a/tailwind/templates/tailwind/widgets/radio_option.html +++ b/tailwind/templates/tailwind/widgets/radio_option.html @@ -1,8 +1 @@ -{% if widget.wrap_label %} - -{% endif %} \ No newline at end of file +{% include "tailwind/widgets/input_option.html" %} \ No newline at end of file diff --git a/tailwind/widgets.py b/tailwind/widgets.py index 97dc5a0d..6dc9202e 100644 --- a/tailwind/widgets.py +++ b/tailwind/widgets.py @@ -1,5 +1,4 @@ import json -from typing import Any, Optional from django import forms @@ -8,12 +7,19 @@ class RadioSelect(forms.RadioSelect): option_template_name = "tailwind/widgets/radio_option.html" +class CheckboxSelectMultiple(forms.CheckboxSelectMultiple): + template_name = 'tailwind/widgets/checkbox_select.html' + option_template_name = 'tailwind/widgets/checkbox_option.html' + + class InputArrayWidget(forms.MultiWidget): template_name = "tailwind/widgets/input_array.html" def get_context(self, name, value, attrs): if type(value) == str: value = json.loads(value) + elif not value: + value = [] value = list(filter(lambda x: x, value)) From 91f400b98e77067f45d0d3d0c8bad03977f43dfa Mon Sep 17 00:00:00 2001 From: Igor Santos Date: Sun, 11 Jun 2023 22:34:27 -0300 Subject: [PATCH 5/6] feat: move input array widget js to file --- tailwind/static/js/input-array.js | 48 +++++++++++++++++ .../tailwind/widgets/input_array.html | 52 +++---------------- tailwind/widgets.py | 10 +++- 3 files changed, 63 insertions(+), 47 deletions(-) create mode 100644 tailwind/static/js/input-array.js diff --git a/tailwind/static/js/input-array.js b/tailwind/static/js/input-array.js new file mode 100644 index 00000000..6cd19794 --- /dev/null +++ b/tailwind/static/js/input-array.js @@ -0,0 +1,48 @@ +(function ($) { + function getInputElement(index, name) { + return ` +
+ + +
+ `; + } + + $(document).ready(function () { + $div = $("[data-fieldset-id]"); + const id = $div.data().fieldsetId; + const name = $div.data().fieldsetName; + const limit = Number($div.data().fieldsetLimit) - 1; + + // console.log("data", { id, name, limit }); + + const $addElement = $(`[data-fieldset-id="${id}"] > button`); + + // Evento para adicionar novo INPUT + $addElement.on("click", function (evt) { + const index = $(`[data-fieldset-id="${id}"] div[data-fieldset]`).length; + $(getInputElement(index, name)).insertBefore(evt.target); + + // Desabilitar botão quando chegar no limite + if (index === limit) { + $addElement.prop("disabled", true); + } + + // Evento para remover INPUT + $(`[data-fieldset-id="${id}"] div[data-fieldset] > button`).on("click", function (evt) { + $(evt.target).parent().remove(); + + // Habilita botão ao remover um itme da lista + $addElement.prop("disabled", false); + }); + }); + + // Evento para remover INPUT + $(`[data-fieldset-id="${id}"] div[data-fieldset] > button`).on("click", function (evt) { + $(evt.target).parent().remove(); + + // Habilita botão ao remover um itme da lista + $addElement.prop("disabled", false); + }); + }); + })(django.jQuery); \ No newline at end of file diff --git a/tailwind/templates/tailwind/widgets/input_array.html b/tailwind/templates/tailwind/widgets/input_array.html index 2ddb9661..8e9eb12e 100644 --- a/tailwind/templates/tailwind/widgets/input_array.html +++ b/tailwind/templates/tailwind/widgets/input_array.html @@ -1,6 +1,10 @@ -
+
{% for value in widget.value %}
@@ -21,48 +25,4 @@
- \ No newline at end of file +{{ widget.media }} \ No newline at end of file diff --git a/tailwind/widgets.py b/tailwind/widgets.py index 6dc9202e..a3d51a8d 100644 --- a/tailwind/widgets.py +++ b/tailwind/widgets.py @@ -15,6 +15,9 @@ class CheckboxSelectMultiple(forms.CheckboxSelectMultiple): class InputArrayWidget(forms.MultiWidget): template_name = "tailwind/widgets/input_array.html" + class Media: + js = ["js/input-array.js"] + def get_context(self, name, value, attrs): if type(value) == str: value = json.loads(value) @@ -24,7 +27,12 @@ def get_context(self, name, value, attrs): value = list(filter(lambda x: x, value)) return { - "widget": {"name": name, "value": value, "attrs": attrs}, + "widget": { + "name": name, + "value": value, + "attrs": attrs, + "media": self.media + }, "input_array_limit": len(self.widgets), } From 01a2bfb6486eb0e02f0646a5d506d3d4703faff1 Mon Sep 17 00:00:00 2001 From: Igor Santos Date: Sun, 11 Jun 2023 22:35:48 -0300 Subject: [PATCH 6/6] feat: add pressure settings interface with reactpy --- contrib/frontend/cms_plugins.py | 2 +- contrib/react/__init__.py | 0 contrib/react/apps.py | 6 + contrib/react/cms_plugins.py | 22 ++++ contrib/react/components.py | 72 +++++++++++ contrib/react/forms.py | 118 ++++++++++++++++++ contrib/react/migrations/0001_initial.py | 28 +++++ contrib/react/migrations/__init__.py | 0 contrib/react/models.py | 8 ++ .../templates/react/admin/change_form.html | 18 +++ .../react/templates/react/plugins/react.html | 0 contrib/react/templates/react/views/csrf.html | 1 + contrib/react/templates/react/views/form.html | 1 + contrib/react/views.py | 42 +++++++ project/asgi.py | 31 +++-- project/settings/base.py | 7 ++ project/urls.py | 1 + requirements.txt | 5 +- 18 files changed, 350 insertions(+), 12 deletions(-) create mode 100644 contrib/react/__init__.py create mode 100644 contrib/react/apps.py create mode 100644 contrib/react/cms_plugins.py create mode 100644 contrib/react/components.py create mode 100644 contrib/react/forms.py create mode 100644 contrib/react/migrations/0001_initial.py create mode 100644 contrib/react/migrations/__init__.py create mode 100644 contrib/react/models.py create mode 100644 contrib/react/templates/react/admin/change_form.html create mode 100644 contrib/react/templates/react/plugins/react.html create mode 100644 contrib/react/templates/react/views/csrf.html create mode 100644 contrib/react/templates/react/views/form.html create mode 100644 contrib/react/views.py diff --git a/contrib/frontend/cms_plugins.py b/contrib/frontend/cms_plugins.py index 35bd6d8e..7cdcd030 100644 --- a/contrib/frontend/cms_plugins.py +++ b/contrib/frontend/cms_plugins.py @@ -14,7 +14,7 @@ class BlockPlugin(CMSPluginBase): module = "Frontend" render_template = "frontend/plugins/block.html" allow_children = True - child_classes = ["PicturePlugin", "TextPlugin", "GridPlugin", "ButtonPlugin"] + child_classes = ["PicturePlugin", "TextPlugin", "GridPlugin", "ButtonPlugin", "ReactPlugin"] prepopulated_fields = {"slug": ("title",)} fieldsets = [ ( diff --git a/contrib/react/__init__.py b/contrib/react/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/contrib/react/apps.py b/contrib/react/apps.py new file mode 100644 index 00000000..04dc04b8 --- /dev/null +++ b/contrib/react/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ReactConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'contrib.react' diff --git a/contrib/react/cms_plugins.py b/contrib/react/cms_plugins.py new file mode 100644 index 00000000..895dfe35 --- /dev/null +++ b/contrib/react/cms_plugins.py @@ -0,0 +1,22 @@ +from cms.plugin_base import CMSPluginBase +from cms.plugin_pool import plugin_pool + +from .models import Email +from .forms import EmailForm + +@plugin_pool.register_plugin +class ReactPlugin(CMSPluginBase): + name = "ReactPlugin" + render_template = "react/plugins/react.html" + # model = Email + # form = EmailForm + change_form_template = "react/admin/change_form.html" + + # def get_form(self, request, obj, change=False, **kwargs): + # return EmailForm + # # return super().get_form(request, obj, change, **kwargs) + + # def save_form(self, request, form, change): + # form = super(ReactPlugin, self).save_form(request, form, change) + + # return form \ No newline at end of file diff --git a/contrib/react/components.py b/contrib/react/components.py new file mode 100644 index 00000000..a8d5119c --- /dev/null +++ b/contrib/react/components.py @@ -0,0 +1,72 @@ +from django.http import HttpRequest +from reactpy import component, html, use_state, event + +from .views import form_view, csrf + + +@component +def Tab(display_text: str, selected: str, set_selected): + def handle_click(event): + set_selected(display_text) + + attributes = {"on_click": handle_click, "class_name": "tab tab-bordered"} + + if selected == display_text: + attributes["class_name"] += " tab-active" + + return html.a(attributes, display_text) + + +@component +def Tabs(selected: str, set_selected): + return html.div( + { + "class_name": "tabs", + }, + Tab("Alvos", selected, set_selected), + Tab("Email", selected, set_selected), + Tab("Envio", selected, set_selected), + Tab("Agradecimento", selected, set_selected), + Tab("Pós-ação", selected, set_selected), + ) + + +@component +def Panels(selected: str): + data, set_data = use_state({}) + + @event(prevent_default=True) + async def handle_submit(event): + new_data = data.copy() + new_data[selected] = {} + for x in event["target"]["elements"]: + # TODO: Muito especifico, entender como melhorar captura do evento + if x['name'] == "sharing": + values = new_data[selected].get(x['name'], []) + values.append(x['value']) + + new_data[selected][x['name']] = values + else: + new_data[selected][x['name']] = x['value'] + + print("new_data", new_data) + set_data(new_data) + + request = HttpRequest() + + return html.div( + {"class_name": "py-8"}, + html.form( + {"on_submit": handle_submit, "method": "POST", "novalidate": True}, + csrf(), + form_view(request, selected, data.get(selected, None)), + html.button({"type": "submit"}, "Salvar"), + ), + ) + + +@component +def SettingsPage(): + selected, set_selected = use_state("Alvos") + + return html.div(Tabs(selected, set_selected), Panels(selected)) diff --git a/contrib/react/forms.py b/contrib/react/forms.py new file mode 100644 index 00000000..cc1c107d --- /dev/null +++ b/contrib/react/forms.py @@ -0,0 +1,118 @@ +from django import forms + +from contrib.campaign.models import SharingChoices +from tailwind.fields import InputArrayField +from tailwind.widgets import RadioSelect, CheckboxSelectMultiple + +from .models import Email + + + +class BaseForm(forms.ModelForm): + + def save(self, commit: bool): + if hasattr(self, "cleaned_data"): + print(self.cleaned_data) + # import ipdb;ipdb.set_trace() + # print("BaseForm.save ->>", self.cleaned_data) + pass + + +class TargetsForm(BaseForm): + targets_type = forms.ChoiceField( + label="Tipo", + choices=((True, "Um grupo de alvos"), (False, "Mais de um grupo de alvos")), + widget=RadioSelect, + initial=True, + ) + targets = InputArrayField(label="Alvos", num_widgets=10) + + class Meta: + model = Email + fields = ["targets_type"] + + +class EmailForm(BaseForm): + email_subject = InputArrayField( + label="Assunto do e-mail para os alvos", num_widgets=10 + ) + + email_body = forms.CharField( + label="Corpo do e-mail para os alvos", widget=forms.Textarea + ) + + disable_editing = forms.ChoiceField( + label="Desabilitar edição do e-mail e do assunto pelos ativistas?", + choices=((True, "Desabilitar"), (False, "Habilitar")), + widget=RadioSelect, + initial=True, + ) + + class Meta: + model = Email + fields = ["email_subject", "email_body", "disable_editing"] + + +class SendForm(BaseForm): + submissions_limit = forms.ChoiceField( + label="Limite de envios únicos", + choices=( + (500, "500 pressões"), + (1000, "1.000 pressões"), + (5000, "5.000 pressões"), + (10000, "10.000 pressões"), + ), + ) + + submissions_interval = forms.ChoiceField( + label="Intervalo de envio", + choices=( + (50, "A cada 50 pressões"), + (100, "A cada 100 pressões"), + (500, "A cada 500 pressões"), + (1000, "A cada 1.000 pressões"), + ), + ) + + class Meta: + model = Email + fields = ["submissions_limit", "submissions_interval"] + + +class ThankForm(BaseForm): + thank_email_subject = forms.CharField( + label="Assunto do e-mail de agradecimento para quem vai pressionar", + max_length=120, + ) + thank_email_body = forms.CharField( + label="Corpo do e-mail de agradecimento", widget=forms.Textarea + ) + sender_name = forms.CharField(label="Remetente", max_length=120) + sender_email = forms.EmailField(label="Email de resposta") + + class Meta: + model = Email + fields = [ + "thank_email_subject", + "sender_name", + "sender_email", + "thank_email_body", + ] + + +class PostActionForm(BaseForm): + sharing = forms.MultipleChoiceField( + label="Opções de compartilhamento", + choices=SharingChoices.choices, + widget=CheckboxSelectMultiple, + ) + whatsapp_text = forms.CharField( + label="Mensagem para o whatsapp", widget=forms.Textarea + ) + + class Meta: + model = Email + fields = [ + "sharing", + "whatsapp_text" + ] diff --git a/contrib/react/migrations/0001_initial.py b/contrib/react/migrations/0001_initial.py new file mode 100644 index 00000000..933bb406 --- /dev/null +++ b/contrib/react/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2 on 2023-06-10 05:12 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('cms', '0022_auto_20180620_1551'), + ] + + operations = [ + migrations.CreateModel( + name='Email', + fields=[ + ('cmsplugin_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='react_email', serialize=False, to='cms.cmsplugin')), + ('subject', models.CharField(max_length=150)), + ('body', models.TextField()), + ], + options={ + 'abstract': False, + }, + bases=('cms.cmsplugin',), + ), + ] diff --git a/contrib/react/migrations/__init__.py b/contrib/react/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/contrib/react/models.py b/contrib/react/models.py new file mode 100644 index 00000000..98c21ab9 --- /dev/null +++ b/contrib/react/models.py @@ -0,0 +1,8 @@ +from django.db import models + +from cms.models import CMSPlugin + + +class Email(CMSPlugin): + subject = models.CharField(max_length=150) + body = models.TextField() \ No newline at end of file diff --git a/contrib/react/templates/react/admin/change_form.html b/contrib/react/templates/react/admin/change_form.html new file mode 100644 index 00000000..95b9d8cf --- /dev/null +++ b/contrib/react/templates/react/admin/change_form.html @@ -0,0 +1,18 @@ +{% extends "admin/change_form.html" %} +{% load i18n admin_urls static admin_modify %} +{% load reactpy %} + +{% block extrastyle %} +{{ block.super }} + + +{% endblock %} + +{% block content %} + + +{% component "contrib.react.components.SettingsPage" %} +{% block submit_buttons_bottom %}{% submit_row %}{% endblock %} + +{% endblock %} \ No newline at end of file diff --git a/contrib/react/templates/react/plugins/react.html b/contrib/react/templates/react/plugins/react.html new file mode 100644 index 00000000..e69de29b diff --git a/contrib/react/templates/react/views/csrf.html b/contrib/react/templates/react/views/csrf.html new file mode 100644 index 00000000..ffe60bb6 --- /dev/null +++ b/contrib/react/templates/react/views/csrf.html @@ -0,0 +1 @@ +{% csrf_token %} \ No newline at end of file diff --git a/contrib/react/templates/react/views/form.html b/contrib/react/templates/react/views/form.html new file mode 100644 index 00000000..f9a859ae --- /dev/null +++ b/contrib/react/templates/react/views/form.html @@ -0,0 +1 @@ +{{ form }} diff --git a/contrib/react/views.py b/contrib/react/views.py new file mode 100644 index 00000000..2f4ebdac --- /dev/null +++ b/contrib/react/views.py @@ -0,0 +1,42 @@ +from django.shortcuts import render +from reactpy_django.components import view_to_component + +from .forms import TargetsForm, EmailForm, SendForm, ThankForm, PostActionForm + + +@view_to_component +def csrf(request): + return render(request, "react/views/csrf.html") + + +form_dict = { + "Alvos": TargetsForm, + "Email": EmailForm, + "Envio": SendForm, + "Agradecimento": ThankForm, + "Pós-ação": PostActionForm, +} + + +@view_to_component +def form_view(request, selected: str, data=None): + FormClass = form_dict.get(selected, TargetsForm) + + if data: + form = FormClass(data) + # print("data", data) + # tuple_fields = [ + # (field_name, data.get(field_name, None)) + # for field_name in FormClass.Meta.fields + # ] + + # tuple_fields = list(filter(lambda x: x[1], tuple_fields)) + + # form = FormClass(dict(tuple_fields)) + + obj = form.save(commit=False) + # import ipdb;ipdb.set_trace() + else: + form = FormClass() + + return render(request, "react/views/form.html", {"form": form}) diff --git a/project/asgi.py b/project/asgi.py index 314b3d28..c6f37349 100644 --- a/project/asgi.py +++ b/project/asgi.py @@ -1,16 +1,27 @@ -""" -ASGI config for project project. +import os -It exposes the ASGI callable as a module-level variable named ``application``. +from django.core.asgi import get_asgi_application -For more information on this file, see -https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ -""" -import os +# Ensure DJANGO_SETTINGS_MODULE is set properly based on your project name! +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") -from django.core.asgi import get_asgi_application +# Fetch ASGI application before importing dependencies that require ORM models. +django_asgi_app = get_asgi_application() + + +from channels.auth import AuthMiddlewareStack # noqa: E402 +from channels.routing import ProtocolTypeRouter, URLRouter # noqa: E402 +from channels.sessions import SessionMiddlewareStack # noqa: E402 + +from reactpy_django import REACTPY_WEBSOCKET_PATH # noqa: E402 -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') -application = get_asgi_application() +application = ProtocolTypeRouter( + { + "http": django_asgi_app, + "websocket": SessionMiddlewareStack( + AuthMiddlewareStack(URLRouter([REACTPY_WEBSOCKET_PATH])) + ), + } +) diff --git a/project/settings/base.py b/project/settings/base.py index abc6312b..28ccee08 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -40,6 +40,7 @@ # Application definition INSTALLED_APPS = [ + "daphne", "tailwind", "djangocms_admin_style", "django.contrib.admin", @@ -63,11 +64,14 @@ "djangocms_text_ckeditor", # Third apps "colorfield", + # reactpy + "reactpy_django", # My Apps "contrib.bonde", "contrib.campaign", "contrib.frontend", "contrib.ga", + "contrib.react", ] MIDDLEWARE = [ @@ -207,3 +211,6 @@ # https://docs.djangoproject.com/en/4.2/ref/contrib/sites/ SITE_ID = 1 + + +ASGI_APPLICATION = "project.asgi.application" \ No newline at end of file diff --git a/project/urls.py b/project/urls.py index e2085bcd..201f8efa 100644 --- a/project/urls.py +++ b/project/urls.py @@ -21,6 +21,7 @@ urlpatterns = [ + path("reactpy/", include("reactpy_django.http.urls")), path("admin/", admin.site.urls), path("", include("cms.urls")) ] diff --git a/requirements.txt b/requirements.txt index 2b3ba0cb..882fe753 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,7 @@ django-colorfield # Authentication all platforms # django-allauth bcrypt -whitenoise \ No newline at end of file +whitenoise +reactpy +reactpy-django +channels[daphne] \ No newline at end of file