├── .gitignore ├── AUTHORS ├── CHANGELOG.txt ├── DESCRIPTION ├── LICENSE ├── MANIFEST.in ├── README.rst ├── manage.py ├── runtests.py ├── setup.py ├── test_requirements.txt ├── tinylinks ├── __init__.py ├── admin.py ├── forms.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── check_tinylink_targets.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── south_migrations │ ├── 0001_initial.py │ ├── 0002_auto__add_field_tinylink_user.py │ ├── 0003_auto__add_field_tinylink_is_broken__add_field_tinylink_last_checked.py │ ├── 0004_auto__add_field_tinylink_amount_of_views.py │ ├── 0005_auto__add_field_tinylink_redirect_location.py │ ├── 0006_auto__add_field_tinylink_validation_error.py │ └── __init__.py ├── templates │ └── tinylinks │ │ ├── notfound.html │ │ ├── statistics.html │ │ ├── tinylink_confirm_delete.html │ │ ├── tinylink_form.html │ │ └── tinylink_list.html ├── tests │ ├── __init__.py │ ├── check_tinylink_targets_tests.py │ ├── forms_tests.py │ ├── models_tests.py │ ├── settings.py │ ├── test_app │ │ ├── __init__.py │ │ ├── fixtures │ │ │ ├── auth.json │ │ │ ├── sites.json │ │ │ └── tinylinks.json │ │ ├── models.py │ │ └── templates │ │ │ ├── 404.html │ │ │ ├── 500.html │ │ │ └── base.html │ ├── test_settings.py │ ├── urls.py │ ├── utils_tests.py │ ├── views.py │ └── views_tests.py ├── urls.py ├── utils.py └── views.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | *.pyc 3 | .tox 4 | coverage/ 5 | db.sqlite 6 | dist/ 7 | static/ 8 | .coverage 9 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Current or previous core committers 2 | 3 | Tobias Lorenz 4 | 5 | Contributors (in alphabetical order) 6 | 7 | * Martin Brochhaus 8 | * Your name could stand here :) 9 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | === (ongoing) === 2 | 3 | === 0.7 === 4 | 5 | * Prepared app for Django 1.9 and Python 3.5 6 | * Removed urllib3 dependencies 7 | 8 | === 0.6 === 9 | 10 | * Improved tests 11 | * Latest requirements 12 | 13 | === 0.5.3 === 14 | 15 | * Fixed irritating status field in admin list 16 | 17 | === 0.5.2 === 18 | 19 | * Show all relevant fields in admin list 20 | 21 | === 0.5.1 === 22 | 23 | * Show amount of views in admin list 24 | 25 | === 0.5 === 26 | 27 | * Added fix for long urls with unicode characters 28 | * Added search fields to admin 29 | 30 | === 0.4 === 31 | 32 | * Important bugfix: Tinylinks can now handle URLs with % characters 33 | * Added function to start a single link validation 34 | 35 | === 0.3 === 36 | 37 | * Added tinylink list view, delete view 38 | * Created management command to check the tinylink target URLs 39 | * Added statistics for staff members 40 | 41 | === 0.2 === 42 | 43 | * Enhanced urls.py so that the shortlink URL can end with or without `/` 44 | * Added admin to test urls.py so that we can login when testing this app in the 45 | browser 46 | 47 | === 0.1 === 48 | 49 | * Initial release 50 | * Use it at your own risk :) 51 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | A reusable Django app that adds a link shortener like bit.ly to your site. 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2012 Martin Brochhaus 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include CHANGELOG 3 | include LICENSE 4 | include README.rst 5 | include requirements.txt 6 | recursive-include tinylinks/templates * 7 | recursive-include tinylinks/tests/test_app/templates * 8 | recursive-include tinylinks/tests/test_app/fixtures * 9 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django Tinylinks 2 | ================ 3 | 4 | A Django application that adds an URL shortener to your site similar to bit.ly. 5 | 6 | Installation 7 | ------------ 8 | 9 | You need to install the following prerequisites in order to use this app:: 10 | 11 | pip install django 12 | pip install requests 13 | 14 | When using Python 2.6, you will also need to install importlib. 15 | 16 | If you want to install the latest stable release from PyPi:: 17 | 18 | $ pip install django-tinylinks 19 | 20 | If you feel adventurous and want to install the latest commit from GitHub:: 21 | 22 | $ pip install -e git://github.com/bitmazk/django-tinylinks.git#egg=tinylinks 23 | 24 | Add ``tinylinks`` to your ``INSTALLED_APPS``:: 25 | 26 | INSTALLED_APPS = ( 27 | ..., 28 | 'tinylinks', 29 | ) 30 | 31 | Add the ``tinylinks`` URLs to your ``urls.py``:: 32 | 33 | urlpatterns = [ 34 | url(r'^s/', include('tinylinks.urls')), 35 | ] 36 | 37 | Don't forget to migrate your database:: 38 | 39 | ./manage.py migrate tinylinks 40 | 41 | Settings 42 | -------- 43 | 44 | TINYLINK_LENGTH 45 | +++++++++++++++ 46 | 47 | Default: 6 48 | 49 | Integer representing the number of characters for your tinylinks. This setting 50 | is used when the app suggests a new tinylink. Regardless of this setting users 51 | will be able to create custom tinylinks with up to 32 characters. 52 | 53 | 54 | TINYLINK_CHECK_INTERVAL 55 | +++++++++++++++++++++++ 56 | 57 | Default: 10 58 | 59 | Number of minutes between two runs of the check command. This number should be 60 | big enough so that one run can complete before the next run is scheduled. 61 | 62 | TINYLINK_CHECK_PERIOD 63 | +++++++++++++++++++++ 64 | 65 | Default: 300 66 | 67 | Number of minutes in which all URLs should have been updated at least 68 | once. If this is 300 it means that within 5 hours we want to update all URLs. 69 | 70 | If ``TINYLINK_CHECK_INTERVAL`` is 10 it means that we will run the command 71 | every 10 minutes. Combined with a total time of 300 minutes, this means that we 72 | can execute the command 300/10=30 times during one period. 73 | 74 | Now we can devide the total number of URLs by 30 and on each run we will 75 | update the X most recent URLs. After 10 runs, we will have updated all URLs. 76 | 77 | Usage 78 | ----- 79 | 80 | Just visit the root URL of the app. Let's assume you hooked the app into your 81 | ``urls.py`` at `s/`, then visit `yoursite.com/s/`. You will see your tinylist 82 | overview. Go to `yoursite.com/s/create/` to see a form to submit a new long URL. 83 | 84 | After submitting, you will be redirected to a new page which shows the 85 | generated short URL. If you want this URL to have a different short URL, just 86 | change the short URL to your liking. 87 | 88 | Now visit `yoursite.com/s/yourshorturl` and you will be redirected to your long 89 | URL. 90 | 91 | Contribute 92 | ---------- 93 | 94 | If you want to contribute to this project, please perform the following steps 95 | 96 | .. code-block:: bash 97 | 98 | # Fork this repository 99 | # Clone your fork 100 | mkvirtualenv -p python2.7 django-tinylinks 101 | make develop 102 | 103 | git co -b feature_branch master 104 | # Implement your feature and tests 105 | git add . && git commit 106 | git push -u origin feature_branch 107 | # Send us a pull request for your feature branch 108 | 109 | In order to run the tests, simply execute ``tox``. This will install two new 110 | environments (for Django 1.8 and Django 1.9) and run the tests against both 111 | environments. 112 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tinylinks.tests.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | This script is used to run tests, create a coverage report and output the 4 | statistics at the end of the tox run. 5 | To run this script just execute ``tox`` 6 | """ 7 | import re 8 | 9 | from fabric.api import local, warn 10 | from fabric.colors import green, red 11 | 12 | 13 | if __name__ == '__main__': 14 | local('flake8 --ignore=E126 --ignore=W391 --statistics' 15 | ' --exclude=submodules,migrations,south_migrations,build,.tox .') 16 | local('coverage run --source="tinylinks" manage.py test -v 2' 17 | ' --traceback --failfast' 18 | ' --settings=tinylinks.tests.settings' 19 | ' --pattern="*_tests.py"') 20 | local('coverage html -d coverage --omit="*__init__*,*/settings/*,' 21 | '*/migrations/*,*/south_migrations/*,*/tests/*,*admin*"') 22 | total_line = local('grep -n pc_cov coverage/index.html', capture=True) 23 | percentage = float(re.findall(r'(\d+)%', total_line)[-1]) 24 | if percentage < 100: 25 | warn(red('Coverage is {0}%'.format(percentage))) 26 | print(green('Coverage is {0}%'.format(percentage))) 27 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | import tinylinks 4 | 5 | 6 | def read(fname): 7 | try: 8 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 9 | except IOError: 10 | return '' 11 | 12 | 13 | setup( 14 | name="django-tinylinks", 15 | version=tinylinks.__version__, 16 | description=read('DESCRIPTION'), 17 | long_description=read('README.rst'), 18 | license='The MIT License', 19 | platforms=['OS Independent'], 20 | keywords='django, url shortener, link shortener', 21 | author='Tobias Lorenz', 22 | author_email='tobias.lorenz@bitlabstudio.com', 23 | url="https://github.com/bitmazk/django-tinylinks", 24 | packages=find_packages(), 25 | include_package_data=True, 26 | install_requires=[ 27 | 'django', 28 | 'requests', 29 | ], 30 | ) 31 | -------------------------------------------------------------------------------- /test_requirements.txt: -------------------------------------------------------------------------------- 1 | coverage 2 | fabric3 3 | paramiko<2.0.0 4 | flake8 5 | ipdb 6 | mixer 7 | tox 8 | django-libs 9 | mock 10 | -------------------------------------------------------------------------------- /tinylinks/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __version__ = '0.7.1' 3 | -------------------------------------------------------------------------------- /tinylinks/admin.py: -------------------------------------------------------------------------------- 1 | """Admin sites for the ``django-tinylinks`` app.""" 2 | from django.contrib import admin 3 | from django.template.defaultfilters import truncatechars 4 | from django.utils.translation import ugettext_lazy as _ 5 | 6 | from tinylinks.forms import TinylinkAdminForm 7 | from tinylinks.models import Tinylink 8 | 9 | 10 | class TinylinkAdmin(admin.ModelAdmin): 11 | list_display = ('short_url', 'url_truncated', 'amount_of_views', 'user', 12 | 'last_checked', 'status', 'validation_error') 13 | search_fields = ['short_url', 'long_url'] 14 | form = TinylinkAdminForm 15 | 16 | def url_truncated(self, obj): 17 | return truncatechars(obj.long_url, 60) 18 | url_truncated.short_description = _('Long URL') 19 | 20 | def status(self, obj): 21 | if not obj.is_broken: 22 | return _('OK') 23 | return _('Link broken') 24 | status.short_description = _('Status') 25 | 26 | 27 | admin.site.register(Tinylink, TinylinkAdmin) 28 | -------------------------------------------------------------------------------- /tinylinks/forms.py: -------------------------------------------------------------------------------- 1 | """Forms for the ``django-tinylinks`` app.""" 2 | import random 3 | 4 | from django import forms 5 | from django.conf import settings 6 | from django.forms.utils import ErrorList 7 | from django.utils.translation import ugettext_lazy as _ 8 | 9 | from .models import Tinylink 10 | from .utils import validate_long_url 11 | 12 | 13 | class TinylinkForm(forms.ModelForm): 14 | """ 15 | Creates and validates long and short URL version. 16 | 17 | """ 18 | def __init__(self, user=None, mode='change-short', *args, **kwargs): 19 | """ 20 | The Regex field validates the URL input. Allowed are only slugified 21 | inputs. 22 | 23 | Examples: 24 | "D834n-qNx2q-jn" <- valid 25 | "D834n qNx2q jn" <- invalid 26 | "D834n_qNx2q/jn" <- invalid 27 | """ 28 | super(TinylinkForm, self).__init__(*args, **kwargs) 29 | if mode == 'change-long': 30 | long_help_text = _("You can now change your long URL.") 31 | else: 32 | long_help_text = _("The long URL isn't editable at the moment.") 33 | self.fields['long_url'] = forms.URLField( 34 | label=self.instance._meta.get_field('long_url').verbose_name, 35 | help_text=long_help_text, 36 | ) 37 | if not self.instance.pk: 38 | # Hide the short URL field to auto-generate a new instance. 39 | self.fields['short_url'].widget = forms.HiddenInput() 40 | self.fields['short_url'].required = False 41 | else: 42 | # Dependent on the user mode, one URL field should not be editable. 43 | if mode == 'change-long': 44 | self.fields['short_url'].widget.attrs['readonly'] = True 45 | self.fields['short_url'].help_text = _( 46 | "The short URL isn't editable at the moment.") 47 | else: 48 | self.fields['long_url'].widget.attrs['readonly'] = True 49 | self.fields['short_url'] = forms.RegexField( 50 | regex=r'^[a-z0-9]+$', 51 | help_text=_("You can add a more readable short URL."), 52 | label=self.instance._meta.get_field( 53 | 'short_url').verbose_name, 54 | ) 55 | self.fields['short_url'].error_messages['invalid'] = _( 56 | "Please use only small letters and digits.") 57 | self.user = user 58 | 59 | def clean(self): 60 | self.cleaned_data = super(TinylinkForm, self).clean() 61 | # If short URL is occupied throw out an error, or fail silent. 62 | try: 63 | twin = Tinylink.objects.get(short_url=self.cleaned_data.get( 64 | 'short_url')) 65 | if not self.instance == twin: 66 | self._errors['short_url'] = ErrorList([_( 67 | 'This short url already exists. Please try another one.')]) 68 | return self.cleaned_data 69 | except Tinylink.DoesNotExist: 70 | pass 71 | # Brothers are entities with the same long URL 72 | brothers = Tinylink.objects.filter(long_url=self.cleaned_data.get( 73 | 'long_url'), user=self.user) 74 | input_url = self.cleaned_data.get('short_url') 75 | 76 | # Only handle with older brothers, if there's no new short URL value 77 | if brothers and not input_url: 78 | # This can only happen, if a user tries to auto-generate a 79 | # short URL with an existing tinylink. She will receive the 80 | # prefilled form with the link's old values. 81 | self.instance = brothers[0] 82 | self.cleaned_data.update( 83 | {'short_url': self.instance.short_url}) 84 | else: 85 | slug = '' 86 | if input_url: 87 | # User can customize their URLs 88 | slug = input_url 89 | # This keeps the unique validation of the short URLs alive. 90 | if not Tinylink.objects.filter(short_url=input_url): 91 | while not slug or Tinylink.objects.filter(short_url=slug): 92 | slug = ''.join( 93 | random.choice('abcdefghijkmnpqrstuvwxyz123456789') 94 | for x in range( 95 | getattr(settings, 'TINYLINK_LENGTH', 6))) 96 | self.cleaned_data.update({'short_url': slug}) 97 | return self.cleaned_data 98 | 99 | def save(self, *args, **kwargs): 100 | if not self.instance.pk: 101 | self.instance.user = self.user 102 | self.instance = super(TinylinkForm, self).save(*args, **kwargs) 103 | return validate_long_url(self.instance) 104 | 105 | class Meta: 106 | model = Tinylink 107 | fields = ('long_url', 'short_url') 108 | 109 | 110 | class TinylinkAdminForm(forms.ModelForm): 111 | """ 112 | Creates and updates long and short URL versions in the Django Admin. 113 | 114 | """ 115 | def __init__(self, *args, **kwargs): 116 | """The Regex field validates the URL input.""" 117 | super(TinylinkAdminForm, self).__init__(*args, **kwargs) 118 | if not self.instance.pk: 119 | # Hide the short URL field to auto-generate a new instance. 120 | self.fields['short_url'].widget = forms.HiddenInput() 121 | self.fields['short_url'].required = False 122 | else: 123 | self.fields['short_url'] = forms.RegexField( 124 | regex=r'^[a-z0-9]+$', 125 | help_text=_("You can add a more readable short URL."), 126 | label=self.instance._meta.get_field('short_url').verbose_name, 127 | ) 128 | self.fields['short_url'].error_messages['invalid'] = _( 129 | "Please use only small letters and digits.") 130 | 131 | def clean(self): 132 | self.cleaned_data = super(TinylinkAdminForm, self).clean() 133 | # If short URL is occupied throw out an error, or fail silent. 134 | try: 135 | twin = Tinylink.objects.get( 136 | short_url=self.cleaned_data.get('short_url'), 137 | ) 138 | except Tinylink.DoesNotExist: 139 | slug = self.cleaned_data.get('short_url') 140 | while not slug or Tinylink.objects.filter(short_url=slug): 141 | slug = ''.join( 142 | random.choice('abcdefghijkmnpqrstuvwxyz123456789') 143 | for x in range(getattr(settings, 'TINYLINK_LENGTH', 6))) 144 | self.cleaned_data.update({'short_url': slug}) 145 | else: 146 | if twin != self.instance: 147 | self._errors['short_url'] = ErrorList([_( 148 | 'This short url already exists. Please try another one.')]) 149 | return self.cleaned_data 150 | 151 | class Meta: 152 | model = Tinylink 153 | fields = ('user', 'long_url', 'short_url') 154 | -------------------------------------------------------------------------------- /tinylinks/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-tinylinks/83fed967197571b18e883473f7661eb36ec2b4ef/tinylinks/management/__init__.py -------------------------------------------------------------------------------- /tinylinks/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-tinylinks/83fed967197571b18e883473f7661eb36ec2b4ef/tinylinks/management/commands/__init__.py -------------------------------------------------------------------------------- /tinylinks/management/commands/check_tinylink_targets.py: -------------------------------------------------------------------------------- 1 | """ 2 | Custom admin command to check all tinylink target URLs. 3 | 4 | It should check in a certain interval during a certain period defined in the 5 | settings by TINYLINK_CHECK_INTERVAL and TINYLINK_CHECK_PERIOD. 6 | After one period, all URLs should be checked for their availability. 7 | 8 | """ 9 | from django.conf import settings 10 | from django.core.management.base import BaseCommand 11 | from django.utils import timezone 12 | 13 | from ...models import Tinylink 14 | from ...utils import validate_long_url 15 | 16 | 17 | class Command(BaseCommand): 18 | """Class for the check_tinylink_targets admin command.""" 19 | def handle(self, *args, **options): 20 | """Handles the check_tinylink_targets admin command.""" 21 | interval = settings.TINYLINK_CHECK_INTERVAL 22 | period = settings.TINYLINK_CHECK_PERIOD 23 | url_amount = Tinylink.objects.all().count() 24 | check_amount = (url_amount / (period / interval)) or 1 25 | for link in Tinylink.objects.order_by('last_checked')[:check_amount]: 26 | validate_long_url(link) 27 | print('[' + timezone.now().strftime('%d.%m.%Y - %H:%M') + 28 | '] Checked ' + str(check_amount) + ' of ' + str(url_amount) + 29 | ' total URLs.') 30 | -------------------------------------------------------------------------------- /tinylinks/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.6 on 2016-05-06 12:26 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='Tinylink', 21 | fields=[ 22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('last_checked', models.DateTimeField(auto_created=True, verbose_name='Last validation')), 24 | ('long_url', models.CharField(max_length=2500, verbose_name='Long URL')), 25 | ('short_url', models.CharField(max_length=32, unique=True, verbose_name='Short URL')), 26 | ('is_broken', models.BooleanField(default=False, verbose_name='Status')), 27 | ('validation_error', models.CharField(default=b'', max_length=100, verbose_name='Validation Error')), 28 | ('amount_of_views', models.PositiveIntegerField(default=0, verbose_name='Amount of views')), 29 | ('redirect_location', models.CharField(default=b'', max_length=2500, verbose_name='Redirect location')), 30 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tinylinks', to=settings.AUTH_USER_MODEL, verbose_name='Author')), 31 | ], 32 | options={ 33 | 'ordering': ['-id'], 34 | }, 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /tinylinks/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-tinylinks/83fed967197571b18e883473f7661eb36ec2b4ef/tinylinks/migrations/__init__.py -------------------------------------------------------------------------------- /tinylinks/models.py: -------------------------------------------------------------------------------- 1 | """Models for the ``django-tinylinks`` app.""" 2 | from django.db import models 3 | from django.conf import settings 4 | from django.utils.encoding import python_2_unicode_compatible 5 | from django.utils.translation import ugettext_lazy as _ 6 | from django.utils.timezone import now, timedelta 7 | 8 | 9 | @python_2_unicode_compatible 10 | class Tinylink(models.Model): 11 | """ 12 | Model to 'translate' long URLs into small ones. 13 | 14 | :user: The author of the tinylink. 15 | :long_url: Long URL version. 16 | :short_url: Shortened URL. 17 | :is_broken: Set if the given long URL couldn't be validated. 18 | :validation_error: Description of the occurred error. 19 | :last_checked: Datetime of the last validation process. 20 | :amount_of_views: Field to count the redirect views. 21 | :redirect_location: Redirect location if the long_url is redirected. 22 | 23 | """ 24 | user = models.ForeignKey( 25 | getattr(settings, 'AUTH_USER_MODEL', 'auth.User'), 26 | verbose_name=_('Author'), 27 | related_name="tinylinks", 28 | ) 29 | 30 | long_url = models.CharField( 31 | max_length=2500, 32 | verbose_name=_('Long URL'), 33 | ) 34 | 35 | short_url = models.CharField( 36 | max_length=32, 37 | verbose_name=_('Short URL'), 38 | unique=True, 39 | ) 40 | 41 | is_broken = models.BooleanField( 42 | default=False, 43 | verbose_name=_('Status'), 44 | ) 45 | 46 | validation_error = models.CharField( 47 | max_length=100, 48 | verbose_name=_('Validation Error'), 49 | default='', 50 | ) 51 | 52 | last_checked = models.DateTimeField( 53 | auto_now_add=True, 54 | verbose_name=_('Last validation'), 55 | ) 56 | 57 | amount_of_views = models.PositiveIntegerField( 58 | default=0, 59 | verbose_name=_('Amount of views'), 60 | ) 61 | 62 | redirect_location = models.CharField( 63 | max_length=2500, 64 | verbose_name=_('Redirect location'), 65 | default='', 66 | ) 67 | 68 | def __str__(self): 69 | return self.short_url 70 | 71 | class Meta: 72 | ordering = ['-pk'] 73 | 74 | def can_be_validated(self): 75 | """ 76 | URL can only be validated if the last validation was at least 1 77 | hour ago 78 | 79 | """ 80 | if self.last_checked < now() - timedelta(minutes=60): 81 | return True 82 | return False 83 | -------------------------------------------------------------------------------- /tinylinks/south_migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | # -*- coding: utf-8 -*- 3 | import datetime 4 | from south.db import db 5 | from south.v2 import SchemaMigration 6 | from django.db import models 7 | 8 | 9 | class Migration(SchemaMigration): 10 | 11 | def forwards(self, orm): 12 | # Adding model 'Tinylink' 13 | db.create_table('tinylinks_tinylink', ( 14 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 15 | ('long_url', self.gf('django.db.models.fields.CharField')(max_length=2500)), 16 | ('short_url', self.gf('django.db.models.fields.CharField')(unique=True, max_length=32)), 17 | )) 18 | db.send_create_signal('tinylinks', ['Tinylink']) 19 | 20 | 21 | def backwards(self, orm): 22 | # Deleting model 'Tinylink' 23 | db.delete_table('tinylinks_tinylink') 24 | 25 | 26 | models = { 27 | 'tinylinks.tinylink': { 28 | 'Meta': {'object_name': 'Tinylink'}, 29 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 30 | 'long_url': ('django.db.models.fields.CharField', [], {'max_length': '2500'}), 31 | 'short_url': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}) 32 | } 33 | } 34 | 35 | complete_apps = ['tinylinks'] -------------------------------------------------------------------------------- /tinylinks/south_migrations/0002_auto__add_field_tinylink_user.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | # -*- coding: utf-8 -*- 3 | import datetime 4 | from south.db import db 5 | from south.v2 import SchemaMigration 6 | from django.db import models 7 | 8 | 9 | # Safe User import for Django < 1.5 10 | try: 11 | from django.contrib.auth import get_user_model 12 | except ImportError: 13 | from django.contrib.auth.models import User 14 | else: 15 | User = get_user_model() 16 | 17 | 18 | # With the default User model these will be 'auth.User' and 'auth.user' 19 | # so instead of using orm['auth.User'] we can use orm[user_orm_label] 20 | user_orm_label = '%s.%s' % (User._meta.app_label, User._meta.object_name) 21 | user_model_label = '%s.%s' % (User._meta.app_label, User._meta.module_name) 22 | 23 | class Migration(SchemaMigration): 24 | 25 | def forwards(self, orm): 26 | # Adding field 'Tinylink.user' 27 | db.add_column('tinylinks_tinylink', 'user', 28 | self.gf('django.db.models.fields.related.ForeignKey')(default=1, to=orm[user_orm_label]), 29 | keep_default=False) 30 | 31 | 32 | def backwards(self, orm): 33 | # Deleting field 'Tinylink.user' 34 | db.delete_column('tinylinks_tinylink', 'user_id') 35 | 36 | 37 | models = { 38 | 'auth.group': { 39 | 'Meta': {'object_name': 'Group'}, 40 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 41 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 42 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 43 | }, 44 | 'auth.permission': { 45 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 46 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 47 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 48 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 49 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 50 | }, 51 | user_model_label: { 52 | 'Meta': { 53 | 'object_name': User.__name__, 54 | 'db_table': "'%s'" % User._meta.db_table 55 | }, 56 | User._meta.pk.attname: ( 57 | 'django.db.models.fields.AutoField', [], 58 | {'primary_key': 'True', 59 | 'db_column': "'%s'" % User._meta.pk.column} 60 | ), 61 | }, 62 | 'contenttypes.contenttype': { 63 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 64 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 65 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 66 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 67 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 68 | }, 69 | 'tinylinks.tinylink': { 70 | 'Meta': {'object_name': 'Tinylink'}, 71 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 72 | 'long_url': ('django.db.models.fields.CharField', [], {'max_length': '2500'}), 73 | 'short_url': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}), 74 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s']" % user_orm_label}) 75 | } 76 | } 77 | 78 | complete_apps = ['tinylinks'] 79 | -------------------------------------------------------------------------------- /tinylinks/south_migrations/0003_auto__add_field_tinylink_is_broken__add_field_tinylink_last_checked.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | # -*- coding: utf-8 -*- 3 | import datetime 4 | from south.db import db 5 | from south.v2 import SchemaMigration 6 | from django.db import models 7 | 8 | 9 | # Safe User import for Django < 1.5 10 | try: 11 | from django.contrib.auth import get_user_model 12 | except ImportError: 13 | from django.contrib.auth.models import User 14 | else: 15 | User = get_user_model() 16 | 17 | 18 | # With the default User model these will be 'auth.User' and 'auth.user' 19 | # so instead of using orm['auth.User'] we can use orm[user_orm_label] 20 | user_orm_label = '%s.%s' % (User._meta.app_label, User._meta.object_name) 21 | user_model_label = '%s.%s' % (User._meta.app_label, User._meta.module_name) 22 | 23 | 24 | class Migration(SchemaMigration): 25 | 26 | def forwards(self, orm): 27 | # Adding field 'Tinylink.is_broken' 28 | db.add_column('tinylinks_tinylink', 'is_broken', 29 | self.gf('django.db.models.fields.BooleanField')(default=False), 30 | keep_default=False) 31 | 32 | # Adding field 'Tinylink.last_checked' 33 | db.add_column('tinylinks_tinylink', 'last_checked', 34 | self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime(2012, 11, 13, 0, 0)), 35 | keep_default=False) 36 | 37 | 38 | def backwards(self, orm): 39 | # Deleting field 'Tinylink.is_broken' 40 | db.delete_column('tinylinks_tinylink', 'is_broken') 41 | 42 | # Deleting field 'Tinylink.last_checked' 43 | db.delete_column('tinylinks_tinylink', 'last_checked') 44 | 45 | 46 | models = { 47 | 'auth.group': { 48 | 'Meta': {'object_name': 'Group'}, 49 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 50 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 51 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 52 | }, 53 | 'auth.permission': { 54 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 55 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 56 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 57 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 58 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 59 | }, 60 | user_model_label: { 61 | 'Meta': { 62 | 'object_name': User.__name__, 63 | 'db_table': "'%s'" % User._meta.db_table 64 | }, 65 | User._meta.pk.attname: ( 66 | 'django.db.models.fields.AutoField', [], 67 | {'primary_key': 'True', 68 | 'db_column': "'%s'" % User._meta.pk.column} 69 | ), 70 | }, 71 | 'contenttypes.contenttype': { 72 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 73 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 74 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 75 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 76 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 77 | }, 78 | 'tinylinks.tinylink': { 79 | 'Meta': {'object_name': 'Tinylink'}, 80 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 81 | 'is_broken': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 82 | 'last_checked': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 11, 13, 0, 0)'}), 83 | 'long_url': ('django.db.models.fields.CharField', [], {'max_length': '2500'}), 84 | 'short_url': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}), 85 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tinylinks'", 'to': "orm['%s']" % user_orm_label}) 86 | } 87 | } 88 | 89 | complete_apps = ['tinylinks'] 90 | -------------------------------------------------------------------------------- /tinylinks/south_migrations/0004_auto__add_field_tinylink_amount_of_views.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | # -*- coding: utf-8 -*- 3 | import datetime 4 | from south.db import db 5 | from south.v2 import SchemaMigration 6 | from django.db import models 7 | 8 | 9 | # Safe User import for Django < 1.5 10 | try: 11 | from django.contrib.auth import get_user_model 12 | except ImportError: 13 | from django.contrib.auth.models import User 14 | else: 15 | User = get_user_model() 16 | 17 | 18 | # With the default User model these will be 'auth.User' and 'auth.user' 19 | # so instead of using orm['auth.User'] we can use orm[user_orm_label] 20 | user_orm_label = '%s.%s' % (User._meta.app_label, User._meta.object_name) 21 | user_model_label = '%s.%s' % (User._meta.app_label, User._meta.module_name) 22 | 23 | 24 | class Migration(SchemaMigration): 25 | 26 | def forwards(self, orm): 27 | # Adding field 'Tinylink.amount_of_views' 28 | db.add_column('tinylinks_tinylink', 'amount_of_views', 29 | self.gf('django.db.models.fields.PositiveIntegerField')(default=0), 30 | keep_default=False) 31 | 32 | 33 | def backwards(self, orm): 34 | # Deleting field 'Tinylink.amount_of_views' 35 | db.delete_column('tinylinks_tinylink', 'amount_of_views') 36 | 37 | 38 | models = { 39 | 'auth.group': { 40 | 'Meta': {'object_name': 'Group'}, 41 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 42 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 43 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 44 | }, 45 | 'auth.permission': { 46 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 47 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 48 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 49 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 50 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 51 | }, 52 | user_model_label: { 53 | 'Meta': { 54 | 'object_name': User.__name__, 55 | 'db_table': "'%s'" % User._meta.db_table 56 | }, 57 | User._meta.pk.attname: ( 58 | 'django.db.models.fields.AutoField', [], 59 | {'primary_key': 'True', 60 | 'db_column': "'%s'" % User._meta.pk.column} 61 | ), 62 | }, 63 | 'contenttypes.contenttype': { 64 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 65 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 66 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 67 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 68 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 69 | }, 70 | 'tinylinks.tinylink': { 71 | 'Meta': {'object_name': 'Tinylink'}, 72 | 'amount_of_views': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), 73 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 74 | 'is_broken': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 75 | 'last_checked': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 11, 13, 0, 0)'}), 76 | 'long_url': ('django.db.models.fields.CharField', [], {'max_length': '2500'}), 77 | 'short_url': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}), 78 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tinylinks'", 'to': "orm['%s']" % user_orm_label}) 79 | } 80 | } 81 | 82 | complete_apps = ['tinylinks'] 83 | -------------------------------------------------------------------------------- /tinylinks/south_migrations/0005_auto__add_field_tinylink_redirect_location.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | # -*- coding: utf-8 -*- 3 | import datetime 4 | from south.db import db 5 | from south.v2 import SchemaMigration 6 | from django.db import models 7 | 8 | 9 | # Safe User import for Django < 1.5 10 | try: 11 | from django.contrib.auth import get_user_model 12 | except ImportError: 13 | from django.contrib.auth.models import User 14 | else: 15 | User = get_user_model() 16 | 17 | 18 | # With the default User model these will be 'auth.User' and 'auth.user' 19 | # so instead of using orm['auth.User'] we can use orm[user_orm_label] 20 | user_orm_label = '%s.%s' % (User._meta.app_label, User._meta.object_name) 21 | user_model_label = '%s.%s' % (User._meta.app_label, User._meta.module_name) 22 | 23 | 24 | class Migration(SchemaMigration): 25 | 26 | def forwards(self, orm): 27 | # Adding field 'Tinylink.redirect_location' 28 | db.add_column('tinylinks_tinylink', 'redirect_location', 29 | self.gf('django.db.models.fields.CharField')(default='', max_length=2500), 30 | keep_default=False) 31 | 32 | 33 | def backwards(self, orm): 34 | # Deleting field 'Tinylink.redirect_location' 35 | db.delete_column('tinylinks_tinylink', 'redirect_location') 36 | 37 | 38 | models = { 39 | 'auth.group': { 40 | 'Meta': {'object_name': 'Group'}, 41 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 42 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 43 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 44 | }, 45 | 'auth.permission': { 46 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 47 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 48 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 49 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 50 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 51 | }, 52 | user_model_label: { 53 | 'Meta': { 54 | 'object_name': User.__name__, 55 | 'db_table': "'%s'" % User._meta.db_table 56 | }, 57 | User._meta.pk.attname: ( 58 | 'django.db.models.fields.AutoField', [], 59 | {'primary_key': 'True', 60 | 'db_column': "'%s'" % User._meta.pk.column} 61 | ), 62 | }, 63 | 'contenttypes.contenttype': { 64 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 65 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 66 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 67 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 68 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 69 | }, 70 | 'tinylinks.tinylink': { 71 | 'Meta': {'ordering': "['-id']", 'object_name': 'Tinylink'}, 72 | 'amount_of_views': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), 73 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 74 | 'is_broken': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 75 | 'last_checked': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 11, 29, 0, 0)'}), 76 | 'long_url': ('django.db.models.fields.CharField', [], {'max_length': '2500'}), 77 | 'redirect_location': ('django.db.models.fields.CharField', [], {'default': 'False', 'max_length': '2500'}), 78 | 'short_url': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}), 79 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tinylinks'", 'to': "orm['%s']" % user_orm_label}) 80 | } 81 | } 82 | 83 | complete_apps = ['tinylinks'] 84 | -------------------------------------------------------------------------------- /tinylinks/south_migrations/0006_auto__add_field_tinylink_validation_error.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | # -*- coding: utf-8 -*- 3 | import datetime 4 | from south.db import db 5 | from south.v2 import SchemaMigration 6 | from django.db import models 7 | 8 | 9 | # Safe User import for Django < 1.5 10 | try: 11 | from django.contrib.auth import get_user_model 12 | except ImportError: 13 | from django.contrib.auth.models import User 14 | else: 15 | User = get_user_model() 16 | 17 | 18 | # With the default User model these will be 'auth.User' and 'auth.user' 19 | # so instead of using orm['auth.User'] we can use orm[user_orm_label] 20 | user_orm_label = '%s.%s' % (User._meta.app_label, User._meta.object_name) 21 | user_model_label = '%s.%s' % (User._meta.app_label, User._meta.module_name) 22 | 23 | 24 | class Migration(SchemaMigration): 25 | 26 | def forwards(self, orm): 27 | # Adding field 'Tinylink.validation_error' 28 | db.add_column('tinylinks_tinylink', 'validation_error', 29 | self.gf('django.db.models.fields.CharField')(default='', max_length=100), 30 | keep_default=False) 31 | 32 | 33 | def backwards(self, orm): 34 | # Deleting field 'Tinylink.validation_error' 35 | db.delete_column('tinylinks_tinylink', 'validation_error') 36 | 37 | 38 | models = { 39 | 'auth.group': { 40 | 'Meta': {'object_name': 'Group'}, 41 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 42 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 43 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 44 | }, 45 | 'auth.permission': { 46 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 47 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 48 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 49 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 50 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 51 | }, 52 | user_model_label: { 53 | 'Meta': { 54 | 'object_name': User.__name__, 55 | 'db_table': "'%s'" % User._meta.db_table 56 | }, 57 | User._meta.pk.attname: ( 58 | 'django.db.models.fields.AutoField', [], 59 | {'primary_key': 'True', 60 | 'db_column': "'%s'" % User._meta.pk.column} 61 | ), 62 | }, 63 | 'contenttypes.contenttype': { 64 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 65 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 66 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 67 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 68 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 69 | }, 70 | 'tinylinks.tinylink': { 71 | 'Meta': {'ordering': "['-id']", 'object_name': 'Tinylink'}, 72 | 'amount_of_views': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), 73 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 74 | 'is_broken': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 75 | 'last_checked': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 11, 29, 0, 0)'}), 76 | 'long_url': ('django.db.models.fields.CharField', [], {'max_length': '2500'}), 77 | 'redirect_location': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '2500'}), 78 | 'short_url': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}), 79 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tinylinks'", 'to': "orm['%s']" % user_orm_label}), 80 | 'validation_error': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100'}) 81 | } 82 | } 83 | 84 | complete_apps = ['tinylinks'] 85 | -------------------------------------------------------------------------------- /tinylinks/south_migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-tinylinks/83fed967197571b18e883473f7661eb36ec2b4ef/tinylinks/south_migrations/__init__.py -------------------------------------------------------------------------------- /tinylinks/templates/tinylinks/notfound.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% block main %} 5 |
{% trans "We are sorry, but your link does not exist or is already gone." %}
7 | {% endblock %} -------------------------------------------------------------------------------- /tinylinks/templates/tinylinks/statistics.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n libs_tags %} 3 | 4 | {% block main %} 5 |{{ object_list.0|get_verbose:"user" }} | 11 |{{ object_list.0|get_verbose:"long_url" }} | 12 |{{ object_list.0|get_verbose:"short_url" }} | 13 |{{ object_list.0|get_verbose:"is_broken" }} | 14 |{{ object_list.0|get_verbose:"last_checked" }} | 15 |{{ object_list.0|get_verbose:"amount_of_views" }} | 16 |
{{ link.user }} | 22 |{{ link.long_url }} | 23 |{{ link.short_url }} | 24 |{% if link.is_broken %}{% trans "Invalid" %}{% else %}{% trans "Valid" %}{% endif %} | 25 |{{ link.last_checked }} | 26 |{{ link.amount_of_views }} | 27 |
{% trans "No tinylinks added yet." %}
33 | {% endif %} 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /tinylinks/templates/tinylinks/tinylink_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% block main %} 5 |{% trans "No tinylinks added yet." %}
45 | {% endif %} 46 | {% trans "Create your Tinylink" %} 47 | {% endblock %} 48 | -------------------------------------------------------------------------------- /tinylinks/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-tinylinks/83fed967197571b18e883473f7661eb36ec2b4ef/tinylinks/tests/__init__.py -------------------------------------------------------------------------------- /tinylinks/tests/check_tinylink_targets_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for the ``check_tinylink_targets`` admin command.""" 2 | from django.core import management 3 | from django.core.urlresolvers import reverse 4 | from django.test import TestCase, LiveServerTestCase 5 | 6 | from mixer.backend.django import mixer 7 | from mock import patch 8 | from requests import Response 9 | 10 | from ..models import Tinylink 11 | 12 | 13 | class CommandTestCase(TestCase, LiveServerTestCase): 14 | """Test class for the ``check_tinylink_targets`` admin command.""" 15 | longMessage = True 16 | 17 | def setUp(self): 18 | """Prepares the testing environment.""" 19 | # database setup 20 | self.tinylink1 = mixer.blend( 21 | 'tinylinks.TinyLink', short_url="vB7f5b", 22 | long_url='{}{}'.format(self.live_server_url, reverse('test_view'))) 23 | self.tinylink2 = mixer.blend( 24 | 'tinylinks.TinyLink', 25 | long_url='http://foobar.foobar', 26 | short_url="cf7GDS", 27 | ) 28 | 29 | @patch('requests.get') 30 | def test_command(self, mock): 31 | resp = Response() 32 | resp.status_code = 200 33 | mock.return_value = resp 34 | 35 | management.call_command('check_tinylink_targets') 36 | # Run twice, because just one link is checked per interval 37 | management.call_command('check_tinylink_targets') 38 | self.assertFalse( 39 | Tinylink.objects.get(pk=self.tinylink1.id).is_broken, 40 | msg=('Should not be broken.'), 41 | ) 42 | self.assertFalse( 43 | Tinylink.objects.get(pk=self.tinylink2.id).is_broken, 44 | msg=('Should not be broken.'), 45 | ) 46 | -------------------------------------------------------------------------------- /tinylinks/tests/forms_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for the forms of the ``django-tinylinks`` app.""" 2 | from django.test import TestCase 3 | 4 | from mixer.backend.django import mixer 5 | 6 | from ..forms import TinylinkForm, TinylinkAdminForm 7 | from ..models import Tinylink 8 | 9 | 10 | class TinylinkFormTestCase(TestCase): 11 | """Test for the ``TinylinkForm`` form class.""" 12 | def test_validates_saves(self): 13 | # Testing if the new link is saved. 14 | user = mixer.blend('auth.User') 15 | data = {'long_url': 'http://www.example.com/FooBar'} 16 | 17 | form = TinylinkForm(data=data, user=user) 18 | 19 | self.assertTrue(form.is_valid(), msg=( 20 | 'If given correct data, the form should be valid.')) 21 | form.save() 22 | self.assertEqual(Tinylink.objects.all().count(), 1, msg=( 23 | 'When save is called, there should be one link in the database.' 24 | ' Got {0}'.format(Tinylink.objects.all().count()))) 25 | 26 | # Testing a 'Twin' submit, if the old inputs matches the new ones. 27 | tinylink = Tinylink.objects.get(pk=1) 28 | data.update({'short_url': tinylink.short_url}) 29 | form = TinylinkForm(data=data, user=user) 30 | self.assertFalse(form.is_valid(), msg=( 31 | 'If the short url is already used, the form should be invalid.')) 32 | 33 | # Testing an input with a new short URL. Now, there are two tinylinks 34 | # with the same long_url. 35 | data.update({'short_url': 'FooBar01'}) 36 | form = TinylinkForm(data=data, user=user) 37 | self.assertTrue(form.is_valid(), msg=( 38 | 'If given correct data, the form should be valid.')) 39 | form.save() 40 | self.assertEqual(Tinylink.objects.all().count(), 2, msg=( 41 | 'When saving with a new short url, there should be one new' 42 | ' tinylink in the database. Got {0}'.format( 43 | Tinylink.objects.all().count()))) 44 | 45 | # Testing the input of an old long URL. There's no new submission, 46 | # only a redirect to the old entity, where this one can be changed. 47 | form = TinylinkForm(data={'long_url': data['long_url']}, user=user) 48 | self.assertTrue(form.is_valid(), msg=( 49 | 'If given correct data, the form should be valid.')) 50 | form.save() 51 | self.assertEqual(Tinylink.objects.all().count(), 2, msg=( 52 | 'When saving with no short url and the same long url, there is no' 53 | ' savement. The user is directed to the already existing tinylink.' 54 | ' Got {0}'.format(Tinylink.objects.all().count()))) 55 | 56 | # Testing the changing of a long url 57 | data = { 58 | 'long_url': 'http://www.example.com/', 59 | 'short_url': tinylink.short_url, 60 | } 61 | form = TinylinkForm(instance=tinylink, data=data, user=user, 62 | mode="change-long") 63 | self.assertTrue(form.is_valid(), msg=( 64 | 'If given correct data, the form should be valid.')) 65 | # If the short_url is owned by another user, throw an error. 66 | new_user = mixer.blend('auth.User') 67 | tinylink.user = new_user 68 | tinylink.save() 69 | data = { 70 | 'long_url': 'http://www.example.com/', 71 | 'short_url': tinylink.short_url, 72 | } 73 | form = TinylinkForm(instance=tinylink, data=data, user=user, 74 | mode="change-long") 75 | self.assertTrue(form.is_valid(), msg=( 76 | 'If the short URL is still unique, the form should be valid.')) 77 | form.save() 78 | self.assertEqual( 79 | Tinylink.objects.filter(short_url=tinylink.short_url).count(), 1) 80 | 81 | 82 | class TinylinkAdminFormTestCase(TestCase): 83 | """Test for the ``TinylinkAdminForm`` form class.""" 84 | def test_validates_saves(self): 85 | # Testing if the new link is saved. 86 | user = mixer.blend('auth.User') 87 | data = { 88 | 'long_url': 'http://www.example.com/FooBar', 89 | 'user': user.pk, 90 | } 91 | 92 | form = TinylinkAdminForm(data=data) 93 | 94 | self.assertTrue(form.is_valid(), msg=( 95 | 'If given correct data, the form should be valid.')) 96 | form.save() 97 | self.assertEqual(Tinylink.objects.all().count(), 1, msg=( 98 | 'When save is called, there should be one link in the database.' 99 | ' Got {0}'.format(Tinylink.objects.all().count()))) 100 | 101 | # Testing a 'Twin' submit, if the old inputs matches the new ones. 102 | tinylink = Tinylink.objects.get(pk=1) 103 | data.update({'short_url': tinylink.short_url}) 104 | form = TinylinkAdminForm(data=data) 105 | self.assertFalse(form.is_valid(), msg=( 106 | 'If the short url is already used, the form should be invalid.')) 107 | 108 | # Testing a fake 'Twin' submit, if the old inputs matches the new ones 109 | # and the object is equal the instance. 110 | data.update({'short_url': tinylink.short_url}) 111 | form = TinylinkAdminForm(data=data, instance=tinylink) 112 | self.assertTrue(form.is_valid(), msg=( 113 | 'If the instance is equal the object, the form should be valid.')) 114 | -------------------------------------------------------------------------------- /tinylinks/tests/models_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for the models of the ``django-tinylinks`` app.""" 2 | from django.test import TestCase, LiveServerTestCase 3 | from django.utils.timezone import now, timedelta 4 | 5 | from mixer.backend.django import mixer 6 | 7 | 8 | class TinylinkTestCase(TestCase, LiveServerTestCase): 9 | """Tests for the ``Tinylink`` model class.""" 10 | def setUp(self): 11 | self.link = mixer.blend( 12 | 'tinylinks.TinyLink', short_url="vB7f5b", 13 | long_url="http://www.example.com/thisisalongURL") 14 | 15 | def test_model(self): 16 | self.assertTrue(str(self.link)) 17 | 18 | def test_can_be_validated(self): 19 | self.assertFalse(self.link.can_be_validated()) 20 | self.link.last_checked = now() - timedelta(minutes=61) 21 | self.assertTrue(self.link.can_be_validated()) 22 | -------------------------------------------------------------------------------- /tinylinks/tests/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | These settings are used by the ``manage.py`` command. 3 | With normal tests we want to use the fastest possible way which is an 4 | in-memory sqlite database but if you want to create South migrations you 5 | need a persistant database. 6 | Unfortunately there seems to be an issue with either South or syncdb so that 7 | defining two routers ("default" and "south") does not work. 8 | """ 9 | from distutils.version import StrictVersion 10 | 11 | import django 12 | 13 | from .test_settings import * # NOQA 14 | 15 | 16 | DATABASES = { 17 | 'default': { 18 | 'ENGINE': 'django.db.backends.sqlite3', 19 | 'NAME': 'db.sqlite', 20 | } 21 | } 22 | 23 | django_version = django.get_version() 24 | if StrictVersion(django_version) < StrictVersion('1.7'): 25 | INSTALLED_APPS.append('south', ) # NOQA 26 | -------------------------------------------------------------------------------- /tinylinks/tests/test_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-tinylinks/83fed967197571b18e883473f7661eb36ec2b4ef/tinylinks/tests/test_app/__init__.py -------------------------------------------------------------------------------- /tinylinks/tests/test_app/fixtures/auth.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 2, 4 | "model": "auth.user", 5 | "fields": { 6 | "username": "user", 7 | "first_name": "", 8 | "last_name": "", 9 | "is_active": true, 10 | "is_superuser": false, 11 | "is_staff": false, 12 | "last_login": "2012-03-15T15:49:28Z", 13 | "groups": [], 14 | "user_permissions": [ 15 | [ 16 | "add_tinylink", 17 | "tinylinks", 18 | "tinylink" 19 | ], 20 | [ 21 | "delete_tinylink", 22 | "tinylinks", 23 | "tinylink" 24 | ] 25 | ], 26 | "password": "md5$lYSPo6olvDci$940e9654a775af45212e9b02dc71c4d9", 27 | "email": "", 28 | "date_joined": "2012-03-15T14:26:21Z" 29 | } 30 | }, 31 | { 32 | "pk": 3, 33 | "model": "auth.user", 34 | "fields": { 35 | "username": "staff", 36 | "first_name": "", 37 | "last_name": "", 38 | "is_active": true, 39 | "is_superuser": false, 40 | "is_staff": true, 41 | "last_login": "2012-03-15T15:49:28Z", 42 | "groups": [], 43 | "user_permissions": [ 44 | [ 45 | "add_tinylink", 46 | "tinylinks", 47 | "tinylink" 48 | ], 49 | [ 50 | "delete_tinylink", 51 | "tinylinks", 52 | "tinylink" 53 | ] 54 | ], 55 | "password": "md5$lYSPo6olvDci$940e9654a775af45212e9b02dc71c4d9", 56 | "email": "", 57 | "date_joined": "2012-03-15T14:26:21Z" 58 | } 59 | } 60 | ] -------------------------------------------------------------------------------- /tinylinks/tests/test_app/fixtures/sites.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "sites.site", 5 | "fields": { 6 | "domain": "localhost:8000", 7 | "name": "localhost:8000" 8 | } 9 | } 10 | ] -------------------------------------------------------------------------------- /tinylinks/tests/test_app/fixtures/tinylinks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 7, 4 | "model": "tinylinks.tinylink", 5 | "fields": { 6 | "amount_of_views": 0, 7 | "short_url": "djangolibs", 8 | "long_url": "https://github.com/bitmazk/django-libs/tree/master/django_libs", 9 | "user": [ 10 | "user" 11 | ], 12 | "last_checked": "2012-11-13T18:31:14.912Z", 13 | "is_broken": false 14 | } 15 | }, 16 | { 17 | "pk": 8, 18 | "model": "tinylinks.tinylink", 19 | "fields": { 20 | "amount_of_views": 1, 21 | "short_url": "jd7nh4", 22 | "long_url": "http://www.google.de/", 23 | "user": [ 24 | "user" 25 | ], 26 | "last_checked": "2012-11-13T18:31:29.000Z", 27 | "is_broken": false 28 | } 29 | }, 30 | { 31 | "pk": 9, 32 | "model": "tinylinks.tinylink", 33 | "fields": { 34 | "amount_of_views": 5, 35 | "short_url": "github", 36 | "long_url": "https://www.github.com/", 37 | "user": [ 38 | "staff" 39 | ], 40 | "last_checked": "2012-11-15T18:31:29.000Z", 41 | "is_broken": false 42 | } 43 | }, 44 | { 45 | "pk": 10, 46 | "model": "tinylinks.tinylink", 47 | "fields": { 48 | "amount_of_views": 3, 49 | "short_url": "pearljam", 50 | "long_url": "http://de.wikipedia.org/wiki/Pearl_Jam", 51 | "user": [ 52 | "staff" 53 | ], 54 | "last_checked": "2012-11-16T18:31:29.000Z", 55 | "is_broken": false 56 | } 57 | } 58 | ] -------------------------------------------------------------------------------- /tinylinks/tests/test_app/models.py: -------------------------------------------------------------------------------- 1 | """Dummy model needed for tests.""" 2 | pass 3 | -------------------------------------------------------------------------------- /tinylinks/tests/test_app/templates/404.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-tinylinks/83fed967197571b18e883473f7661eb36ec2b4ef/tinylinks/tests/test_app/templates/404.html -------------------------------------------------------------------------------- /tinylinks/tests/test_app/templates/500.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-tinylinks/83fed967197571b18e883473f7661eb36ec2b4ef/tinylinks/tests/test_app/templates/500.html -------------------------------------------------------------------------------- /tinylinks/tests/test_app/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |