diff --git a/app/org_eleicoes/votepeloclima/candidature/fields.py b/app/org_eleicoes/votepeloclima/candidature/fields.py index 82171df7..046623b3 100644 --- a/app/org_eleicoes/votepeloclima/candidature/fields.py +++ b/app/org_eleicoes/votepeloclima/candidature/fields.py @@ -388,9 +388,9 @@ def get_bound_field(self, form, field_name): class ToogleButtonInput(forms.CheckboxInput): template_name = "forms/widgets/toggle_button.html" - @property - def media(self): - return forms.Media(css={"screen": ["css/icons.css"]}) + # @property + # def media(self): + # return forms.Media(css={"screen": ["css/icons.css"]}) def __init__(self, text_html, icon_name=None, *args, **kwargs): self.text_html = text_html @@ -469,3 +469,13 @@ def get_bound_field(self, form, field_name): bound_field.label = mark_safe(bound_field.label) return bound_field + + +class ButtonCheckboxSelectMultiple(forms.CheckboxSelectMultiple): + template_name = "forms/widgets/button_input_select.html" + option_template_name = "forms/widgets/button_input_option.html" + + +class ButtonRadioSelect(forms.RadioSelect): + template_name = "forms/widgets/button_input_select.html" + option_template_name = "forms/widgets/button_input_option.html" \ No newline at end of file diff --git a/app/org_eleicoes/votepeloclima/candidature/forms.py b/app/org_eleicoes/votepeloclima/candidature/forms.py index 7c3c6c93..93a0dbc9 100644 --- a/app/org_eleicoes/votepeloclima/candidature/forms.py +++ b/app/org_eleicoes/votepeloclima/candidature/forms.py @@ -600,4 +600,4 @@ class Meta: ("sobre-sua-trajetoria", TrackForm), ("complemente-seu-perfil", ProfileForm), ("checkout", CheckoutForm), -] +] \ No newline at end of file diff --git a/app/org_eleicoes/votepeloclima/candidature/forms/__init__.py b/app/org_eleicoes/votepeloclima/candidature/forms/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/org_eleicoes/votepeloclima/candidature/forms/filters.py b/app/org_eleicoes/votepeloclima/candidature/forms/filters.py new file mode 100644 index 00000000..28f49aa7 --- /dev/null +++ b/app/org_eleicoes/votepeloclima/candidature/forms/filters.py @@ -0,0 +1,140 @@ +from django import forms +from django.utils.functional import lazy + +from crispy_forms.layout import Layout, Div +from crispy_forms.helper import FormHelper +# from django_select2.forms import Select2Widget + +from ..layout import NoCrispyField +from ..choices import Gender, Color +from ..fields import CepField, ButtonCheckboxSelectMultiple, ButtonRadioSelect +from ..locations_utils import get_ufs, get_choices +from ..models import Candidature + +from .register import ProposeForm + + +class RemoveRequiredMixin: + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.helper = FormHelper() + self.helper.form_tag = False + self.helper.disable_csrf = True + + for field_name in self.fields: + self.fields[field_name].required = False + + +class FilterFormHeader(RemoveRequiredMixin, forms.ModelForm): + keyword = forms.CharField( + label="Buscar por temas ou nomes", + widget=forms.TextInput(attrs={"placeholder": "Digite um tema ou nome"}), + ) + state = CepField( + field="state", + label="Estado", + placeholder="Todos os estados", + choices=[("", "Todos os estados")] + lazy(get_ufs, list)(), + ) + city = CepField( + field="city", parent="state", label="Município", placeholder="Selecione" + ) + + class Meta: + model = Candidature + fields = [ + "state", + "city", + "intended_position", + "political_party", + "keyword", + ] + widgets = { + # "political_party": Select2Widget() + } + + class Media: + js = ["https://code.jquery.com/jquery-3.5.1.min.js"] + + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + state = self.data.get("state") + if state: + self.fields["city"].choices = [("", "Selecione")] + get_choices(state) + + +class FilterFormSidebar(RemoveRequiredMixin, forms.ModelForm): + proposes = forms.MultipleChoiceField( + label="Propostas", widget=ButtonCheckboxSelectMultiple + ) + mandate_type = forms.ChoiceField( + label="Tipo de mandato", + choices=(("", "Todos"), ("individual", "Individual"), ("coletivo", "Mandato coletivo")), + widget=ButtonRadioSelect, + ) + gender = forms.MultipleChoiceField( + label="Gênero", choices=Gender.choices[1:], widget=ButtonCheckboxSelectMultiple + ) + color = forms.MultipleChoiceField( + label="Raça", choices=Color.choices[1:], widget=ButtonCheckboxSelectMultiple + ) + + class Meta: + model = Candidature + fields = ["proposes", "mandate_type", "gender", "color"] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.fields["proposes"].choices = self.get_proposes_choices() + + self.helper.layout = Layout( + NoCrispyField("proposes"), + NoCrispyField("mandate_type"), + NoCrispyField("gender"), + NoCrispyField("color"), + ) + + def get_proposes_choices(self): + form = ProposeForm() + choices = [] + for field_name in form.fields: + if field_name != "properties": + choices.append((field_name, form.fields[field_name].checkbox_label)) + + return choices + + +class FilterFactoryForm(object): + + def __init__(self, data=None): + self._errors = {} + self.data = data + + self.header = FilterFormHeader(self.data) + self.sidebar = FilterFormSidebar(self.data) + + def is_valid(self): + is_valid = True + + if not self.header.is_valid(): + is_valid = False + self._errors.update(self.header.errors) + + if not self.sidebar.is_valid(): + is_valid = False + self._errors.update(self.sidebar.errors) + + return is_valid + + @property + def errors(self): + return self._errors if len(self._errors) > 0 else None + + @property + def cleaned_data(self): + return {**self.header.cleaned_data, **self.sidebar.cleaned_data} diff --git a/app/org_eleicoes/votepeloclima/candidature/forms/register.py b/app/org_eleicoes/votepeloclima/candidature/forms/register.py new file mode 100644 index 00000000..0a113949 --- /dev/null +++ b/app/org_eleicoes/votepeloclima/candidature/forms/register.py @@ -0,0 +1,603 @@ +from django import forms +from django.core.exceptions import ValidationError +from django.utils.functional import lazy +from django.templatetags.static import static + +from captcha.widgets import ReCaptchaV2Checkbox +from entangled.forms import EntangledModelFormMixin +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Layout, Div, Field, HTML +from bootstrap_datepicker_plus.widgets import DatePickerInput + +from ..locations_utils import get_ufs, get_choices +from ..choices import ( + PoliticalParty, + IntendedPosition, + Education, + Color, + Gender, + Sexuality, +) +from ..models import CandidatureFlow +from ..fields import ( + ValidateOnceReCaptchaField, + CheckboxTextField, + InlineArrayField, + CepField, + ToggleButtonField, + VideoField, + InputMask, + HTMLBooleanField, +) +from ..layout import NoCrispyField, FileField + + +class DisabledMixin: + + def __init__(self, disabled=False, *args, **kwargs): + super().__init__(*args, **kwargs) + + if disabled: + self.disabled = disabled + for field_name in self.fields: + self.fields[field_name].widget.attrs.update( + {"readonly": True, "disabled": True} + ) + + +class CaptchaForm(EntangledModelFormMixin, forms.ModelForm): + captcha = ValidateOnceReCaptchaField(widget=ReCaptchaV2Checkbox()) + + class Meta: + # Sobrescreve o template na view para criar uma página customizada + model = CandidatureFlow + entangled_fields = {"properties": ["captcha"]} + untangled_fields = [] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.fields["captcha"].label = "" + + +class AppointmentForm(EntangledModelFormMixin, DisabledMixin, forms.ModelForm): + appointment_1 = ToggleButtonField( + icon_name="ds-icon-compromisso-1", + text_html="Políticas de adaptação das cidades para reduzir tragédias", + ) + appointment_2 = ToggleButtonField( + icon_name="ds-icon-compromisso-2", + text_html="Políticas para redução de emissões e transição energética", + ) + appointment_3 = ToggleButtonField( + icon_name="ds-icon-compromisso-3", + text_html="Políticas sociais de apoio às populações atingidas", + ) + appointment_4 = ToggleButtonField( + icon_name="ds-icon-compromisso-4", + text_html="Transição climática com justiça social, racial e de gênero", + ) + appointment_5 = ToggleButtonField( + icon_name="ds-icon-compromisso-5", + text_html="Proteção ambiental e de recursos naturais", + ) + appointment_6 = ToggleButtonField( + icon_name="ds-icon-compromisso-6", + text_html="Incentivo à participação popular e ao engajamento da juventude", + ) + appointment_7 = ToggleButtonField( + icon_name="ds-icon-compromisso-7", + text_html="Investimentos em pesquisa e inovação para enfrentar a crise climática", + ) + appointment_8 = ToggleButtonField( + icon_name="ds-icon-compromisso-8", + text_html="Valorização de saberes tradicionais e tecnologias sociais na busca de soluções", + ) + + class Meta: + title = "Você assume compromisso com..." + description = "Esses são os valores e princípios básicos que todos os candidatos devem assumir ao criar um perfil no Vote pelo Clima. Eles representam compromissos essenciais e serão visíveis aos eleitores, evidenciando sua dedicação a um futuro sustentável. Para continuar, é necessário selecionar todos os compromissos." + model = CandidatureFlow + entangled_fields = { + "properties": [ + "appointment_1", + "appointment_2", + "appointment_3", + "appointment_4", + "appointment_5", + "appointment_6", + "appointment_7", + "appointment_8", + ] + } + untangled_fields = [] + + +class PersonalForm(EntangledModelFormMixin, DisabledMixin, forms.ModelForm): + legal_name = forms.CharField( + label="Nome", + widget=forms.TextInput(attrs={"placeholder": "Digite seu nome completo"}), + ) + email = forms.EmailField( + label="E-mail", + widget=forms.EmailInput(attrs={"placeholder": "Digite seu e-mail"}), + ) + cpf = forms.CharField( + label="CPF", + help_text="CPF é necessário para confirmar sua identidade junto ao TSE", + widget=InputMask( + mask="000.000.000-00", attrs={"placeholder": "Digite seu CPF"} + ), + ) + birth_date = forms.DateField( + label="Data de nascimento", + widget=DatePickerInput( + attrs={"placeholder": "dd/mm/yyyy"}, + options={"locale": "pt-BR", "format": "DD/MM/YYYY"}, + ), + localize="pt-BR", + ) + + class Meta: + title = "Informações pessoais" + description = "Vamos lá! Essas informações são essenciais para verificar sua candidatura e garantir a segurança. Se for uma candidatura coletiva, a pessoa responsável deve ter os dados registrados no TSE." + model = CandidatureFlow + entangled_fields = { + "properties": ["legal_name", "email", "cpf", "birth_date"] + } + untangled_fields = [] + + class Media: + js = ["https://code.jquery.com/jquery-3.5.1.min.js"] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_tag = False + self.helper.layout = Layout( + Div( + Div(Field("legal_name"), css_class="g-col-12 g-col-md-6"), + Div(Field("email"), css_class="g-col-12 g-col-md-6"), + Div(Field("cpf"), css_class="g-col-12 g-col-md-6"), + Div(Field("birth_date"), css_class="g-col-12 g-col-md-6"), + css_class="grid", + style="grid-row-gap:0;", + ) + ) + + +class ApplicationForm(EntangledModelFormMixin, DisabledMixin, forms.ModelForm): + ballot_name = forms.CharField( + label="Nome na urna", + help_text="Nome público, registrado no TSE.", + widget=forms.TextInput( + attrs={"placeholder": "Digite o nome que aparecerá na urna"} + ), + ) + number_id = forms.IntegerField( + label="Número na urna", + min_value=1, + help_text="Número da candidatura", + widget=forms.NumberInput( + attrs={"placeholder": "Digite seu número de identificação"} + ), + ) + intended_position = forms.ChoiceField( + label="Cargo pretendido", + choices=IntendedPosition.choices, + help_text="Selecione o cargo que você está concorrendo", + ) + state = CepField( + field="state", + label="Estado", + placeholder="Selecione", + choices=[("", "Selecione")] + lazy(get_ufs, list)(), + help_text="Estado onde você está concorrendo", + ) + city = CepField( + field="city", + parent="state", + label="Cidade", + placeholder="Selecione", + help_text="Cidade onde você está concorrendo", + ) + is_collective_mandate = forms.BooleanField( + label="É um mandato coletivo?", + required=False, + initial=False, + widget=forms.RadioSelect(choices=((True, "Sim"), (False, "Não"))), + ) + political_party = forms.ChoiceField( + label="Partido político", choices=PoliticalParty.choices + ) + deputy_mayor = forms.CharField( + label="Nome vice-prefeitura", + widget=forms.TextInput(attrs={"placeholder": "Digite o nome"}), + required=False, + ) + deputy_mayor_political_party = forms.ChoiceField( + label="Partido político", choices=PoliticalParty.choices, required=False + ) + + class Meta: + title = "Dados de candidatura" + description = "Preencha os detalhes sobre sua candidatura." + model = CandidatureFlow + entangled_fields = { + "properties": [ + "ballot_name", + "number_id", + "intended_position", + "state", + "city", + "is_collective_mandate", + "political_party", + "deputy_mayor", + "deputy_mayor_political_party", + ] + } + untangled_fields = [] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_tag = False + # TODO: investigar porque quando usamos layout o select2 duplica o campo + self.helper.layout = Layout( + Div( + Div(Field("ballot_name"), css_class="g-col-12 g-col-md-6"), + Div(Field("number_id"), css_class="g-col-12 g-col-md-6"), + Div(Field("intended_position"), css_class="g-col-12 g-col-md-6"), + Div(Field("political_party"), css_class="g-col-12 g-col-md-6"), + Div(Field("state"), css_class="g-col-12 g-col-md-6"), + Div(Field("city"), css_class="g-col-12 g-col-md-6"), + Div( + NoCrispyField("is_collective_mandate"), + css_class="g-col-12 g-col-md-6 mb-3", + ), + Div( + HTML( + """ +
Adicione informações somente em caso de candidaturas para prefeitura
+ """ + ), + css_class="g-col-12 g-col-md-12", + ), + Div(Field("deputy_mayor"), css_class="g-col-12 g-col-md-6"), + Div( + Field("deputy_mayor_political_party"), + css_class="g-col-12 g-col-md-6", + ), + css_class="grid", + style="grid-row-gap:0;", + ) + ) + data = kwargs.get("data", None) + instance = kwargs.get("instance", None) + + if data or instance: + state = None + if instance: + state = instance.properties.get("state", None) + if data: + state = data.get("informacoes-de-candidatura-state", None) + + if state: + self.fields["city"].choices = get_choices(state) + + def clean(self): + cleaned_data = super().clean() + intended_position = cleaned_data.get("intended_position") + deputy_mayor = cleaned_data.get("deputy_mayor") + deputy_mayor_political_party = cleaned_data.get("deputy_mayor_political_party") + + if intended_position == IntendedPosition.prefeitura and not deputy_mayor: + self.add_error("deputy_mayor", "Esse campo é obrigatório") + + if ( + intended_position == IntendedPosition.prefeitura + and not deputy_mayor_political_party + ): + self.add_error("deputy_mayor_political_party", "Esse campo é obrigatório") + + +propose_text_label = "Proposta" +propose_text_help_text = ( + "Descreva brevemente sua proposta. Até 600 caracteres." +) + + +class ProposeForm(EntangledModelFormMixin, DisabledMixin, forms.ModelForm): + transporte_e_mobilidade = CheckboxTextField( + checkbox_label="Transporte e Mobilidade", + help_text="Transporte coletivo gratuito e de qualidade, modais com menos emissões e mobilidade ativa.", + text_label=propose_text_label, + text_help_text=propose_text_help_text, + required=False, + max_length=600 + ) + gestao_de_residuos = CheckboxTextField( + checkbox_label="Gestão de Resíduos", + help_text="Compostagem de resíduos orgânicos, economia circular, mais iniciativas de catadores e catadoras de materiais recicláveis, uso de materiais biodegradáveis.", + text_label=propose_text_label, + text_help_text=propose_text_help_text, + required=False, + max_length=600 + + ) + povos_originarios_tradicionais = CheckboxTextField( + checkbox_label="Povos e comunidades tradicionais", + help_text="Direitos, reconhecimento e valorização de conhecimentos e tecnologias de povos e comunidades tradicionais.", + text_label=propose_text_label, + text_help_text=propose_text_help_text, + required=False, + max_length=600 + + ) + educacao_climatica = CheckboxTextField( + checkbox_label="Educação Climática", + help_text="Ensino sobre meio ambiente e mudanças climáticas nas escolas, formação profissional para empregos verdes, formação de agentes populares para gestão do risco climático.", + text_label=propose_text_label, + text_help_text=propose_text_help_text, + required=False, + max_length=600 + + ) + combate_racismo_ambiental = CheckboxTextField( + checkbox_label="Enfrentamento ao racismo ambiental", + help_text="Cultura viva, segurança cidadã, participação ativa das comunidades, protagonismo de pessoas negras, indígenas e jovens na construção de soluções.", + text_label=propose_text_label, + text_help_text=propose_text_help_text, + required=False, + max_length=600 + + ) + moradia_digna = CheckboxTextField( + checkbox_label="Moradia Digna", + help_text="Políticas habitacionais justas e participativas, moradia resiliente aos impactos de eventos climáticos extremos, eficiência hídrica e energética.", + text_label=propose_text_label, + text_help_text=propose_text_help_text, + required=False, + max_length=600 + + ) + transicao_energetica = CheckboxTextField( + checkbox_label="Transição energética justa", + help_text="Mais fontes de energias renováveis que substituam gradualmente o uso de fontes poluidoras e garantam os direitos socioambientais das populações em seus territórios.", + text_label=propose_text_label, + text_help_text=propose_text_help_text, + required=False, + max_length=600 + + ) + agricultura_sustentavel = CheckboxTextField( + checkbox_label="Alimentos saudáveis", + help_text="Agricultura livre de agrotóxicos, produção agroecológica, agricultura familiar e mais vegetais na mesa da população.", + text_label=propose_text_label, + text_help_text=propose_text_help_text, + required=False, + max_length=600 + + ) + direito_a_cidade = CheckboxTextField( + checkbox_label="Direito à Cidade", + help_text="Mais áreas verdes e parques públicos, menos ilhas de calor, segurança pública e bem-estar urbano, cidades mais sustentáveis e inclusivas.", + text_label=propose_text_label, + text_help_text=propose_text_help_text, + required=False, + max_length=600 + + ) + adaptacao_reducao_desastres = CheckboxTextField( + checkbox_label="Adaptação e redução de desastres", + help_text="Políticas públicas para pessoas atingidas por eventos climáticos extremos, projetos e recursos para infraestrutura resiliente, monitoramento e resposta rápida a desastres ambientais.", + text_label=propose_text_label, + text_help_text=propose_text_help_text, + required=False, + max_length=600 + + ) + direito_dos_animais = CheckboxTextField( + checkbox_label="Proteção de animais", + help_text="Habitats da fauna local protegidos, controle de zoonoses para evitar doenças vetoriais agravadas pela crise climática.", + text_label=propose_text_label, + text_help_text=propose_text_help_text, + required=False, + max_length=600 + + ) + economia_verde = CheckboxTextField( + checkbox_label="Economia Verde", + help_text="Indústrias e processos produtivos sem carbono, bioeconomia, novos empregos verdes.", + text_label=propose_text_label, + text_help_text=propose_text_help_text, + required=False, + max_length=600 + + ) + pessoas_afetadas_desastres = CheckboxTextField( + checkbox_label="Pessoas afetadas por desastres", + help_text="Políticas públicas de recuperação ambiental e assistência imediata, incluindo moradias sustentáveis e serviços de saúde física e mental, para comunidades impactadas por desastres ambientais.", + text_label=propose_text_label, + text_help_text=propose_text_help_text, + required=False, + max_length=600 + + ) + + + class Meta: + title = "Suas propostas" + model = CandidatureFlow + entangled_fields = { + "properties": [ + "transporte_e_mobilidade", + "gestao_de_residuos", + "povos_originarios_tradicionais", + "educacao_climatica", + "combate_racismo_ambiental", + "moradia_digna", + "transicao_energetica", + "agricultura_sustentavel", + "direito_a_cidade", + "adaptacao_reducao_desastres", + "direito_dos_animais", + "economia_verde", + "pessoas_afetadas_desastres" + ] + } + untangled_fields = [] + + def clean(self): + cleaned_data = super().clean() + selected_size = len(list(filter(lambda x: bool(x), cleaned_data.values()))) + if selected_size > 3: + raise ValidationError("Selecione apenas 3 bandeiras") + elif selected_size == 0: + raise ValidationError("Selecione ao menos 1 bandeira") + return cleaned_data + + +class TrackForm(EntangledModelFormMixin, DisabledMixin, forms.ModelForm): + education = forms.ChoiceField( + label="Escolaridade", required=False, choices=Education.choices + ) + employment = forms.CharField(label="Ocupação", required=False) + short_description = forms.CharField( + label="Minibio", + widget=forms.Textarea(attrs={"placeholder": "Escreva uma breve biografia"}), + help_text="Fale um pouco sobre você e sua jornada até aqui. Até 500 caracteres.", + max_length=500 + ) + milestones = InlineArrayField( + forms.CharField(max_length=140, required=False), + required=False, + label="Histórico de atuação", + item_label="Realização", + add_button_text="Adicionar outra", + help_text="Adicione momentos e realizações marcantes da sua trajetória. Até 150 caracteres.", + placeholder="Recebi o Prêmio XYZ pela Iniciativa Ambiental", + ) + + def clean_milestones(self): + value = self.cleaned_data["milestones"] + return value + + class Meta: + title = "Trajetória" + description = "Compartilhe um pouco sobre sua trajetória. Essas informações ajudarão os eleitores a conhecerem melhor sua história e seu compromisso com a causa." + model = CandidatureFlow + entangled_fields = { + "properties": [ + "education", + "employment", + "short_description", + "milestones", + ] + } + untangled_fields = [] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_tag = False + self.helper.layout = Layout( + Div( + Div(Field("education"), css_class="g-col-12 g-col-md-6"), + Div(Field("employment"), css_class="g-col-12 g-col-md-6"), + Div(Field("short_description"), css_class="g-col-12"), + Div( + HTML("""