prune_captcha 1.10.0__tar.gz → 1.12.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. {prune_captcha-1.10.0 → prune_captcha-1.12.0}/PKG-INFO +10 -41
  2. {prune_captcha-1.10.0 → prune_captcha-1.12.0}/README.md +9 -40
  3. prune_captcha-1.12.0/captcha_prune/utils.py +44 -0
  4. {prune_captcha-1.10.0 → prune_captcha-1.12.0}/prune_captcha.egg-info/PKG-INFO +10 -41
  5. {prune_captcha-1.10.0 → prune_captcha-1.12.0}/prune_captcha.egg-info/SOURCES.txt +0 -3
  6. {prune_captcha-1.10.0 → prune_captcha-1.12.0}/pyproject.toml +1 -1
  7. prune_captcha-1.10.0/captcha_prune/urls.py +0 -10
  8. prune_captcha-1.10.0/captcha_prune/utils.py +0 -68
  9. prune_captcha-1.10.0/captcha_prune/views.py +0 -45
  10. prune_captcha-1.10.0/commons/decorators.py +0 -41
  11. {prune_captcha-1.10.0 → prune_captcha-1.12.0}/captcha_prune/__init__.py +0 -0
  12. {prune_captcha-1.10.0 → prune_captcha-1.12.0}/captcha_prune/apps.py +0 -0
  13. {prune_captcha-1.10.0 → prune_captcha-1.12.0}/captcha_prune/asgi.py +0 -0
  14. {prune_captcha-1.10.0 → prune_captcha-1.12.0}/captcha_prune/migrations/0001_initial.py +0 -0
  15. {prune_captcha-1.10.0 → prune_captcha-1.12.0}/captcha_prune/migrations/0002_remove_captcha_created_at_remove_captcha_pos_x_and_more.py +0 -0
  16. {prune_captcha-1.10.0 → prune_captcha-1.12.0}/captcha_prune/migrations/0003_captcha_created_at_captcha_updated_at.py +0 -0
  17. {prune_captcha-1.10.0 → prune_captcha-1.12.0}/captcha_prune/migrations/__init__.py +0 -0
  18. {prune_captcha-1.10.0 → prune_captcha-1.12.0}/captcha_prune/models.py +0 -0
  19. {prune_captcha-1.10.0 → prune_captcha-1.12.0}/captcha_prune/payloads.py +0 -0
  20. {prune_captcha-1.10.0 → prune_captcha-1.12.0}/captcha_prune/settings.py +0 -0
  21. {prune_captcha-1.10.0 → prune_captcha-1.12.0}/captcha_prune/wsgi.py +0 -0
  22. {prune_captcha-1.10.0 → prune_captcha-1.12.0}/commons/base_model.py +0 -0
  23. {prune_captcha-1.10.0 → prune_captcha-1.12.0}/prune_captcha.egg-info/dependency_links.txt +0 -0
  24. {prune_captcha-1.10.0 → prune_captcha-1.12.0}/prune_captcha.egg-info/requires.txt +0 -0
  25. {prune_captcha-1.10.0 → prune_captcha-1.12.0}/prune_captcha.egg-info/top_level.txt +0 -0
  26. {prune_captcha-1.10.0 → prune_captcha-1.12.0}/setup.cfg +0 -0
  27. {prune_captcha-1.10.0 → prune_captcha-1.12.0}/tests/__init__.py +0 -0
  28. {prune_captcha-1.10.0 → prune_captcha-1.12.0}/tests/captcha_prune/__init__.py +0 -0
  29. {prune_captcha-1.10.0 → prune_captcha-1.12.0}/tests/captcha_prune/test_views.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prune_captcha
3
- Version: 1.10.0
3
+ Version: 1.12.0
4
4
  Summary: A tool to protect formulaire from spam.
5
5
  Author-email: Arnout <bastien@prune.sh>
6
6
  Project-URL: Made_by, https://prune.sh/
@@ -61,21 +61,21 @@ Don't hesitate to regularly run `poetry update`, as the captcha evolves with tim
61
61
  In `settings.py`, set the path to the images used for the puzzle:
62
62
 
63
63
  ```python
64
- PUZZLE_IMAGE_STATIC_PATH = "website/static/website/images/"
64
+ PUZZLE_IMAGE_STATIC_PATH = "website/static/website/images/puzzles/"
65
65
  ```
66
66
 
67
67
  ### Utilisation
68
68
 
69
69
  - GET request (form display)
70
70
 
71
- - Use create_and_get_puzzle to generate the captcha data:
71
+ - Use create_captcha to generate the captcha data:
72
72
 
73
73
  ```python
74
- from captcha_prune.utils import create_and_get_puzzle
74
+ from captcha_prune.utils import create_captcha
75
75
  ```
76
76
 
77
77
  ```python
78
- puzzle = create_and_get_puzzle(request)
78
+ puzzle = create_captcha(request)
79
79
  ```
80
80
 
81
81
  - Passes the data into the context under the puzzle variable:
@@ -103,50 +103,19 @@ PUZZLE_IMAGE_STATIC_PATH = "website/static/website/images/"
103
103
  ```
104
104
 
105
105
  ```python
106
- response = verify_captcha(
107
- request, redirect("/"), redirect("website:contact-page"), form
108
- )
106
+ response = verify_captcha(request)
109
107
  ```
110
108
 
111
- - None if the captcha is correct.
109
+ - True if the captcha is correct, else False.
112
110
 
113
- - Redirects in case of expired session or incorrect captcha:
114
-
115
- ```python
116
- if response is not None:
117
- return response
118
- ```
119
-
120
- ### Example
121
-
122
- ```python
123
- def contact_view(request):
124
- if request.method == "POST":
125
- form = ContactForm(request.POST)
126
- if form.is_valid():
127
- response = verify_captcha(
128
- request, redirect("/"), redirect("website:contact-page"), form
129
- )
130
- if response is not None:
131
- return response
132
- messages.success(request, "Formulaire soumis avec succès.")
133
- return redirect("/")
134
- else:
135
- puzzle = create_and_get_puzzle(request)
136
- else:
137
- form = ContactForm()
138
- puzzle = create_and_get_puzzle(request)
139
- return render(
140
- request,
141
- "website/pages/contact/page.html",
142
- {"form": form, "puzzle": puzzle},
143
- )
144
- ```
111
+ - Redirects in case of incorrect captcha.
145
112
 
146
113
  # Available Versions
147
114
 
148
115
  | Version | Date | Notes |
149
116
  | ------- | ---------- | ---------------------------------- |
117
+ | 1.12.0 | 2025-05-21 | removed views, added utils, ... |
118
+ | 1.11.0 | 2025-05-21 | removed utils |
150
119
  | 1.10.0 | 2025-05-20 | fix documentation, removed ... |
151
120
  | 1.9.0 | 2025-05-20 | puzzle images path fixed |
152
121
  | 1.8.0 | 2025-05-20 | added migrations |
@@ -43,21 +43,21 @@ Don't hesitate to regularly run `poetry update`, as the captcha evolves with tim
43
43
  In `settings.py`, set the path to the images used for the puzzle:
44
44
 
45
45
  ```python
46
- PUZZLE_IMAGE_STATIC_PATH = "website/static/website/images/"
46
+ PUZZLE_IMAGE_STATIC_PATH = "website/static/website/images/puzzles/"
47
47
  ```
48
48
 
49
49
  ### Utilisation
50
50
 
51
51
  - GET request (form display)
52
52
 
53
- - Use create_and_get_puzzle to generate the captcha data:
53
+ - Use create_captcha to generate the captcha data:
54
54
 
55
55
  ```python
56
- from captcha_prune.utils import create_and_get_puzzle
56
+ from captcha_prune.utils import create_captcha
57
57
  ```
58
58
 
59
59
  ```python
60
- puzzle = create_and_get_puzzle(request)
60
+ puzzle = create_captcha(request)
61
61
  ```
62
62
 
63
63
  - Passes the data into the context under the puzzle variable:
@@ -85,50 +85,19 @@ PUZZLE_IMAGE_STATIC_PATH = "website/static/website/images/"
85
85
  ```
86
86
 
87
87
  ```python
88
- response = verify_captcha(
89
- request, redirect("/"), redirect("website:contact-page"), form
90
- )
88
+ response = verify_captcha(request)
91
89
  ```
92
90
 
93
- - None if the captcha is correct.
91
+ - True if the captcha is correct, else False.
94
92
 
95
- - Redirects in case of expired session or incorrect captcha:
96
-
97
- ```python
98
- if response is not None:
99
- return response
100
- ```
101
-
102
- ### Example
103
-
104
- ```python
105
- def contact_view(request):
106
- if request.method == "POST":
107
- form = ContactForm(request.POST)
108
- if form.is_valid():
109
- response = verify_captcha(
110
- request, redirect("/"), redirect("website:contact-page"), form
111
- )
112
- if response is not None:
113
- return response
114
- messages.success(request, "Formulaire soumis avec succès.")
115
- return redirect("/")
116
- else:
117
- puzzle = create_and_get_puzzle(request)
118
- else:
119
- form = ContactForm()
120
- puzzle = create_and_get_puzzle(request)
121
- return render(
122
- request,
123
- "website/pages/contact/page.html",
124
- {"form": form, "puzzle": puzzle},
125
- )
126
- ```
93
+ - Redirects in case of incorrect captcha.
127
94
 
128
95
  # Available Versions
129
96
 
130
97
  | Version | Date | Notes |
131
98
  | ------- | ---------- | ---------------------------------- |
99
+ | 1.12.0 | 2025-05-21 | removed views, added utils, ... |
100
+ | 1.11.0 | 2025-05-21 | removed utils |
132
101
  | 1.10.0 | 2025-05-20 | fix documentation, removed ... |
133
102
  | 1.9.0 | 2025-05-20 | puzzle images path fixed |
134
103
  | 1.8.0 | 2025-05-20 | added migrations |
@@ -0,0 +1,44 @@
1
+ import os
2
+ import random
3
+
4
+ from django.conf import settings
5
+ from django.shortcuts import get_object_or_404
6
+
7
+ from captcha_prune.models import Captcha
8
+
9
+
10
+ def create_and_get_captcha() -> dict:
11
+ captcha = Captcha.objects.create()
12
+
13
+ _, _, puzzle_images_path = settings.PUZZLE_IMAGE_STATIC_PATH.rpartition("static/")
14
+ puzzle_images = [
15
+ f
16
+ for f in os.listdir(settings.PUZZLE_IMAGE_STATIC_PATH)
17
+ if f.lower().endswith((".jpg", ".jpeg", ".png", ".gif", ".webp"))
18
+ ]
19
+ selected_image = random.choice(puzzle_images)
20
+
21
+ return {
22
+ "uuid": captcha.uuid,
23
+ "width": captcha.width,
24
+ "height": captcha.height,
25
+ "piece_width": captcha.piece_width,
26
+ "piece_height": captcha.piece_height,
27
+ "pos_x_solution": captcha.pos_x_solution,
28
+ "pos_y_solution": captcha.pos_y_solution,
29
+ "piece_pos_x": captcha.piece_pos_x,
30
+ "piece_pos_y": captcha.piece_pos_y,
31
+ "image": f"{puzzle_images_path}{selected_image}",
32
+ }
33
+
34
+
35
+ def verify_captcha(puzzle_uuid: str, pos_x_answer: int, pos_y_answer: int) -> bool:
36
+ captcha = get_object_or_404(Captcha, uuid=puzzle_uuid)
37
+
38
+ if (
39
+ abs(captcha.pos_x_solution - pos_x_answer) <= captcha.precision
40
+ and abs(captcha.pos_y_solution - pos_y_answer) <= captcha.precision
41
+ ):
42
+ return True
43
+ else:
44
+ return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prune_captcha
3
- Version: 1.10.0
3
+ Version: 1.12.0
4
4
  Summary: A tool to protect formulaire from spam.
5
5
  Author-email: Arnout <bastien@prune.sh>
6
6
  Project-URL: Made_by, https://prune.sh/
@@ -61,21 +61,21 @@ Don't hesitate to regularly run `poetry update`, as the captcha evolves with tim
61
61
  In `settings.py`, set the path to the images used for the puzzle:
62
62
 
63
63
  ```python
64
- PUZZLE_IMAGE_STATIC_PATH = "website/static/website/images/"
64
+ PUZZLE_IMAGE_STATIC_PATH = "website/static/website/images/puzzles/"
65
65
  ```
66
66
 
67
67
  ### Utilisation
68
68
 
69
69
  - GET request (form display)
70
70
 
71
- - Use create_and_get_puzzle to generate the captcha data:
71
+ - Use create_captcha to generate the captcha data:
72
72
 
73
73
  ```python
74
- from captcha_prune.utils import create_and_get_puzzle
74
+ from captcha_prune.utils import create_captcha
75
75
  ```
76
76
 
77
77
  ```python
78
- puzzle = create_and_get_puzzle(request)
78
+ puzzle = create_captcha(request)
79
79
  ```
80
80
 
81
81
  - Passes the data into the context under the puzzle variable:
@@ -103,50 +103,19 @@ PUZZLE_IMAGE_STATIC_PATH = "website/static/website/images/"
103
103
  ```
104
104
 
105
105
  ```python
106
- response = verify_captcha(
107
- request, redirect("/"), redirect("website:contact-page"), form
108
- )
106
+ response = verify_captcha(request)
109
107
  ```
110
108
 
111
- - None if the captcha is correct.
109
+ - True if the captcha is correct, else False.
112
110
 
113
- - Redirects in case of expired session or incorrect captcha:
114
-
115
- ```python
116
- if response is not None:
117
- return response
118
- ```
119
-
120
- ### Example
121
-
122
- ```python
123
- def contact_view(request):
124
- if request.method == "POST":
125
- form = ContactForm(request.POST)
126
- if form.is_valid():
127
- response = verify_captcha(
128
- request, redirect("/"), redirect("website:contact-page"), form
129
- )
130
- if response is not None:
131
- return response
132
- messages.success(request, "Formulaire soumis avec succès.")
133
- return redirect("/")
134
- else:
135
- puzzle = create_and_get_puzzle(request)
136
- else:
137
- form = ContactForm()
138
- puzzle = create_and_get_puzzle(request)
139
- return render(
140
- request,
141
- "website/pages/contact/page.html",
142
- {"form": form, "puzzle": puzzle},
143
- )
144
- ```
111
+ - Redirects in case of incorrect captcha.
145
112
 
146
113
  # Available Versions
147
114
 
148
115
  | Version | Date | Notes |
149
116
  | ------- | ---------- | ---------------------------------- |
117
+ | 1.12.0 | 2025-05-21 | removed views, added utils, ... |
118
+ | 1.11.0 | 2025-05-21 | removed utils |
150
119
  | 1.10.0 | 2025-05-20 | fix documentation, removed ... |
151
120
  | 1.9.0 | 2025-05-20 | puzzle images path fixed |
152
121
  | 1.8.0 | 2025-05-20 | added migrations |
@@ -6,16 +6,13 @@ captcha_prune/asgi.py
6
6
  captcha_prune/models.py
7
7
  captcha_prune/payloads.py
8
8
  captcha_prune/settings.py
9
- captcha_prune/urls.py
10
9
  captcha_prune/utils.py
11
- captcha_prune/views.py
12
10
  captcha_prune/wsgi.py
13
11
  captcha_prune/migrations/0001_initial.py
14
12
  captcha_prune/migrations/0002_remove_captcha_created_at_remove_captcha_pos_x_and_more.py
15
13
  captcha_prune/migrations/0003_captcha_created_at_captcha_updated_at.py
16
14
  captcha_prune/migrations/__init__.py
17
15
  commons/base_model.py
18
- commons/decorators.py
19
16
  prune_captcha.egg-info/PKG-INFO
20
17
  prune_captcha.egg-info/SOURCES.txt
21
18
  prune_captcha.egg-info/dependency_links.txt
@@ -20,7 +20,7 @@ classifiers = [
20
20
  keywords = ["captcha", "django", "code-quality"]
21
21
  urls = {Made_by = "https://prune.sh/"}
22
22
  name = "prune_captcha"
23
- version = "1.10.0"
23
+ version = "1.12.0"
24
24
  description = "A tool to protect formulaire from spam."
25
25
  readme = "README.md"
26
26
  dependencies = [
@@ -1,10 +0,0 @@
1
- from django.urls import path
2
-
3
- from captcha_prune.views import create_captcha_view, verify_captcha_view
4
-
5
- app_name = "captcha"
6
-
7
- urlpatterns = [
8
- path("", create_captcha_view, name="create-captcha"),
9
- path("<str:uuid>/", verify_captcha_view, name="verify-captcha"),
10
- ]
@@ -1,68 +0,0 @@
1
- import os
2
- import random
3
- from urllib.parse import urlencode
4
-
5
- import requests
6
- from django.conf import settings
7
- from django.contrib import messages
8
- from django.http import HttpRequest, HttpResponse
9
- from django.urls import reverse
10
-
11
-
12
- def create_and_get_puzzle(request: HttpRequest) -> HttpResponse | dict:
13
- _, _, puzzle_images_path = settings.PUZZLE_IMAGE_STATIC_PATH.rpartition("static/")
14
- try:
15
- response = requests.post(
16
- request.build_absolute_uri(reverse("captcha:create-captcha"))
17
- ).json()
18
- except requests.RequestException:
19
- return HttpResponse(status=502)
20
- request.session["puzzle_uuid"] = response["uuid"]
21
- puzzle_images = [
22
- f
23
- for f in os.listdir(settings.PUZZLE_IMAGE_STATIC_PATH)
24
- if f.lower().endswith((".jpg", ".jpeg", ".png", ".gif"))
25
- ]
26
- selected_image = random.choice(puzzle_images)
27
- return {
28
- "uuid": response["uuid"],
29
- "width": response["width"],
30
- "height": response["height"],
31
- "piece_width": response["piece_width"],
32
- "piece_height": response["piece_height"],
33
- "pos_x_solution": response["pos_x_solution"],
34
- "pos_y_solution": response["pos_y_solution"],
35
- "piece_pos_x": response["piece_pos_x"],
36
- "piece_pos_y": response["piece_pos_y"],
37
- "image": f"{puzzle_images_path}{selected_image}",
38
- }
39
-
40
-
41
- def verify_captcha(
42
- request: HttpRequest,
43
- session_expired: HttpResponse,
44
- incorrect_captcha: HttpResponse,
45
- form,
46
- ) -> HttpResponse | None:
47
- puzzle_uuid = request.session.get("puzzle_uuid")
48
- if not puzzle_uuid:
49
- messages.error(request, "La session a expiré.")
50
- return session_expired
51
- try:
52
- base_url = request.build_absolute_uri(
53
- reverse("captcha:verify-captcha", kwargs={"uuid": puzzle_uuid}),
54
- )
55
- data = {
56
- "pos_x_answer": request.POST.get("pos_x_answer"),
57
- "pos_y_answer": request.POST.get("pos_y_answer"),
58
- }
59
- query_string = urlencode(data)
60
- full_url = f"{base_url}?{query_string}"
61
- response = requests.get(full_url)
62
- if response.status_code == 401:
63
- messages.error(request, "Captcha incorrect. Veuillez réessayer.")
64
- return incorrect_captcha
65
- del form.fields["pos_x_answer"]
66
- del form.fields["pos_y_answer"]
67
- except requests.RequestException:
68
- return HttpResponse(status=502)
@@ -1,45 +0,0 @@
1
- from django.http import HttpRequest, HttpResponse, JsonResponse
2
- from django.shortcuts import get_object_or_404
3
- from django.views.decorators.csrf import csrf_exempt
4
- from django.views.decorators.http import require_GET, require_POST
5
-
6
- from captcha_prune.models import Captcha
7
- from captcha_prune.payloads import PuzzleAnswerPayload
8
- from commons.decorators import use_payload
9
-
10
-
11
- @require_POST
12
- @csrf_exempt
13
- def create_captcha_view(request: HttpRequest) -> HttpResponse:
14
- captcha = Captcha.objects.create()
15
- return JsonResponse(
16
- status=201,
17
- data={
18
- "uuid": str(captcha.uuid),
19
- "width": captcha.width,
20
- "height": captcha.height,
21
- "piece_width": captcha.piece_width,
22
- "piece_height": captcha.piece_height,
23
- "pos_x_solution": captcha.pos_x_solution,
24
- "pos_y_solution": captcha.pos_y_solution,
25
- "piece_pos_x": captcha.piece_pos_x,
26
- "piece_pos_y": captcha.piece_pos_y,
27
- },
28
- )
29
-
30
-
31
- @require_GET
32
- @use_payload(PuzzleAnswerPayload)
33
- def verify_captcha_view(
34
- request: HttpRequest, uuid: str, payload: PuzzleAnswerPayload
35
- ) -> HttpResponse:
36
- captcha = get_object_or_404(Captcha, uuid=uuid)
37
- pos_x_answer = payload.pos_x_answer
38
- pos_y_answer = payload.pos_y_answer
39
- if (
40
- abs(captcha.pos_x_solution - pos_x_answer) <= captcha.precision
41
- and abs(captcha.pos_y_solution - pos_y_answer) <= captcha.precision
42
- ):
43
- return HttpResponse(status=304)
44
- else:
45
- return HttpResponse(status=401)
@@ -1,41 +0,0 @@
1
- import functools
2
- from typing import get_origin
3
-
4
- from django.http import HttpResponse
5
- from pydantic import ValidationError
6
-
7
-
8
- def use_payload(payload_class):
9
- def decorator(func):
10
- @functools.wraps(func)
11
- def wrapper(request, *args, **kwargs):
12
- data = {}
13
-
14
- if request.method == "POST":
15
- body = request.POST
16
- elif request.method == "GET":
17
- body = request.GET
18
- else:
19
- raise NotImplementedError
20
-
21
- for field_name, field in payload_class.model_fields.items():
22
- url_name = field.alias if field.alias else field_name
23
- is_list_field = get_origin(field.annotation) is list
24
- url_value = (
25
- body.getlist(url_name) if is_list_field else body.get(url_name)
26
- )
27
- if url_value is not None:
28
- data[url_name] = url_value
29
-
30
- try:
31
- payload = payload_class.model_validate(data)
32
- except ValidationError as e:
33
- return HttpResponse(
34
- e.json(), headers={"content_type": "application/json"}, status=400
35
- )
36
-
37
- return func(request, *args, payload=payload, **kwargs)
38
-
39
- return wrapper
40
-
41
- return decorator
File without changes