Skip to content

Commit c1236c7

Browse files
Merge pull request #17 from maxsonferovante/fix/sanitize-special-characters-certificate-names
fix: Corrige problema com caracteres especiais nos nomes dos certific…
2 parents cdfc4ec + 4d7391f commit c1236c7

2 files changed

Lines changed: 178 additions & 2 deletions

File tree

models/participant.py

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,16 @@
22
from typing import Optional
33
from .certificate import Certificate
44
from .event import Event
5+
import re
6+
import unicodedata
7+
import logging
58

69
import random
710
import string
811

12+
# Configure logger for this module
13+
logger = logging.getLogger(__name__)
14+
915
class Participant(BaseModel):
1016
first_name: str
1117
last_name: str
@@ -32,13 +38,83 @@ def name_completed(self):
3238

3339
return name_completed.title()
3440

41+
def _sanitize_filename(self, text: str) -> str:
42+
"""
43+
Sanitiza uma string para ser usada como nome de arquivo no S3.
44+
Remove ou substitui caracteres especiais que podem causar problemas em URLs.
45+
46+
Args:
47+
text (str): Texto a ser sanitizado
48+
49+
Returns:
50+
str: Texto sanitizado adequado para nomes de arquivo S3
51+
"""
52+
# Normaliza caracteres unicode (remove acentos)
53+
text = unicodedata.normalize('NFD', text)
54+
text = ''.join(char for char in text if unicodedata.category(char) != 'Mn')
55+
56+
# Substitui caracteres especiais comuns por versões seguras
57+
special_chars = {
58+
'º': 'o',
59+
'ª': 'a',
60+
'×': 'x',
61+
'@': '_at_',
62+
'&': '_and_',
63+
'+': '_plus_',
64+
'=': '_equals_',
65+
'%': '_percent_',
66+
'#': '_hash_',
67+
'?': '_question_',
68+
'/': '_slash_',
69+
'\\': '_backslash_',
70+
':': '_colon_',
71+
';': '_semicolon_',
72+
'<': '_lt_',
73+
'>': '_gt_',
74+
'|': '_pipe_',
75+
'*': '_star_',
76+
'"': '_quote_',
77+
"'": '_apostrophe_'
78+
}
79+
80+
# Aplica as substituições de caracteres especiais
81+
for char, replacement in special_chars.items():
82+
text = text.replace(char, replacement)
83+
84+
# Remove caracteres que não são alfanuméricos, espaços, hífens ou underscores
85+
text = re.sub(r'[^\w\s\-_]', '', text)
86+
87+
# Substitui espaços múltiplos por um único espaço
88+
text = re.sub(r'\s+', ' ', text)
89+
90+
# Substitui espaços por underscores
91+
text = text.replace(' ', '_')
92+
93+
# Remove underscores múltiplos consecutivos
94+
text = re.sub(r'_+', '_', text)
95+
96+
# Remove underscores do início e fim
97+
text = text.strip('_')
98+
99+
return text
35100

36101

37102
def formated_validation_code(self):
38103
self.validation_code = self.validation_code.upper()
39104
return f"{self.validation_code[0:3]}-{self.validation_code[3:6]}-{self.validation_code[6:9]}"
40105

41106
def create_name_certificate(self):
42-
name_certificate = self.name_completed() + self.event.product_name + "_" + self.formated_validation_code() + ".png"
43-
name_certificate = name_certificate.replace(" ", "_")
107+
# Sanitiza o nome do participante e o nome do produto separadamente
108+
sanitized_name = self._sanitize_filename(self.name_completed())
109+
sanitized_product = self._sanitize_filename(self.event.product_name)
110+
sanitized_validation = self._sanitize_filename(self.formated_validation_code())
111+
112+
# Combina os componentes sanitizados
113+
name_certificate = f"{sanitized_name}{sanitized_product}_{sanitized_validation}.png"
114+
115+
logger.info(f"Nome do participante antes da sanitização: {self.name_completed()}")
116+
logger.info(f"Nome do produto antes da sanitização: {self.event.product_name}")
117+
logger.info(f"Código de validação antes da sanitização: {self.formated_validation_code()}")
118+
logger.info(f"Nome do certificado após a sanitização: {name_certificate}")
119+
44120
return name_certificate

tests/test_participant.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,5 +125,105 @@ def test_name_completed_with_multiple_names(mock_certificate, mock_event):
125125
completed_name = participant.name_completed()
126126
assert len(completed_name.split()) == 3
127127
assert completed_name == "Jardel Silva Santos"
128+
129+
def test_sanitize_filename_special_characters(mock_certificate):
130+
"""Testa se a função de sanitização remove adequadamente caracteres especiais"""
131+
# Cria um evento com nome contendo caracteres especiais
132+
event_with_special_chars = Event(
133+
order_id=452,
134+
product_id=316,
135+
product_name="87º Python Floripa × CODECON @ UNICESUSC",
136+
date=datetime.strptime("2025-03-26 20:55:25", "%Y-%m-%d %H:%M:%S"),
137+
time_checkin=datetime.strptime("2025-03-26 20:55:44", "%Y-%m-%d %H:%M:%S"),
138+
checkin_latitude=-27.5460492,
139+
checkin_longitude=-48.6227075
140+
)
141+
142+
participant = Participant(
143+
first_name="Rodrigo",
144+
last_name="Farah",
145+
email="rodrigo@example.com",
146+
phone="(48) 98866-7447",
147+
cpf="000.000.000-00",
148+
certificate=mock_certificate,
149+
event=event_with_special_chars
150+
)
151+
152+
# Testa a função de sanitização diretamente
153+
sanitized = participant._sanitize_filename("87º Python Floripa × CODECON @ UNICESUSC")
154+
155+
# Verifica se caracteres especiais foram sanitizados
156+
assert 'º' not in sanitized
157+
assert '×' not in sanitized
158+
assert '@' not in sanitized
159+
assert sanitized == "87o_Python_Floripa_x_CODECON_at_UNICESUSC"
160+
161+
def test_create_name_certificate_with_special_characters(mock_certificate):
162+
"""Testa se o nome do certificado é gerado corretamente com caracteres especiais"""
163+
# Cria um evento com nome contendo caracteres especiais (igual ao exemplo do usuário)
164+
event_with_special_chars = Event(
165+
order_id=1397,
166+
product_id=1324,
167+
product_name="87º Python Floripa × CODECON @ UNICESUSC",
168+
date=datetime.strptime("2025-06-19 11:15:31", "%Y-%m-%d %H:%M:%S"),
169+
time_checkin=datetime.strptime("2025-06-19 11:15:31", "%Y-%m-%d %H:%M:%S"),
170+
checkin_latitude=-27.5460492,
171+
checkin_longitude=-48.6227075
172+
)
173+
174+
participant = Participant(
175+
first_name="Rodrigo",
176+
last_name="Farah",
177+
email="rodrigo.farah@example.com",
178+
phone="(48) 98866-7447",
179+
cpf="000.000.000-00",
180+
certificate=mock_certificate,
181+
event=event_with_special_chars
182+
)
183+
184+
name_certificate = participant.create_name_certificate()
185+
186+
# Verifica se o nome do certificado não contém caracteres especiais problemáticos
187+
assert 'º' not in name_certificate
188+
assert '×' not in name_certificate
189+
assert '@' not in name_certificate
190+
assert name_certificate.endswith(".png")
191+
192+
# Verifica se contém elementos esperados (sanitizados)
193+
assert "Rodrigo_Farah" in name_certificate
194+
assert "87o_Python_Floripa_x_CODECON_at_UNICESUSC" in name_certificate
195+
196+
# Verifica se o nome é válido para URL (não contém caracteres que precisam ser encoded)
197+
import re
198+
# Permite apenas caracteres alfanuméricos, hífens, underscores e pontos
199+
assert re.match(r'^[\w\-_.]+$', name_certificate), f"Nome do certificado contém caracteres inválidos: {name_certificate}"
200+
201+
def test_sanitize_filename_edge_cases(mock_certificate, mock_event):
202+
"""Testa casos extremos da função de sanitização"""
203+
participant = Participant(
204+
first_name="Test",
205+
last_name="User",
206+
email="test@example.com",
207+
phone="(48) 98866-7447",
208+
cpf="000.000.000-00",
209+
certificate=mock_certificate,
210+
event=mock_event
211+
)
212+
213+
# Testa strings com múltiplos caracteres especiais
214+
test_cases = {
215+
"": "", # String vazia
216+
" ": "", # Apenas espaços
217+
"___": "", # Apenas underscores
218+
"Test @@@ Test": "Test_at_at_at_Test", # Múltiplos símbolos
219+
"Açaí & Café": "Acai_and_Cafe", # Acentos e símbolos
220+
"100% Success!!!": "100_percent_Success", # Porcentagem e exclamações
221+
"C++ Programming": "C_plus_plus_Programming", # Símbolos de programação
222+
"file/path\\name": "file_slash_path_backslash_name", # Separadores de caminho
223+
}
224+
225+
for input_text, expected_output in test_cases.items():
226+
result = participant._sanitize_filename(input_text)
227+
assert result == expected_output, f"Para '{input_text}', esperado '{expected_output}', obtido '{result}'"
128228

129229

0 commit comments

Comments
 (0)