diff --git a/app/contrib/actions/pressure/cms_plugins.py b/app/contrib/actions/pressure/cms_plugins.py index fb70a3a1..c48d877f 100644 --- a/app/contrib/actions/pressure/cms_plugins.py +++ b/app/contrib/actions/pressure/cms_plugins.py @@ -1,21 +1,30 @@ from cms.plugin_base import CMSPluginBase from cms.plugin_pool import plugin_pool -from .forms import PressurePluginForm, PressureAjaxForm +from contrib.bonde.plugin_base import BondeWidgetPluginBase +from .forms import CreatePressurePluginForm, EditPressurePluginForm, PressureAjaxForm from .models import PressurePluginModel @plugin_pool.register_plugin -class PressurePlugin(CMSPluginBase): +class PressurePlugin(BondeWidgetPluginBase): name = "Pressão" module = "Estrategia" render_template = "pressure/pressure_plugin.html" model = PressurePluginModel - form = PressurePluginForm - cache = False + edit_form_class = EditPressurePluginForm + add_form_class = CreatePressurePluginForm + fieldsets = [ + ( + "Pressão por e-mail", + { + "fields": ["targets", "pressure_email_subject", "pressure_email_content"], + }, + ), + ] def render(self, context, instance, placeholder): - obj = instance.get_widget() + obj = instance.widget request = context["request"] initial = ( { @@ -29,7 +38,7 @@ def render(self, context, instance, placeholder): form = PressureAjaxForm(initial=initial) if instance.reference_id: - scheme = request.scheme if request.scheme else 'https' + scheme = request.scheme if request.scheme else "https" url = f"{scheme}://{request.get_host()}{request.path}" form = PressureAjaxForm( diff --git a/app/contrib/actions/pressure/forms.py b/app/contrib/actions/pressure/forms.py index 18d375c2..b155dc56 100644 --- a/app/contrib/actions/pressure/forms.py +++ b/app/contrib/actions/pressure/forms.py @@ -1,23 +1,99 @@ import json import jwt +import re import requests from django import forms from django.conf import settings -from contrib.bonde.forms import ReferenceBaseModelForm +from contrib.bonde.models import WidgetKind +from contrib.bonde.forms import ChangeReferenceBaseModelForm, CreateReferenceBaseModelForm from tailwind.forms import StyledBaseForm from .models import PressurePluginModel -class PressurePluginForm(ReferenceBaseModelForm): - action_kind = "pressure" +class ArrayWidget(forms.TextInput): + class Media: + css = { + "all": [ + "//cdn.jsdelivr.net/npm/@yaireo/tagify/dist/tagify.css", + ] + } + js = [ + "//cdn.jsdelivr.net/npm/@yaireo/tagify/dist/jQuery.tagify.min.js", + "pressure/js/array-widget.js", + ] - class Meta(ReferenceBaseModelForm.Meta): + def __init__(self, attrs={}): + classes = attrs.get("class", None) + attrs = { + **attrs, + "class": classes + " array-widget" if classes else "array-widget", + } + + super().__init__(attrs=attrs) + + +class CreatePressurePluginForm(CreateReferenceBaseModelForm): + # Settings Widget Kind Form + action_kind = WidgetKind.pressure + + class Meta(CreateReferenceBaseModelForm.Meta): + abstract = False + model = PressurePluginModel + + +class EditPressurePluginForm(ChangeReferenceBaseModelForm): + # Settings Widget Kind Form + action_kind = WidgetKind.pressure + + # Extra fields + targets = forms.CharField( + label="Alvos", + widget=ArrayWidget(attrs={"placeholder": "Nome do alvo: "}), + help_text="Escreva nome e e-mail do alvo desta forma: \"Nome do alvo <email@provedor.com>\" e pressione ENTER." + ) + + pressure_email_subject = forms.CharField( + label="Assunto do e-mail de pressão", required=False + ) + pressure_email_content = forms.CharField( + label="Corpo do e-mail de pressão", widget=forms.Textarea, required=False + ) + + class Meta(ChangeReferenceBaseModelForm.Meta): abstract = False model = PressurePluginModel + def prepare_fields(self): + super().prepare_fields() + + obj = self.instance.widget + self.fields["pressure_email_subject"].initial = obj.settings.get( + "pressure_subject" + ) + self.fields["pressure_email_content"].initial = obj.settings.get( + "pressure_body" + ) + + self.fields["targets"].initial = ",".join(obj.settings.get("targets")) + + def update_widget_settings(self, widget, commit=True): + widget = super().update_widget_settings(widget, commit=False) + + widget.settings["pressure_subject"] = self.cleaned_data[ + "pressure_email_subject" + ] + widget.settings["pressure_body"] = self.cleaned_data["pressure_email_content"] + + widget.settings["targets"] = list(map(lambda x: x['value'], json.loads(self.cleaned_data["targets"]))) + + if commit: + widget.save() + + return widget + class PressureAjaxForm(StyledBaseForm): reference_id = forms.IntegerField(widget=forms.HiddenInput) diff --git a/app/contrib/actions/pressure/models.py b/app/contrib/actions/pressure/models.py index 2cbb81cd..81c9d53a 100644 --- a/app/contrib/actions/pressure/models.py +++ b/app/contrib/actions/pressure/models.py @@ -1,22 +1,11 @@ -from django.db import models +# from django.db import models -from cms.models import CMSPlugin +# from cms.models import CMSPlugin -from contrib.bonde.models import Widget +from contrib.bonde.models import BondeBasePluginModel -class PressurePluginModel(CMSPlugin): +class PressurePluginModel(BondeBasePluginModel): """ """ - reference_id = models.IntegerField( - null=True, - blank=True, - help_text="ID de referência da widget na plataforma Bonde" - ) - - - def get_widget(self) -> Widget | None: - if not self.reference_id: - return None - - return Widget.objects.get(id=self.reference_id) + pass \ No newline at end of file diff --git a/app/contrib/actions/pressure/static/bonde/css/edit-widget-form.css b/app/contrib/actions/pressure/static/bonde/css/edit-widget-form.css new file mode 100644 index 00000000..e725cf3e --- /dev/null +++ b/app/contrib/actions/pressure/static/bonde/css/edit-widget-form.css @@ -0,0 +1,17 @@ +.tabs { + display: flex; + flex-direction: row; +} + +.tabs > h2 { + cursor: pointer; + padding: 10px 20px 5px 5px; +} + +.tabs > .tab-active { + border-bottom: 2px solid black; +} + +.tabs-content > fieldset { + padding: 25px 0 !important; +} \ No newline at end of file diff --git a/app/contrib/actions/pressure/static/bonde/js/edit-widget-form.js b/app/contrib/actions/pressure/static/bonde/js/edit-widget-form.js new file mode 100644 index 00000000..fe9b4b27 --- /dev/null +++ b/app/contrib/actions/pressure/static/bonde/js/edit-widget-form.js @@ -0,0 +1,42 @@ +(function ($) { + "use strict"; + + $(function () { + let actived = 0; + + function toggle(index) { + if (index == actived) { + $(".tabs-content > fieldset").eq(index).show(); + } else { + $(".tabs-content > fieldset").hide(); + $(".tabs > h2").removeClass("tab-active"); + + $(".tabs > h2").eq(index).addClass("tab-active"); + $(".tabs-content > fieldset").eq(index).show(); + } + actived = index + } + + // Cria estrutura básica para tabs + $("fieldset").eq(0).before('
'); + $("fieldset").eq(0).before('
'); + + $(".tabs").append($("fieldset > h2").detach()); + $(".tabs-content").append($("fieldset").detach()); + + // Adiciona eventos + $(".tabs > h2").each(function(index) { + // Default is first open + if (index !== 0) { + $(".tabs-content > fieldset").eq(index).hide(); + } else { + $(".tabs > h2").eq(index).addClass("tab-active"); + } + + $(".tabs > h2").eq(index).on("click", function() { + toggle(index) + // $(".tabs-content > fieldset").eq(index).show(); + }); + }); + }); +}(window.jQuery)); \ No newline at end of file diff --git a/app/contrib/actions/pressure/static/pressure/js/array-widget.js b/app/contrib/actions/pressure/static/pressure/js/array-widget.js new file mode 100644 index 00000000..91fcabc3 --- /dev/null +++ b/app/contrib/actions/pressure/static/pressure/js/array-widget.js @@ -0,0 +1,49 @@ +(function ($) { + "use strict"; + + const patternTargetEmail = /[a-zà-úA-ZÀ-Ú\s]+<(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))>/ + + // function validate (value) { + // const patternTargetEmail = /[a-zà-úA-ZÀ-Ú\s]+<(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))>/ + // return patternTargetEmail.test(value) + // } + + // function add (value) { + // const name = value.split("<")[0].trim(); + // const email = value.split("<")[1].trim().replace(">", ""); + + // console.log({ name, email }); + // $("input.array-widget").after("

" + name + " " + email + "

"); + // } + + + $(function () { + $("input.array-widget") + .tagify({ + pattern: patternTargetEmail + }) + .on("add", function (e, tagData) { + console.log("add", { e, tagData }); + }) + .on("remove", function(e, tagData) { + console.log("remove", { e, tagData }); + }) + .on("invalid", function(e, tagData) { + console.log("invalid", { e, tagData }); + }); + + $("input.array-widget").on("change", function (evt) { + console.log(evt.target) + }); + // $("input.array-widget").on("keydown", function(evt) { + // if (evt.originalEvent.code === "Slash") { + // console.log("processa a tag") + + // console.log("validate", validate(evt.target.value)) + // if (validate(evt.target.value)) { + // add(evt.target.value) + // } + // } + // }); + }); +}(window.jQuery)); \ No newline at end of file diff --git a/app/contrib/actions/pressure/static/pressure/js/pressure.js b/app/contrib/actions/pressure/static/pressure/js/pressure.js index bcde4b59..79c9be47 100644 --- a/app/contrib/actions/pressure/static/pressure/js/pressure.js +++ b/app/contrib/actions/pressure/static/pressure/js/pressure.js @@ -39,8 +39,10 @@ } function doneResponse({ success, html }) { - $("#pressureWrapper").empty(); - $("#pressureWrapper").html(html); + var instanceId = $('.pressure-plugin').data('instance-id'); + + $('#pressureWrapper-' + instanceId).empty(); + $('#pressureWrapper-' + instanceId).html(html); $("#copyToClipboard").on("click", function () { const textToCopy = window.location.href; diff --git a/app/contrib/actions/pressure/templates/pressure/pressure_plugin.html b/app/contrib/actions/pressure/templates/pressure/pressure_plugin.html index 672cfbf6..58c87a1e 100644 --- a/app/contrib/actions/pressure/templates/pressure/pressure_plugin.html +++ b/app/contrib/actions/pressure/templates/pressure/pressure_plugin.html @@ -3,17 +3,18 @@ {% addtoblock "css" %} {% if settings.main_color %} @@ -25,7 +26,7 @@ {% endaddtoblock %} -
+

{{settings.title}}

diff --git a/app/contrib/bonde/forms.py b/app/contrib/bonde/forms.py index 40a93f38..cb5f32df 100644 --- a/app/contrib/bonde/forms.py +++ b/app/contrib/bonde/forms.py @@ -1,10 +1,16 @@ +from typing import Any from django import forms +from django.db import transaction +from colorfield.fields import ColorWidget +from .models import Widget, Block, Mobilization from .widgets import ActionSelectWidget, ActionChoices +from django.core.exceptions import ValidationError class ReferenceBaseModelForm(forms.ModelForm): - """ """ + # Settings Widget Kind Form + action_kind = None class Meta: abstract = True @@ -19,10 +25,150 @@ class Media: required=False, ) - action_kind = None - def __init__(self, *args, **kwargs): super(ReferenceBaseModelForm, self).__init__(*args, **kwargs) # Configurar tipo de ação para filtrar widgets self.fields["reference_id"].choices = ActionChoices(self.action_kind) + + if ( + self.instance + and self.instance.reference_id + and hasattr(self, "prepare_fields") + ): + self.prepare_fields() + + +class CreateReferenceBaseModelForm(ReferenceBaseModelForm): + """ """ + + class Meta: + abstract = True + fields = ["reference_id"] + + # Create Mobilization and Widget Fields + community_id = forms.IntegerField(widget=forms.HiddenInput) + name = forms.CharField(label="Nome da Pressão", max_length=100, required=False) + # block_id = forms.IntegerField(widget=forms.HiddenInput) + # kind = forms.CharField(widget=forms.HiddenInput) + # settings = forms.JSONField() + + def clean(self): + cleaned_data = super().clean() + + reference_id = cleaned_data.get('reference_id') + name = cleaned_data.get('name') + + if not reference_id and not name: + raise forms.ValidationError({"name": "Este campo é obrigatório para a criação de um novo widget."}) + + return cleaned_data + + @transaction.atomic + def save(self, commit=False) -> Any: + self.instance = super().save(commit=False) + reference_id = self.cleaned_data.get('reference_id') + + if not reference_id: + mob = Mobilization.objects.create( + name=self.cleaned_data["name"], + community_id=self.cleaned_data["community_id"], + ) + block = Block.objects.create(mobilization=mob) + widget = Widget.objects.create( + block=block, + kind=self.action_kind, + settings=dict(targets=[]), + ) + + self.instance.reference_id = widget.id + + if commit: + self.instance.save() + + # TODO: Verificar salvar sem necessidade + # self.update_widget_settings(widget=self.instance.widget, commit=True) + + return self.instance + + +class ChangeReferenceBaseModelForm(ReferenceBaseModelForm): + """ """ + + class Meta: + abstract = True + fields = ["reference_id"] + + class Media: + js = ( + "//ajax.googleapis.com/ajax/libs/jquery/3.7.0/jquery.min.js", + "bonde/js/edit-widget-form.js", + ) + css = {"all": ["bonde/css/edit-widget-form.css"]} + + # Settings Fields + title = forms.CharField(label="Título do formulário", required=False) + button_text = forms.CharField(label="Texto do botão", required=False) + main_color = forms.CharField( + label="Cor principal", widget=ColorWidget, required=False + ) + count_text = forms.CharField(label="Contador", required=False) + + whatsapp_text = forms.CharField( + label="Texto de compartilhamento por WhatsApp", + widget=forms.Textarea, + required=False, + ) + + # Post action Fields + sender_name = forms.CharField(label="Remetente", required=False) + sender_email = forms.CharField(label="E-mail de resposta", required=False) + email_subject = forms.CharField(label="Assunto do e-mail", required=False) + email_text = forms.CharField( + label="Corpo do e-mail", widget=forms.Textarea, required=False + ) + + def prepare_fields(self): + obj = self.instance.widget + + self.fields["title"].initial = ( + obj.settings.get("call_to_action") + or obj.settings.get("title_text") + or obj.settings.get("title") + ) + self.fields["button_text"].initial = obj.settings.get("button_text") + self.fields["main_color"].initial = obj.settings.get("main_color") + self.fields["count_text"].initial = obj.settings.get("count_text") + + self.fields["whatsapp_text"].initial = obj.settings.get("whatsapp_text") + + self.fields["sender_name"].initial = obj.settings.get("sender_name") + self.fields["sender_email"].initial = obj.settings.get("sender_email") + self.fields["email_subject"].initial = obj.settings.get("email_subject") + self.fields["email_text"].initial = obj.settings.get("email_text") + + def update_widget_settings(self, widget, commit=True): + widget.settings["call_to_action"] = self.cleaned_data["title"] + widget.settings["button_text"] = self.cleaned_data["button_text"] + widget.settings["main_color"] = self.cleaned_data["main_color"] + widget.settings["count_text"] = self.cleaned_data["count_text"] + + widget.settings["whatsapp_text"] = self.cleaned_data["whatsapp_text"] + + widget.settings["sender_name"] = self.cleaned_data["sender_name"] + widget.settings["sender_email"] = self.cleaned_data["sender_email"] + widget.settings["email_subject"] = self.cleaned_data["email_subject"] + widget.settings["email_text"] = self.cleaned_data["email_text"] + + if commit: + widget.save() + + return widget + + def save(self, commit=False) -> Any: + self.instance = super().save(commit) + + # TODO: Verificar salvar sem necessidade + self.update_widget_settings(widget=self.instance.widget, commit=True) + + return self.instance diff --git a/app/contrib/bonde/models.py b/app/contrib/bonde/models.py index 18dddd8e..d48e61b9 100644 --- a/app/contrib/bonde/models.py +++ b/app/contrib/bonde/models.py @@ -9,6 +9,8 @@ from django.conf import settings from django.contrib.sites.models import Site +from cms.models import CMSPlugin + class User(models.Model): # provider = models.CharField(max_length=100) @@ -143,7 +145,7 @@ class MobilizationStatus(models.TextChoices): class Mobilization(models.Model): name = models.CharField(max_length=266, blank=True, null=True) - created_at = models.DateTimeField() + created_at = models.DateTimeField(auto_now_add=True) # 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) @@ -165,7 +167,7 @@ class Mobilization(models.Model): # 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) + updated_at = models.DateTimeField(blank=True, null=True, auto_now=True) # theme = models.ForeignKey('Themes', models.DO_NOTHING, blank=True, null=True) objects = RequestManager(lookup_field="community") @@ -179,8 +181,8 @@ class Block(models.Model): mobilization = models.ForeignKey( Mobilization, models.DO_NOTHING, blank=True, null=True ) - created_at = models.DateTimeField() - updated_at = models.DateTimeField() + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) # 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) @@ -236,8 +238,8 @@ class Widget(models.Model): kind = models.CharField( max_length=50, choices=WidgetKind.choices, blank=True, null=True ) - created_at = models.DateTimeField() - updated_at = models.DateTimeField() + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) # sm_size = models.IntegerField(blank=True, null=True) # md_size = models.IntegerField(blank=True, null=True) # lg_size = models.IntegerField(blank=True, null=True) @@ -318,3 +320,26 @@ class Meta: def __str__(self): return f'ID: {self.id} / WidgetID: {self.widget_id}' + + +class BondeBasePluginModel(CMSPlugin): + """ + """ + reference_id = models.IntegerField( + null=True, + blank=True, + help_text="ID de referência da widget na plataforma Bonde" + ) + + class Meta: + abstract = True + + def get_widget(self) -> Widget | None: + if not self.reference_id: + return None + + return Widget.objects.get(id=self.reference_id) + + @property + def widget(self) -> Widget | None: + return self.get_widget() \ No newline at end of file diff --git a/app/contrib/bonde/plugin_base.py b/app/contrib/bonde/plugin_base.py new file mode 100644 index 00000000..fbf0dc70 --- /dev/null +++ b/app/contrib/bonde/plugin_base.py @@ -0,0 +1,61 @@ +from cms.plugin_base import CMSPluginBase + +from .models import Community + +from contrib.actions.pressure.forms import CreatePressurePluginForm, EditPressurePluginForm + + +class BondeWidgetPluginBase(CMSPluginBase): + edit_form_class = EditPressurePluginForm + add_form_class = CreatePressurePluginForm + + def get_form(self, request, obj=None, **kwargs): + if obj and obj.reference_id: + form_class = self.edit_form_class + else: + form_class = self.add_form_class + + kwargs['form'] = form_class + + form = super(BondeWidgetPluginBase, self).get_form(request, obj, **kwargs) + + # Configura valores iniciais + if form_class == self.add_form_class: + c = Community.objects.on_site(request).first() + if c: + form.base_fields['community_id'].initial = c.id + + return form + + def get_fieldsets(self, request, obj): + if obj and obj.reference_id: + return [ + ( + "Agradecimento", + { + "fields": [ + "sender_name", + "sender_email", + "email_subject", + "email_text", + ] + }, + ), + ( + "Formulário", + { + "fields": ["title", "button_text", "main_color", "count_text"], + }, + ), + ( + "Compartilhamento", + { + "fields": ["whatsapp_text"], + }, + ), + ] + self.fieldsets + + return [ + (None, {"fields": ["reference_id"]}), + ("Ou cria uma nova widget", {"fields": ["name", "community_id"]}), + ] \ No newline at end of file