Add a basic link embiggener
authorDavid Kerkeslager <kerkeslager@gmail.com>
Thu, 2 Sep 2021 16:47:54 +0000 (12:47 -0400)
committerDavid Kerkeslager <kerkeslager@gmail.com>
Thu, 2 Sep 2021 16:47:54 +0000 (12:47 -0400)
src/bigly/serializers.py
src/bigly/static/bigly/scripts.js [new file with mode: 0644]
src/bigly/static/bigly/styles.css
src/bigly/templates/bigly/index.html
src/bigly/templates/bigly/link_info.html [new file with mode: 0644]
src/bigly/test_views.py [new file with mode: 0644]
src/bigly/urls.py
src/bigly/views.py

index bc948fd..8a9a5c4 100644 (file)
@@ -2,3 +2,4 @@ from rest_framework import serializers
 
 class FollowRedirectsSerializer(serializers.Serializer):
     link = serializers.URLField(required=True)
+    remove_utm = serializers.BooleanField(required=False)
diff --git a/src/bigly/static/bigly/scripts.js b/src/bigly/static/bigly/scripts.js
new file mode 100644 (file)
index 0000000..a76cfe4
--- /dev/null
@@ -0,0 +1 @@
+console.log('Hello, world');
index 5eaa9c4..b1bc6cf 100644 (file)
@@ -1,3 +1,61 @@
+html, body, h1, h2, input {
+  background: none;
+  border: none;
+  border-radius: 0;
+  font-size: 14pt;
+  font-weight: normal;
+  margin: 0;
+  padding: 0;
+  outline: none;
+}
+
+html {
+  font-family: Verdana, Helvetica, sans-serif;
+  height: 100%;
+  width: 100%;
+  margin: 0;
+  padding: 0;
+}
+
 body {
-  color: magenta;
+  max-width: calc(100% - 6em);
+  margin: 3em auto;
+}
+
+h1 {
+  font-size: 12vw;
+  text-align: center;
+}
+
+h2 {
+  font-size: 6vw;
+  text-align: center;
+}
+
+form {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin: 3em auto;
+  max-width: 30em;
+  width: 100%;
+}
+
+form input[type=url] {
+  border-bottom: 2px solid black;
+  width: 100%;
+  padding: 4px;
+}
+
+form input[type=submit] {
+  border: 2px solid black;
+  border-radius: 5px;
+  margin-top: 1em;
+  padding: 5px 8px;
+  cursor: pointer;
+}
+
+form label {
+  font-size: 12pt;
+  width: 100%;
 }
index 4178319..17bf9f7 100644 (file)
   <body>
     <h1>bigly</h1>
     <h2>make links big again</h2>
-    Hello, world
 
-    <div id='app'>
-      <noscript>
-        Foo
-      </noscript>
-    </div>
+    <form action='{% url "embiggen" %}' method='get'>
+      <input type='url' name='link' autofocus></input>
+      <label for='link'>Link</label>
+      <input type='checkbox' name='remove_utm' checked></input>
+      <label for='remove_utm'>Remove UTM Parameters</label>
+      <input type='submit' value='Embiggen'></input>
+    </form>
 
-    <script src='bigly/scripts.js'></script>
+    <script src='{% static "bigly/scripts.js" %}'></script>
   </body>
 </html>
 
diff --git a/src/bigly/templates/bigly/link_info.html b/src/bigly/templates/bigly/link_info.html
new file mode 100644 (file)
index 0000000..bdd5265
--- /dev/null
@@ -0,0 +1,23 @@
+{% load static %}
+<!doctype html>
+<html lang='en'>
+  <head>
+    <title>bigly</title>
+
+    <meta charset='utf-8'/>
+    <meta name='viewport' content='width=device-width, initial-scale=1'/>
+    <meta name='description' content='A tool for unshortening links that have been shortened with a link shortener.'/>
+    <meta name='author' content='David Kerkeslager'/>
+
+    <link rel='stylesheet' href='{% static "bigly/styles.css" %}'/>
+  </head>
+
+  <body>
+    <table>
+      <tr>
+        <td>Link:</td>
+        <td><a href='{{ link }}'>{{ link }}</a></td>
+      </tr>
+    </table>
+  </body>
+</html>
diff --git a/src/bigly/test_views.py b/src/bigly/test_views.py
new file mode 100644 (file)
index 0000000..8d9df71
--- /dev/null
@@ -0,0 +1,25 @@
+from django.test import TestCase
+
+from . import views
+
+class RemoveUTMTestCase(TestCase):
+    def test_preserves_basic(self):
+        expected = 'http://www.myhostname.com/my/path?my=param&my=otherparam#myanchor'
+        actual = views._remove_utm(expected)
+        self.assertEqual(expected, actual)
+
+    def test_removes_utm(self):
+        input_url = 'http://www.myhostname.com/my/path?utm_param=param&my=param#myanchor'
+        expected = 'http://www.myhostname.com/my/path?my=param#myanchor'
+        actual = views._remove_utm(input_url)
+        self.assertEqual(expected, actual)
+
+    def test_preserves_empty_parameters(self):
+        expected = 'http://www.myhostname.com/my/path?my_empty_param=&my=param#myanchor'
+        actual = views._remove_utm(expected)
+        self.assertEqual(expected, actual)
+
+    def test_preserves_flags(self):
+        expected = 'http://www.myhostname.com/my/path?my_flag&my=param#myanchor'
+        actual = views._remove_utm(expected)
+        self.assertEqual(expected, actual)
index d074512..ae0210d 100644 (file)
@@ -20,6 +20,7 @@ from . import views
 
 urlpatterns = (
     path('admin/', admin.site.urls),
-    path('api/v1/follow-redirects', views.api_follow_redirects),
+    path('api/v1/follow-redirects', views.api_follow_redirects, name='api:follow-redirects'),
     path('', views.index),
+    path('embiggen', views.embiggen, name='embiggen'),
 )
index 46bccd6..f88753a 100644 (file)
@@ -1,3 +1,6 @@
+from urllib.parse import urlparse, urlunparse, parse_qs
+
+from django.shortcuts import render
 from django.views.generic.base import TemplateView
 
 from rest_framework import status, viewsets
@@ -6,11 +9,81 @@ import requests
 
 from . import serializers
 
+def _remove_utm(link):
+    parsed_link = urlparse(link)
+    parsed_link = parsed_link._replace(
+        query='&'.join(
+            [
+                p
+                for p in parsed_link.query.split('&')
+                if not p.startswith('utm_')
+            ]
+        )
+    )
+
+    return ''.join((
+        parsed_link.scheme + '://' if parsed_link.scheme else '',
+        parsed_link.netloc,
+        parsed_link.path,
+        ';' + parsed_link.params if parsed_link.params else '',
+        '?' + parsed_link.query if parsed_link.query else '',
+        '#' + parsed_link.fragment if parsed_link.fragment else '',
+    ))
+
+def _follow_redirects(link, remove_utm):
+    while True:
+        if remove_utm:
+            link = _remove_utm(link)
+
+        response = requests.head(link)
+
+        # TODO Handle timeouts
+
+        if 301 <= response.status_code and response.status_code <= 308:
+            # TODO Handle the different kinds of redirects correctly
+
+            link = response.headers.get('Location')
+
+            if not link:
+                # TODO Handle this
+                raise Exception()
+
+        # TODO Handle error responses
+        else:
+            return {
+                'link': link,
+                'status': response.status_code,
+            }
+
 class IndexView(TemplateView):
     template_name = 'bigly/index.html'
 
 index = IndexView.as_view()
 
+def embiggen(request):
+    serializer = serializers.FollowRedirectsSerializer(data=request.GET)
+
+    if not serializer.is_valid():
+        return render(
+            request,
+            'bigly/index.html',
+            {
+                'errors': serializer.errors,
+            },
+            status=400,
+        )
+
+    result = _follow_redirects(
+        link = serializer.data['link'],
+        remove_utm = serializer.data['remove_utm'],
+    )
+
+    return render(
+        request,
+        'bigly/link_info.html',
+        result,
+    )
+
 class FollowRedirectsViewSet(viewsets.ViewSet):
     serializer_class = serializers.FollowRedirectsSerializer
 
@@ -23,32 +96,15 @@ class FollowRedirectsViewSet(viewsets.ViewSet):
                 status=status.HTTP_400_BAD_REQUEST,
             )
 
-        link = serializer.data['link']
-
-        while True:
-            response = requests.head(link)
-
-            # TODO Handle timeouts
-
-            if 301 <= response.status_code and response.status_code <= 308:
-                # TODO Handle the different kinds of redirects correctly
-
-                link = response.headers.get('Location')
-
-                if not link:
-                    # TODO Handle this
-                    raise Exception()
-
-            # TODO Handle error responses
-            else:
-                return Response(
-                    {
-                        'link': link,
-                        'status': response.status_code,
-                    },
-                    status=status.HTTP_200_OK,
-                )
+        result = _follow_redirects(
+            link = serializer.data['link'],
+            remove_utm = serializer.data['remove_utm'],
+        )
 
+        return Response(
+            result,
+            status=status.HTTP_200_OK,
+        )
 
 api_follow_redirects = FollowRedirectsViewSet.as_view({
     'get': 'follow_redirects',