From c2def3a16a1bf359a7e44f358163cd7252058764 Mon Sep 17 00:00:00 2001 From: Leandro de Souza Date: Wed, 2 Mar 2022 15:31:56 -0300 Subject: [PATCH 1/3] Feature (widget): Adds AutoBrazilianZipCodeInput widget --- src/brazilian_zipcode/__init__.py | 1 + .../static/getAddressInfo.js | 88 +++++++++++++++++++ src/brazilian_zipcode/widgets.py | 13 +++ 3 files changed, 102 insertions(+) create mode 100644 src/brazilian_zipcode/static/getAddressInfo.js create mode 100644 src/brazilian_zipcode/widgets.py diff --git a/src/brazilian_zipcode/__init__.py b/src/brazilian_zipcode/__init__.py index d228e6d..47a3958 100644 --- a/src/brazilian_zipcode/__init__.py +++ b/src/brazilian_zipcode/__init__.py @@ -1,2 +1,3 @@ from .forms import BrazilianZipCodeField +from .widgets import AutoBrazilianZipCodeInput from .objects import BrazilianAddress diff --git a/src/brazilian_zipcode/static/getAddressInfo.js b/src/brazilian_zipcode/static/getAddressInfo.js new file mode 100644 index 0000000..cfb0a80 --- /dev/null +++ b/src/brazilian_zipcode/static/getAddressInfo.js @@ -0,0 +1,88 @@ +const zipcodeSelector = "[meta_id='meta_zipcode_info']"; +const saveButtonSelector = "[name='_save']" + +function isNumeric(str) { + if (typeof str != "string") return false + return !isNaN(str) && + !isNaN(parseFloat(str)) + } + +function onlyDigits(value) { + return value.replace(/\D/g, "") +}; + +function removeReadOnlyInput(label) { + const previousElement = document.querySelector(`[meta-id='${label}']`); + if (previousElement !== null) { + previousElement.remove(); + } +} + +function createReadOnlyInput(label, value) { + const input = document.querySelector(zipcodeSelector); + const parentDiv = input.parentNode.parentNode.parentNode; + + removeReadOnlyInput(label); + const div = document.createElement("div"); + div.classList.add("form-row"); + div.setAttribute("meta-id", label) + + const valueDiv = document.createElement("div"); + const labelElement = document.createElement("label"); + + labelElement.innerText = label + ":"; + labelElement.classList.add("required"); + div.appendChild(labelElement); + + valueDiv.classList.add("readonly"); + valueDiv.innerText = value; + div.appendChild(valueDiv); + + parentDiv.appendChild(div); +} + +function getAddressInfo(zipcode) { + const saveButton = document.querySelector(saveButtonSelector); + + fetch(`/api/address_info?zipcode=${zipcode}`).then((response) => { + if (response.status !== 200) { + saveButton.setAttribute('disabled', true) + removeReadOnlyInput("Rua"); + removeReadOnlyInput("Bairro"); + removeReadOnlyInput("Cidade"); + removeReadOnlyInput("UF"); + window.alert("CEP não encontrado ou serviço indisponível"); + return + } + response.json().then(data => { + createReadOnlyInput("Rua", data.street); + createReadOnlyInput("Bairro", data.district); + createReadOnlyInput("Cidade", data.city); + createReadOnlyInput("UF", data.state_initials); + }) + }) + saveButton.removeAttribute('disabled'); +}; + +function handleOnFocusOutEvent(event) { + var zipcode = event.target.value; + zipcode = onlyDigits(zipcode); + + const input = document.querySelector(zipcodeSelector); + input.value = zipcode; + + if (zipcode.length !== 8) { + return; + } + if (!isNumeric(zipcode)) { + return + } + getAddressInfo(zipcode); +}; + + + +$(document).ready(function () { + const input = document.querySelector(zipcodeMetaId); + input.addEventListener("focusout", handleOnFocusOutEvent) +}); \ No newline at end of file diff --git a/src/brazilian_zipcode/widgets.py b/src/brazilian_zipcode/widgets.py new file mode 100644 index 0000000..e39dffd --- /dev/null +++ b/src/brazilian_zipcode/widgets.py @@ -0,0 +1,13 @@ +from django.forms import widgets +from django.utils.safestring import mark_safe + + +class AutoBrazilianZipCodeInput(widgets.TextInput): + + def render(self, name, value, **attrs): + attrs['meta_id'] = 'meta_zipcode_info' + return super().render(name, value, attrs) + + + class Media: + js = ("getAddressInfo.js",) From d7842a383cce97b6fc0c4f530c484c2816a88da8 Mon Sep 17 00:00:00 2001 From: Leandro de Souza Date: Wed, 2 Mar 2022 15:48:42 -0300 Subject: [PATCH 2/3] Refactor (readme): Adds new documentation to widget --- README.md | 97 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 87 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 2ac9fb8..827870a 100644 --- a/README.md +++ b/README.md @@ -24,23 +24,36 @@ class YourForm(forms.Form): ```python # your_app/views.py -from django.http import HttpRequest +from django.http import HttpRequest, HttpResponse from brazilian_zipcode import BrazilianAddress from your_app.forms import YourForm def your_view(request: HttpRequest): form = YourForm(data=request.POST) - form.is_valid(raise_exception=True) - - form.zipcode_info # This attr is an instance of: `BrazilianAddress` - print(form.zipcode_info.street) - print(form.zipcode_info.zipcode) - print(form.zipcode_info.district) - print(form.zipcode_info.city) - print(form.zipcode_info.state_initials) + if not form.is_valid(): + return HttpResponse(status=400) + + zipcode_info = form.cleaned_date.get("zipcode_info") + print(zipcode_info.street) + print(zipcode_info.zipcode) + print(zipcode_info.district) + print(zipcode_info.city) + print(zipcode_info.state_initials) + return HttpResponse(status=200) +``` + +```python +# your_app/urls.py + +from django.urls import path +from . import views + +urlpatterns = [ ... + path('your_view/', views.your_view) +] ``` ### As an API @@ -75,9 +88,70 @@ That returns a `JSON` like: If the service is unavailable, you may receive a 404 response. + +### As an Widget (Requires the API step) + +You can use this widget to automatically show the address on the admin using the widget `AutoBrazilianZipCodeInput`. +After the user inputs the zipcode, it will make an get request to the API endpoint and displays the result directly on the admin. +Follow these steps: + +1; Create your form: + +```python +# your_app.forms.py + +from django import forms +from brazilian_zipcode import BrazilianZipCodeField +from brazilian_zipcode.widgets import AutoBrazilianZipCodeInput +from your_app.models import YourModel + + +class MyAddressForm(forms.ModelForm): + # Here we used ModelForm, but you can use any other form. + + zipcode_info = BrazilianZipCodeField(widget=AutoBrazilianZipCodeInput()) + + class Meta: + model = YourModel + fields = ['zipcode_info'] + +``` + +2; Set the widget on the field: + +```python +# your_app.forms.py + +class MyAddressForm(forms.ModelForm): + # Here we used ModelForm, but you can use any other form. + + zipcode_info = BrazilianZipCodeField(widget=AutoBrazilianZipCodeInput()) + # You can also use any other CharField you like +``` + +3; Set the form on the admin: + +```python +# your_app.admin.py + +from django.contrib import admin +from .models import Address +from .forms import MyAddressForm + +# Register your models here. +@admin.register(Address) +class MyAdressAdmin(admin.ModelAdmin): + form = MyAddressForm + +``` + +4; Collect the required JavaScript: +> python3 manage.py collectstatic + ### Writing your own Address Retrieve Service -To provide your own service for retrieving addresses, you should: +You can also provide your custom service for retrieving addresses. This will be used on all use cases stated above. +To do so you should: 1; Create a Class that implements an `classmethod` called `get_address_info`: @@ -124,3 +198,6 @@ class YourAddressRetrievingService: ADDRESS_PARSER_CLASS = "your_app.module.YourAddressRetrievingService" ``` + + +Enjoy! :D From 97a3fb6ac452ff7bae8924d0eb4a21812e1e8e51 Mon Sep 17 00:00:00 2001 From: Leandro de Souza Date: Wed, 2 Mar 2022 15:52:38 -0300 Subject: [PATCH 3/3] Fix (formatting): Runs black on widget --- src/brazilian_zipcode/widgets.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/brazilian_zipcode/widgets.py b/src/brazilian_zipcode/widgets.py index e39dffd..f98433c 100644 --- a/src/brazilian_zipcode/widgets.py +++ b/src/brazilian_zipcode/widgets.py @@ -3,11 +3,9 @@ class AutoBrazilianZipCodeInput(widgets.TextInput): - def render(self, name, value, **attrs): - attrs['meta_id'] = 'meta_zipcode_info' + attrs["meta_id"] = "meta_zipcode_info" return super().render(name, value, attrs) - class Media: js = ("getAddressInfo.js",)