-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a shortlinks Django app. (#2167)
During our latest NYCx meeting we discussed the possibility of using a short URL service like bit.ly to provide branded short URLs, to make links to external resources provided on the textbot take up less precious characters. Unfortunately, using bit.ly to create URLs with custom domains isn't very cheap (about $40 per month) and they don't have any kind of nonprofit discount. As an alternative, I figured it shouldn't be too hard to roll-our-own extremely simplistic short URL service as a feature on the tenant platform. Admins could log into our Django admin backend and use a basic UI to create urls that would be of the form `https://app.justfix.nyc/s/<some short id>`. Aside from being useful to shorten URLs though, creating a layer of redirection has some other advantages: * **We have the ability to update where our short URLs point.** This means, for example, if we send a text message that says "go here to learn more about rent reductions" and points to a URL on a government website, it that URL eventually 404's and the user is going through their SMS text history and taps the link, it will 404 too. However, if the link points to a short URL we've provided, and we change the link's target once it 404's, then tapping that link won't 404 anymore. * **We can change our external URLs in just one place.** For instance, if we have a single short URL that points to a government page on obtaining rent reductions, and we link to our short URL in our learning center articles and our tenant platform and our text bot, then if the target of that URL ever changes, we can just update the short URL target, instead of having to manually update the content of all our properties. * **We could regularly sweep through our short URLs and be alerted of any that 404.** To figure out which links have rotted, we can simply iterate through all the short URLs in our database and regularly check which ones 404. This adds the feature via a new Django app called `shortlinks`, which Outreach Coordinators have the ability to create and modify. ## Limitations * This doesn't currently record visits to shortlinks in any way, so we can't easily do metrics (unless we parse our log files, I guess). * These links aren't _super_ short, e.g. `app.justfix.nyc/s/boop` could be further shortened to something like `jf.nyc/boop`, but that would take more work. * There's no version history, so it's not easy to revert a change to a shortlink. We could add [django-reversion](https://django-reversion.readthedocs.io/) to get this, though.
- Loading branch information
Showing
15 changed files
with
140 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
from project.util.admin_util import admin_field | ||
from django.contrib import admin | ||
|
||
from project.util.site_util import absolute_reverse | ||
from . import models | ||
|
||
|
||
@admin.register(models.Link) | ||
class LinkAdmin(admin.ModelAdmin): | ||
list_display = ["title", "slug", "short_link", "url"] | ||
|
||
fields = ["title", "url", "slug", "short_link", "description"] | ||
|
||
readonly_fields = ["short_link"] | ||
|
||
add_fields = ["title", "url", "slug", "description"] | ||
|
||
@admin_field(admin_order_field="slug") | ||
def short_link(self, obj): | ||
if obj is not None and obj.pk: | ||
return absolute_reverse("shortlinks:redirect", kwargs={"slug": obj.slug}) | ||
|
||
return "(This will be set once you save the link.)" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from django.apps import AppConfig | ||
|
||
|
||
class ShortlinksConfig(AppConfig): | ||
default_auto_field = "django.db.models.BigAutoField" | ||
name = "shortlinks" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
# Generated by Django 3.2.4 on 2021-07-21 10:53 | ||
|
||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
initial = True | ||
|
||
dependencies = [ | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name='Link', | ||
fields=[ | ||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
('created_at', models.DateTimeField(auto_now_add=True)), | ||
('updated_at', models.DateTimeField(auto_now=True)), | ||
('url', models.URLField(help_text='The destination of the link.')), | ||
('title', models.CharField(help_text='The title of the link.', max_length=200)), | ||
('slug', models.SlugField(help_text='The slug of the link. This will be used in the short link, so try to keep it short yet (hopefully) memorable.', max_length=200, unique=True)), | ||
('description', models.TextField(blank=True, help_text='A description of the link. Optional.')), | ||
], | ||
), | ||
] |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
from django.db import models | ||
|
||
|
||
class Link(models.Model): | ||
created_at = models.DateTimeField(auto_now_add=True) | ||
|
||
updated_at = models.DateTimeField(auto_now=True) | ||
|
||
url = models.URLField(help_text="The destination of the link.") | ||
|
||
title = models.CharField(max_length=200, help_text="The title of the link.") | ||
|
||
slug = models.SlugField( | ||
max_length=200, | ||
help_text=( | ||
"The slug of the link. This will be used in the short link, so " | ||
"try to keep it short yet (hopefully) memorable." | ||
), | ||
unique=True, | ||
) | ||
|
||
description = models.TextField(help_text="A description of the link. Optional.", blank=True) | ||
|
||
def __str__(self): | ||
return self.title |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import factory | ||
|
||
from shortlinks.models import Link | ||
|
||
|
||
class LinkFactory(factory.django.DjangoModelFactory): | ||
class Meta: | ||
model = Link | ||
|
||
title = "Housing Court Answers" | ||
|
||
url = "http://housingcourtanswers.org/" | ||
|
||
slug = "hca" | ||
|
||
description = "Our awesome partner's website." |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
from shortlinks.models import Link | ||
|
||
|
||
class TestLink: | ||
def test_str_works(self): | ||
link = Link(title="Boop") | ||
assert str(link) == "Boop" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import pytest | ||
|
||
from .factories import LinkFactory | ||
|
||
|
||
@pytest.mark.parametrize("slug", ["hca", "hca-is_K00L"]) | ||
def test_redirect_works(db, client, slug): | ||
LinkFactory(slug=slug) | ||
res = client.get(f"/s/{slug}") | ||
assert res.status_code == 302 | ||
assert res["Location"] == "http://housingcourtanswers.org/" | ||
|
||
|
||
def test_redirect_404s_on_invalid_slug(db, client, disable_locale_middleware): | ||
res = client.get("/s/hca") | ||
assert res.status_code == 404 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
from django.urls import re_path | ||
|
||
from . import views | ||
|
||
app_name = "shortlinks" | ||
|
||
urlpatterns = [ | ||
re_path(r"(?P<slug>[A-Za-z0-9\-_]+)", views.redirect_to_link, name="redirect"), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
from django.http import HttpResponseRedirect | ||
from django.shortcuts import get_object_or_404 | ||
|
||
from .models import Link | ||
|
||
|
||
def redirect_to_link(request, slug): | ||
link = get_object_or_404(Link, slug=slug) | ||
return HttpResponseRedirect(link.url) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters