Skip to content

Commit c4badd8

Browse files
feat(service): support backend-based message generation with context formatting
1 parent 617e39b commit c4badd8

File tree

5 files changed

+115
-14
lines changed

5 files changed

+115
-14
lines changed

CHANGELOG.rst

+13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
Release Notes
22
-------------
33

4+
[3.1.0]
5+
^^^^^^^
6+
7+
Added
8+
"""""
9+
- Allow custom backends to override `generate_message(security_code, context=None)` for dynamic message generation at runtime.
10+
- `context` parameter support added to `send_verification()` for passing additional formatting data dynamically.
11+
12+
Changed
13+
"""""""
14+
- `PhoneVerificationService` now delegates message generation to the backend if `generate_message()` is implemented.
15+
- Moved `phone_settings` inside `__init__` for better error handling when `PHONE_VERIFICATION` is missing from settings.
16+
417
[3.0.1]
518
^^^^^^^
619

docs/customization.rst

+12
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@ Step 3: Implement the Backend Class
7171
'text': message,
7272
})
7373
74+
def generate_message(self, security_code, context=None):
75+
"""You can optionally override the message formatting by
76+
defining a `generate_message()` method in your backend.
77+
This method receives the `security_code` and an optional
78+
`context` dictionary passed at runtime, giving you more
79+
flexibility than using a static `MESSAGE` template."""
80+
username = context.get("username", "User") if context else "User"
81+
return f"Hi {username}, your OTP is {security_code}."
82+
7483
def send_bulk_sms(self, numbers, message):
7584
for number in numbers:
7685
self.send_sms(number, message)
@@ -107,6 +116,9 @@ Step 1: Implement the Sandbox Backend
107116
'text': message,
108117
})
109118
119+
def generate_message(self, security_code, context=None):
120+
return f"[SANDBOX] Your code is {security_code}"
121+
110122
def send_bulk_sms(self, numbers, message):
111123
for number in numbers:
112124
self.send_sms(number, message)

phone_verify/backends/base.py

+13
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,16 @@ def validate_security_code(self, security_code, phone_number, session_token):
148148
stored_verification.save()
149149

150150
return stored_verification, self.SECURITY_CODE_VALID
151+
152+
def generate_message(self, security_code, context=None):
153+
"""
154+
Optionally override this method to customize the verification message.
155+
156+
If this method returns None, the PhoneVerificationService will fall back
157+
to the default message defined in settings.
158+
159+
:param security_code: The generated verification code.
160+
:param context: Optional dictionary with runtime values.
161+
:return: A string message or None to trigger fallback.
162+
"""
163+
return None

phone_verify/services.py

+28-14
Original file line numberDiff line numberDiff line change
@@ -20,33 +20,47 @@
2020

2121

2222
class PhoneVerificationService(object):
23-
try:
24-
phone_settings = settings.PHONE_VERIFICATION
25-
except AttributeError as e:
26-
raise ImproperlyConfigured("Please define PHONE_VERIFICATION in settings") from e
27-
28-
verification_message = phone_settings.get("MESSAGE", DEFAULT_MESSAGE)
2923

3024
def __init__(self, phone_number, backend=None):
25+
try:
26+
self.phone_settings = settings.PHONE_VERIFICATION
27+
except AttributeError as e:
28+
raise ImproperlyConfigured("Please define PHONE_VERIFICATION in settings") from e
3129
self._check_required_settings()
3230
if backend is None:
3331
self.backend = get_sms_backend(phone_number=phone_number)
32+
else:
33+
self.backend = backend
3434

35-
def send_verification(self, number, security_code):
35+
self.verification_message = self.phone_settings.get("MESSAGE", DEFAULT_MESSAGE)
36+
37+
def send_verification(self, number, security_code, context=None):
3638
"""
3739
Send a verification text to the given number to verify.
3840
3941
:param number: the phone number of recipient.
42+
:param security_code: generated code to verify
43+
:param context: optional dictionary for custom message formatting
4044
"""
41-
message = self._generate_message(security_code)
42-
45+
message = self._generate_message(security_code, context)
4346
self.backend.send_sms(number, message)
4447

45-
def _generate_message(self, security_code):
46-
return self.verification_message.format(
47-
app=settings.PHONE_VERIFICATION.get("APP_NAME", DEFAULT_APP_NAME),
48-
security_code=security_code,
49-
)
48+
def _generate_message(self, security_code, context=None):
49+
# If the backend has its own message generator, prefer it
50+
if hasattr(self.backend, "generate_message") and callable(self.backend.generate_message):
51+
message = self.backend.generate_message(security_code, context=context)
52+
if message:
53+
return message
54+
55+
# Default fallback
56+
format_context = {
57+
"app": settings.PHONE_VERIFICATION.get("APP_NAME", DEFAULT_APP_NAME),
58+
"security_code": security_code,
59+
}
60+
if context:
61+
format_context.update(context)
62+
63+
return self.verification_message.format(**format_context)
5064

5165
def _check_required_settings(self):
5266
required_settings = {

tests/test_services.py

+49
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,52 @@ def test_exception_is_raised_when_no_settings(client, backend):
8282
importlib.reload(phone_verify.services)
8383
PhoneVerificationService(phone_number="+13478379634")
8484
assert exc.info == "Please define PHONE_VERIFICATION in settings"
85+
86+
87+
class DummyBackend:
88+
def __init__(self):
89+
self.sent_messages = []
90+
91+
def send_sms(self, number, message):
92+
self.sent_messages.append((number, message))
93+
94+
95+
class CustomBackendWithMessage(DummyBackend):
96+
def generate_message(self, security_code, context=None):
97+
return f"Custom: {security_code} / {context.get('extra', '')}"
98+
99+
100+
@pytest.mark.django_db
101+
def test_generate_message_default_fallback(settings):
102+
settings.PHONE_VERIFICATION = {
103+
'BACKEND': 'tests.test_services.DummyBackend',
104+
'OPTIONS': {},
105+
'TOKEN_LENGTH': 6,
106+
'MESSAGE': 'Code: {security_code} from {app}, note: {extra}',
107+
'APP_NAME': 'TestApp',
108+
'SECURITY_CODE_EXPIRATION_TIME': 300,
109+
'VERIFY_SECURITY_CODE_ONLY_ONCE': True,
110+
}
111+
112+
svc = PhoneVerificationService(phone_number="+1234567890", backend=DummyBackend())
113+
msg = svc._generate_message("123456", context={"extra": "extra-info"})
114+
115+
assert msg == "Code: 123456 from TestApp, note: extra-info"
116+
117+
118+
@pytest.mark.django_db
119+
def test_generate_message_from_custom_backend(settings):
120+
settings.PHONE_VERIFICATION = {
121+
'BACKEND': 'tests.test_services.CustomBackendWithMessage',
122+
'OPTIONS': {},
123+
'TOKEN_LENGTH': 6,
124+
'MESSAGE': 'SHOULD NOT BE USED',
125+
'APP_NAME': 'TestApp',
126+
'SECURITY_CODE_EXPIRATION_TIME': 300,
127+
'VERIFY_SECURITY_CODE_ONLY_ONCE': True,
128+
}
129+
130+
svc = PhoneVerificationService(phone_number="+1234567890", backend=CustomBackendWithMessage())
131+
msg = svc._generate_message("999999", context={"extra": "runtime"})
132+
133+
assert msg == "Custom: 999999 / runtime"

0 commit comments

Comments
 (0)