Commit fda41933 authored by Eyad Hasan's avatar Eyad Hasan

Merge remote-tracking branch 'origin/2.2-dev' into 145-linkNotFoundPage

parents e94df0dc 4cb7d508
Pipeline #1245 passed with stage
in 1 minute and 21 seconds
......@@ -15,4 +15,5 @@ venv
htmlcov/
.idea
__pycache__/
.vscode
\ No newline at end of file
.vscode
go/sourceme.sh
......@@ -10,18 +10,25 @@ variables:
before_script:
- apt-get update -qy
- apt-get install -y mysql-client libmysqlclient-dev python-mysqldb
- apt-get install -y mysql-client libmysqlclient-dev python-mysqldb redis-server
- pip install -r requirements/ci.txt
- nohup redis-server &
- cd go/
- cp settings/settings.py.template settings/settings.py
- cp settings/secret.py.template settings/secret.py
- export SECRET_KEY=$(dd if=/dev/urandom count=100 | tr -dc "A-Za-z0-9" | fold -w 60 | head -n1 2>/dev/null)
- export DJANGO_DEBUG="True"
- sed -i settings/secret.py -e 's/DB_NAME.*/DB_NAME = \"go\"/'
- sed -i settings/secret.py -e 's/DB_USER.*/DB_USER = \"root\"/'
- sed -i settings/secret.py -e 's/DB_PASSWORD.*/DB_PASSWORD = \"root\"/'
- sed -i settings/secret.py -e 's/DB_HOST.*/DB_HOST = \"mysql\"/'
- sed -i settings/secret.py -e 's/SECRET_KEY.*/SECRET_KEY = \"${SECRET_KEY}\"/'
- export GO_SECRET_KEY=$(dd if=/dev/urandom count=100 | tr -dc "A-Za-z0-9" | fold -w 60 | head -n1 2>/dev/null)
- export GO_DB_NAME="go"
- export GO_DB_USER="root"
- export GO_DB_PASSWORD="root"
- export GO_DB_HOST="mysql"
- export GO_DB_PORT=3306
- export GO_ALLOWED_HOSTS="*"
- export GO_EMAIL_DOMAIN="@masonlive.gmu.edu"
- export GO_CAS_URL="https://cas.srct.gmu.edu/"
- export GO_EMAIL_HOST=
- export GO_EMAIL_PORT=
- export GO_EMAIL_HOST_USER=
- export GO_EMAIL_HOST_PASSWORD=
- export GO_EMAIL_FROM=
- export GO_EMAIL_TO=
- python manage.py makemigrations
- python manage.py makemigrations go
- python manage.py migrate
......
## Summary
# Summary
Here you should include two to three sentences explaining the thought process
about the current issue. Maybe a picture? Some details that could best help someone,
especially someone new, understand the goal of the issue and how they should best
approach the problem.
## Helpful Links
# Helpful Links
Here you should include a bullet point list of links to documentation, stack overflow,
whatever, that could help guide someone on what it is they are trying to do.
Essentially, a list of links to point them in the right direction.
......@@ -177,6 +177,40 @@ https://git.gmu.edu/srct/go/wikis/manual-setup
# Some words about contributing to Go.
## Testing
You are _very strongly_ encouraged to write test cases where applicible for
code that you contribute to the repo. This is not a rule at the moment but rather
a strong suggestion. It's good practice for corporate land and will also ensure
your code works. Additionally, there are quite a few example ones to look at in
the repo and on Google.
### Running Unit Tests
Unit tests are run on every commit sent to gitlab though that can be a pain to
rely on. Here's how to run them locally:
#### Docker
Docker is not supported currently for running unit tests. If you're able to get
it set up, open a merge request and I'll merge it in.
#### Vagrant
vagrant up
vagrant ssh
cd /vagrant
source venv/bin/activate
cd go
source sourceme.sh
python manage.py test
#### Manual Setup
Assuming you are within your virtualenv:
python manage.py test
## CONTRIBUTING.md
This document goes into detail about how to contribute to the repo, plus some
......@@ -198,6 +232,7 @@ Once in the admin page go to "registered users", and create a new registered use
use the same username and Full Name as your main account and select "approved" in the bottom row.
## Coding style
You should adhere to the style of the repo code. Consistancy is key! PEP8 guidelines
are strongly reccomended but not enforced at the time. Please comment your code,
I will not accept commits that contain uncommented code.
......
......@@ -95,7 +95,7 @@ Vagrant.configure(2) do |config|
venv_path: "/vagrant/venv",
cas_url: "https://cas.srct.gmu.edu/",
app_path: "/vagrant/go",
settings_path: "/vagrant/go/settings",
sourceme_dest: "/vagrant/go/",
superuser: "dhaynes3"
}
}
......
......@@ -11,20 +11,22 @@ services:
depends_on:
- db
environment:
- debug=True
- host=*
- email_domain=@masonlive.gmu.edu
- cas_url=https://cas.srct.gmu.edu/
- GO_ALLOWED_HOSTS=*
- GO_EMAIL_DOMAIN=@masonlive.gmu.edu
- GO_CAS_URL=https://cas.srct.gmu.edu/
- GO_DB_NAME=go
- GO_DB_USER=go
- GO_DB_PASSWORD=go
- GO_DB_HOST=db
- GO_DB_PORT=3306
- GO_EMAIL_HOST=
- GO_EMAIL_PORT=
- GO_EMAIL_HOST_USER=
- GO_EMAIL_HOST_PASSWORD=
- GO_EMAIL_FROM=
- GO_EMAIL_TO=
- superuser=dhaynes3
- DB_NAME=go
- DB_USER=go
- DB_PASSWORD=go
- DB_HOST=db
- PIWIK_URL=
- EMAIL_HOST=
- EMAIL_PORT=
- EMAIL_HOST_USER=
- EMAIL_HOST_PASSWORD=
db:
image: mysql
environment:
......
"""
go/admin.py
"""
# Future Imports
from __future__ import unicode_literals, absolute_import, print_function, division
from __future__ import (absolute_import, division, print_function,
unicode_literals)
# Django Imports
from django.contrib import admin
......
"""
go/cas_callbacks.py
"""
# Future Imports
from __future__ import unicode_literals, absolute_import, print_function, division
from __future__ import (absolute_import, division, print_function,
unicode_literals)
# Django Imports
from django.contrib.auth.models import User
from django.conf import settings
from django.contrib.auth.models import User
# Other Imports
import requests
......
"""
go/forms.py
"""
# Future Imports
from __future__ import unicode_literals, absolute_import, print_function, division
from __future__ import (absolute_import, division, print_function,
unicode_literals)
# Python stdlib Imports
from datetime import datetime, timedelta
from six.moves import urllib
# Django Imports
from django import forms
from django.core.exceptions import ValidationError
from django.utils.safestring import mark_safe
from django.forms import (BooleanField, CharField, ChoiceField, DateTimeField,
ModelForm, RadioSelect, SlugField, Textarea,
TextInput, URLField, URLInput)
from django.utils import timezone
from django.utils.safestring import mark_safe
# App Imports
from go.models import URL, RegisteredUser
# Other Imports
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Fieldset, Submit, HTML, Div, Field
from crispy_forms.bootstrap import StrictButton, PrependedText, Accordion, AccordionGroup
from bootstrap3_datetime.widgets import DateTimePicker
from crispy_forms.bootstrap import (Accordion, AccordionGroup, PrependedText,
StrictButton)
from crispy_forms.helper import FormHelper
from crispy_forms.layout import HTML, Div, Field, Fieldset, Layout, Submit
class URLForm(forms.ModelForm):
class URLForm(ModelForm):
"""
The form that is used in URL creation.
"""
......@@ -59,11 +68,11 @@ class URLForm(forms.ModelForm):
return target
# Custom target URL field
target = forms.URLField(
target = URLField(
required=True,
label='Long URL (Required)',
max_length=1000,
widget=forms.URLInput(attrs={
widget=URLInput(attrs={
'placeholder': 'https://yoursite.com/'
})
)
......@@ -85,10 +94,10 @@ class URLForm(forms.ModelForm):
raise ValidationError('Short url already exists.')
# Custom short-url field with validators.
short = forms.SlugField(
short = SlugField(
required=False,
label='Short URL (Optional)',
widget=forms.TextInput(),
widget=TextInput(),
validators=[unique_short],
max_length=20,
min_length=3,
......@@ -113,12 +122,12 @@ class URLForm(forms.ModelForm):
)
# Add preset expiration choices.
expires = forms.ChoiceField(
expires = ChoiceField(
required=True,
label='Expiration (Required)',
choices=EXPIRATION_CHOICES,
initial=NEVER,
widget=forms.RadioSelect(),
widget=RadioSelect(),
)
def valid_date(value):
......@@ -135,7 +144,7 @@ class URLForm(forms.ModelForm):
# Add a custom expiration choice.
expires_custom = forms.DateTimeField(
expires_custom = DateTimeField(
required=False,
label='Custom Date',
input_formats=['%m-%d-%Y'],
......@@ -226,37 +235,107 @@ class URLForm(forms.ModelForm):
# what attributes are included
fields = ['target']
class SignupForm(forms.ModelForm):
class EditForm(URLForm):
def __init__(self, *args, **kwargs):
"""
On initialization of the form, crispy forms renders this layout
"""
# Grab that host info
self.host = kwargs.pop('host', None)
super(URLForm, self).__init__(*args, **kwargs)
# Define the basics for crispy-forms
self.helper = FormHelper()
self.helper.form_method = 'POST'
# Some xtra vars for form css purposes
self.helper.form_class = 'form-horizontal'
self.helper.label_class = 'col-md-1'
self.helper.field_class = 'col-md-6'
# The main "layout" defined
self.helper.layout = Layout(
Fieldset('',
#######################
Accordion(
# Step 1: Long URL
AccordionGroup('Step 1: Long URL',
Div(
HTML("""
<h4>Modify the URL you would like to shorten:</h4>
<br />"""),
'target',
style="background: rgb(#F6F6F6);"),
active=True,
template='crispy/accordian-group.html'),
# Step 2: Short URL
AccordionGroup('Step 2: Short URL',
Div(
HTML("""
<h4>Modify the Go address:</h4>
<br />"""),
PrependedText(
'short', 'https://go.gmu.edu/', template='crispy/customPrepended.html'),
style="background: rgb(#F6F6F6);"),
active=True,
template='crispy/accordian-group.html',),
# Step 3: Expiration
AccordionGroup('Step 3: URL Expiration',
Div(
HTML("""
<h4>Modify the expiration date:</h4>
<br />"""),
'expires',
Field('expires_custom', template="crispy/customDateField.html"),
style="background: rgb(#F6F6F6);"),
active=True,
template='crispy/accordian-group.html'),
# FIN
template='crispy/accordian.html'),
#######################
HTML("""
<br />"""),
StrictButton('Submit Changes', css_class="btn btn-primary btn-md col-md-4", type='submit')))
class Meta(URLForm.Meta):
# what attributes are included
fields = URLForm.Meta.fields
class SignupForm(ModelForm):
"""
The form that is used when a user is signing up to be a RegisteredUser
"""
# The full name of the RegisteredUser
full_name = forms.CharField(
full_name = CharField(
required=True,
label='Full Name (Required)',
max_length=100,
widget=forms.TextInput(),
widget=TextInput(),
)
# The RegisteredUser's chosen organization
organization = forms.CharField(
organization = CharField(
required=True,
label='Organization (Required)',
max_length=100,
widget=forms.TextInput(),
widget=TextInput(),
)
# The RegisteredUser's reason for signing up to us Go
description = forms.CharField(
description = CharField(
required=False,
label='Description (Optional)',
max_length=200,
widget=forms.Textarea(),
widget=Textarea(),
)
# A user becomes registered when they agree to the TOS
registered = forms.BooleanField(
registered = BooleanField(
required=True,
# ***Need to replace lower url with production URL***
# ie. go.gmu.edu/about#terms
......
"""
go/commands/expirelinks.py
"""
# Future Imports
from __future__ import unicode_literals, absolute_import, print_function, division
from __future__ import (absolute_import, division, print_function,
unicode_literals)
# Django Imports
from django.core.management.base import BaseCommand
......@@ -19,7 +24,8 @@ class Command(BaseCommand):
def handle(self, *args, **options):
"""
The handle function handles the main component of the django-admin command.
The handle function handles the main component of the django-admin
command.
"""
# Loop through a list of all URL objects that have expired
......
"""
go/commands/test_expirelinks.py
"""
# Future Imports
from __future__ import unicode_literals, absolute_import, print_function, division
from __future__ import (absolute_import, division, print_function,
unicode_literals)
# Python stdlib Imports
from datetime import timedelta
# Django Imports
from django.test import TestCase
from django.contrib.auth.models import User
from django.utils import timezone
from django.core.management import call_command
from django.test import TestCase
from django.utils import timezone
# App Imports
from .expirelinks import Command
from go.models import URL, RegisteredUser
class ExpireLinksTest(TestCase):
......@@ -44,6 +48,7 @@ class ExpireLinksTest(TestCase):
current_url.expires = yesterday
second_url.expires = tomorrow
current_url.save()
second_url.save()
def test_expirelinks(self):
"""
......
"""
go/models.py
"""
# Future Imports
from __future__ import unicode_literals, absolute_import, print_function, division
from __future__ import (absolute_import, division, print_function,
unicode_literals)
# Python stdlib Imports
import string
# Django Imports
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from django.core.cache import cache
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible
# Other Imports
from hashids import Hashids # http://hashids.org/python/
from hashids import Hashids # http://hashids.org/python/
# generate the salt and initialize Hashids
hashids = Hashids(salt="srct.gmu.edu", alphabet=(string.ascii_lowercase + string.digits))
HASHIDS = Hashids(
salt="srct.gmu.edu", alphabet=(string.ascii_lowercase + string.digits)
)
@python_2_unicode_compatible
class RegisteredUser(models.Model):
"""
This is simply a wrapper model for the user object which, if an object
This is simply a wrapper model for the user object which, if an object
exists, indicates that that user is registered.
"""
......@@ -58,13 +65,15 @@ class RegisteredUser(models.Model):
str(RegisteredUser)
"""
return '<Registered User: %s - Approval Status: %s>' % (self.user, self.approved)
return '<Registered User: %s - Approval Status: %s>' % (
self.user, self.approved
)
@receiver(post_save, sender=User)
def handle_regUser_creation(sender, instance, created, **kwargs):
"""
When a post_save is called on a User object (and it is newly created), this
When a post_save is called on a User object (and it is newly created), this
is called to create an associated RegisteredUser
"""
......@@ -105,7 +114,9 @@ class URL(models.Model):
print(URL)
"""
return '<Owner: %s - Target URL: %s>' % (self.owner.user, self.target)
return '<Owner: %s - Target URL: %s>' % (
self.owner.user, self.target
)
class Meta:
"""
......@@ -124,13 +135,14 @@ class URL(models.Model):
if cache.get("hashids_counter") is None:
cache.set("hashids_counter", URL.objects.count())
cache.incr("hashids_counter")
short = hashids.encrypt(cache.get("hashids_counter"))
short = HASHIDS.encrypt(cache.get("hashids_counter"))
tries = 1
while tries < 100:
try:
URL.objects.get(short__iexact = short)
URL.objects.get(short__iexact=short)
tries += 1
cache.incr("hashids_counter")
except URL.DoesNotExist as ex:
print(ex)
return short
return None
<!-- include the base html template -->
{% extends 'layouts/base.html' %}
<!-- Tell Django to load static files -->
{% load staticfiles %}
<!-- load django crispy forms' tags -->
{% load crispy_forms_tags %}
<!-- define the page title block -->
{% block title %}
SRCT Go &bull; Edit Link
{% endblock %}
<!-- define the content block for the page -->
{% block content %}
<!-- define the page header div -->
<div class="page-header" id="banner">
<div class="row">
<div class="col-md-12">
<h1><strong>
<span class="fa-stack fa-lg">
<i class="fa fa-circle fa-stack-2x"></i>
<i class="fa fa-wrench fa-stack-1x fa-inverse"></i>
</span>
<i class="fa">Edit Link</i>
</strong></h1>
</div>
</div>
</div>
<!-- call django crispy forms to render the go link creation form here -->
{% crispy form %}
<!-- load some JS to hide/show the custom date field -->
<script src="{% static "js/new_link.js" %}"></script>
{% endblock %}
......@@ -23,7 +23,7 @@ SRCT Go &bull; My Links
</div>
</div>
<div class="newlink" style="text-align: right;">
<a href="{% url 'index' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add new link</a>
<a href="{% url 'new_link' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add new link</a>
</div>
</div>
......
<!-- include the base html template -->
{% extends 'layouts/base.html' %}
<!-- Tell Django to load static files -->
{% load staticfiles %}
<!-- load django crispy forms' tags -->
{% load crispy_forms_tags %}
<!-- define the page title block -->
{% block title %}
SRCT Go &bull; A University Branded URL Shortener
SRCT Go &bull; New Link
{% endblock %}
<!-- define the content block for the page -->
......@@ -31,32 +34,6 @@ SRCT Go &bull; A University Branded URL Shortener
{% crispy form %}
<!-- load some JS to hide/show the custom date field -->
<script type="text/javascript">
$(function() {
// hide by default
$("div_id_expires_custom").hide();
// if the expires_custom checkbox is checked..
if ($("#id_expires_5").is(":checked")) {
// display the field
$("#div_id_expires_custom").slideDown();
} else {
// keep it up
$("#div_id_expires_custom").slideUp();
}
// if the expires_custom checkbox is clicked..
$("#div_id_expires").click(function() {
// if the expires_custom checkbox is checked..
if ($("#id_expires_5").is(":checked")) {
// display the field
$("#div_id_expires_custom").slideDown();
} else {
// keep it hidden
$("#div_id_expires_custom").slideUp();
}
})
})
</script>
<script src="{% static "js/new_link.js" %}"></script>
{% endblock %}
......@@ -35,8 +35,6 @@
rel="stylesheet">
<link rel="stylesheet" href="{% static "css/bootswatch.min.css" %}" />
<link rel="stylesheet" href="{% static "css/styles.css" %}" />
<link rel="stylesheet" href="{% static "css/style-link-box.css" %}" />
<link rel="stylesheet" href="{% static "css/style-link-box.css" %}" />
<!-- Load in global js -->
<script src="{% static "js/jquery.min.js" %}"></script>
......@@ -58,8 +56,5 @@
<!-- Load in the footer template -->
{% include 'layouts/footer.html' %}