From 3e9c0f6b486fc627f07c8ef723bc3def2254357c Mon Sep 17 00:00:00 2001
From: Ran Benita
Date: Tue, 16 Apr 2024 19:32:15 +0300
Subject: [PATCH] Add support for `segno` as alternative to `qrcode` for QR
image generation
Fix #141.
---
CHANGES.rst | 13 ++++++++++++
docs/source/overview.rst | 5 +++--
pyproject.toml | 4 ++++
src/django_otp/plugins/otp_hotp/admin.py | 9 ++------
.../templates/otp_hotp/admin/config.html | 2 +-
src/django_otp/plugins/otp_totp/admin.py | 9 ++------
.../templates/otp_totp/admin/config.html | 2 +-
src/django_otp/qr.py | 21 +++++++++++++++++++
8 files changed, 47 insertions(+), 18 deletions(-)
create mode 100644 src/django_otp/qr.py
diff --git a/CHANGES.rst b/CHANGES.rst
index de33b269..c083faf3 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,3 +1,16 @@
+Pending
+--------------------------------------------------------------------------------
+
+- `#141`_: Support alternative QR code library `segno`_.
+
+ Previously, only the `qrcode`_ library was supported.
+
+ Use ``segno`` by installing ``django-otp[segno]`` or just install the
+ ``segno`` package.
+
+.. _#141: https://github.com/django-otp/django-otp/issues/141
+.. _segno: https://pypi.python.org/pypi/segno/
+
v1.4.1 - April 10, 2024 - Minor EmailDevice updates
--------------------------------------------------------------------------------
diff --git a/docs/source/overview.rst b/docs/source/overview.rst
index 081b61fd..4fdd8092 100644
--- a/docs/source/overview.rst
+++ b/docs/source/overview.rst
@@ -211,12 +211,13 @@ django-otp includes support for several standard device types.
:class:`~django_otp.plugins.otp_totp.models.TOTPDevice` handle standard OTP
algorithms, which can be used with a variety of OTP generators. For example,
it's easy to pair these devices with `Google Authenticator`_ using the `otpauth
-URL scheme`_. If you have the `qrcode`_ package installed, the admin interface
-will generate QR Codes for you.
+URL scheme`_. If you have either the `segno`_ or `qrcode`_ packages installed,
+the admin interface will generate QR Codes for you.
.. _Google Authenticator: https://github.com/google/google-authenticator
.. _otpauth URL scheme: https://github.com/google/google-authenticator/wiki/Key-Uri-Format
+.. _segno: https://pypi.python.org/pypi/segno/
.. _qrcode: https://pypi.python.org/pypi/qrcode/
diff --git a/pyproject.toml b/pyproject.toml
index 8719e3e0..3086c268 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -23,6 +23,9 @@ dependencies = [
]
[project.optional-dependencies]
+segno = [
+ "segno",
+]
qrcode = [
"qrcode",
]
@@ -38,6 +41,7 @@ Documentation = "https://django-otp-official.readthedocs.io/"
[tool.hatch.envs.default]
features = [
+ "segno",
"qrcode",
]
dependencies = [
diff --git a/src/django_otp/plugins/otp_hotp/admin.py b/src/django_otp/plugins/otp_hotp/admin.py
index 861c6998..90ddf2dc 100644
--- a/src/django_otp/plugins/otp_hotp/admin.py
+++ b/src/django_otp/plugins/otp_hotp/admin.py
@@ -7,6 +7,7 @@
from django.utils.html import format_html
from django_otp.conf import settings
+from django_otp.qrcode import write_qrcode_image
from .models import HOTPDevice
@@ -150,14 +151,8 @@ def qrcode_view(self, request, pk):
raise PermissionDenied()
try:
- import qrcode
- import qrcode.image.svg
-
- img = qrcode.make(
- device.config_url, image_factory=qrcode.image.svg.SvgImage
- )
response = HttpResponse(content_type='image/svg+xml')
- img.save(response)
+ write_qrcode_image(device.config_url, response)
except ImportError:
response = HttpResponse('', status=503)
diff --git a/src/django_otp/plugins/otp_hotp/templates/otp_hotp/admin/config.html b/src/django_otp/plugins/otp_hotp/templates/otp_hotp/admin/config.html
index cbc43346..37c9d221 100644
--- a/src/django_otp/plugins/otp_hotp/templates/otp_hotp/admin/config.html
+++ b/src/django_otp/plugins/otp_hotp/templates/otp_hotp/admin/config.html
@@ -10,7 +10,7 @@
>
- Install qrcode or use the URL below:
+ Install segno or qrcode or use the URL below:
{{ device.config_url }}
diff --git a/src/django_otp/plugins/otp_totp/admin.py b/src/django_otp/plugins/otp_totp/admin.py
index 836d889b..9d80e555 100644
--- a/src/django_otp/plugins/otp_totp/admin.py
+++ b/src/django_otp/plugins/otp_totp/admin.py
@@ -7,6 +7,7 @@
from django.utils.html import format_html
from django_otp.conf import settings
+from django_otp.qr import write_qrcode_image
from .models import TOTPDevice
@@ -150,14 +151,8 @@ def qrcode_view(self, request, pk):
raise PermissionDenied()
try:
- import qrcode
- import qrcode.image.svg
-
- img = qrcode.make(
- device.config_url, image_factory=qrcode.image.svg.SvgImage
- )
response = HttpResponse(content_type='image/svg+xml')
- img.save(response)
+ write_qrcode_image(device.config_url, response)
except ImportError:
response = HttpResponse('', status=503)
diff --git a/src/django_otp/plugins/otp_totp/templates/otp_totp/admin/config.html b/src/django_otp/plugins/otp_totp/templates/otp_totp/admin/config.html
index 8350a95a..aeb14c6d 100644
--- a/src/django_otp/plugins/otp_totp/templates/otp_totp/admin/config.html
+++ b/src/django_otp/plugins/otp_totp/templates/otp_totp/admin/config.html
@@ -10,7 +10,7 @@
>
- Install qrcode or use the URL below:
+ Install segno or qrcode or use the URL below:
{{ device.config_url }}
diff --git a/src/django_otp/qr.py b/src/django_otp/qr.py
new file mode 100644
index 00000000..0db982f5
--- /dev/null
+++ b/src/django_otp/qr.py
@@ -0,0 +1,21 @@
+def write_qrcode_image(data, out):
+ """Write a QR code image for data to out.
+
+ The written image is in image/svg+xml format.
+
+ One of `qrcode` or `segno` are required. If neither is found, raises
+ ModuleNotFoundError.
+ """
+ try:
+ import qrcode
+ import qrcode.image.svg
+
+ img = qrcode.make(
+ data, image_factory=qrcode.image.svg.SvgImage
+ )
+ img.save(out)
+ except ModuleNotFoundError:
+ import segno
+
+ img = segno.make(data)
+ img.save(out, kind='svg')