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.
@@ -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
+ [![PyPI version](https://badge.fury.io/py/django-clickify.svg)](https://badge.fury.io/py/django-clickify)
37
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ [![PyPI version](https://badge.fury.io/py/django-clickify.svg)](https://badge.fury.io/py/django-clickify)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,6 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class ClickifyConfig(AppConfig):
5
+ default_auto_field = 'django.db.models.BigAutoField'
6
+ name = 'clickify'
@@ -0,0 +1,8 @@
1
+ from django.urls import path
2
+ from .drf_views import TrackClickAPIView
3
+
4
+ app_name = "clickify-drf"
5
+
6
+ urlpatterns = [
7
+ path('<slug:slug>/', TrackClickAPIView.as_view(), name='track_click_api'),
8
+ ]
@@ -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}"
@@ -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,8 @@
1
+ from django.urls import path
2
+ from .views import track_click
3
+
4
+ app_name = 'clickify'
5
+
6
+ urlpatterns = [
7
+ path('<slug:slug>', track_click, name='track_click'),
8
+ ]
@@ -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
+ ]