django-clickify 0.1.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.
- django_clickify-0.1.0/LICENSE +21 -0
- django_clickify-0.1.0/PKG-INFO +276 -0
- django_clickify-0.1.0/README.md +243 -0
- django_clickify-0.1.0/clickify/__init__.py +0 -0
- django_clickify-0.1.0/clickify/admin.py +24 -0
- django_clickify-0.1.0/clickify/apps.py +6 -0
- django_clickify-0.1.0/clickify/drf_urls.py +8 -0
- django_clickify-0.1.0/clickify/drf_views.py +46 -0
- django_clickify-0.1.0/clickify/middleware.py +22 -0
- django_clickify-0.1.0/clickify/migrations/0001_initial.py +80 -0
- django_clickify-0.1.0/clickify/migrations/__init__.py +0 -0
- django_clickify-0.1.0/clickify/models.py +46 -0
- django_clickify-0.1.0/clickify/templatetags/__init__.py +0 -0
- django_clickify-0.1.0/clickify/templatetags/clickify_tags.py +14 -0
- django_clickify-0.1.0/clickify/urls.py +8 -0
- django_clickify-0.1.0/clickify/utils.py +55 -0
- django_clickify-0.1.0/clickify/views.py +22 -0
- django_clickify-0.1.0/pyproject.toml +57 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Romjan Ali
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: django-clickify
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Django app to track file downloads with rate limiting, IP filtering, and geolocation.
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: django,click,tracker,ratelimit,ipfilter,geolocation
|
|
7
|
+
Author: Romjan Ali
|
|
8
|
+
Author-email: romjanvr5@gmail.com
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Web Environment
|
|
12
|
+
Classifier: Framework :: Django
|
|
13
|
+
Classifier: Framework :: Django :: 4.2
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Provides-Extra: drf
|
|
26
|
+
Requires-Dist: django (>=4.2)
|
|
27
|
+
Requires-Dist: django-ipware (>=4.0)
|
|
28
|
+
Requires-Dist: django-ratelimit (>=4.1)
|
|
29
|
+
Requires-Dist: requests (>=2.20)
|
|
30
|
+
Project-URL: Homepage, https://github.com/romjanxr/django-clickify
|
|
31
|
+
Project-URL: Repository, https://github.com/romjanxr/django-clickify
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# Django Clickify
|
|
35
|
+
|
|
36
|
+
[](https://badge.fury.io/py/django-clickify)
|
|
37
|
+
[](https://opensource.org/licenses/MIT)
|
|
38
|
+
|
|
39
|
+
A simple Django app to track clicks on any link (e.g., affiliate links, outbound links, file downloads) with rate limiting, IP filtering, and geolocation.
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
* **Click Tracking**: Logs every click on a tracked link, including IP address, user agent, and timestamp.
|
|
44
|
+
* **Geolocation**: Automatically enriches click logs with the country and city of the request's IP address via a web API.
|
|
45
|
+
* **Rate Limiting**: Prevents abuse by limiting the number of clicks per IP address in a given timeframe.
|
|
46
|
+
* **IP Filtering**: Easily configure allowlists and blocklists for IP addresses.
|
|
47
|
+
* **Secure**: Protects against path traversal attacks.
|
|
48
|
+
* **Django Admin Integration**: Create and manage your tracked links directly in the Django admin.
|
|
49
|
+
* **Template Tag & DRF View**: Provides both a simple template tag for traditional Django templates and a DRF API view for headless/JavaScript applications.
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
1. Install the package from PyPI:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install django-clickify
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
2. Add `'clickify'` to your `INSTALLED_APPS` in `settings.py`:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
INSTALLED_APPS = [
|
|
63
|
+
# ...
|
|
64
|
+
'clickify',
|
|
65
|
+
]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
3. Run migrations to create the necessary database models:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
python manage.py migrate
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
4. **For API support (Optional)**: If you plan to use the DRF view, you must also install `djangorestframework` and add it to your `INSTALLED_APPS`.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install django-clickify[drf]
|
|
78
|
+
```
|
|
79
|
+
```python
|
|
80
|
+
INSTALLED_APPS = [
|
|
81
|
+
# ...
|
|
82
|
+
'rest_framework',
|
|
83
|
+
'clickify',
|
|
84
|
+
]
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Configuration
|
|
88
|
+
|
|
89
|
+
### 1. Middleware (for IP Filtering)
|
|
90
|
+
|
|
91
|
+
To enable the IP allowlist and blocklist feature, add the `IPFilterMiddleware` to your `settings.py`.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
MIDDLEWARE = [
|
|
95
|
+
# ...
|
|
96
|
+
'clickify.middleware.IPFilterMiddleware',
|
|
97
|
+
# ...
|
|
98
|
+
]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 2. Settings (Optional)
|
|
102
|
+
|
|
103
|
+
You can customize the behavior of `django-clickify` by adding the following settings to your `settings.py`:
|
|
104
|
+
|
|
105
|
+
* `CLICKIFY_GEOLOCATION`: A boolean to enable or disable geolocation. Defaults to `True`.
|
|
106
|
+
* `CLICKIFY_RATE_LIMIT`: The rate limit for clicks. Defaults to `'5/m'`.
|
|
107
|
+
* `CLICKIFY_IP_ALLOWLIST`: A list of IP addresses that are always allowed. Defaults to `[]`.
|
|
108
|
+
* `CLICKIFY_IP_BLOCKLIST`: A list of IP addresses that are always blocked. Defaults to `[]`.
|
|
109
|
+
|
|
110
|
+
## Testing
|
|
111
|
+
|
|
112
|
+
To run the tests for this project, you'll need to have `pytest` and `pytest-django` installed. You can install them with:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
pip install pytest pytest-django
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Then, you can run the tests from the root of the project with:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
poetry run pytest
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
This will run all the tests in the `tests/` directory.
|
|
125
|
+
|
|
126
|
+
## Usage
|
|
127
|
+
|
|
128
|
+
### Option 1: Template-Based Usage
|
|
129
|
+
|
|
130
|
+
This is the standard way to use the app in traditional Django projects.
|
|
131
|
+
|
|
132
|
+
#### Step 1: Create a Tracked Link
|
|
133
|
+
|
|
134
|
+
In your Django Admin, go to the "Clickify" section and create a new "Tracked Link". This target can be any URL you want to track clicks on.
|
|
135
|
+
|
|
136
|
+
The `Target Url` can point to any type of file (e.g., PDF, ZIP, MP3, MP4, TXT) or any webpage. The link can be hosted anywhere, such as Amazon S3, a personal blog, or an affiliate partner's site.
|
|
137
|
+
|
|
138
|
+
* **Name:** `Monthly Report PDF`
|
|
139
|
+
* **Slug:** `monthly-report-pdf` (this will be auto-populated from the name)
|
|
140
|
+
* **Target Url:** `https://your-s3-bucket.s3.amazonaws.com/reports/monthly-summary.pdf`
|
|
141
|
+
|
|
142
|
+
#### Step 2: Include Clickify URLs
|
|
143
|
+
|
|
144
|
+
In your project's `urls.py`, include the `clickify` URL patterns.
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
# your_project/urls.py
|
|
148
|
+
from django.urls import path, include
|
|
149
|
+
|
|
150
|
+
urlpatterns = [
|
|
151
|
+
# ... your other urls
|
|
152
|
+
path('track/', include('clickify.urls', namespace='clickify')),
|
|
153
|
+
]
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
#### Step 3: Create the Tracked Link
|
|
157
|
+
|
|
158
|
+
In your Django template, use the `track_url` template tag to generate the tracking link. Use the slug of the `TrackedLink` you created in Step 1.
|
|
159
|
+
|
|
160
|
+
```html
|
|
161
|
+
<!-- your_app/templates/my_template.html -->
|
|
162
|
+
{% load clickify_tags %}
|
|
163
|
+
|
|
164
|
+
<a href="{% track_url 'monthly-report-pdf' %}">
|
|
165
|
+
Download Monthly Summary
|
|
166
|
+
</a>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Option 2: API Usage (for Headless/JS Frameworks)
|
|
170
|
+
|
|
171
|
+
If you are using a JavaScript frontend (like React, Vue, etc.) or need a programmatic way to get a tracked URL, you can use the DRF API endpoint.
|
|
172
|
+
|
|
173
|
+
#### Step 1: Create a Tracked Link
|
|
174
|
+
|
|
175
|
+
Follow Step 1 from the template-based usage above.
|
|
176
|
+
|
|
177
|
+
#### Step 2: Include Clickify DRF URLs
|
|
178
|
+
|
|
179
|
+
In your project's `urls.py`, include the `clickify.drf_urls` patterns.
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
# your_project/urls.py
|
|
183
|
+
from django.urls import path, include
|
|
184
|
+
|
|
185
|
+
urlpatterns = [
|
|
186
|
+
# ... your other urls
|
|
187
|
+
path('api/track/', include('clickify.drf_urls', namespace='clickify-api')),
|
|
188
|
+
]
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
#### Step 3: Make the API Request
|
|
192
|
+
|
|
193
|
+
From your frontend, make a `POST` request to the API endpoint using the slug of your `TrackedLink`.
|
|
194
|
+
|
|
195
|
+
**Endpoint**: `POST /api/track/<slug>/`
|
|
196
|
+
|
|
197
|
+
A successful request will track the click and return the actual file URL, which you can then use to trigger the click or redirection on the client-side.
|
|
198
|
+
|
|
199
|
+
**Example using JavaScript `fetch`:**
|
|
200
|
+
|
|
201
|
+
```javascript
|
|
202
|
+
fetch('/api/track/monthly-report-pdf/', {
|
|
203
|
+
method: 'POST',
|
|
204
|
+
headers: {
|
|
205
|
+
// Include CSRF token if necessary for your setup
|
|
206
|
+
'X-CSRFToken': 'YourCsrfTokenHere'
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
.then(response => response.json())
|
|
210
|
+
.then(data => {
|
|
211
|
+
if (data.target_url) {
|
|
212
|
+
console.log("Click tracked. Redirecting to:", data.target_url);
|
|
213
|
+
// Redirect the user to the URL
|
|
214
|
+
window.location.href = data.target_url;
|
|
215
|
+
} else {
|
|
216
|
+
console.error("Failed to track click:", data);
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
.catch(error => {
|
|
220
|
+
console.error('Error:', error);
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Successful Response (`200 OK`):**
|
|
225
|
+
```json
|
|
226
|
+
{
|
|
227
|
+
"message": "Click tracked successfully",
|
|
228
|
+
"target_url": "https://your-s3-bucket.s3.amazonaws.com/reports/monthly-summary.pdf"
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**Failure Responses**
|
|
233
|
+
|
|
234
|
+
If the request fails, you might receive one of the following error responses:
|
|
235
|
+
|
|
236
|
+
* **404 Not Found:**
|
|
237
|
+
|
|
238
|
+
```json
|
|
239
|
+
{
|
|
240
|
+
"detail": "Not found."
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
* **429 Too Many Requests:**
|
|
245
|
+
|
|
246
|
+
```json
|
|
247
|
+
{
|
|
248
|
+
"error": "Rate limit exceeded. Please try again later"
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
* **403 Forbidden:** (If IP filtering is enabled and the IP is blocked)
|
|
253
|
+
|
|
254
|
+
This will typically return a plain text response like:
|
|
255
|
+
```
|
|
256
|
+
IP address blocked.
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### How It Works
|
|
260
|
+
|
|
261
|
+
1. A user clicks a tracked link (`/track/monthly-report-pdf/`) or a `POST` request is sent to the API.
|
|
262
|
+
2. The view or API view records the click event in the database, associating it with the correct `TrackedLink`.
|
|
263
|
+
3. The standard view issues a `302 Redirect` to the `target_url`. The API view returns a JSON response containing the `target_url`.
|
|
264
|
+
4. The user's browser is redirected to the final destination.
|
|
265
|
+
|
|
266
|
+
This approach is powerful because if you ever need to change the link's destination, you only need to update the `Target Url` in the Django Admin. All your tracked links and API calls will continue to work correctly.
|
|
267
|
+
|
|
268
|
+
## Contributing
|
|
269
|
+
|
|
270
|
+
Contributions are welcome! If you'd like to contribute to this project, please follow these steps:
|
|
271
|
+
|
|
272
|
+
1. Fork the repository.
|
|
273
|
+
2. Create a new branch for your feature or bug fix.
|
|
274
|
+
3. Make your changes and add tests for them.
|
|
275
|
+
4. Ensure the tests pass by running `poetry run pytest`.
|
|
276
|
+
5. Create a pull request with a clear description of your changes.
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# Django Clickify
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/py/django-clickify)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
A simple Django app to track clicks on any link (e.g., affiliate links, outbound links, file downloads) with rate limiting, IP filtering, and geolocation.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
* **Click Tracking**: Logs every click on a tracked link, including IP address, user agent, and timestamp.
|
|
11
|
+
* **Geolocation**: Automatically enriches click logs with the country and city of the request's IP address via a web API.
|
|
12
|
+
* **Rate Limiting**: Prevents abuse by limiting the number of clicks per IP address in a given timeframe.
|
|
13
|
+
* **IP Filtering**: Easily configure allowlists and blocklists for IP addresses.
|
|
14
|
+
* **Secure**: Protects against path traversal attacks.
|
|
15
|
+
* **Django Admin Integration**: Create and manage your tracked links directly in the Django admin.
|
|
16
|
+
* **Template Tag & DRF View**: Provides both a simple template tag for traditional Django templates and a DRF API view for headless/JavaScript applications.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
1. Install the package from PyPI:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install django-clickify
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
2. Add `'clickify'` to your `INSTALLED_APPS` in `settings.py`:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
INSTALLED_APPS = [
|
|
30
|
+
# ...
|
|
31
|
+
'clickify',
|
|
32
|
+
]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
3. Run migrations to create the necessary database models:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
python manage.py migrate
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
4. **For API support (Optional)**: If you plan to use the DRF view, you must also install `djangorestframework` and add it to your `INSTALLED_APPS`.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install django-clickify[drf]
|
|
45
|
+
```
|
|
46
|
+
```python
|
|
47
|
+
INSTALLED_APPS = [
|
|
48
|
+
# ...
|
|
49
|
+
'rest_framework',
|
|
50
|
+
'clickify',
|
|
51
|
+
]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Configuration
|
|
55
|
+
|
|
56
|
+
### 1. Middleware (for IP Filtering)
|
|
57
|
+
|
|
58
|
+
To enable the IP allowlist and blocklist feature, add the `IPFilterMiddleware` to your `settings.py`.
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
MIDDLEWARE = [
|
|
62
|
+
# ...
|
|
63
|
+
'clickify.middleware.IPFilterMiddleware',
|
|
64
|
+
# ...
|
|
65
|
+
]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 2. Settings (Optional)
|
|
69
|
+
|
|
70
|
+
You can customize the behavior of `django-clickify` by adding the following settings to your `settings.py`:
|
|
71
|
+
|
|
72
|
+
* `CLICKIFY_GEOLOCATION`: A boolean to enable or disable geolocation. Defaults to `True`.
|
|
73
|
+
* `CLICKIFY_RATE_LIMIT`: The rate limit for clicks. Defaults to `'5/m'`.
|
|
74
|
+
* `CLICKIFY_IP_ALLOWLIST`: A list of IP addresses that are always allowed. Defaults to `[]`.
|
|
75
|
+
* `CLICKIFY_IP_BLOCKLIST`: A list of IP addresses that are always blocked. Defaults to `[]`.
|
|
76
|
+
|
|
77
|
+
## Testing
|
|
78
|
+
|
|
79
|
+
To run the tests for this project, you'll need to have `pytest` and `pytest-django` installed. You can install them with:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pip install pytest pytest-django
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Then, you can run the tests from the root of the project with:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
poetry run pytest
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
This will run all the tests in the `tests/` directory.
|
|
92
|
+
|
|
93
|
+
## Usage
|
|
94
|
+
|
|
95
|
+
### Option 1: Template-Based Usage
|
|
96
|
+
|
|
97
|
+
This is the standard way to use the app in traditional Django projects.
|
|
98
|
+
|
|
99
|
+
#### Step 1: Create a Tracked Link
|
|
100
|
+
|
|
101
|
+
In your Django Admin, go to the "Clickify" section and create a new "Tracked Link". This target can be any URL you want to track clicks on.
|
|
102
|
+
|
|
103
|
+
The `Target Url` can point to any type of file (e.g., PDF, ZIP, MP3, MP4, TXT) or any webpage. The link can be hosted anywhere, such as Amazon S3, a personal blog, or an affiliate partner's site.
|
|
104
|
+
|
|
105
|
+
* **Name:** `Monthly Report PDF`
|
|
106
|
+
* **Slug:** `monthly-report-pdf` (this will be auto-populated from the name)
|
|
107
|
+
* **Target Url:** `https://your-s3-bucket.s3.amazonaws.com/reports/monthly-summary.pdf`
|
|
108
|
+
|
|
109
|
+
#### Step 2: Include Clickify URLs
|
|
110
|
+
|
|
111
|
+
In your project's `urls.py`, include the `clickify` URL patterns.
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
# your_project/urls.py
|
|
115
|
+
from django.urls import path, include
|
|
116
|
+
|
|
117
|
+
urlpatterns = [
|
|
118
|
+
# ... your other urls
|
|
119
|
+
path('track/', include('clickify.urls', namespace='clickify')),
|
|
120
|
+
]
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
#### Step 3: Create the Tracked Link
|
|
124
|
+
|
|
125
|
+
In your Django template, use the `track_url` template tag to generate the tracking link. Use the slug of the `TrackedLink` you created in Step 1.
|
|
126
|
+
|
|
127
|
+
```html
|
|
128
|
+
<!-- your_app/templates/my_template.html -->
|
|
129
|
+
{% load clickify_tags %}
|
|
130
|
+
|
|
131
|
+
<a href="{% track_url 'monthly-report-pdf' %}">
|
|
132
|
+
Download Monthly Summary
|
|
133
|
+
</a>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Option 2: API Usage (for Headless/JS Frameworks)
|
|
137
|
+
|
|
138
|
+
If you are using a JavaScript frontend (like React, Vue, etc.) or need a programmatic way to get a tracked URL, you can use the DRF API endpoint.
|
|
139
|
+
|
|
140
|
+
#### Step 1: Create a Tracked Link
|
|
141
|
+
|
|
142
|
+
Follow Step 1 from the template-based usage above.
|
|
143
|
+
|
|
144
|
+
#### Step 2: Include Clickify DRF URLs
|
|
145
|
+
|
|
146
|
+
In your project's `urls.py`, include the `clickify.drf_urls` patterns.
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
# your_project/urls.py
|
|
150
|
+
from django.urls import path, include
|
|
151
|
+
|
|
152
|
+
urlpatterns = [
|
|
153
|
+
# ... your other urls
|
|
154
|
+
path('api/track/', include('clickify.drf_urls', namespace='clickify-api')),
|
|
155
|
+
]
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
#### Step 3: Make the API Request
|
|
159
|
+
|
|
160
|
+
From your frontend, make a `POST` request to the API endpoint using the slug of your `TrackedLink`.
|
|
161
|
+
|
|
162
|
+
**Endpoint**: `POST /api/track/<slug>/`
|
|
163
|
+
|
|
164
|
+
A successful request will track the click and return the actual file URL, which you can then use to trigger the click or redirection on the client-side.
|
|
165
|
+
|
|
166
|
+
**Example using JavaScript `fetch`:**
|
|
167
|
+
|
|
168
|
+
```javascript
|
|
169
|
+
fetch('/api/track/monthly-report-pdf/', {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
headers: {
|
|
172
|
+
// Include CSRF token if necessary for your setup
|
|
173
|
+
'X-CSRFToken': 'YourCsrfTokenHere'
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
.then(response => response.json())
|
|
177
|
+
.then(data => {
|
|
178
|
+
if (data.target_url) {
|
|
179
|
+
console.log("Click tracked. Redirecting to:", data.target_url);
|
|
180
|
+
// Redirect the user to the URL
|
|
181
|
+
window.location.href = data.target_url;
|
|
182
|
+
} else {
|
|
183
|
+
console.error("Failed to track click:", data);
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
.catch(error => {
|
|
187
|
+
console.error('Error:', error);
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Successful Response (`200 OK`):**
|
|
192
|
+
```json
|
|
193
|
+
{
|
|
194
|
+
"message": "Click tracked successfully",
|
|
195
|
+
"target_url": "https://your-s3-bucket.s3.amazonaws.com/reports/monthly-summary.pdf"
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Failure Responses**
|
|
200
|
+
|
|
201
|
+
If the request fails, you might receive one of the following error responses:
|
|
202
|
+
|
|
203
|
+
* **404 Not Found:**
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
{
|
|
207
|
+
"detail": "Not found."
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
* **429 Too Many Requests:**
|
|
212
|
+
|
|
213
|
+
```json
|
|
214
|
+
{
|
|
215
|
+
"error": "Rate limit exceeded. Please try again later"
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
* **403 Forbidden:** (If IP filtering is enabled and the IP is blocked)
|
|
220
|
+
|
|
221
|
+
This will typically return a plain text response like:
|
|
222
|
+
```
|
|
223
|
+
IP address blocked.
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### How It Works
|
|
227
|
+
|
|
228
|
+
1. A user clicks a tracked link (`/track/monthly-report-pdf/`) or a `POST` request is sent to the API.
|
|
229
|
+
2. The view or API view records the click event in the database, associating it with the correct `TrackedLink`.
|
|
230
|
+
3. The standard view issues a `302 Redirect` to the `target_url`. The API view returns a JSON response containing the `target_url`.
|
|
231
|
+
4. The user's browser is redirected to the final destination.
|
|
232
|
+
|
|
233
|
+
This approach is powerful because if you ever need to change the link's destination, you only need to update the `Target Url` in the Django Admin. All your tracked links and API calls will continue to work correctly.
|
|
234
|
+
|
|
235
|
+
## Contributing
|
|
236
|
+
|
|
237
|
+
Contributions are welcome! If you'd like to contribute to this project, please follow these steps:
|
|
238
|
+
|
|
239
|
+
1. Fork the repository.
|
|
240
|
+
2. Create a new branch for your feature or bug fix.
|
|
241
|
+
3. Make your changes and add tests for them.
|
|
242
|
+
4. Ensure the tests pass by running `poetry run pytest`.
|
|
243
|
+
5. Create a pull request with a clear description of your changes.
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
from .models import TrackedLink, ClickLog
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@admin.register(TrackedLink)
|
|
6
|
+
class TrackedLinkAdmin(admin.ModelAdmin):
|
|
7
|
+
list_display = ('name', 'slug', 'target_url', 'created_at')
|
|
8
|
+
search_fields = ('name', 'slug', 'target_url')
|
|
9
|
+
prepopulated_fields = {'slug': ('name')}
|
|
10
|
+
list_filter = ('created_at')
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@admin.register(ClickLog)
|
|
14
|
+
class ClickLogAdmin(admin.ModelAdmin):
|
|
15
|
+
list_display = ('target', 'ip_address', 'country', 'city', 'timestamp')
|
|
16
|
+
search_fields = ('target__name', 'ip_address', 'country', 'city')
|
|
17
|
+
list_filter = ('target', 'country', 'timestamp')
|
|
18
|
+
readonly_fields = [field.name for field in ClickLog._meta.fields]
|
|
19
|
+
|
|
20
|
+
def has_add_permission(self, request):
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
def has_delete_permission(self, request, obj=...):
|
|
24
|
+
return False
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from rest_framework.views import APIView
|
|
2
|
+
from rest_framework.response import Response
|
|
3
|
+
from rest_framework import status
|
|
4
|
+
from django.shortcuts import get_object_or_404
|
|
5
|
+
from django_ratelimit.decorators import ratelimit
|
|
6
|
+
from django.utils.decorators import method_decorator
|
|
7
|
+
from django.conf import settings
|
|
8
|
+
from .models import TrackedLink
|
|
9
|
+
from .views import track_click
|
|
10
|
+
from .utils import create_click_log
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@method_decorator(
|
|
14
|
+
ratelimit(
|
|
15
|
+
key='ip',
|
|
16
|
+
rate=lambda r, g: getattr(settings, 'CLICKIFY_RATE_LIMIT', '5/m'),
|
|
17
|
+
block=True
|
|
18
|
+
),
|
|
19
|
+
name='post'
|
|
20
|
+
)
|
|
21
|
+
class TrackClickAPIView(APIView):
|
|
22
|
+
""" An API View to track a click for a TrackedLink. """
|
|
23
|
+
|
|
24
|
+
def post(self, request, slug, format=None):
|
|
25
|
+
""" Tracks a click for the given slug. """
|
|
26
|
+
target = get_object_or_404(TrackedLink, slug=slug)
|
|
27
|
+
|
|
28
|
+
# Use the helper function with the underlying Django request
|
|
29
|
+
create_click_log(target=target, request=request._request)
|
|
30
|
+
|
|
31
|
+
return Response(
|
|
32
|
+
{"message": "Click tracked successfully",
|
|
33
|
+
"target_url": target.target_url},
|
|
34
|
+
status=status.HTTP_200_OK
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def throttled(self, request, wait):
|
|
38
|
+
"""
|
|
39
|
+
Custom handler for when a request is rate-limited
|
|
40
|
+
Note: This is for DRF's own throttling, not django-ratelimit.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
return Response(
|
|
44
|
+
{"error": "Rate limit exceeded. Please try again later"},
|
|
45
|
+
status=status.HTTP_429_TOO_MANY_REQUESTS
|
|
46
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from django.conf import settings
|
|
2
|
+
from django.core.exceptions import PermissionDenied
|
|
3
|
+
from ipware import get_client_ip
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class IPFilterMiddleware:
|
|
7
|
+
def __init__(self, get_response):
|
|
8
|
+
self.get_response = get_response
|
|
9
|
+
|
|
10
|
+
def __call__(self, request):
|
|
11
|
+
ip_allowlist = getattr(settings, 'CLICKIFY_IP_ALLOWLIST', [])
|
|
12
|
+
ip_blocklist = getattr(settings, 'CLICKIFY_IP_BLOCKLIST', [])
|
|
13
|
+
client_ip, _ = get_client_ip(request)
|
|
14
|
+
|
|
15
|
+
if ip_allowlist and client_ip not in ip_allowlist:
|
|
16
|
+
raise PermissionDenied("IP address not allowed.")
|
|
17
|
+
|
|
18
|
+
if client_ip in ip_blocklist:
|
|
19
|
+
raise PermissionDenied("IP address blocked.")
|
|
20
|
+
|
|
21
|
+
response = self.get_response(request)
|
|
22
|
+
return response
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Generated by Django 4.2.23 on 2025-08-28 09:03
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
import django.db.models.deletion
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
|
|
10
|
+
initial = True
|
|
11
|
+
|
|
12
|
+
dependencies = []
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
migrations.CreateModel(
|
|
16
|
+
name="TrackedLink",
|
|
17
|
+
fields=[
|
|
18
|
+
(
|
|
19
|
+
"id",
|
|
20
|
+
models.UUIDField(
|
|
21
|
+
default=uuid.uuid4,
|
|
22
|
+
editable=False,
|
|
23
|
+
primary_key=True,
|
|
24
|
+
serialize=False,
|
|
25
|
+
),
|
|
26
|
+
),
|
|
27
|
+
(
|
|
28
|
+
"name",
|
|
29
|
+
models.CharField(
|
|
30
|
+
help_text="A user-friendly name for tracked link, e.g., Monthly Report PDF",
|
|
31
|
+
max_length=255,
|
|
32
|
+
),
|
|
33
|
+
),
|
|
34
|
+
(
|
|
35
|
+
"slug",
|
|
36
|
+
models.SlugField(
|
|
37
|
+
help_text="A unique slug for the URL. E.g., 'monthly-report-pdf' ",
|
|
38
|
+
max_length=255,
|
|
39
|
+
unique=True,
|
|
40
|
+
),
|
|
41
|
+
),
|
|
42
|
+
(
|
|
43
|
+
"target_url",
|
|
44
|
+
models.URLField(
|
|
45
|
+
help_text="The actual URL to the destination (e.g., on S3, a blog post, an affiliate link, etc.)",
|
|
46
|
+
max_length=2048,
|
|
47
|
+
),
|
|
48
|
+
),
|
|
49
|
+
("created_at", models.DateTimeField(auto_now_add=True)),
|
|
50
|
+
("updated_at", models.DateTimeField(auto_now=True)),
|
|
51
|
+
],
|
|
52
|
+
),
|
|
53
|
+
migrations.CreateModel(
|
|
54
|
+
name="ClickLog",
|
|
55
|
+
fields=[
|
|
56
|
+
(
|
|
57
|
+
"id",
|
|
58
|
+
models.BigAutoField(
|
|
59
|
+
auto_created=True,
|
|
60
|
+
primary_key=True,
|
|
61
|
+
serialize=False,
|
|
62
|
+
verbose_name="ID",
|
|
63
|
+
),
|
|
64
|
+
),
|
|
65
|
+
("ip_address", models.GenericIPAddressField()),
|
|
66
|
+
("user_agent", models.TextField()),
|
|
67
|
+
("timestamp", models.DateTimeField(auto_now_add=True)),
|
|
68
|
+
("country", models.CharField(blank=True, max_length=100, null=True)),
|
|
69
|
+
("city", models.CharField(blank=True, max_length=100, null=True)),
|
|
70
|
+
(
|
|
71
|
+
"target",
|
|
72
|
+
models.ForeignKey(
|
|
73
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
74
|
+
related_name="clicks",
|
|
75
|
+
to="clickify.trackedlink",
|
|
76
|
+
),
|
|
77
|
+
),
|
|
78
|
+
],
|
|
79
|
+
),
|
|
80
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
import uuid
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TrackedLink(models.Model):
|
|
6
|
+
"""
|
|
7
|
+
Represents a link that can be tracked.
|
|
8
|
+
This model decouples the link from its actual URL,
|
|
9
|
+
allowing the URL to change without losing the click history.
|
|
10
|
+
"""
|
|
11
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
12
|
+
name = models.CharField(max_length=255,
|
|
13
|
+
help_text="A user-friendly name for tracked link, e.g., Monthly Report PDF"
|
|
14
|
+
)
|
|
15
|
+
slug = models.SlugField(
|
|
16
|
+
max_length=255,
|
|
17
|
+
unique=True,
|
|
18
|
+
help_text="A unique slug for the URL. E.g., 'monthly-report-pdf' "
|
|
19
|
+
)
|
|
20
|
+
target_url = models.URLField(
|
|
21
|
+
max_length=2048,
|
|
22
|
+
help_text="The actual URL to the destination (e.g., on S3, a blog post, an affiliate link, etc.)")
|
|
23
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
24
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
25
|
+
|
|
26
|
+
def __str__(self):
|
|
27
|
+
return self.name
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ClickLog(models.Model):
|
|
31
|
+
"""
|
|
32
|
+
Logs a single click event for a TrackedLink.
|
|
33
|
+
"""
|
|
34
|
+
target = models.ForeignKey(
|
|
35
|
+
TrackedLink,
|
|
36
|
+
on_delete=models.CASCADE,
|
|
37
|
+
related_name='clicks'
|
|
38
|
+
)
|
|
39
|
+
ip_address = models.GenericIPAddressField()
|
|
40
|
+
user_agent = models.TextField()
|
|
41
|
+
timestamp = models.DateTimeField(auto_now_add=True)
|
|
42
|
+
country = models.CharField(max_length=100, blank=True, null=True)
|
|
43
|
+
city = models.CharField(max_length=100, blank=True, null=True)
|
|
44
|
+
|
|
45
|
+
def __str__(self):
|
|
46
|
+
return f"Click on {self.target.name} at {self.timestamp}"
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from django import template
|
|
2
|
+
from django.urls import reverse
|
|
3
|
+
|
|
4
|
+
register = template.Library()
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@register.simple_tag
|
|
8
|
+
def track_url(slug):
|
|
9
|
+
"""
|
|
10
|
+
A template tag that returns the tracked URL for a TrackedLink slug.
|
|
11
|
+
Usage: {% track_url 'my-link-slug' %}
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
return reverse('clickify:track_click', kwargs={'slug': slug})
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from django.conf import settings
|
|
3
|
+
from .models import ClickLog
|
|
4
|
+
from ipware import get_client_ip
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_geolocation(ip_address):
|
|
8
|
+
"""
|
|
9
|
+
Get the geolocation for a given IP address using the ip-api.com service.
|
|
10
|
+
This function should only be called for public, routable IP addresses.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
# Geolocation can be disabled globally in settings
|
|
14
|
+
if not getattr(settings, 'CLICKIFY_GEOLOCATION', True):
|
|
15
|
+
return None, None
|
|
16
|
+
|
|
17
|
+
if not ip_address:
|
|
18
|
+
return None, None
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
# The API endpoint. We request only the fields we need
|
|
22
|
+
url = f"http://ip-api.com/json/{ip_address}?fields=status,country,city"
|
|
23
|
+
response = requests.get(url, timeout=2)
|
|
24
|
+
response.raise_for_status() # Raise an exception for bad status codes (4xx or 5xx)
|
|
25
|
+
data = response.json()
|
|
26
|
+
|
|
27
|
+
if data.get('status') == 'success':
|
|
28
|
+
return data.get('country'), data.get('city')
|
|
29
|
+
else:
|
|
30
|
+
return None, None
|
|
31
|
+
except (requests.RequestException, ValueError):
|
|
32
|
+
# Catch network errors, timeouts, or JSON decoding errors
|
|
33
|
+
return None, None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def create_click_log(target, request):
|
|
37
|
+
"""
|
|
38
|
+
Helper function to create a ClickLog object.
|
|
39
|
+
This contains the core tracking logic that can be reused by both the standard view and the DRF API view.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
ip, is_routable = get_client_ip(request)
|
|
43
|
+
user_agent = request.META.get("HTTP_USER_AGENT", "")
|
|
44
|
+
|
|
45
|
+
country, city = (None, None)
|
|
46
|
+
if is_routable:
|
|
47
|
+
country, city = get_geolocation(ip)
|
|
48
|
+
|
|
49
|
+
ClickLog.objects.create(
|
|
50
|
+
target=target,
|
|
51
|
+
ip_address=ip,
|
|
52
|
+
user_agent=user_agent,
|
|
53
|
+
country=country,
|
|
54
|
+
city=city
|
|
55
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from django.http import HttpResponseRedirect, HttpResponseForbidden
|
|
2
|
+
from django.shortcuts import get_object_or_404
|
|
3
|
+
from django.conf import settings
|
|
4
|
+
from django_ratelimit.exceptions import Ratelimited
|
|
5
|
+
from django_ratelimit.decorators import ratelimit
|
|
6
|
+
from .models import TrackedLink
|
|
7
|
+
from .utils import create_click_log
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@ratelimit(key='ip', rate=lambda r, g: getattr(settings, 'CLICKIFY_RATE_LIMIT', '5/m'), block=True)
|
|
11
|
+
def track_click(request, slug):
|
|
12
|
+
"""
|
|
13
|
+
Tracks a click for a TrackedLink and then redirects to its actual URL.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
target = get_object_or_404(TrackedLink, slug=slug)
|
|
18
|
+
# Call the helper function to do the actual tracking
|
|
19
|
+
create_click_log(target=target, request=request)
|
|
20
|
+
return HttpResponseRedirect(target.target_url)
|
|
21
|
+
except Ratelimited:
|
|
22
|
+
return HttpResponseForbidden("Rate limit exceeded. Please try again later.")
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "django-clickify"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A Django app to track file downloads with rate limiting, IP filtering, and geolocation."
|
|
5
|
+
authors = ["Romjan Ali <romjanvr5@gmail.com>"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
homepage = "https://github.com/romjanxr/django-clickify"
|
|
9
|
+
repository = "https://github.com/romjanxr/django-clickify"
|
|
10
|
+
keywords = ["django", "click", "tracker", "ratelimit", "ipfilter", "geolocation"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 4 - Beta",
|
|
13
|
+
"Environment :: Web Environment",
|
|
14
|
+
"Framework :: Django",
|
|
15
|
+
"Framework :: Django :: 4.2",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
25
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
|
+
]
|
|
27
|
+
packages = [
|
|
28
|
+
{ include = "clickify" }
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[tool.poetry.dependencies]
|
|
32
|
+
python = ">=3.10"
|
|
33
|
+
django = ">=4.2"
|
|
34
|
+
django-ipware = ">=4.0" # Using a more flexible specifier
|
|
35
|
+
django-ratelimit = ">=4.1" # Using a more flexible specifier
|
|
36
|
+
requests = ">=2.20" # Added for the new geolocation API
|
|
37
|
+
|
|
38
|
+
[tool.poetry.extras]
|
|
39
|
+
drf = ["djangorestframework"]
|
|
40
|
+
|
|
41
|
+
[tool.poetry.group.dev.dependencies]
|
|
42
|
+
pytest = ">=8.0"
|
|
43
|
+
pytest-django = ">=4.5"
|
|
44
|
+
black = {version = "^24.0", extras = ["d"]}
|
|
45
|
+
isort = ">=5.10"
|
|
46
|
+
djangorestframework = "^3.16.1"
|
|
47
|
+
|
|
48
|
+
[build-system]
|
|
49
|
+
requires = ["poetry-core>=1.0.0"]
|
|
50
|
+
build-backend = "poetry.core.masonry.api"
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
DJANGO_SETTINGS_MODULE = "tests.settings"
|
|
54
|
+
python_files = "tests.py test_*.py *_tests.py"
|
|
55
|
+
filterwarnings = [
|
|
56
|
+
"ignore::DeprecationWarning:django.core.cache.backends.filebased",
|
|
57
|
+
]
|