From 3be6cde9222fe705a8c09241d83c6928797f7dd3 Mon Sep 17 00:00:00 2001 From: Fareck Allony Date: Thu, 22 Oct 2020 11:15:19 +0300 Subject: [PATCH 01/15] created yourls db importer custom command --- tinylinks/management/commands/_config.py | 21 ++++++ tinylinks/management/commands/_queries.py | 5 ++ .../management/commands/import_yourls_db.py | 69 +++++++++++++++++++ 3 files changed, 95 insertions(+) create mode 100644 tinylinks/management/commands/_config.py create mode 100644 tinylinks/management/commands/_queries.py create mode 100644 tinylinks/management/commands/import_yourls_db.py diff --git a/tinylinks/management/commands/_config.py b/tinylinks/management/commands/_config.py new file mode 100644 index 0000000..25b2621 --- /dev/null +++ b/tinylinks/management/commands/_config.py @@ -0,0 +1,21 @@ +user = '' +password = '' +host = '127.0.0.1' +database = '' +raise_on_warnings = True + +config = { + 'user': user, + 'password': password, + 'host': host, + 'database': database, + 'raise_on_warnings': raise_on_warnings +} + +def set_configs(user=None, password=None, database=None): + config['user'] = user + config['password'] = password + config['database'] = database + + + diff --git a/tinylinks/management/commands/_queries.py b/tinylinks/management/commands/_queries.py new file mode 100644 index 0000000..bdad5d5 --- /dev/null +++ b/tinylinks/management/commands/_queries.py @@ -0,0 +1,5 @@ +TINYLINK_QUERY = """ SELECT url, keyword + FROM yourls_url;""" + +TINYLINKLOG_QUERY = """ SELECT referrer, user_agent, ip_address, click_time + FROM yourls_log;""" \ No newline at end of file diff --git a/tinylinks/management/commands/import_yourls_db.py b/tinylinks/management/commands/import_yourls_db.py new file mode 100644 index 0000000..f68e08e --- /dev/null +++ b/tinylinks/management/commands/import_yourls_db.py @@ -0,0 +1,69 @@ +from __future__ import print_function + +from typing import List + +from django.core.management.base import BaseCommand, CommandError +from tinylinks.models import Tinylink, TinylinkLog +from tinylinks.management.commands import _queries, _config +import mysql.connector + + +class Command(BaseCommand): + + def get_tinylinks_query_data(self) -> List[tuple]: + cnx = mysql.connector.connect(**_config.config) + cursor = cnx.cursor() + cursor.execute(_queries.TINYLINK_QUERY) + data = [(str(long_url), short_url) for (long_url, short_url) in cursor] + cnx.close() + cursor.close() + return data + + def insert_tinylinks(self): + data = self.get_tinylinks_query_data() + tinylinks_to_add = [ + Tinylink(long_url=long_url, short_url=shorturl) + for long_url, shorturl in data + ] + Tinylink.objects.bulk_create(tinylinks_to_add) + + def get_tinylinks_logs_query_data(self) -> List[tuple]: + cnx = mysql.connector.connect(**_config.config) + cursor = cnx.cursor() + cursor.execute(_queries.TINYLINKLOG_QUERY) + data = [(referrer, user_agent, ip_address, click_time) + for (referrer, user_agent, ip_address, click_time) in cursor] + cnx.close() + cursor.close() + return data + + def insert_tinylinks_logs(self): + data = self.get_tinylinks_logs_query_data() + tinylinks_logs_to_add = [ + TinylinkLog(referrer=referrer, user_agent=user_agent, + remote_ip=remote_ip, datetime=datetime) + for referrer, user_agent, remote_ip, datetime in data + ] + TinylinkLog.objects.bulk_create(tinylinks_logs_to_add) + + def add_arguments(self, parser): + parser.add_argument('username', nargs='+', type=str) + parser.add_argument('paassword', nargs='+', type=str) + parser.add_argument('dbname', nargs='+', type=str) + + def handle(self, *args, **options): + _config.set_configs(user=options['username'][0], + password=options['paassword'][0], + database=options['dbname'][0]) + self.insert_tinylinks() + self.insert_tinylinks_logs() + + + + + + + + + + From 20fa3811526df7399e1450fcfc74bcbc0c5abc12 Mon Sep 17 00:00:00 2001 From: Fareck Allony Date: Thu, 29 Oct 2020 13:08:32 +0300 Subject: [PATCH 02/15] small fix (edited super call in setUp method in TinylinkRestViewTest class.) --- tinylinks/tests/tests.py | 1 - 1 file changed, 1 deletion(-) mode change 100644 => 100755 tinylinks/tests/tests.py diff --git a/tinylinks/tests/tests.py b/tinylinks/tests/tests.py old mode 100644 new mode 100755 index d33d457..5090d43 --- a/tinylinks/tests/tests.py +++ b/tinylinks/tests/tests.py @@ -126,7 +126,6 @@ def test_api_urls(self): class TinylinkRestViewTest(APITestCase): def setUp(self): - super(TinylinkViewTest, self).setUp() self.user = User.objects.create_superuser( username="gerges", email="gerges_test@kuwaitnet.com", From f204d805ded767077db625cc0940f5a3b9159382 Mon Sep 17 00:00:00 2001 From: Fareck Allony Date: Wed, 17 Feb 2021 08:51:02 +0200 Subject: [PATCH 03/15] updated .gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 58a71a6..2bd8be2 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,7 @@ db.sqlite dist/ static/ .idea/ +.venv/ +.venv +.env +.env/ \ No newline at end of file From a2a66571a279e4798dcfa41c1c9c05c64a601b17 Mon Sep 17 00:00:00 2001 From: Fareck Allony Date: Thu, 22 Oct 2020 11:15:19 +0300 Subject: [PATCH 04/15] created yourls db importer custom command --- tinylinks/management/commands/_config.py | 21 ++++++ tinylinks/management/commands/_queries.py | 5 ++ .../management/commands/import_yourls_db.py | 69 +++++++++++++++++++ 3 files changed, 95 insertions(+) create mode 100644 tinylinks/management/commands/_config.py create mode 100644 tinylinks/management/commands/_queries.py create mode 100644 tinylinks/management/commands/import_yourls_db.py diff --git a/tinylinks/management/commands/_config.py b/tinylinks/management/commands/_config.py new file mode 100644 index 0000000..25b2621 --- /dev/null +++ b/tinylinks/management/commands/_config.py @@ -0,0 +1,21 @@ +user = '' +password = '' +host = '127.0.0.1' +database = '' +raise_on_warnings = True + +config = { + 'user': user, + 'password': password, + 'host': host, + 'database': database, + 'raise_on_warnings': raise_on_warnings +} + +def set_configs(user=None, password=None, database=None): + config['user'] = user + config['password'] = password + config['database'] = database + + + diff --git a/tinylinks/management/commands/_queries.py b/tinylinks/management/commands/_queries.py new file mode 100644 index 0000000..bdad5d5 --- /dev/null +++ b/tinylinks/management/commands/_queries.py @@ -0,0 +1,5 @@ +TINYLINK_QUERY = """ SELECT url, keyword + FROM yourls_url;""" + +TINYLINKLOG_QUERY = """ SELECT referrer, user_agent, ip_address, click_time + FROM yourls_log;""" \ No newline at end of file diff --git a/tinylinks/management/commands/import_yourls_db.py b/tinylinks/management/commands/import_yourls_db.py new file mode 100644 index 0000000..f68e08e --- /dev/null +++ b/tinylinks/management/commands/import_yourls_db.py @@ -0,0 +1,69 @@ +from __future__ import print_function + +from typing import List + +from django.core.management.base import BaseCommand, CommandError +from tinylinks.models import Tinylink, TinylinkLog +from tinylinks.management.commands import _queries, _config +import mysql.connector + + +class Command(BaseCommand): + + def get_tinylinks_query_data(self) -> List[tuple]: + cnx = mysql.connector.connect(**_config.config) + cursor = cnx.cursor() + cursor.execute(_queries.TINYLINK_QUERY) + data = [(str(long_url), short_url) for (long_url, short_url) in cursor] + cnx.close() + cursor.close() + return data + + def insert_tinylinks(self): + data = self.get_tinylinks_query_data() + tinylinks_to_add = [ + Tinylink(long_url=long_url, short_url=shorturl) + for long_url, shorturl in data + ] + Tinylink.objects.bulk_create(tinylinks_to_add) + + def get_tinylinks_logs_query_data(self) -> List[tuple]: + cnx = mysql.connector.connect(**_config.config) + cursor = cnx.cursor() + cursor.execute(_queries.TINYLINKLOG_QUERY) + data = [(referrer, user_agent, ip_address, click_time) + for (referrer, user_agent, ip_address, click_time) in cursor] + cnx.close() + cursor.close() + return data + + def insert_tinylinks_logs(self): + data = self.get_tinylinks_logs_query_data() + tinylinks_logs_to_add = [ + TinylinkLog(referrer=referrer, user_agent=user_agent, + remote_ip=remote_ip, datetime=datetime) + for referrer, user_agent, remote_ip, datetime in data + ] + TinylinkLog.objects.bulk_create(tinylinks_logs_to_add) + + def add_arguments(self, parser): + parser.add_argument('username', nargs='+', type=str) + parser.add_argument('paassword', nargs='+', type=str) + parser.add_argument('dbname', nargs='+', type=str) + + def handle(self, *args, **options): + _config.set_configs(user=options['username'][0], + password=options['paassword'][0], + database=options['dbname'][0]) + self.insert_tinylinks() + self.insert_tinylinks_logs() + + + + + + + + + + From 410cf9fd2a80061dc4accfa3d1be379bc2601759 Mon Sep 17 00:00:00 2001 From: Fareck Allony Date: Sat, 20 Feb 2021 05:55:50 +0200 Subject: [PATCH 05/15] updated .gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 58a71a6..7c568e9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,7 @@ db.sqlite dist/ static/ .idea/ +.venv/ +.venv +.env/ +.env \ No newline at end of file From ad1eb6db0bb650df32a3078c4c01028fb4a44561 Mon Sep 17 00:00:00 2001 From: Fareck Allony Date: Sat, 20 Feb 2021 06:02:11 +0200 Subject: [PATCH 06/15] added CustomDefaultRouterAPIView --- tinylinks/views.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tinylinks/views.py b/tinylinks/views.py index f397c0c..354dfd7 100644 --- a/tinylinks/views.py +++ b/tinylinks/views.py @@ -15,6 +15,7 @@ ) from rest_framework.authentication import TokenAuthentication from rest_framework.decorators import permission_classes +from rest_framework.routers import APIRootView from tinylinks.forms import TinylinkForm from tinylinks.models import Tinylink, TinylinkLog, validate_long_url @@ -266,6 +267,13 @@ class UserViewSet(viewsets.ReadOnlyModelViewSet): permission_classes = (permissions.IsAuthenticated,) +class CustomDefaultRouterAPIView(APIRootView): + + + def get(self, request, *args, **kwargs): + return Response({}) + + def database_statistics(): """ Helper function to retrieve total number of tinylinks and the sum of From 9dcfb0c1f759f3d72ca812c28c6ba244892cf0a5 Mon Sep 17 00:00:00 2001 From: Fareck Allony Date: Sat, 20 Feb 2021 06:07:21 +0200 Subject: [PATCH 07/15] api/ endpoint shows nothing --- tinylinks/urls.py | 3 ++- tinylinks/utils/__init__.py | 0 tinylinks/utils/router.py | 7 +++++++ 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 tinylinks/utils/__init__.py create mode 100644 tinylinks/utils/router.py diff --git a/tinylinks/urls.py b/tinylinks/urls.py index 10f0bc6..a8418ba 100644 --- a/tinylinks/urls.py +++ b/tinylinks/urls.py @@ -6,6 +6,7 @@ from django.views.generic import TemplateView from rest_framework.routers import DefaultRouter +from tinylinks.utils.router import CustomDefaultRouter from tinylinks.views import ( StatisticsView, TinylinkCreateView, @@ -22,7 +23,7 @@ ) # Create router and register our API viewsets with it. -router = DefaultRouter() +router = CustomDefaultRouter() router.register( r"{}".format(getattr(settings, "TINYLINK_API_PREFIX", "tinylinks")), TinylinkViewSet ) diff --git a/tinylinks/utils/__init__.py b/tinylinks/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tinylinks/utils/router.py b/tinylinks/utils/router.py new file mode 100644 index 0000000..b1ad1e9 --- /dev/null +++ b/tinylinks/utils/router.py @@ -0,0 +1,7 @@ +from rest_framework.routers import DefaultRouter + +from tinylinks.views import CustomDefaultRouterAPIView + + +class CustomDefaultRouter(DefaultRouter): + APIRootView = CustomDefaultRouterAPIView From 99eb9a28386e1a71a14efd86bdacd930385a35f0 Mon Sep 17 00:00:00 2001 From: Fareck Allony Date: Sat, 20 Feb 2021 07:26:52 +0200 Subject: [PATCH 08/15] added prefix for short_url --- tinylinks/models.py | 6 ++++++ tinylinks/templates/tinylinks/statistics.html | 2 +- tinylinks/templates/tinylinks/tinylink_confirm_delete.html | 4 ++-- tinylinks/templates/tinylinks/tinylink_form.html | 2 +- tinylinks/templates/tinylinks/tinylink_list.html | 5 +++-- tinylinks/urls.py | 3 ++- tinylinks/views.py | 1 + 7 files changed, 16 insertions(+), 7 deletions(-) diff --git a/tinylinks/models.py b/tinylinks/models.py index 49fa829..70790a1 100644 --- a/tinylinks/models.py +++ b/tinylinks/models.py @@ -2,6 +2,8 @@ from urllib.request import build_opener, HTTPCookieProcessor, Request, urlopen from http.cookiejar import CookieJar import socket + +from django.conf import settings from django.contrib.auth import get_user_model from django.db import models from django.utils.translation import ugettext_lazy as _ @@ -148,6 +150,10 @@ class Tinylink(models.Model): default='', ) + def get_short_url(self) -> str: + return r"{}/".format(getattr(settings, "TINYLINK_SHORT_URL_PREFIX", "prefix")) \ + + str(self.short_url) + def __unicode__(self): return self.short_url diff --git a/tinylinks/templates/tinylinks/statistics.html b/tinylinks/templates/tinylinks/statistics.html index f2c118c..d15ce5c 100644 --- a/tinylinks/templates/tinylinks/statistics.html +++ b/tinylinks/templates/tinylinks/statistics.html @@ -21,7 +21,7 @@

{% trans "Statistics" %}

{{ link.user }} {{ link.long_url }} - {{ link.short_url }} + {{ link.get_short_url }} {% if link.is_broken %}{% trans "Invalid" %}{% else %}{% trans "Valid" %}{% endif %} {{ link.last_checked }} {{ link.amount_of_views }} diff --git a/tinylinks/templates/tinylinks/tinylink_confirm_delete.html b/tinylinks/templates/tinylinks/tinylink_confirm_delete.html index a88e741..b2d42c6 100644 --- a/tinylinks/templates/tinylinks/tinylink_confirm_delete.html +++ b/tinylinks/templates/tinylinks/tinylink_confirm_delete.html @@ -3,10 +3,10 @@ {% block main %}
-

{% trans "Delete tinylink" %} {{ object.short_url }} that points to {{ object.long_url }}

+

{% trans "Delete tinylink" %} {{ object.get_short_url }} that points to {{ object.long_url }}

{% csrf_token %} -

{% trans "Do you really want to delete this tinylink" %} {{ object.short_url }}?

+

{% trans "Do you really want to delete this tinylink" %} {{ object.get_short_url }}?

{% trans "or Back" %}
diff --git a/tinylinks/templates/tinylinks/tinylink_form.html b/tinylinks/templates/tinylinks/tinylink_form.html index 3681b70..8598aba 100644 --- a/tinylinks/templates/tinylinks/tinylink_form.html +++ b/tinylinks/templates/tinylinks/tinylink_form.html @@ -7,7 +7,7 @@ {% if mode == "change-short" %}

{% trans "Change short URL of" %} {{ form.instance.long_url }}

{% else %} -

{% trans "Change long URL of" %} {{ form.instance.short_url }}

+

{% trans "Change long URL of" %} {{ form.instance.get_short_url }}

{% endif %} {% else %}

{% trans "Shorten your URL" %}

diff --git a/tinylinks/templates/tinylinks/tinylink_list.html b/tinylinks/templates/tinylinks/tinylink_list.html index e735691..30fae0a 100644 --- a/tinylinks/templates/tinylinks/tinylink_list.html +++ b/tinylinks/templates/tinylinks/tinylink_list.html @@ -28,9 +28,10 @@

{% trans "Your Tinylinks" %}

{{ link.user }} {{ link.long_url }} - {{ link.short_url }} + {{ link.get_short_url }} + {{ link.get_short_url }} - {{ link.short_url }}.qr + {{ link.get_short_url }}.qr {% if link.is_broken %}{% trans "Invalid" %}{% else %}{% trans "Valid" %}{% endif %} diff --git a/tinylinks/urls.py b/tinylinks/urls.py index a8418ba..669a356 100644 --- a/tinylinks/urls.py +++ b/tinylinks/urls.py @@ -84,7 +84,8 @@ r"^api/expand/(?P\w+)/$", tinylink_expand, name="api_tinylink_expand" ), re_path( - r"^(?P[a-zA-Z0-9-]+)/?$", + r"^{}/".format(getattr(settings, "TINYLINK_SHORT_URL_PREFIX", "prefix")) + + r"(?P[a-zA-Z0-9-]+)/?$", TinylinkRedirectView.as_view(), name="tinylink_redirect", ), diff --git a/tinylinks/views.py b/tinylinks/views.py index 354dfd7..088aa02 100644 --- a/tinylinks/views.py +++ b/tinylinks/views.py @@ -148,6 +148,7 @@ class TinylinkRedirectView(RedirectView): """ def dispatch(self, *args, **kwargs): + print("in dispatch") if kwargs.get("short_url"): try: tinylink = Tinylink.objects.get(short_url=kwargs.get("short_url")) From fe5da710d6ef99fce70b4d8982c4523bfe44cb83 Mon Sep 17 00:00:00 2001 From: Fareck Allony Date: Sat, 20 Feb 2021 07:28:22 +0200 Subject: [PATCH 09/15] ran black against project directory --- manage.py | 3 +- setup.py | 20 ++-- tinylinks/__init__.py | 2 +- tinylinks/admin.py | 38 ++++-- tinylinks/forms.py | 106 +++++++++-------- tinylinks/management/commands/_config.py | 28 +++-- tinylinks/management/commands/_queries.py | 2 +- .../commands/check_tinylink_targets.py | 17 ++- .../management/commands/import_yourls_db.py | 39 +++---- tinylinks/management/commands/track_views.py | 46 +++++--- tinylinks/migrations/0001_initial.py | 109 ++++++++++++++---- tinylinks/models.py | 57 ++++----- tinylinks/piwik.py | 27 +++-- tinylinks/templatetags/verbose_names.py | 2 + tinylinks/tests/factories.py | 16 +-- tinylinks/tests/settings.py | 17 +-- tinylinks/tests/test_settings.py | 69 +++++------ tinylinks/tests/tests.py | 4 +- tinylinks/tests/urls.py | 4 +- tinylinks/utils.py | 2 +- tinylinks/views.py | 2 - 21 files changed, 362 insertions(+), 248 deletions(-) diff --git a/manage.py b/manage.py index 43cea93..8ac6618 100755 --- a/manage.py +++ b/manage.py @@ -3,8 +3,7 @@ import sys if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", - "tinylinks.tests.settings") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tinylinks.tests.settings") from django.core.management import execute_from_command_line diff --git a/setup.py b/setup.py index 46d9af9..a6d68b8 100644 --- a/setup.py +++ b/setup.py @@ -7,24 +7,24 @@ def read(fname): try: return open(os.path.join(os.path.dirname(__file__), fname)).read() except IOError: - return '' + return "" setup( name="django-tinylinks", version=tinylinks.__version__, - description=read('DESCRIPTION'), - long_description=read('README.rst'), - license='The MIT License', - platforms=['OS Independent'], - keywords='django, url shortener, link shortener', - author='Tobias Lorenz', - author_email='tobias.lorenz@bitmazk.com', + description=read("DESCRIPTION"), + long_description=read("README.rst"), + license="The MIT License", + platforms=["OS Independent"], + keywords="django, url shortener, link shortener", + author="Tobias Lorenz", + author_email="tobias.lorenz@bitmazk.com", url="https://github.com/bitmazk/django-tinylinks", packages=find_packages(), include_package_data=True, tests_require=[ - 'factory_boy', + "factory_boy", ], - test_suite='tinylinks.tests.runtests.runtests', + test_suite="tinylinks.tests.runtests.runtests", ) diff --git a/tinylinks/__init__.py b/tinylinks/__init__.py index b92b8ac..6e1ef8d 100644 --- a/tinylinks/__init__.py +++ b/tinylinks/__init__.py @@ -1,2 +1,2 @@ # -*- coding: utf-8 -*- -__version__ = '0.5.4' +__version__ = "0.5.4" diff --git a/tinylinks/admin.py b/tinylinks/admin.py index 545c6c2..9179319 100644 --- a/tinylinks/admin.py +++ b/tinylinks/admin.py @@ -10,35 +10,51 @@ class TinylinkAdmin(admin.ModelAdmin): - list_display = ('short_url', 'url_truncated', 'amount_of_views', 'user', - 'last_checked', 'status', 'validation_error',) - search_fields = ['short_url', 'long_url'] + list_display = ( + "short_url", + "url_truncated", + "amount_of_views", + "user", + "last_checked", + "status", + "validation_error", + ) + search_fields = ["short_url", "long_url"] form = TinylinkAdminForm fieldsets = [ - ('Tinylink', {'fields': ['user', 'long_url', 'short_url', ]}), + ( + "Tinylink", + { + "fields": [ + "user", + "long_url", + "short_url", + ] + }, + ), ] def url_truncated(self, obj): return truncatechars(obj.long_url, 60) - url_truncated.short_description = _('Long URL') + url_truncated.short_description = _("Long URL") def status(self, obj): if not obj.is_broken: - return _('OK') - return _('Link broken') + return _("OK") + return _("Link broken") - status.short_description = _('Status') + status.short_description = _("Status") admin.site.register(Tinylink, TinylinkAdmin) class TinylinkLogAdmin(admin.ModelAdmin): - list_display = ('tinylink', 'datetime', 'remote_ip', 'tracked') - readonly_fields = ('datetime',) - date_hierarchy = 'datetime' + list_display = ("tinylink", "datetime", "remote_ip", "tracked") + readonly_fields = ("datetime",) + date_hierarchy = "datetime" admin.site.register(TinylinkLog, TinylinkLogAdmin) diff --git a/tinylinks/forms.py b/tinylinks/forms.py index 940f491..3426ca6 100644 --- a/tinylinks/forms.py +++ b/tinylinks/forms.py @@ -14,7 +14,7 @@ class TinylinkForm(forms.ModelForm): """ - def __init__(self, user=None, mode='change-short', *args, **kwargs): + def __init__(self, user=None, mode="change-short", *args, **kwargs): """ The Regex field validates the URL input. Allowed are only slugified inputs. @@ -25,40 +25,41 @@ def __init__(self, user=None, mode='change-short', *args, **kwargs): "D834n_qNx2q/jn" <- invalid """ super(TinylinkForm, self).__init__(*args, **kwargs) - if mode == 'change-long': + if mode == "change-long": long_help_text = _("You can now change your long URL.") else: long_help_text = _("The long URL isn't editable at the moment.") - self.fields['long_url'] = forms.URLField( - label=self.instance._meta.get_field( - 'long_url').verbose_name, + self.fields["long_url"] = forms.URLField( + label=self.instance._meta.get_field("long_url").verbose_name, help_text=long_help_text, ) if not self.instance.pk: # Hide the short URL field to auto-generate a new instance. - self.fields['short_url'].widget = forms.HiddenInput() - self.fields['short_url'].required = False + self.fields["short_url"].widget = forms.HiddenInput() + self.fields["short_url"].required = False else: # Dependent on the user mode, one URL field should not be editable. - if mode == 'change-long': - self.fields['short_url'].widget.attrs['readonly'] = True - self.fields['short_url'].help_text = _( - "The short URL isn't editable at the moment.") + if mode == "change-long": + self.fields["short_url"].widget.attrs["readonly"] = True + self.fields["short_url"].help_text = _( + "The short URL isn't editable at the moment." + ) else: - self.fields['long_url'].widget.attrs['readonly'] = True - self.fields['short_url'] = forms.RegexField( - regex=r'^[a-z0-9]+$', + self.fields["long_url"].widget.attrs["readonly"] = True + self.fields["short_url"] = forms.RegexField( + regex=r"^[a-z0-9]+$", # error_message=(_("Please use only small letters and" # " digits.")), - help_text=_("You can add a more readable short URL. Please use only small letters and" - " digits."), - label=self.instance._meta.get_field( - 'short_url').verbose_name, + help_text=_( + "You can add a more readable short URL. Please use only small letters and" + " digits." + ), + label=self.instance._meta.get_field("short_url").verbose_name, ) # Style the form fields with Bootstrap 3 - self.fields['long_url'].widget.attrs.update({'class': 'form-control'}) - self.fields['short_url'].widget.attrs.update({'class': 'form-control'}) + self.fields["long_url"].widget.attrs.update({"class": "form-control"}) + self.fields["short_url"].widget.attrs.update({"class": "form-control"}) self.user = user @@ -66,17 +67,19 @@ def clean(self): self.cleaned_data = super(TinylinkForm, self).clean() # If short URL is occupied throw out an error, or fail silent. try: - twin = Tinylink.objects.get(short_url=self.cleaned_data.get( - 'short_url')) + twin = Tinylink.objects.get(short_url=self.cleaned_data.get("short_url")) if not self.instance == twin: - raise forms.ValidationError(_('This short url already exists. Please try another one.')) + raise forms.ValidationError( + _("This short url already exists. Please try another one.") + ) return self.cleaned_data except Tinylink.DoesNotExist: pass # Brothers are entities with the same long URL - brothers = Tinylink.objects.filter(long_url=self.cleaned_data.get( - 'long_url'), user=self.user) - input_url = self.cleaned_data.get('short_url') + brothers = Tinylink.objects.filter( + long_url=self.cleaned_data.get("long_url"), user=self.user + ) + input_url = self.cleaned_data.get("short_url") # Only handle with older brothers, if there's no new short URL value if brothers and not input_url: @@ -84,21 +87,20 @@ def clean(self): # short URL with an existing tinylink. She will receive the # prefilled form with the link's old values. self.instance = brothers[0] - self.cleaned_data.update( - {'short_url': self.instance.short_url}) + self.cleaned_data.update({"short_url": self.instance.short_url}) else: - slug = '' + slug = "" if input_url: # User can customize their URLs slug = input_url # This keeps the unique validation of the short URLs alive. if not Tinylink.objects.filter(short_url=input_url): while not slug or Tinylink.objects.filter(short_url=slug): - slug = ''.join( - random.choice('abcdefghijkmnpqrstuvwxyz123456789') - for x in range( - getattr(settings, 'TINYLINK_LENGTH', 6))) - self.cleaned_data.update({'short_url': slug}) + slug = "".join( + random.choice("abcdefghijkmnpqrstuvwxyz123456789") + for x in range(getattr(settings, "TINYLINK_LENGTH", 6)) + ) + self.cleaned_data.update({"short_url": slug}) return self.cleaned_data def save(self, *args, **kwargs): @@ -109,7 +111,7 @@ def save(self, *args, **kwargs): class Meta: model = Tinylink - fields = ('long_url', 'short_url') + fields = ("long_url", "short_url") class TinylinkAdminForm(forms.ModelForm): @@ -123,14 +125,16 @@ def __init__(self, *args, **kwargs): super(TinylinkAdminForm, self).__init__(*args, **kwargs) if not self.instance.pk: # Hide the short URL field to auto-generate a new instance. - self.fields['short_url'].widget = forms.HiddenInput() - self.fields['short_url'].required = False + self.fields["short_url"].widget = forms.HiddenInput() + self.fields["short_url"].required = False else: - self.fields['short_url'] = forms.RegexField( - regex=r'^[a-z0-9]+$', + self.fields["short_url"] = forms.RegexField( + regex=r"^[a-z0-9]+$", # error_message=(_("Please use only small letters and digits.")), - help_text=_("You can add a more readable short URL. Please use only small letters and digits."), - label=self.instance._meta.get_field('short_url').verbose_name, + help_text=_( + "You can add a more readable short URL. Please use only small letters and digits." + ), + label=self.instance._meta.get_field("short_url").verbose_name, ) def clean(self): @@ -138,21 +142,23 @@ def clean(self): # If short URL is occupied throw out an error, or fail silent. try: twin = Tinylink.objects.get( - short_url=self.cleaned_data.get('short_url'), + short_url=self.cleaned_data.get("short_url"), ) except Tinylink.DoesNotExist: - slug = self.cleaned_data.get('short_url') + slug = self.cleaned_data.get("short_url") while not slug or Tinylink.objects.filter(short_url=slug): - slug = ''.join( - random.choice('abcdefghijkmnpqrstuvwxyz123456789') - for x in range(getattr(settings, 'TINYLINK_LENGTH', 6))) - self.cleaned_data.update({'short_url': slug}) + slug = "".join( + random.choice("abcdefghijkmnpqrstuvwxyz123456789") + for x in range(getattr(settings, "TINYLINK_LENGTH", 6)) + ) + self.cleaned_data.update({"short_url": slug}) else: if twin != self.instance: - self._errors['short_url'] = forms.util.ErrorList([_( - 'This short url already exists. Please try another one.')]) + self._errors["short_url"] = forms.util.ErrorList( + [_("This short url already exists. Please try another one.")] + ) return self.cleaned_data class Meta: model = Tinylink - fields = ('user', 'long_url', 'short_url') + fields = ("user", "long_url", "short_url") diff --git a/tinylinks/management/commands/_config.py b/tinylinks/management/commands/_config.py index 25b2621..96b7551 100644 --- a/tinylinks/management/commands/_config.py +++ b/tinylinks/management/commands/_config.py @@ -1,21 +1,19 @@ -user = '' -password = '' -host = '127.0.0.1' -database = '' +user = "" +password = "" +host = "127.0.0.1" +database = "" raise_on_warnings = True config = { - 'user': user, - 'password': password, - 'host': host, - 'database': database, - 'raise_on_warnings': raise_on_warnings + "user": user, + "password": password, + "host": host, + "database": database, + "raise_on_warnings": raise_on_warnings, } -def set_configs(user=None, password=None, database=None): - config['user'] = user - config['password'] = password - config['database'] = database - - +def set_configs(user=None, password=None, database=None): + config["user"] = user + config["password"] = password + config["database"] = database diff --git a/tinylinks/management/commands/_queries.py b/tinylinks/management/commands/_queries.py index bdad5d5..1207cd8 100644 --- a/tinylinks/management/commands/_queries.py +++ b/tinylinks/management/commands/_queries.py @@ -2,4 +2,4 @@ FROM yourls_url;""" TINYLINKLOG_QUERY = """ SELECT referrer, user_agent, ip_address, click_time - FROM yourls_log;""" \ No newline at end of file + FROM yourls_log;""" diff --git a/tinylinks/management/commands/check_tinylink_targets.py b/tinylinks/management/commands/check_tinylink_targets.py index 621103e..964e501 100644 --- a/tinylinks/management/commands/check_tinylink_targets.py +++ b/tinylinks/management/commands/check_tinylink_targets.py @@ -15,14 +15,23 @@ class Command(BaseCommand): """Class for the check_tinylink_targets admin command.""" + def handle(self, *args, **options): """Handles the check_tinylink_targets admin command.""" interval = settings.TINYLINK_CHECK_INTERVAL period = settings.TINYLINK_CHECK_PERIOD url_amount = Tinylink.objects.all().count() check_amount = (url_amount / (period / interval)) or 1 - for link in Tinylink.objects.order_by('last_checked')[:check_amount]: + for link in Tinylink.objects.order_by("last_checked")[:check_amount]: validate_long_url(link) - print(('[' + timezone.now().strftime('%d.%m.%Y - %H:%M') + - '] Checked ' + str(check_amount) + ' of ' + str(url_amount) + - ' total URLs.')) + print( + ( + "[" + + timezone.now().strftime("%d.%m.%Y - %H:%M") + + "] Checked " + + str(check_amount) + + " of " + + str(url_amount) + + " total URLs." + ) + ) diff --git a/tinylinks/management/commands/import_yourls_db.py b/tinylinks/management/commands/import_yourls_db.py index f68e08e..fe14b05 100644 --- a/tinylinks/management/commands/import_yourls_db.py +++ b/tinylinks/management/commands/import_yourls_db.py @@ -9,7 +9,6 @@ class Command(BaseCommand): - def get_tinylinks_query_data(self) -> List[tuple]: cnx = mysql.connector.connect(**_config.config) cursor = cnx.cursor() @@ -31,8 +30,10 @@ def get_tinylinks_logs_query_data(self) -> List[tuple]: cnx = mysql.connector.connect(**_config.config) cursor = cnx.cursor() cursor.execute(_queries.TINYLINKLOG_QUERY) - data = [(referrer, user_agent, ip_address, click_time) - for (referrer, user_agent, ip_address, click_time) in cursor] + data = [ + (referrer, user_agent, ip_address, click_time) + for (referrer, user_agent, ip_address, click_time) in cursor + ] cnx.close() cursor.close() return data @@ -40,30 +41,26 @@ def get_tinylinks_logs_query_data(self) -> List[tuple]: def insert_tinylinks_logs(self): data = self.get_tinylinks_logs_query_data() tinylinks_logs_to_add = [ - TinylinkLog(referrer=referrer, user_agent=user_agent, - remote_ip=remote_ip, datetime=datetime) + TinylinkLog( + referrer=referrer, + user_agent=user_agent, + remote_ip=remote_ip, + datetime=datetime, + ) for referrer, user_agent, remote_ip, datetime in data ] TinylinkLog.objects.bulk_create(tinylinks_logs_to_add) def add_arguments(self, parser): - parser.add_argument('username', nargs='+', type=str) - parser.add_argument('paassword', nargs='+', type=str) - parser.add_argument('dbname', nargs='+', type=str) + parser.add_argument("username", nargs="+", type=str) + parser.add_argument("paassword", nargs="+", type=str) + parser.add_argument("dbname", nargs="+", type=str) def handle(self, *args, **options): - _config.set_configs(user=options['username'][0], - password=options['paassword'][0], - database=options['dbname'][0]) + _config.set_configs( + user=options["username"][0], + password=options["paassword"][0], + database=options["dbname"][0], + ) self.insert_tinylinks() self.insert_tinylinks_logs() - - - - - - - - - - diff --git a/tinylinks/management/commands/track_views.py b/tinylinks/management/commands/track_views.py index 621af04..2bd3be8 100644 --- a/tinylinks/management/commands/track_views.py +++ b/tinylinks/management/commands/track_views.py @@ -7,6 +7,7 @@ from tinylinks.models import TinylinkLog import json import random + try: from urllib import urlencode except ImportError: @@ -19,7 +20,7 @@ if settings.GEOIP_PATH: G = GeoIP() else: - country = '' + country = "" class Command(BaseCommand): @@ -28,38 +29,49 @@ def track_to_piwik(self, views): for view in views: url = view.tinylink params = parse_cookie(view.cookie) - address = 'http://%s/%s' % (CURRENT_DOMAIN, url.short_url) + address = "http://%s/%s" % (CURRENT_DOMAIN, url.short_url) if settings.GEOIP_PATH: - country = G.country(view.remote_ip).get('country_code').lower() - params.update({'rand': random.randint(0, 1000000), 'url': address, 'urlref': view.referrer, - 'ua': view.user_agent.encode('utf-8'), 'cip': view.remote_ip, - 'cdt': view.datetime.strftime("%Y-%m-%d %H:%M:%S"), - 'country': country, 'new_visit': 1, - 'idsite': settings.PIWIK_ID, 'rec': 1, 'token_auth': settings.PIWIK_TOKEN, - 'action_name': url.long_url.encode('utf-8'), 'apiv': 1}) + country = G.country(view.remote_ip).get("country_code").lower() + params.update( + { + "rand": random.randint(0, 1000000), + "url": address, + "urlref": view.referrer, + "ua": view.user_agent.encode("utf-8"), + "cip": view.remote_ip, + "cdt": view.datetime.strftime("%Y-%m-%d %H:%M:%S"), + "country": country, + "new_visit": 1, + "idsite": settings.PIWIK_ID, + "rec": 1, + "token_auth": settings.PIWIK_TOKEN, + "action_name": url.long_url.encode("utf-8"), + "apiv": 1, + } + ) - res = '?' + urlencode(params) + res = "?" + urlencode(params) visits.append(res) - payload = {'requests': visits, 'token_auth': settings.PIWIK_TOKEN} - #print payload + payload = {"requests": visits, "token_auth": settings.PIWIK_TOKEN} + # print payload req = urllib.request.Request(settings.PIWIK_URL) - req.add_header('Content-Type', 'application/json') + req.add_header("Content-Type", "application/json") response = urllib.request.urlopen(req, json.dumps(payload)) - print(('Another %d views tracked well!' % TRACK_OFFSET)) + print(("Another %d views tracked well!" % TRACK_OFFSET)) TinylinkLog.objects.filter(pk__in=views).update(tracked=True) def handle(self, *args, **options): num_visits = TinylinkLog.objects.filter(tracked=False).count() - print(('Untracked views: %d' % num_visits)) + print(("Untracked views: %d" % num_visits)) if num_visits == 0: return - views = TinylinkLog.objects.filter(tracked=False).prefetch_related('tinylink') + views = TinylinkLog.objects.filter(tracked=False).prefetch_related("tinylink") views_count = views.count() if views_count < TRACK_OFFSET: self.track_to_piwik(views) else: i = 0 while views_count > i: - self.track_to_piwik(views[i:i + TRACK_OFFSET]) + self.track_to_piwik(views[i : i + TRACK_OFFSET]) i += TRACK_OFFSET diff --git a/tinylinks/migrations/0001_initial.py b/tinylinks/migrations/0001_initial.py index 57c4002..33c1874 100644 --- a/tinylinks/migrations/0001_initial.py +++ b/tinylinks/migrations/0001_initial.py @@ -16,36 +16,103 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Tinylink', + name="Tinylink", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('long_url', models.CharField(max_length=2500, verbose_name='Long URL')), - ('short_url', models.CharField(max_length=32, unique=True, verbose_name='Short URL')), - ('is_broken', models.BooleanField(default=False, verbose_name='Status')), - ('validation_error', models.CharField(default='', max_length=100, verbose_name='Validation Error')), - ('last_checked', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Last validation')), - ('amount_of_views', models.PositiveIntegerField(default=0, verbose_name='Amount of views')), - ('redirect_location', models.CharField(default='', max_length=2500, verbose_name='Redirect location')), - ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tinylinks', to=settings.AUTH_USER_MODEL, verbose_name='Author')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "long_url", + models.CharField(max_length=2500, verbose_name="Long URL"), + ), + ( + "short_url", + models.CharField( + max_length=32, unique=True, verbose_name="Short URL" + ), + ), + ( + "is_broken", + models.BooleanField(default=False, verbose_name="Status"), + ), + ( + "validation_error", + models.CharField( + default="", max_length=100, verbose_name="Validation Error" + ), + ), + ( + "last_checked", + models.DateTimeField( + default=django.utils.timezone.now, + verbose_name="Last validation", + ), + ), + ( + "amount_of_views", + models.PositiveIntegerField( + default=0, verbose_name="Amount of views" + ), + ), + ( + "redirect_location", + models.CharField( + default="", max_length=2500, verbose_name="Redirect location" + ), + ), + ( + "user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="tinylinks", + to=settings.AUTH_USER_MODEL, + verbose_name="Author", + ), + ), ], options={ - 'ordering': ['-id'], + "ordering": ["-id"], }, ), migrations.CreateModel( - name='TinylinkLog', + name="TinylinkLog", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('referrer', models.URLField(blank=True, max_length=512)), - ('user_agent', models.TextField()), - ('cookie', models.CharField(blank=True, default='', max_length=127)), - ('remote_ip', models.GenericIPAddressField()), - ('datetime', models.DateTimeField(auto_now_add=True)), - ('tracked', models.BooleanField(default=False)), - ('tinylink', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='tinylinks.Tinylink', verbose_name='Tinylink')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("referrer", models.URLField(blank=True, max_length=512)), + ("user_agent", models.TextField()), + ("cookie", models.CharField(blank=True, default="", max_length=127)), + ("remote_ip", models.GenericIPAddressField()), + ("datetime", models.DateTimeField(auto_now_add=True)), + ("tracked", models.BooleanField(default=False)), + ( + "tinylink", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="tinylinks.Tinylink", + verbose_name="Tinylink", + ), + ), ], options={ - 'ordering': ('-datetime',), + "ordering": ("-datetime",), }, ), ] diff --git a/tinylinks/models.py b/tinylinks/models.py index 70790a1..4be0a08 100644 --- a/tinylinks/models.py +++ b/tinylinks/models.py @@ -25,21 +25,21 @@ def get_url_response(pool, link, url): """ response = False link.is_broken = True - link.redirect_location = '' + link.redirect_location = "" # Try to encode e.g. chinese letters try: - url = url.encode('utf-8') + url = url.encode("utf-8") except UnicodeEncodeError: - link.validation_error = _('Unicode error. Check URL characters.') + link.validation_error = _("Unicode error. Check URL characters.") return False try: - response = pool.urlopen('GET', url.decode(), retries=2, timeout=8.0) + response = pool.urlopen("GET", url.decode(), retries=2, timeout=8.0) except TimeoutError: - link.validation_error = _('Timeout after 8 seconds.') + link.validation_error = _("Timeout after 8 seconds.") except MaxRetryError: - link.validation_error = _('Failed after retrying twice.') + link.validation_error = _("Failed after retrying twice.") except (HTTPError, socket.gaierror): - link.validation_error = _('Not found.') + link.validation_error = _("Not found.") return response @@ -55,7 +55,7 @@ def validate_long_url(link): link.is_broken = False elif response and response.status == 302: # If link is redirected, validate the redirect location. - if link.long_url.endswith('.pdf'): + if link.long_url.endswith(".pdf"): # Non-save pdf exception, to avoid relative path redirects link.is_broken = False else: @@ -103,62 +103,64 @@ class Tinylink(models.Model): :redirect_location: Redirect location if the long_url is redirected. """ + user = models.ForeignKey( User, - verbose_name=_('Author'), + verbose_name=_("Author"), related_name="tinylinks", null=True, blank=True, - on_delete=models.SET_NULL + on_delete=models.SET_NULL, ) long_url = models.CharField( max_length=2500, - verbose_name=_('Long URL'), + verbose_name=_("Long URL"), ) short_url = models.CharField( max_length=32, - verbose_name=_('Short URL'), + verbose_name=_("Short URL"), unique=True, ) is_broken = models.BooleanField( default=False, - verbose_name=_('Status'), + verbose_name=_("Status"), ) validation_error = models.CharField( max_length=100, - verbose_name=_('Validation Error'), - default='', + verbose_name=_("Validation Error"), + default="", ) last_checked = models.DateTimeField( default=timezone.now, - verbose_name=_('Last validation'), + verbose_name=_("Last validation"), ) amount_of_views = models.PositiveIntegerField( default=0, - verbose_name=_('Amount of views'), + verbose_name=_("Amount of views"), ) redirect_location = models.CharField( max_length=2500, - verbose_name=_('Redirect location'), - default='', + verbose_name=_("Redirect location"), + default="", ) def get_short_url(self) -> str: - return r"{}/".format(getattr(settings, "TINYLINK_SHORT_URL_PREFIX", "prefix")) \ - + str(self.short_url) + return r"{}/".format( + getattr(settings, "TINYLINK_SHORT_URL_PREFIX", "prefix") + ) + str(self.short_url) def __unicode__(self): return self.short_url class Meta: - ordering = ['-id'] + ordering = ["-id"] def can_be_validated(self): """ @@ -176,12 +178,13 @@ class TinylinkLog(models.Model): Model to log the usage of the short links """ + tinylink = models.ForeignKey( - 'Tinylink', - verbose_name=_('Tinylink'), + "Tinylink", + verbose_name=_("Tinylink"), blank=True, null=True, - on_delete=models.SET_NULL + on_delete=models.SET_NULL, ) referrer = models.URLField( @@ -194,7 +197,7 @@ class TinylinkLog(models.Model): cookie = models.CharField( max_length=127, blank=True, - default='', + default="", ) remote_ip = models.GenericIPAddressField() @@ -204,4 +207,4 @@ class TinylinkLog(models.Model): tracked = models.BooleanField(default=False) class Meta: - ordering = ('-datetime',) + ordering = ("-datetime",) diff --git a/tinylinks/piwik.py b/tinylinks/piwik.py index b558b60..21ec90c 100644 --- a/tinylinks/piwik.py +++ b/tinylinks/piwik.py @@ -27,9 +27,14 @@ def _get_random_visitor_id(id_length): def parse_cookie(cookie): if cookie: - cookie = cookie.split('.') - return {'_id': cookie[0], '_idts': cookie[1], '_idvc': int(cookie[2]), 'unknown': cookie[3], - '_viewts': cookie[4]} + cookie = cookie.split(".") + return { + "_id": cookie[0], + "_idts": cookie[1], + "_idvc": int(cookie[2]), + "unknown": cookie[3], + "_viewts": cookie[4], + } else: return {} @@ -44,7 +49,7 @@ def _calculate_visit(viewts, now_ts): def _compose_cookie(parsed): - return '{_id}.{_idts}.{_idvc}.{unknown}.{_viewts}.'.format(**parsed) + return "{_id}.{_idts}.{_idvc}.{unknown}.{_viewts}.".format(**parsed) def response_cookie(cookie): @@ -54,10 +59,16 @@ def response_cookie(cookie): parsed = parse_cookie(cookie) else: visitor_id = _get_random_visitor_id(16) - parsed = {'_id': visitor_id, '_idts': now, '_idvc': 0, '_viewts': now, 'unknown': now} + parsed = { + "_id": visitor_id, + "_idts": now, + "_idvc": 0, + "_viewts": now, + "unknown": now, + } cookie = _compose_cookie(parsed) - parsed['unknown'] = now - parsed['_viewts'] = _calculate_visit(parsed['_viewts'], now) - parsed['_idvc'] += 1 + parsed["unknown"] = now + parsed["_viewts"] = _calculate_visit(parsed["_viewts"], now) + parsed["_idvc"] += 1 return _compose_cookie(parsed), cookie diff --git a/tinylinks/templatetags/verbose_names.py b/tinylinks/templatetags/verbose_names.py index 1e7e769..bbe9963 100644 --- a/tinylinks/templatetags/verbose_names.py +++ b/tinylinks/templatetags/verbose_names.py @@ -1,6 +1,8 @@ from django import template + register = template.Library() + @register.simple_tag def get_verbose_field_name(instance, field_name): """ diff --git a/tinylinks/tests/factories.py b/tinylinks/tests/factories.py index 7c3b51b..3e15468 100644 --- a/tinylinks/tests/factories.py +++ b/tinylinks/tests/factories.py @@ -22,12 +22,13 @@ class UserFactory(factory.Factory): """ Factory for model User """ + class Meta: model = User - username = 'test' - email = 'gunjan@kuwaitnet.com' - password = make_password('test1234') + username = "test" + email = "gunjan@kuwaitnet.com" + password = make_password("test1234") is_active = True is_superuser = True @@ -36,11 +37,12 @@ class TinyLogFactory(factory.django.DjangoModelFactory): """ Factory for model User """ + class Meta: model = TinylinkLog - referrer = 'http://www.example.com/thisisalongURL' - user_agent = 'Gunjan Modi' - cookie = 'test:test' - remote_ip = '127.0.0.1' + referrer = "http://www.example.com/thisisalongURL" + user_agent = "Gunjan Modi" + cookie = "test:test" + remote_ip = "127.0.0.1" tracked = True diff --git a/tinylinks/tests/settings.py b/tinylinks/tests/settings.py index 1bd980b..fcffef2 100644 --- a/tinylinks/tests/settings.py +++ b/tinylinks/tests/settings.py @@ -13,19 +13,20 @@ DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': 'db.sqlite', + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": "db.sqlite", } } -SECRET_KEY = 'ts4u!_y8xp!yvyxtvmk3046fxaoz_ubh4n2&qtsm&*z(-d(b07' +SECRET_KEY = "ts4u!_y8xp!yvyxtvmk3046fxaoz_ubh4n2&qtsm&*z(-d(b07" PIWIK_ID = 1 -PIWIK_URL = 'http://127.0.0.1/piwik/piwik.php' -PIWIK_TOKEN = '60a437e4e088521ca506fcfcf4f57522' +PIWIK_URL = "http://127.0.0.1/piwik/piwik.php" +PIWIK_TOKEN = "60a437e4e088521ca506fcfcf4f57522" import os + BASE_DIR = os.path.abspath(os.path.dirname(__file__)) -GEOIP_PATH = os.path.join(BASE_DIR, 'geoip') -GEOIP_PATH = '/development/django-tinylinks/geoip' +GEOIP_PATH = os.path.join(BASE_DIR, "geoip") +GEOIP_PATH = "/development/django-tinylinks/geoip" diff --git a/tinylinks/tests/test_settings.py b/tinylinks/tests/test_settings.py index 7f145df..0be9d3c 100644 --- a/tinylinks/tests/test_settings.py +++ b/tinylinks/tests/test_settings.py @@ -9,9 +9,7 @@ TINYLINK_CHECK_INTERVAL = 10 TINYLINK_CHECK_PERIOD = 300 -PASSWORD_HASHERS = ( - 'django.contrib.auth.hashers.MD5PasswordHasher', -) +PASSWORD_HASHERS = ("django.contrib.auth.hashers.MD5PasswordHasher",) CURRENT_DIR = os.path.dirname(__file__) @@ -22,23 +20,23 @@ } } -ROOT_URLCONF = 'tinylinks.tests.urls' +ROOT_URLCONF = "tinylinks.tests.urls" DJANGO_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.admindocs', - 'django.contrib.contenttypes', - 'django.contrib.messages', - 'django.contrib.sessions', - 'django.contrib.staticfiles', - 'django.contrib.sitemaps', - 'django.contrib.sites', - 'rest_framework', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.admindocs", + "django.contrib.contenttypes", + "django.contrib.messages", + "django.contrib.sessions", + "django.contrib.staticfiles", + "django.contrib.sitemaps", + "django.contrib.sites", + "rest_framework", ] CUSTOM_APPS = [ - 'tinylinks', + "tinylinks", ] INSTALLED_APPS = DJANGO_APPS + CUSTOM_APPS @@ -55,35 +53,32 @@ TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(os.path.join(CURRENT_DIR, "templates"))], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [os.path.join(os.path.join(CURRENT_DIR, "templates"))], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], - }, }, ] MIDDLEWARE = ( - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ) -STATIC_URL = '/static/' +STATIC_URL = "/static/" -STATIC_ROOT = os.path.join(CURRENT_DIR, '../../static/') +STATIC_ROOT = os.path.join(CURRENT_DIR, "../../static/") -STATICFILES_DIRS = ( - os.path.join(CURRENT_DIR, 'test_static'), -) +STATICFILES_DIRS = (os.path.join(CURRENT_DIR, "test_static"),) diff --git a/tinylinks/tests/tests.py b/tinylinks/tests/tests.py index 2f9293a..b514df3 100755 --- a/tinylinks/tests/tests.py +++ b/tinylinks/tests/tests.py @@ -152,9 +152,7 @@ def test_unauthenticated_admin_links_list(self): print( "\nTesting:\nTiny Link List Links : user.is_admin=False & user in not authenticated" ) - self.assertEqual( - tiny_link_list_response.status_code, status.HTTP_403_FORBIDDEN - ) + self.assertEqual(tiny_link_list_response.status_code, status.HTTP_403_FORBIDDEN) def test_authenticated_admin_create_link(self): self.query_parameter = "https://soundcloud.com/discover" diff --git a/tinylinks/tests/urls.py b/tinylinks/tests/urls.py index ac6e1cf..0488841 100644 --- a/tinylinks/tests/urls.py +++ b/tinylinks/tests/urls.py @@ -12,6 +12,6 @@ urlpatterns = [ - re_path(r'^administration/', admin.site.urls), - re_path(r'^s/', include('tinylinks.urls')), + re_path(r"^administration/", admin.site.urls), + re_path(r"^s/", include("tinylinks.urls")), ] diff --git a/tinylinks/utils.py b/tinylinks/utils.py index b605f7c..54282b7 100644 --- a/tinylinks/utils.py +++ b/tinylinks/utils.py @@ -2,7 +2,7 @@ def shortify_url(url): - data = {'data': {'long_url': url}, 'mode': None} + data = {"data": {"long_url": url}, "mode": None} form = TinylinkForm(**data) if form.is_valid(): obj = form.save() diff --git a/tinylinks/views.py b/tinylinks/views.py index 088aa02..5dd4cff 100644 --- a/tinylinks/views.py +++ b/tinylinks/views.py @@ -269,8 +269,6 @@ class UserViewSet(viewsets.ReadOnlyModelViewSet): class CustomDefaultRouterAPIView(APIRootView): - - def get(self, request, *args, **kwargs): return Response({}) From e5a0850b8a15c2d4012beba5d7e0512f7ca91546 Mon Sep 17 00:00:00 2001 From: Fareck Allony Date: Sat, 20 Feb 2021 07:33:13 +0200 Subject: [PATCH 10/15] removed duplicate line --- tinylinks/templates/tinylinks/tinylink_list.html | 1 - 1 file changed, 1 deletion(-) diff --git a/tinylinks/templates/tinylinks/tinylink_list.html b/tinylinks/templates/tinylinks/tinylink_list.html index 30fae0a..ba863e8 100644 --- a/tinylinks/templates/tinylinks/tinylink_list.html +++ b/tinylinks/templates/tinylinks/tinylink_list.html @@ -29,7 +29,6 @@

{% trans "Your Tinylinks" %}

{{ link.user }} {{ link.long_url }} {{ link.get_short_url }} - {{ link.get_short_url }} {{ link.get_short_url }}.qr From 9e415e4b7ad1ee224b392e2534209ad92b8924a6 Mon Sep 17 00:00:00 2001 From: Fareck Allony Date: Sat, 20 Feb 2021 07:35:00 +0200 Subject: [PATCH 11/15] cleaned print statements --- tinylinks/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tinylinks/views.py b/tinylinks/views.py index 5dd4cff..99f9050 100644 --- a/tinylinks/views.py +++ b/tinylinks/views.py @@ -148,7 +148,6 @@ class TinylinkRedirectView(RedirectView): """ def dispatch(self, *args, **kwargs): - print("in dispatch") if kwargs.get("short_url"): try: tinylink = Tinylink.objects.get(short_url=kwargs.get("short_url")) From 6fd0d380c009193384d8fb707bd1261dfa2ef822 Mon Sep 17 00:00:00 2001 From: Fareck Allony Date: Sat, 20 Feb 2021 08:33:00 +0200 Subject: [PATCH 12/15] added pre-commit --- .pre-commit-config.yaml | 77 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a201f33 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,77 @@ +exclude: | + (?x) + # Maybe reactivate this when all README files include prettier ignore tags? + ^README\.rst$| + # You don't usually want a bot to modify your legal texts + (LICENSE.*|COPYING.*) +default_language_version: + python: python3 +repos: + - repo: https://github.com/psf/black + rev: 19.10b0 + hooks: + - id: black + exclude: /migrations/* + - repo: https://github.com/prettier/pre-commit + rev: "v2.1.2" + hooks: + - id: prettier + exclude: /dist/*$|\.html$|\.d.ts$ + - repo: https://github.com/pre-commit/mirrors-eslint + rev: v6.8.0 + hooks: + - id: eslint + exclude: /static/* + verbose: true + args: + - --color + - --fix + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.4.0 + hooks: + - id: trailing-whitespace + # exclude autogenerated files + exclude: /README\.rst$|\.pot?$ + - id: end-of-file-fixer + # exclude autogenerated files + exclude: /README\.rst$|\.pot?$ + - id: debug-statements + - id: fix-encoding-pragma + args: ["--remove"] + - id: check-case-conflict + - id: check-docstring-first + - id: check-executables-have-shebangs + - id: check-merge-conflict + # exclude files where underlines are not distinguishable from merge conflicts + exclude: /README\.rst$|^docs/.*\.rst$ + - id: check-symlinks + - id: check-xml + - id: mixed-line-ending + args: ["--fix=lf"] + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.7.9 + hooks: + - id: flake8 + name: flake8 except __init__.py + exclude: (/__init__\.py$|/migrations/*) + additional_dependencies: ["flake8-bugbear==19.8.0"] + - id: flake8 + name: flake8 only __init__.py + args: ["--extend-ignore=F401"] # ignore unused imports in __init__.py + files: /__init__\.py$ + additional_dependencies: ["flake8-bugbear==19.8.0"] + - repo: https://github.com/asottile/pyupgrade + rev: v1.26.2 + hooks: + - id: pyupgrade + exclude: /migrations/* + - repo: https://github.com/pre-commit/mirrors-isort + rev: v4.3.21 + hooks: + - id: isort + name: isort except __init__.py + exclude: (/__init__\.py$|/migrations/*) + - repo: https://github.com/ecugol/pre-commit-hooks-django + rev: v0.2.1 + hooks: + - id: check-untracked-migrations From 850bf86553e31a0598ff760835c5bd789da39bb1 Mon Sep 17 00:00:00 2001 From: Fareck Allony Date: Sat, 20 Feb 2021 10:37:06 +0200 Subject: [PATCH 13/15] ran pre-commit --- .eslintrc.yml | 177 ++++++++++++++++++ .flake8 | 11 ++ .gitignore | 2 +- .pre-commit-config.yaml | 2 +- .prettierrc.yml | 7 + CHANGELOG.txt | 2 +- setup.py | 8 +- tinylinks/__init__.py | 1 - tinylinks/admin.py | 14 +- tinylinks/forms.py | 11 +- .../commands/check_tinylink_targets.py | 17 +- .../management/commands/import_yourls_db.py | 6 +- tinylinks/management/commands/track_views.py | 24 +-- tinylinks/models.py | 48 ++--- tinylinks/piwik.py | 4 +- tinylinks/serializers.py | 73 ++++---- tinylinks/tests/factories.py | 1 + tinylinks/tests/settings.py | 11 +- tinylinks/tests/test_settings.py | 7 +- tinylinks/tests/tests.py | 3 - tinylinks/urls.py | 44 +---- tinylinks/utils/router.py | 1 - tinylinks/views.py | 58 ++---- 23 files changed, 315 insertions(+), 217 deletions(-) create mode 100644 .eslintrc.yml create mode 100644 .flake8 create mode 100644 .prettierrc.yml mode change 100755 => 100644 tinylinks/tests/tests.py diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 0000000..265a8eb --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,177 @@ +env: + browser: true + +# See https://github.com/OCA/odoo-community.org/issues/37#issuecomment-470686449 +parserOptions: + ecmaVersion: 2017 + +# Globals available in Odoo that shouldn't produce errorings +globals: + _: readonly + $: readonly + fuzzy: readonly + jQuery: readonly + moment: readonly + Promise: readonly + +# Styling is handled by Prettier, so we only need to enable AST rules; +# see https://github.com/OCA/maintainer-quality-tools/pull/618#issuecomment-558576890 +rules: + accessor-pairs: warn + array-callback-return: warn + callback-return: warn + capitalized-comments: + - warn + - always + - ignoreConsecutiveComments: true + ignoreInlineComments: true + complexity: + - warn + - 15 + constructor-super: warn + dot-notation: warn + eqeqeq: warn + global-require: warn + handle-callback-err: warn + id-blacklist: warn + id-match: warn + init-declarations: error + max-depth: warn + max-nested-callbacks: warn + max-statements-per-line: warn + no-alert: warn + no-array-constructor: warn + no-caller: warn + no-case-declarations: warn + no-class-assign: warn + no-cond-assign: error + no-const-assign: error + no-constant-condition: warn + no-control-regex: warn + no-debugger: error + no-delete-var: warn + no-div-regex: warn + no-dupe-args: error + no-dupe-class-members: error + no-dupe-keys: error + no-duplicate-case: error + no-duplicate-imports: error + no-else-return: warn + no-empty-character-class: warn + no-empty-pattern: error + no-empty: warn + no-eq-null: error + no-eval: error + no-ex-assign: error + no-extend-native: warn + no-extra-bind: warn + no-extra-boolean-cast: warn + no-extra-label: warn + no-fallthrough: warn + no-func-assign: error + no-global-assign: error + no-implicit-coercion: + - warn + - allow: ["~"] + no-implicit-globals: warn + no-implied-eval: warn + no-inline-comments: warn + no-inner-declarations: warn + no-invalid-regexp: warn + no-irregular-whitespace: warn + no-iterator: warn + no-label-var: warn + no-labels: warn + no-lone-blocks: warn + no-lonely-if: error + no-mixed-requires: error + no-multi-str: warn + no-native-reassign: error + no-negated-condition: warn + no-negated-in-lhs: error + no-new-func: warn + no-new-object: warn + no-new-require: warn + no-new-symbol: warn + no-new-wrappers: warn + no-new: warn + no-obj-calls: warn + no-octal-escape: warn + no-octal: warn + no-param-reassign: warn + no-path-concat: warn + no-process-env: warn + no-process-exit: warn + no-proto: warn + no-prototype-builtins: warn + no-redeclare: warn + no-regex-spaces: warn + no-restricted-globals: warn + no-restricted-imports: warn + no-restricted-modules: warn + no-restricted-syntax: warn + no-return-assign: error + no-script-url: warn + no-self-assign: warn + no-self-compare: warn + no-sequences: warn + no-shadow-restricted-names: warn + no-shadow: warn + no-sparse-arrays: warn + no-sync: warn + no-this-before-super: warn + no-throw-literal: warn + no-undef-init: warn + no-undef: error + no-unmodified-loop-condition: warn + no-unneeded-ternary: error + no-unreachable: error + no-unsafe-finally: error + no-unused-expressions: error + no-unused-labels: error + no-unused-vars: error + no-use-before-define: error + no-useless-call: warn + no-useless-computed-key: warn + no-useless-concat: warn + no-useless-constructor: warn + no-useless-escape: warn + no-useless-rename: warn + no-void: warn + no-with: warn + operator-assignment: [error, always] + prefer-const: warn + radix: warn + require-yield: warn + sort-imports: warn + spaced-comment: [error, always] + strict: [error, function] + use-isnan: error + valid-jsdoc: + - warn + - prefer: + arg: param + argument: param + augments: extends + constructor: class + exception: throws + func: function + method: function + prop: property + return: returns + virtual: abstract + yield: yields + preferType: + array: Array + bool: Boolean + boolean: Boolean + number: Number + object: Object + str: String + string: String + requireParamDescription: false + requireReturn: false + requireReturnDescription: false + requireReturnType: false + valid-typeof: warn + yoda: warn diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..d995d23 --- /dev/null +++ b/.flake8 @@ -0,0 +1,11 @@ +[flake8] +max-line-length = 88 +max-complexity = 16 +# B = bugbear +# B9 = bugbear opinionated (incl line length) +select = C,E,F,W,B,B9 +# E203: whitespace before ':' (black behaviour) +# E501: flake8 line length (covered by bugbear B950) +# E231: missing whitespace after +# W503: line break before binary operator (black behaviour) +ignore = E231, E203,E501,W503 diff --git a/.gitignore b/.gitignore index 7c568e9..c024af9 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,4 @@ static/ .venv/ .venv .env/ -.env \ No newline at end of file +.env diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a201f33..215cd9e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: hooks: - id: black exclude: /migrations/* - - repo: https://github.com/prettier/pre-commit + - repo: https://github.com/pre-commit/mirrors-prettier rev: "v2.1.2" hooks: - id: prettier diff --git a/.prettierrc.yml b/.prettierrc.yml new file mode 100644 index 0000000..128d271 --- /dev/null +++ b/.prettierrc.yml @@ -0,0 +1,7 @@ +# Defaults for all prettier-supported languages. +# Prettier will complete this with settings from .editorconfig file. +bracketSpacing: false +printWidth: 88 +proseWrap: always +semi: false +trailingComma: "es5" diff --git a/CHANGELOG.txt b/CHANGELOG.txt index a70f921..602aee9 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -2,7 +2,7 @@ === 0.6.1 === -* Fixed Tinylinks restful API +* Fixed Tinylinks restful API === 0.6 === diff --git a/setup.py b/setup.py index a6d68b8..504c8fb 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,7 @@ import os -from setuptools import setup, find_packages + +from setuptools import find_packages, setup + import tinylinks @@ -23,8 +25,6 @@ def read(fname): url="https://github.com/bitmazk/django-tinylinks", packages=find_packages(), include_package_data=True, - tests_require=[ - "factory_boy", - ], + tests_require=["factory_boy",], test_suite="tinylinks.tests.runtests.runtests", ) diff --git a/tinylinks/__init__.py b/tinylinks/__init__.py index 6e1ef8d..6b27eee 100644 --- a/tinylinks/__init__.py +++ b/tinylinks/__init__.py @@ -1,2 +1 @@ -# -*- coding: utf-8 -*- __version__ = "0.5.4" diff --git a/tinylinks/admin.py b/tinylinks/admin.py index 9179319..9a8ae85 100644 --- a/tinylinks/admin.py +++ b/tinylinks/admin.py @@ -2,9 +2,6 @@ from django.contrib import admin from django.template.defaultfilters import truncatechars from django.utils.translation import ugettext_lazy as _ - -from django.template.loader import render_to_string - from tinylinks.forms import TinylinkAdminForm from tinylinks.models import Tinylink, TinylinkLog @@ -23,16 +20,7 @@ class TinylinkAdmin(admin.ModelAdmin): form = TinylinkAdminForm fieldsets = [ - ( - "Tinylink", - { - "fields": [ - "user", - "long_url", - "short_url", - ] - }, - ), + ("Tinylink", {"fields": ["user", "long_url", "short_url",]},), ] def url_truncated(self, obj): diff --git a/tinylinks/forms.py b/tinylinks/forms.py index 3426ca6..eaec896 100644 --- a/tinylinks/forms.py +++ b/tinylinks/forms.py @@ -4,7 +4,6 @@ from django import forms from django.conf import settings from django.utils.translation import ugettext_lazy as _ - from tinylinks.models import Tinylink, validate_long_url @@ -51,7 +50,8 @@ def __init__(self, user=None, mode="change-short", *args, **kwargs): # error_message=(_("Please use only small letters and" # " digits.")), help_text=_( - "You can add a more readable short URL. Please use only small letters and" + "You can add a more readable short URL. Please use only" + " small letters and" " digits." ), label=self.instance._meta.get_field("short_url").verbose_name, @@ -132,7 +132,8 @@ def __init__(self, *args, **kwargs): regex=r"^[a-z0-9]+$", # error_message=(_("Please use only small letters and digits.")), help_text=_( - "You can add a more readable short URL. Please use only small letters and digits." + "You can add a more readable short URL." + "Please use only small letters and digits." ), label=self.instance._meta.get_field("short_url").verbose_name, ) @@ -141,9 +142,7 @@ def clean(self): self.cleaned_data = super(TinylinkAdminForm, self).clean() # If short URL is occupied throw out an error, or fail silent. try: - twin = Tinylink.objects.get( - short_url=self.cleaned_data.get("short_url"), - ) + twin = Tinylink.objects.get(short_url=self.cleaned_data.get("short_url"),) except Tinylink.DoesNotExist: slug = self.cleaned_data.get("short_url") while not slug or Tinylink.objects.filter(short_url=slug): diff --git a/tinylinks/management/commands/check_tinylink_targets.py b/tinylinks/management/commands/check_tinylink_targets.py index 964e501..e012046 100644 --- a/tinylinks/management/commands/check_tinylink_targets.py +++ b/tinylinks/management/commands/check_tinylink_targets.py @@ -9,7 +9,6 @@ from django.conf import settings from django.core.management.base import BaseCommand from django.utils import timezone - from tinylinks.models import Tinylink, validate_long_url @@ -25,13 +24,11 @@ def handle(self, *args, **options): for link in Tinylink.objects.order_by("last_checked")[:check_amount]: validate_long_url(link) print( - ( - "[" - + timezone.now().strftime("%d.%m.%Y - %H:%M") - + "] Checked " - + str(check_amount) - + " of " - + str(url_amount) - + " total URLs." - ) + "[" + + timezone.now().strftime("%d.%m.%Y - %H:%M") + + "] Checked " + + str(check_amount) + + " of " + + str(url_amount) + + " total URLs." ) diff --git a/tinylinks/management/commands/import_yourls_db.py b/tinylinks/management/commands/import_yourls_db.py index fe14b05..1936b24 100644 --- a/tinylinks/management/commands/import_yourls_db.py +++ b/tinylinks/management/commands/import_yourls_db.py @@ -2,10 +2,10 @@ from typing import List -from django.core.management.base import BaseCommand, CommandError -from tinylinks.models import Tinylink, TinylinkLog -from tinylinks.management.commands import _queries, _config import mysql.connector +from django.core.management.base import BaseCommand +from tinylinks.management.commands import _config, _queries +from tinylinks.models import Tinylink, TinylinkLog class Command(BaseCommand): diff --git a/tinylinks/management/commands/track_views.py b/tinylinks/management/commands/track_views.py index 2bd3be8..5823e37 100644 --- a/tinylinks/management/commands/track_views.py +++ b/tinylinks/management/commands/track_views.py @@ -1,18 +1,20 @@ -from django.core.management.base import BaseCommand +import json +import random +import urllib.error +import urllib.parse +import urllib.request + from django.conf import settings -from django.contrib.sites.models import Site from django.contrib.gis.geoip import GeoIP - -from tinylinks.piwik import parse_cookie +from django.contrib.sites.models import Site +from django.core.management.base import BaseCommand from tinylinks.models import TinylinkLog -import json -import random +from tinylinks.piwik import parse_cookie try: from urllib import urlencode except ImportError: from urllib.parse import urlencode -import urllib.request, urllib.error, urllib.parse CURRENT_DOMAIN = Site.objects.get_current().domain TRACK_OFFSET = 50 @@ -29,7 +31,7 @@ def track_to_piwik(self, views): for view in views: url = view.tinylink params = parse_cookie(view.cookie) - address = "http://%s/%s" % (CURRENT_DOMAIN, url.short_url) + address = "http://{}/{}".format(CURRENT_DOMAIN, url.short_url) if settings.GEOIP_PATH: country = G.country(view.remote_ip).get("country_code").lower() params.update( @@ -56,13 +58,13 @@ def track_to_piwik(self, views): # print payload req = urllib.request.Request(settings.PIWIK_URL) req.add_header("Content-Type", "application/json") - response = urllib.request.urlopen(req, json.dumps(payload)) - print(("Another %d views tracked well!" % TRACK_OFFSET)) + urllib.request.urlopen(req, json.dumps(payload)) + print("Another %d views tracked well!" % TRACK_OFFSET) TinylinkLog.objects.filter(pk__in=views).update(tracked=True) def handle(self, *args, **options): num_visits = TinylinkLog.objects.filter(tracked=False).count() - print(("Untracked views: %d" % num_visits)) + print("Untracked views: %d" % num_visits) if num_visits == 0: return diff --git a/tinylinks/models.py b/tinylinks/models.py index 4be0a08..ee473d6 100644 --- a/tinylinks/models.py +++ b/tinylinks/models.py @@ -1,19 +1,16 @@ """Models for the ``django-tinylinks`` app.""" -from urllib.request import build_opener, HTTPCookieProcessor, Request, urlopen -from http.cookiejar import CookieJar import socket +from http.cookiejar import CookieJar +from urllib.request import HTTPCookieProcessor, Request, build_opener, urlopen from django.conf import settings from django.contrib.auth import get_user_model from django.db import models -from django.utils.translation import ugettext_lazy as _ from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ from urllib3 import PoolManager from urllib3.exceptions import HTTPError, MaxRetryError, TimeoutError -from django.template.loader import render_to_string -from django.utils.safestring import mark_safe - User = get_user_model() @@ -113,42 +110,28 @@ class Tinylink(models.Model): on_delete=models.SET_NULL, ) - long_url = models.CharField( - max_length=2500, - verbose_name=_("Long URL"), - ) + long_url = models.CharField(max_length=2500, verbose_name=_("Long URL"),) short_url = models.CharField( - max_length=32, - verbose_name=_("Short URL"), - unique=True, + max_length=32, verbose_name=_("Short URL"), unique=True, ) - is_broken = models.BooleanField( - default=False, - verbose_name=_("Status"), - ) + is_broken = models.BooleanField(default=False, verbose_name=_("Status"),) validation_error = models.CharField( - max_length=100, - verbose_name=_("Validation Error"), - default="", + max_length=100, verbose_name=_("Validation Error"), default="", ) last_checked = models.DateTimeField( - default=timezone.now, - verbose_name=_("Last validation"), + default=timezone.now, verbose_name=_("Last validation"), ) amount_of_views = models.PositiveIntegerField( - default=0, - verbose_name=_("Amount of views"), + default=0, verbose_name=_("Amount of views"), ) redirect_location = models.CharField( - max_length=2500, - verbose_name=_("Redirect location"), - default="", + max_length=2500, verbose_name=_("Redirect location"), default="", ) def get_short_url(self) -> str: @@ -187,18 +170,11 @@ class TinylinkLog(models.Model): on_delete=models.SET_NULL, ) - referrer = models.URLField( - blank=True, - max_length=512, - ) + referrer = models.URLField(blank=True, max_length=512,) user_agent = models.TextField() - cookie = models.CharField( - max_length=127, - blank=True, - default="", - ) + cookie = models.CharField(max_length=127, blank=True, default="",) remote_ip = models.GenericIPAddressField() diff --git a/tinylinks/piwik.py b/tinylinks/piwik.py index 21ec90c..7e5af74 100644 --- a/tinylinks/piwik.py +++ b/tinylinks/piwik.py @@ -1,7 +1,7 @@ +import os +from datetime import datetime, timedelta from hashlib import md5 from time import time -from datetime import datetime, timedelta -import os def __get_random_string(length=500): diff --git a/tinylinks/serializers.py b/tinylinks/serializers.py index ea2fdc5..9950132 100644 --- a/tinylinks/serializers.py +++ b/tinylinks/serializers.py @@ -1,10 +1,8 @@ from django.conf import settings from django.contrib.auth import get_user_model -from django.forms import widgets from django.utils.crypto import get_random_string from rest_framework import serializers -from tinylinks.models import Tinylink, TinylinkLog -from django.utils import timezone +from tinylinks.models import Tinylink User = get_user_model() @@ -45,40 +43,41 @@ def create(self, validated_data): return instance -""" -class TinylinkSerializer(serializers.Serializer): - pk = serializers.Field() # Note: `Field` is an untyped read-only field. - #user = serializers.PrimaryKeyRelatedField() - user = serializers.Field(source='user.username') - long_url = serializers.CharField(max_length=2500) - short_url = serializers.CharField(required=False, max_length=32) - is_broken = serializers.BooleanField(required=False) - validation_error = serializers.CharField(max_length=100, - required=False, default='') - last_checked = serializers.DateTimeField(default=timezone.now(), - required=False) - amount_of_views = serializers.IntegerField(default=0) - - def restore_object(self, attrs, instance=None): - #Create or update a new tinylink instance, given a dictionary - #of deserialized field values. - - #Note that if we don't define this method, then deserializing - #data will simply return a dictionary of items. - if instance: - # Update existing instance - #instance.user = attrs.get('user', instance.user) - instance.long_url = attrs.get('long_url', instance.long_url) - instance.short_url = attrs.get('short_url', instance.short_url) - instance.is_broken = attrs.get('is_broken', instance.is_broken) - instance.validation_error = attrs.get('validation_error', instance.validation_error) - instance.last_checked = attrs.get('last_checked', instance.last_checked) - instance.amount_of_views = attrs.get('amount_of_views', instance.amount_of_views) - return instance - - # Create new instance - return Tinylink(**attrs) -""" +# class TinylinkSerializer(serializers.Serializer): +# pk = serializers.Field() # Note: `Field` is an untyped read-only field. +# #user = serializers.PrimaryKeyRelatedField() +# user = serializers.Field(source='user.username') +# long_url = serializers.CharField(max_length=2500) +# short_url = serializers.CharField(required=False, max_length=32) +# is_broken = serializers.BooleanField(required=False) +# validation_error = serializers.CharField(max_length=100, +# required=False, default='') +# last_checked = serializers.DateTimeField(default=timezone.now(), +# required=False) +# amount_of_views = serializers.IntegerField(default=0) +# +# def restore_object(self, attrs, instance=None): +# #Create or update a new tinylink instance, given a dictionary +# #of deserialized field values. +# +# #Note that if we don't define this method, then deserializing +# #data will simply return a dictionary of items. +# if instance: +# # Update existing instance +# #instance.user = attrs.get('user', instance.user) +# instance.long_url = attrs.get('long_url', instance.long_url) +# instance.short_url = attrs.get('short_url', instance.short_url) +# instance.is_broken = attrs.get('is_broken', instance.is_broken) +# instance.validation_error = attrs.get('validation_error', +# instance.validation_error) +# instance.last_checked = attrs.get('last_checked', +# instance.last_checked) +# instance.amount_of_views = attrs.get('amount_of_views', +# instance.amount_of_views) +# return instance +# +# # Create new instance +# return Tinylink(**attrs) # class TinylinkLogSerializer(serializers.Serializer): diff --git a/tinylinks/tests/factories.py b/tinylinks/tests/factories.py index 3e15468..0086054 100644 --- a/tinylinks/tests/factories.py +++ b/tinylinks/tests/factories.py @@ -5,6 +5,7 @@ import factory from django.contrib.auth import get_user_model from django.contrib.auth.hashers import make_password + from ..models import Tinylink, TinylinkLog User = get_user_model() diff --git a/tinylinks/tests/settings.py b/tinylinks/tests/settings.py index fcffef2..0d74972 100644 --- a/tinylinks/tests/settings.py +++ b/tinylinks/tests/settings.py @@ -9,15 +9,11 @@ defining two routers ("default" and "south") does not work. """ -from tinylinks.tests.test_settings import * # NOQA +import os +from tinylinks.tests.test_settings import * # NOQA -DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": "db.sqlite", - } -} +DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": "db.sqlite",}} SECRET_KEY = "ts4u!_y8xp!yvyxtvmk3046fxaoz_ubh4n2&qtsm&*z(-d(b07" @@ -25,7 +21,6 @@ PIWIK_URL = "http://127.0.0.1/piwik/piwik.php" PIWIK_TOKEN = "60a437e4e088521ca506fcfcf4f57522" -import os BASE_DIR = os.path.abspath(os.path.dirname(__file__)) GEOIP_PATH = os.path.join(BASE_DIR, "geoip") diff --git a/tinylinks/tests/test_settings.py b/tinylinks/tests/test_settings.py index 0be9d3c..73d90e1 100644 --- a/tinylinks/tests/test_settings.py +++ b/tinylinks/tests/test_settings.py @@ -13,12 +13,7 @@ CURRENT_DIR = os.path.dirname(__file__) -DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": ":memory:", - } -} +DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:",}} ROOT_URLCONF = "tinylinks.tests.urls" diff --git a/tinylinks/tests/tests.py b/tinylinks/tests/tests.py old mode 100755 new mode 100644 index b514df3..f8595c8 --- a/tinylinks/tests/tests.py +++ b/tinylinks/tests/tests.py @@ -1,5 +1,3 @@ -import base64 - from django.contrib.auth import get_user_model from django.contrib.auth.hashers import make_password from rest_framework import status @@ -7,7 +5,6 @@ from rest_framework.test import APITestCase from ..models import Tinylink, TinylinkLog -from .factories import TinylinkFactory, UserFactory, TinyLogFactory User = get_user_model() diff --git a/tinylinks/urls.py b/tinylinks/urls.py index 669a356..395abf7 100644 --- a/tinylinks/urls.py +++ b/tinylinks/urls.py @@ -2,25 +2,13 @@ from django.conf import settings from django.conf.urls import include from django.urls import re_path -from django.views.generic import RedirectView -from django.views.generic import TemplateView -from rest_framework.routers import DefaultRouter - +from django.views.generic import RedirectView, TemplateView from tinylinks.utils.router import CustomDefaultRouter -from tinylinks.views import ( - StatisticsView, - TinylinkCreateView, - TinylinkDeleteView, - TinylinkListView, - TinylinkRedirectView, - TinylinkUpdateView, - TinylinkViewSet, - UserViewSet, - db_stats, - stats, - tinylink_stats, - tinylink_expand, -) +from tinylinks.views import (StatisticsView, TinylinkCreateView, + TinylinkDeleteView, TinylinkListView, + TinylinkRedirectView, TinylinkUpdateView, + TinylinkViewSet, UserViewSet, db_stats, stats, + tinylink_expand, tinylink_stats) # Create router and register our API viewsets with it. router = CustomDefaultRouter() @@ -38,9 +26,7 @@ name="tinylink_update", ), re_path( - r"^delete/(?P\d+)/$", - TinylinkDeleteView.as_view(), - name="tinylink_delete", + r"^delete/(?P\d+)/$", TinylinkDeleteView.as_view(), name="tinylink_delete", ), ] @@ -62,19 +48,9 @@ ] urlpatterns += [ - re_path( - r"^statistics/?$", - StatisticsView.as_view(), - name="tinylink_statistics", - ), - re_path( - r"^api/", - include(router.urls), - ), - re_path( - r"^auth/", - include("rest_framework.urls", namespace="rest_framework"), - ), + re_path(r"^statistics/?$", StatisticsView.as_view(), name="tinylink_statistics",), + re_path(r"^api/", include(router.urls),), + re_path(r"^auth/", include("rest_framework.urls", namespace="rest_framework"),), re_path(r"^api/db-stats/$", db_stats, name="api_db_stats"), re_path(r"^api/stats/$", stats, name="api_stats"), re_path( diff --git a/tinylinks/utils/router.py b/tinylinks/utils/router.py index b1ad1e9..d8c25f0 100644 --- a/tinylinks/utils/router.py +++ b/tinylinks/utils/router.py @@ -1,5 +1,4 @@ from rest_framework.routers import DefaultRouter - from tinylinks.views import CustomDefaultRouterAPIView diff --git a/tinylinks/views.py b/tinylinks/views.py index 99f9050..60c5ca6 100644 --- a/tinylinks/views.py +++ b/tinylinks/views.py @@ -1,38 +1,27 @@ """Views for the ``django-tinylinks`` application.""" +import re + from django.contrib.auth import get_user_model from django.contrib.auth.decorators import permission_required -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger -from django.db.models import Count, Sum +from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator +from django.db.models import Q, Sum from django.http import Http404 +from django.shortcuts import get_list_or_404 from django.urls import reverse from django.utils.decorators import method_decorator -from django.views.generic import ( - CreateView, - DeleteView, - ListView, - RedirectView, - UpdateView, -) -from rest_framework.authentication import TokenAuthentication -from rest_framework.decorators import permission_classes +from django.views.generic import (CreateView, DeleteView, ListView, + RedirectView, UpdateView) +from rest_framework import permissions, status, viewsets +from rest_framework.authentication import (BasicAuthentication, + SessionAuthentication, + TokenAuthentication) +from rest_framework.decorators import api_view, permission_classes +from rest_framework.response import Response from rest_framework.routers import APIRootView - from tinylinks.forms import TinylinkForm from tinylinks.models import Tinylink, TinylinkLog, validate_long_url - -from rest_framework import generics, permissions, viewsets, status -from rest_framework.authentication import SessionAuthentication, BasicAuthentication -from rest_framework.decorators import api_view -from rest_framework.response import Response -from rest_framework.views import APIView from tinylinks.serializers import TinylinkSerializer, UserSerializer -from django.shortcuts import get_list_or_404 -from django.db.models import Q -from django.utils.crypto import get_random_string - -import re - User = get_user_model() piwik_id = re.compile(r"^_pk_id") @@ -70,10 +59,7 @@ def get(self, request, *args, **kwargs): def get_form_kwargs(self): kwargs = super(TinylinkViewMixin, self).get_form_kwargs() kwargs.update( - { - "user": self.request.user, - "mode": self.mode, - } + {"user": self.request.user, "mode": self.mode,} ) return kwargs @@ -194,7 +180,7 @@ def get_redirect_url(self, **kwargs): url = self.url args = self.request.META.get("QUERY_STRING", "") if args and self.query_string: - url = "%s?%s" % (url, args) + url = "{}?{}".format(url, args) return url else: return None @@ -287,9 +273,7 @@ def database_statistics(): @api_view(["GET"]) @permission_classes( - [ - permissions.IsAuthenticated, - ] + [permissions.IsAuthenticated,] ) def db_stats(request): """ @@ -303,9 +287,7 @@ def db_stats(request): @api_view(["GET"]) @permission_classes( - [ - permissions.IsAuthenticated, - ] + [permissions.IsAuthenticated,] ) def stats(request): """ @@ -316,7 +298,7 @@ def stats(request): try: paginate_by = int(request.QUERY_PARAMS.get("paginate_by", "")) page = int(request.QUERY_PARAMS.get("page", "")) - except: + except BaseException: paginate_by = 10 page = 1 @@ -350,9 +332,7 @@ def stats(request): @api_view(["GET"]) @permission_classes( - [ - permissions.IsAuthenticated, - ] + [permissions.IsAuthenticated,] ) def tinylink_stats(request, short_url): """ From a8d2e960ea72ec03779fdc85fc9112879540d585 Mon Sep 17 00:00:00 2001 From: Fareck Allony Date: Tue, 15 Jun 2021 08:44:27 +0300 Subject: [PATCH 14/15] added prefix for post response --- tinylinks/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tinylinks/views.py b/tinylinks/views.py index 60c5ca6..38fb69c 100644 --- a/tinylinks/views.py +++ b/tinylinks/views.py @@ -235,7 +235,7 @@ def create(self, request, *args, **kwargs): headers = self.get_success_headers(serializer.data) data = { "id": instance.id, - "short_url": request.build_absolute_uri("/%s" % instance.short_url), + "short_url": request.build_absolute_uri("/%s" % instance.get_short_url()), "long_url": instance.long_url, } From f7700d8f89f1f8e6bf32a6586eb71d0936b19d5d Mon Sep 17 00:00:00 2001 From: Fareck Allony Date: Thu, 15 Jul 2021 12:40:43 +0300 Subject: [PATCH 15/15] using environ for config --- requirements.txt | 1 + tinylinks/management/commands/_config.py | 18 ++++++++---------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/requirements.txt b/requirements.txt index 9eab3ff..7569014 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,4 @@ djangorestframework==3.9.2 # Packages needed for running the tests. Needed by contributors. # ============================================================== factory-boy==2.11.1 +python-environ==0.4.54 diff --git a/tinylinks/management/commands/_config.py b/tinylinks/management/commands/_config.py index 96b7551..6f24106 100644 --- a/tinylinks/management/commands/_config.py +++ b/tinylinks/management/commands/_config.py @@ -1,15 +1,13 @@ -user = "" -password = "" -host = "127.0.0.1" -database = "" -raise_on_warnings = True +import environ +env = environ.Env() +env.read_env(".env") config = { - "user": user, - "password": password, - "host": host, - "database": database, - "raise_on_warnings": raise_on_warnings, + "user": env.str("MYSQL_USER", ""), + "password": env.str("MYSQL_PASSWORD", ""), + "host": env.str("MYSQL_HOSTNAME", ""), + "database": env.str("MYSQL_DATABASE", ""), + "raise_on_warnings": env.bool("MYSQL_RAISE_ON_WARNING", ""), }