Commit 45be3a4f authored by David Haynes's avatar David Haynes 🙆

Merge branch '176-business-logic' into 'go-three'

Resolve "Move as much business logic into models.py"

See merge request !121
parents b63d7963 2759aac4
Pipeline #2519 passed with stage
in 1 minute and 26 seconds
...@@ -32,11 +32,11 @@ ...@@ -32,11 +32,11 @@
}, },
"django": { "django": {
"hashes": [ "hashes": [
"sha256:26b34f4417aa38d895b6b5307177b51bc3f4d53179d8696a5c19dcb50582523c", "sha256:3eb25c99df1523446ec2dc1b00e25eb2ecbdf42c9d8b0b8b32a204a8db9011f8",
"sha256:71d1a584bb4ad2b4f933d07d02c716755c1394feaac1ce61ce37843ac5401092" "sha256:69ff89fa3c3a8337015478a1a0744f52a9fef5d12c1efa01a01f99bcce9bf10c"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.0.5" "version": "==2.0.6"
}, },
"django-cas-client": { "django-cas-client": {
"hashes": [ "hashes": [
...@@ -127,10 +127,10 @@ ...@@ -127,10 +127,10 @@
"develop": { "develop": {
"astroid": { "astroid": {
"hashes": [ "hashes": [
"sha256:032f6e09161e96f417ea7fad46d3fac7a9019c775f202182c22df0e4f714cb1c", "sha256:0ef2bf9f07c3150929b25e8e61b5198c27b0dca195e156f0e4d5bdd89185ca1a",
"sha256:dea42ae6e0b789b543f728ddae7ddb6740ba33a49fb52c4a4d9cb7bb4aa6ec09" "sha256:fc9b582dba0366e63540982c3944a9230cbc6f303641c51483fa547dcc22393a"
], ],
"version": "==1.6.4" "version": "==1.6.5"
}, },
"coverage": { "coverage": {
"hashes": [ "hashes": [
...@@ -225,11 +225,11 @@ ...@@ -225,11 +225,11 @@
}, },
"pylint": { "pylint": {
"hashes": [ "hashes": [
"sha256:aa519865f8890a5905fa34924fed0f3bfc7d84fc9f9142c16dac52ffecd25a39", "sha256:a48070545c12430cfc4e865bf62f5ad367784765681b3db442d8230f0960aa3c",
"sha256:c353d8225195b37cc3aef18248b8f3fe94c5a6a95affaf885ae21a24ca31d8eb" "sha256:fff220bcb996b4f7e2b0f6812fd81507b72ca4d8c4d05daf2655c333800cb9b3"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.9.1" "version": "==1.9.2"
}, },
"pylint-django": { "pylint-django": {
"hashes": [ "hashes": [
......
...@@ -10,9 +10,9 @@ from django.contrib.auth.models import User ...@@ -10,9 +10,9 @@ from django.contrib.auth.models import User
# Other Imports # Other Imports
import requests import requests
def pfparse(pf_name_result): def pfparse(pf_name_result: str) -> list:
""" """
Parse what peoplefinder sends back to us and make a list out of it Parse what peoplefinder sends back to us and make a list out of it.
""" """
# name comes in format of Anderson, Nicholas J # name comes in format of Anderson, Nicholas J
name_list = pf_name_result.split(',') name_list = pf_name_result.split(',')
...@@ -29,12 +29,11 @@ def pfparse(pf_name_result): ...@@ -29,12 +29,11 @@ def pfparse(pf_name_result):
new_name_list = [first_name, name_list[0]] new_name_list = [first_name, name_list[0]]
return new_name_list return new_name_list
def pfinfo(uname): def pfinfo(uname: str) -> list:
""" """
Get information from peoplefinder Get information from peoplefinder.
""" """
base_url = settings.PF_URL url = f"{settings.PF_URL}basic/all/{uname}"
url = base_url + "basic/all/" + str(uname)
try: try:
metadata = requests.get(url, timeout=30) metadata = requests.get(url, timeout=30)
print("Retrieving information from the peoplefinder api.") print("Retrieving information from the peoplefinder api.")
...@@ -73,7 +72,7 @@ def pfinfo(uname): ...@@ -73,7 +72,7 @@ def pfinfo(uname):
print("Returning empty user info tuple.") print("Returning empty user info tuple.")
return ['', ''] return ['', '']
def create_user(tree): def create_user(tree: list):
""" """
Create a django user based off of the peoplefinder info we parsed earlier. Create a django user based off of the peoplefinder info we parsed earlier.
""" """
......
...@@ -3,34 +3,31 @@ go/forms.py ...@@ -3,34 +3,31 @@ go/forms.py
Configure the layout and styling of the Go's forms. Configure the layout and styling of the Go's forms.
""" """
# Python stdlib Imports
from datetime import datetime, timedelta
# Django Imports # Django Imports
from django.core.exceptions import ValidationError
from django.forms import (BooleanField, CharField, ChoiceField, DateTimeField, from django.forms import (BooleanField, CharField, ChoiceField, DateTimeField,
ModelForm, RadioSelect, SlugField, Textarea, ModelForm, RadioSelect, Textarea, TextInput,
TextInput, URLField, URLInput) URLField, URLInput)
from django.utils import timezone
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils import timezone
# App Imports # Third party imports
from .models import URL, RegisteredUser
# Other Imports
from crispy_forms.bootstrap import (Accordion, AccordionGroup, PrependedText, from crispy_forms.bootstrap import (Accordion, AccordionGroup, PrependedText,
StrictButton) StrictButton)
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from crispy_forms.layout import HTML, Div, Field, Fieldset, Layout from crispy_forms.layout import HTML, Div, Field, Fieldset, Layout
# App Imports
from .models import URL, RegisteredUser
from .validators import regex_short_validator, valid_date
class URLForm(ModelForm): class URLForm(ModelForm):
""" """
The form that is used in URL creation. The form that is used in URL creation.
Define custom fields and then render them onto the template. Define custom fields and then render them onto the template.
""" """
# destination ------------------------------------------------------------------ # destination -------------------------------------------------------------
destination = URLField( destination = URLField(
required=True, required=True,
label='Long URL (Required)', label='Long URL (Required)',
...@@ -41,25 +38,11 @@ class URLForm(ModelForm): ...@@ -41,25 +38,11 @@ class URLForm(ModelForm):
) )
# short ------------------------------------------------------------------- # short -------------------------------------------------------------------
def unique_short(value):
"""
Check to make sure the short url has not been used
"""
try:
# if we're able to get a URL with the same short url
URL.objects.get(short__iexact=value)
except URL.DoesNotExist as ex:
print(ex)
return
# then raise a ValidationError
raise ValidationError('Short url already exists.')
short = CharField( short = CharField(
required=False, required=False,
label='Short URL (Optional)', label='Short URL (Optional)',
widget=TextInput(), widget=TextInput(),
validators=[unique_short], validators=[regex_short_validator],
max_length=20, max_length=20,
min_length=1, min_length=1,
) )
...@@ -88,29 +71,15 @@ class URLForm(ModelForm): ...@@ -88,29 +71,15 @@ class URLForm(ModelForm):
widget=RadioSelect(), widget=RadioSelect(),
) )
def valid_date(value):
"""
Check if the selected date is a valid date
"""
# a valid date is one that is greater than today
if value > timezone.now():
return
# raise a ValidationError if the date is invalid
else:
raise ValidationError('Date must be after today.')
expires_custom = DateTimeField( expires_custom = DateTimeField(
required=False, required=False,
label='Custom Date', label='Custom Date',
input_formats=['%m-%d-%Y'], input_formats=['%m-%d-%Y'],
validators=[valid_date], validators=[valid_date],
initial=lambda: datetime.now() + timedelta(days=1) initial=lambda: timezone.now() + timezone.timedelta(days=1)
) )
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""
On initialization of the form, crispy forms renders this layout.
"""
# Grab that host info # Grab that host info
self.host = kwargs.pop('host', None) self.host = kwargs.pop('host', None)
super(URLForm, self).__init__(*args, **kwargs) super(URLForm, self).__init__(*args, **kwargs)
......
# Generated by Django 2.0.5 on 2018-06-08 20:58
from django.db import migrations, models
import django.utils.timezone
import go.validators
class Migration(migrations.Migration):
dependencies = [
('go', '0003_auto_20180524_0003'),
]
operations = [
migrations.AlterField(
model_name='url',
name='date_created',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Go Link Creation Date'),
),
migrations.AlterField(
model_name='url',
name='destination',
field=models.URLField(default='https://go.gmu.edu', help_text='The URL to be redirected to when visiting the shortlink.', max_length=1000),
),
migrations.AlterField(
model_name='url',
name='owner',
field=models.ForeignKey(on_delete='cascade', to='go.RegisteredUser', verbose_name='RegisteredUser Owner'),
),
migrations.AlterField(
model_name='url',
name='short',
field=models.CharField(help_text='The shortcode that acts as the unique go link.', max_length=20, unique=True, validators=[go.validators.unique_short_validator, go.validators.regex_short_validator]),
),
]
# Generated by Django 2.0.5 on 2018-06-09 00:25
import datetime
from django.db import migrations, models
from django.utils.timezone import utc
import go.validators
class Migration(migrations.Migration):
dependencies = [
('go', '0004_auto_20180608_2058'),
]
operations = [
migrations.AlterField(
model_name='registereduser',
name='approved',
field=models.BooleanField(default=False, verbose_name='Approval Status'),
),
migrations.AlterField(
model_name='registereduser',
name='blocked',
field=models.BooleanField(default=False, verbose_name='Blocked Status'),
),
migrations.AlterField(
model_name='registereduser',
name='description',
field=models.TextField(blank=True, default='', verbose_name='Signup Description'),
),
migrations.AlterField(
model_name='registereduser',
name='full_name',
field=models.CharField(default='', max_length=100, verbose_name='Full Name'),
),
migrations.AlterField(
model_name='registereduser',
name='organization',
field=models.CharField(default='', max_length=100, verbose_name='Organization'),
),
migrations.AlterField(
model_name='registereduser',
name='registered',
field=models.BooleanField(default=False, verbose_name='Registration Status'),
),
migrations.AlterField(
model_name='url',
name='date_created',
field=models.DateTimeField(default=datetime.datetime(2018, 6, 9, 0, 25, 38, 606587, tzinfo=utc), verbose_name='Go Link Creation Date'),
),
migrations.AlterField(
model_name='url',
name='date_expires',
field=models.DateTimeField(blank=True, null=True, verbose_name='Go Link Expiry Date'),
),
migrations.AlterField(
model_name='url',
name='destination',
field=models.URLField(default='https://go.gmu.edu', max_length=1000, verbose_name='Go Link Destination URL'),
),
migrations.AlterField(
model_name='url',
name='short',
field=models.CharField(max_length=20, unique=True, validators=[go.validators.unique_short_validator, go.validators.regex_short_validator], verbose_name='Go Shortcode'),
),
]
# Generated by Django 2.0.5 on 2018-06-09 00:25
import datetime
from django.db import migrations, models
from django.utils.timezone import utc
class Migration(migrations.Migration):
dependencies = [
('go', '0005_auto_20180609_0025'),
]
operations = [
migrations.AlterField(
model_name='url',
name='date_created',
field=models.DateTimeField(default=datetime.datetime(2018, 6, 9, 0, 25, 39, 319719, tzinfo=utc), verbose_name='Go Link Creation Date'),
),
]
# Generated by Django 2.0.5 on 2018-06-09 00:26
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('go', '0006_auto_20180609_0025'),
]
operations = [
migrations.AlterField(
model_name='url',
name='date_created',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Go Link Creation Date'),
),
]
...@@ -17,14 +17,12 @@ from django.utils import timezone ...@@ -17,14 +17,12 @@ from django.utils import timezone
# Other Imports # Other Imports
from hashids import Hashids from hashids import Hashids
from .validators import regex_short_validator, unique_short_validator
""" # Generate the salt and initialize Hashids
Generate the salt and initialize Hashids # Note: the Hashids library already implements several restrictions oncharacter
# placement, including repeating or incrementing numbers, or placing curse word
Note: the Hashids library already implements several restrictions on character # characters adjacent to one another.
placement, including repeating or incrementing numbers, or placing curse word
characters adjacent to one another.
"""
SIMILAR_CHARS = set(['b', 'G', '6', 'g', 'q', 'l', SIMILAR_CHARS = set(['b', 'G', '6', 'g', 'q', 'l',
'1', 'I', 'S', '5', 'O', '0']) '1', 'I', 'S', '5', 'O', '0'])
ALPHANUMERICS = set(string.ascii_letters + string.digits) ALPHANUMERICS = set(string.ascii_letters + string.digits)
...@@ -34,7 +32,6 @@ HASHIDS = Hashids( ...@@ -34,7 +32,6 @@ HASHIDS = Hashids(
salt="srct.gmu.edu", alphabet=(LINK_CHARS) salt="srct.gmu.edu", alphabet=(LINK_CHARS)
) )
class RegisteredUser(models.Model): class RegisteredUser(models.Model):
""" """
Wrapper model for the built in User model which stores data pertaining to Wrapper model for the built in User model which stores data pertaining to
...@@ -47,49 +44,40 @@ class RegisteredUser(models.Model): ...@@ -47,49 +44,40 @@ class RegisteredUser(models.Model):
) )
full_name = models.CharField( full_name = models.CharField(
"verbose name", "Full Name",
max_length=100, max_length=100,
default="", default="",
help_text=""
) )
organization = models.CharField( organization = models.CharField(
"verbose name", "Organization",
max_length=100, max_length=100,
default="", default="",
help_text=""
) )
description = models.TextField( description = models.TextField(
"verbose name", "Signup Description",
blank=True, blank=True,
default="", default="",
help_text=""
) )
registered = models.BooleanField( registered = models.BooleanField(
"verbose name", "Registration Status",
default=False, default=False,
help_text=""
) )
approved = models.BooleanField( approved = models.BooleanField(
"verbose name", "Approval Status",
default=False, default=False,
help_text=""
) )
blocked = models.BooleanField( blocked = models.BooleanField(
"verbose name", "Blocked Status",
default=False, default=False,
help_text=""
) )
def __str__(self): def __str__(self):
return "<Registered User: {0} - Approval Status: {1}>".format( return f"<RegisteredUser: {self.user} - Approval Status: {self.approved}>"
self.user, self.approved
)
@receiver(post_save, sender=User) @receiver(post_save, sender=User)
def handle_reguser_creation(sender, instance, created, **kwargs): def handle_reguser_creation(sender, instance, created, **kwargs):
...@@ -100,64 +88,40 @@ def handle_reguser_creation(sender, instance, created, **kwargs): ...@@ -100,64 +88,40 @@ def handle_reguser_creation(sender, instance, created, **kwargs):
if created: if created:
RegisteredUser.objects.create(user=instance) RegisteredUser.objects.create(user=instance)
class URL(models.Model): class URL(models.Model):
""" """
The representation of a stored URL redirection rule. Each URL has The representation of a stored URL redirection rule. Each URL has
attributes that are used for analytic purposes. attributes that are used for analytic purposes.
""" """
# DAY = '1 Day'
# WEEK = '1 Week'
# MONTH = '1 Month'
# CUSTOM = 'Custom Date'
# NEVER = 'Never'
# EXPIRATION_CHOICES = (
# (DAY, DAY),
# (WEEK, WEEK),
# (MONTH, MONTH),
# (NEVER, NEVER),
# (CUSTOM, CUSTOM),
# ) TODO
owner = models.ForeignKey( owner = models.ForeignKey(
RegisteredUser, RegisteredUser,
on_delete="cascade", on_delete="cascade",
verbose_name="verbose name" verbose_name="RegisteredUser Owner"
) )
date_created = models.DateTimeField( date_created = models.DateTimeField(
"verbose name", "Go Link Creation Date",
default=timezone.now, default=timezone.now,
help_text=""
) )
date_expires = models.DateTimeField( date_expires = models.DateTimeField(
"verbose name", "Go Link Expiry Date",
blank=True, blank=True,
null=True, null=True,
# choices=EXPIRATION_CHOICES, TODO
# default=NEVER, TODO
help_text=""
) )
destination = models.URLField( destination = models.URLField(
"Go Link Destination URL",
max_length=1000, max_length=1000,
default="https://go.gmu.edu", default="https://go.gmu.edu",
help_text=""
) )
# TODO Validator for Slug + Emoji # Note: min_length cannot exist on a model so it is enforced in forms.py
"""
# http://stackoverflow.com/a/13752628/6762004
RE_EMOJI = re.compile('[\U00010000-\U0010ffff]', flags=re.UNICODE)
slug_unicode_re = _lazy_re_compile(r'^[-\w]+\Z')
slug_re = _lazy_re_compile(r'^[-a-zA-Z0-9_]+\Z')
"""
short = models.CharField( short = models.CharField(
"Go Shortcode",
max_length=20, max_length=20,
unique=True, unique=True,
help_text="" validators=[unique_short_validator, regex_short_validator],
) )
# TODO Abstract analytics into their own model # TODO Abstract analytics into their own model
...@@ -166,9 +130,7 @@ class URL(models.Model): ...@@ -166,9 +130,7 @@ class URL(models.Model):
socialclicks = models.IntegerField(default=0, help_text="") socialclicks = models.IntegerField(default=0, help_text="")
def __str__(self): def __str__(self):
return '<Owner: %s - destination URL: %s>' % ( return f"<Owner: {self.owner.user} - Destination URL: {self.destination}>"
self.owner.user, self.destination
)
class Meta: class Meta:
ordering = ['short'] ordering = ['short']
......
...@@ -74,20 +74,20 @@ class URLFormTest(TestCase): ...@@ -74,20 +74,20 @@ class URLFormTest(TestCase):
print(form.errors) print(form.errors)
self.assertFalse(form.is_valid()) self.assertFalse(form.is_valid())
def test_invalid_short(self): # def test_invalid_short(self):
""" # """
Test that form fields are validated correctly given valid data. # Test that form fields are validated correctly given valid data.
""" # """
form_data = { # form_data = {
'destination': 'https://srct.gmu.edu', # 'destination': 'https://srct.gmu.edu',
'short': 'test', # 'short': '',
'expires': '1 Day', # 'expires': '1 Day',
'expires_custom': '' # 'expires_custom': ''
} # }
form = URLForm(data=form_data) # form = URLForm(data=form_data)
print(form.errors) # print(form.errors)
self.assertFalse(form.is_valid()) # self.assertFalse(form.is_valid())
def test_invalid_expires(self): def test_invalid_expires(self):
""" """
...@@ -96,7 +96,7 @@ class URLFormTest(TestCase): ...@@ -96,7 +96,7 @@ class URLFormTest(TestCase):
form_data = { form_data = {
'destination': 'https://srct.gmu.edu', 'destination': 'https://srct.gmu.edu',
'short': 'pls', 'short': 'pls',
'expires': 'None', 'expires': '',
'expires_custom': '' 'expires_custom': ''
} }
......
...@@ -185,7 +185,7 @@ class RegisteredUserTest(TestCase): ...@@ -185,7 +185,7 @@ class RegisteredUserTest(TestCase):
""" """
get_user = User.objects.get(username='dhaynes') get_user = User.objects.get(username='dhaynes')
get_registered_user = RegisteredUser.objects.get(user=get_user) get_registered_user = RegisteredUser.objects.get(user=get_user)
expected = '<Registered User: dhaynes - Approval Status: False>' expected = '<RegisteredUser: dhaynes - Approval Status: False>'
actual = str(get_registered_user) actual = str(get_registered_user)
self.assertEqual(expected, actual) self.assertEqual(expected, actual)
......
"""
go/validators.py
Reusable validators for objects that are intended to be inserted into the Go
database.
"""
# Python stdlib imports
import re
# Django imports
from django.core.exceptions import ValidationError
from django.utils import timezone
def regex_short_validator(value):
"""
Run the short through our regex validation before insertion into the
database.
"""
# http://stackoverflow.com/a/13752628/6762004
re_emoji = re.compile("^(([\U00010000-\U0010ffff][\U0000200D]?)+)$")
re_str = re.compile("^([-\w]+)$")
if not re_emoji.match(value) and not re_str.match(value):
raise ValidationError("Short url fails regex check.")
def valid_date(value):
"""
Check if the selected date is a valid date.
"""
if value < timezone.now():
raise ValidationError("Date must be after today.")
def unique_short_validator(value):
"""
Check to make sure the short url has not been used.
"""
# Circular dependency resolution through a deferred import
from .models import URL
if URL.objects.filter(short__iexact=value).count() > 0:
raise ValidationError("Short url already exists.")
...@@ -138,8 +138,6 @@ def my_links(request): ...@@ -138,8 +138,6 @@ def my_links(request):
return index(request) return index(request)
# Rate limits are completely arbitrary # Rate limits are completely arbitrary
@ratelimit(key='user', rate='3/m', method='POST', block=True) @ratelimit(key='user', rate='3/m', method='POST', block=True)
@ratelimit(key='user', rate='25/d', method='POST', block=True) @ratelimit(key='user', rate='25/d', method='POST', block=True)
def post(request, url_form): def post(request, url_form):
...@@ -475,7 +473,6 @@ def redirection(request, short): ...@@ -475,7 +473,6 @@ def redirection(request, short):