Commit c0551abb authored by Zac Wood's avatar Zac Wood

Merge branch 'zac-updates' into 'master'

Python 3.7, Django 2, auto approve users

See merge request !133
parents 32b99e08 28a9349e
Pipeline #4558 passed with stages
in 2 minutes and 1 second
......@@ -11,7 +11,7 @@ variables:
before_script:
- apt-get update -qy
- apt-get install -y mysql-client libmysqlclient-dev python-mysqldb redis-server
- apt-get install -y default-libmysqlclient-dev python-mysqldb redis-server
- pip install -r requirements/ci.txt
- nohup redis-server &
- cd go/
......@@ -35,20 +35,20 @@ before_script:
- python manage.py migrate
- echo "from django.contrib.auth import get_user_model; User = get_user_model(); User.objects.create_superuser('root', 'root@srct.gmu.edu', 'root') " | python manage.py shell
Go-py2.7:
image: library/python:2.7
stage: test
script:
- python manage.py test
# Go-py2.7:
# image: library/python:2.7
# stage: test
# script:
# - python manage.py test
Go-py3.4:
image: library/python:3.4
stage: test
script:
- python manage.py test
# Go-py3.4:
# image: library/python:3.4
# stage: test
# script:
# - python manage.py test
Go-py3.5:
image: library/python:3.5
Go-py3.7:
image: library/python:3.7
stage: test
script:
- python manage.py test
......@@ -61,9 +61,10 @@ Go-py3.5:
# - coverage run --source=go --omit=*migrations/*,*admin.py,*manage.py,*wsgi.py,*settings.py,*secret.py,*__init__.py,*.pyc,*templates/*,*static/* manage.py test
# - coverage html -i && grep pc_cov htmlcov/index.html | egrep -o "[0-9]+\%" | awk '{ print "covered " $1;}'
Go-flake8:
image: library/python:3.5
image: library/python:3.7
stage: lint
script:
- pip install flake8
- flake8 go/ --statistics --exit-zero
\ No newline at end of file
- flake8 go/ --statistics --exit-zero
version: '2'
version: '3.7'
services:
web:
build: .
......@@ -13,7 +13,7 @@ services:
environment:
- GO_ALLOWED_HOSTS=*
- GO_EMAIL_DOMAIN=@masonlive.gmu.edu
- GO_CAS_URL=https://cas.srct.gmu.edu/
- GO_CAS_URL=https://login.gmu.edu/
- GO_DB_NAME=go
- GO_DB_USER=go
- GO_DB_PASSWORD=go
......@@ -25,10 +25,11 @@ services:
- GO_EMAIL_HOST_PASSWORD=
- GO_EMAIL_FROM=
- GO_EMAIL_TO=
- superuser=dhaynes3
- GO_SECRET_KEY=spookyspecret
- superuser=zwood2
db:
image: mysql
image: mysql:5.7
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
MYSQL_DATABASE: go
......
......@@ -2,10 +2,6 @@
go/admin.py
"""
# Future Imports
from __future__ import (absolute_import, division, print_function,
unicode_literals)
# Django Imports
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
......
......@@ -2,85 +2,13 @@
go/cas_callbacks.py
"""
# Future Imports
from __future__ import (absolute_import, division, print_function,
unicode_literals)
# Django Imports
from django.conf import settings
from django.contrib.auth.models import User
# Other Imports
import requests
def pfparse(pf_name_result):
"""
Parse what peoplefinder sends back to us and make a list out of it
"""
# name comes in format of Anderson, Nicholas J
name_list = pf_name_result.split(',')
# there's random whitespace with the first name
first_name_section = name_list[1].strip()
# check if there's a middle initial
mi_q = first_name_section.split(' ')
# make sure that the additional elements aren't multiple names
if len(mi_q[-1]) == 1:
first_name = ' '.join(mi_q[:-1])
else:
first_name = first_name_section
# our list containing the name of the person in a usable list
new_name_list = [first_name, name_list[0]]
return new_name_list
def pfinfo(uname):
"""
Get information from peoplefinder
"""
base_url = settings.PF_URL
url = base_url + "basic/all/" + str(uname)
try:
metadata = requests.get(url, timeout=5)
print("Retrieving information from the peoplefinder api.")
metadata.raise_for_status()
except requests.exceptions.RequestException as ex:
print("Cannot resolve to peoplefinder api:", ex)
print("Returning empty user info tuple.")
return ['', '']
else:
pfjson = metadata.json()
try:
if len(pfjson['results']) == 1:
if pfjson['method'] == 'peoplefinder':
name_str = pfjson['results'][0]['name']
name = pfparse(name_str)
elif pfjson['method'] == 'ldap':
name = [pfjson['results'][0]['givenname'], pfjson['results'][0]['surname']]
else:
name = pfjson['results'][0]['name']
return name
else:
if pfjson['method'] == 'peoplefinder':
name_str = pfjson['results'][1]['name']
name = pfparse(name_str)
elif pfjson['method'] == 'ldap':
name = [pfjson['results'][1]['givenname'], pfjson['results'][1]['surname']]
else:
name = pfjson['results'][0]['name']
return name
# if the name is not in peoplefinder, return empty first and last name
except IndexError as ex:
print("Name not found in peoplefinder.")
return ['', '']
except Exception as ex:
print("Unknown peoplefinder error:", ex)
print("Returning empty user info tuple.")
return ['', '']
def create_user(tree):
"""
Create a django user based off of the peoplefinder info we parsed earlier
Create a django user based off of the CAS info
"""
print("Parsing CAS information.")
......@@ -90,9 +18,6 @@ def create_user(tree):
except Exception as ex:
print("CAS callback unsuccessful:", ex)
# error handling in pfinfo function
info_name = pfinfo(username)
try:
if user_created:
print("Created user object %s." % username)
......@@ -106,11 +31,7 @@ def create_user(tree):
user.save()
print("Added user's email, %s." % email_str)
user.first_name = info_name[0]
user.last_name = info_name[1]
user.save()
print("Added user's name, %s %s." % (info_name[0], info_name[1]))
print("User object creation process completed.")
else:
......
"""
go/forms.py
"""
# Future Imports
from __future__ import (absolute_import, division, print_function,
unicode_literals)
# Python stdlib Imports
from datetime import datetime, timedelta
......@@ -20,7 +17,7 @@ from django.utils.safestring import mark_safe
from .models import URL, RegisteredUser
# Other Imports
from bootstrap3_datetime.widgets import DateTimePicker
# from bootstrap3_datetime.widgets import DateTimePicker
from crispy_forms.bootstrap import (Accordion, AccordionGroup, PrependedText,
StrictButton)
from crispy_forms.helper import FormHelper
......@@ -94,7 +91,7 @@ class URLForm(ModelForm):
DAY = '1 Day'
WEEK = '1 Week'
MONTH = '1 Month'
CUSTOM = 'Custom Date'
# CUSTOM = 'Custom Date'
NEVER = 'Never'
# Define a tuple of string date standards to be used as our date choices
......@@ -103,7 +100,7 @@ class URLForm(ModelForm):
(WEEK, WEEK),
(MONTH, MONTH),
(NEVER, NEVER),
(CUSTOM, CUSTOM),
# (CUSTOM, CUSTOM),
)
# Add preset expiration choices.
......@@ -129,22 +126,22 @@ class URLForm(ModelForm):
# Add a custom expiration choice.
expires_custom = DateTimeField(
required=False,
label='Custom Date',
input_formats=['%m-%d-%Y'],
validators=[valid_date],
initial=lambda: datetime.now() + timedelta(days=1),
widget=DateTimePicker(
options={
"format": "MM-DD-YYYY",
"pickTime": False,
},
icon_attrs={
"class": "fa fa-calendar",
},
)
)
# expires_custom = DateTimeField(
# required=False,
# label='Custom Date',
# input_formats=['%m-%d-%Y'],
# validators=[valid_date],
# initial=lambda: datetime.now() + timedelta(days=1),
# widget=DateTimePicker(
# options={
# "format": "MM-DD-YYYY",
# "pickTime": False,
# },
# icon_attrs={
# "class": "fa fa-calendar",
# },
# )
# )
def __init__(self, *args, **kwargs):
"""
......
......@@ -2,10 +2,6 @@
go/commands/expirelinks.py
"""
# Future Imports
from __future__ import (absolute_import, division, print_function,
unicode_literals)
# Django Imports
from django.core.management.base import BaseCommand
from django.utils import timezone
......
......@@ -2,10 +2,6 @@
go/commands/test_expirelinks.py
"""
# Future Imports
from __future__ import (absolute_import, division, print_function,
unicode_literals)
# Python stdlib Imports
from datetime import timedelta
......
......@@ -2,10 +2,6 @@
go/models.py
"""
# Future Imports
from __future__ import (absolute_import, division, print_function,
unicode_literals)
# Python stdlib Imports
import string
......@@ -16,7 +12,8 @@ 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
from django.conf import settings
from django.core.mail import EmailMessage, send_mail
# Other Imports
from hashids import Hashids # http://hashids.org/python/
......@@ -26,7 +23,6 @@ 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
......@@ -34,7 +30,7 @@ class RegisteredUser(models.Model):
"""
# Let's associate a User to this RegisteredUser
user = models.OneToOneField(User)
user = models.OneToOneField(User, on_delete=models.CASCADE)
# What is your name?
full_name = models.CharField(
......@@ -52,10 +48,10 @@ class RegisteredUser(models.Model):
description = models.TextField(blank=True)
# Have you filled out the registration form?
registered = models.BooleanField(default=False)
registered = models.BooleanField(default=True)
# Are you approved to use Go?
approved = models.BooleanField(default=False)
approved = models.BooleanField(default=True)
# Is this User Blocked?
blocked = models.BooleanField(default=False)
......@@ -79,9 +75,25 @@ def handle_regUser_creation(sender, instance, created, **kwargs):
if created:
RegisteredUser.objects.create(user=instance)
# Don't send mail for now
#
# user_mail = instance.username + settings.EMAIL_DOMAIN
# send_mail(
# 'We have received your Go application!',
# ######################
# 'Hey there %s,\n\n'
# 'The Go admins have received your application and are '
# 'currently in the process of reviewing it.\n\n'
# 'You will receive another email when you have been '
# 'approved.\n\n'
# '- Go Admins'
# % (str(instance.username)),
# ######################
# settings.EMAIL_FROM,
# [user_mail]
# )
@python_2_unicode_compatible
class URL(models.Model):
"""
This model represents a stored URL redirection rule. Each URL has an
......@@ -90,7 +102,7 @@ class URL(models.Model):
"""
# Who is the owner of this Go link
owner = models.ForeignKey(RegisteredUser)
owner = models.ForeignKey(RegisteredUser, on_delete=models.CASCADE)
# When was this link created?
date_created = models.DateTimeField(default=timezone.now)
......@@ -133,11 +145,17 @@ class URL(models.Model):
should be updated to be simpler
"""
if cache.get("hashids_counter") is None:
print(URL.objects.count())
cache.set("hashids_counter", URL.objects.count())
print(cache.get("hashids_counter"))
tries = 1
while tries < 100:
try:
short = HASHIDS.encrypt(cache.get("hashids_counter"))
counter = cache.get("hashids_counter")
if counter is None:
short = HASHIDS.encrypt(0)
else:
short = HASHIDS.encrypt(counter)
tries += 1
cache.incr("hashids_counter")
URL.objects.get(short__iexact=short)
......
......@@ -25,126 +25,7 @@
</div>
</div>
<!-- Table 1 -->
<!-- define the div where we can select users from a table to judge them -->
<div class="row">
<div class="col-md-12">
<input class="inputfilter" type="text" id="appliedInput"
placeholder="Search Usernames or Full Names">
<h3>Users awaiting moderation</h3>
<form method="post" action="useradmin">
<!-- csrf protection -->
{% csrf_token %}
<!-- define out table of users that need approval -->
<table class="table table-striped table-hover" id="appliedTable">
<!-- define the header row -->
<thead>
<tr>
<th>Selected</th>
<th>Username</th>
<th>Full Name</th>
<th>Description</th>
</tr>
</thead>
<!-- define the body rows -->
<tbody>
<!-- loop through all users in the need_approval list -->
{% for unapproved in need_approval %}
<!-- ..and make a new row for each user -->
<tr>
<td><input type="checkbox" name="username" value={{ unapproved.user }}></td>
<td>{{ unapproved.user }}</td>
<td>{{ unapproved.full_name }}</td>
<td>{{ unapproved.description|default:"No description provided" }}</td>
</tr>
<!-- unless it's empty in which case we show nothing -->
{% empty %}
<tr>
<td>none</td>
<td>none</td>
<td>none</td>
<td>none</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- a div containing our form submission buttons -->
<div class="form-group">
<!-- the modal for approve doesn't work-->
<a class="btn btn-primary btn-sm" data-target="#approveModal" data-toggle="modal"> Approve </a>
<!-- input type="submit" name="_approve" value="Approve" class="btn btn-primary btn-sm"-->
<a class="btn btn-danger btn-sm" data-target="#denyModal" data-toggle="modal"> Deny </a>
<a class="btn btn-default btn-sm btn-blockUsr" data-target="#blockModal" data-toggle="modal"
style="background-color: black; color: white;"> Block </a>
<!--Define Approve Modal-->
<div id="approveModal" class="modal fade" role="dialog" tabindex="-1">
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content" style="background-color:#f5f5f5; border-radius: 7px">
<div class="modal-header" style="text-align:center;">
<h4 class="modal-title text-center" style="font-weight:bold; font-size: 21px !important;">
Are you sure you would like to approve a user?</h4>
</div>
<div class="modal-body" style="padding-bottom: 80px">
<a type="button" class="btn btn-success btn-lg" style="border-width: 0px;float:left; width:49%; background-color: #A9B0AD; color: #ffffff; border-radius: 4px;"
data-dismiss="modal">Cancel</a>
<input type="submit" name="_approve" value="Approve" class="btn btn-primary btn-lg"
style="border-width: 0px;float:right; width:49%; background-color: #00331a; color: #ffffff; border-radius: 4px;">
</div>
</div>
</div>
</div>
<!--Define Deny Modal-->
<div id="denyModal" class="modal fade" role="dialog" tabindex="-1">
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content" style="background-color:#f5f5f5; border-radius: 7px">
<div class="modal-header" style="text-align:center;">
<h4 class="modal-title text-center" style="font-weight:bold; font-size: 21px !important;">
Are you sure you would like to deny a user?</h4>
<h6 style="font-weight:500; margin-top: 0px; margin-bottom: 0px;">
Please remember the user's feelings</h6>
</div>
<div class="modal-body" style="padding-bottom: 80px">
<a type="button" class="btn btn-success btn-lg" style="border-width: 0px;float:left; width:49%; background-color: #A9B0AD; color: #ffffff; border-radius: 4px;"
data-dismiss="modal">Cancel</a>
<input type="submit" name="_deny" value="Deny" class="btn btn-danger btn-lg" style="border-width: 0px;float:right; width:49%; background-color: #ac1d37; color: #ffffff; border-radius: 4px;">
</div>
</div>
</div>
</div>
<!--Define Block Modal-->
<!-- Note there is another block modal for the other block button below -->
<div id="blockModal" class="modal fade" role="dialog" tabindex="-1">
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content" style="background-color:#f5f5f5; border-radius: 7px">
<div class="modal-header" style="text-align:center;">
<h4 class="modal-title text-center" style="font-weight:bold; font-size: 21px !important;">
Are you sure you would like to block a user?</h4>
<h6 style="font-weight:500; margin-top: 0px; margin-bottom: 0px;">
Please remember the user's feelings</h6>
</div>
<div class="modal-body" style="padding-bottom: 80px">
<a type="button" class="btn btn-success btn-lg" style="border-width: 0px;float:left; width:49%; background-color: #A9B0AD; color: #ffffff; border-radius: 4px;"
data-dismiss="modal">Cancel</a>
<input type="submit" name="_block" value="Block" class="btn btn-danger btn-lg" style="border-width: 0px; float:right; width:49%;background-color: #000000; color: #ffffff; border-radius: 4px;">
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
<!-- Table 2 -->
<!-- define the table that displays blocked users and allows us to unblock them -->
<div class="row">
<div class="col-md-12">
......@@ -160,9 +41,7 @@
<tr>
<th>Selected</th>
<th>Username</th>
<th>Full Name</th>
<th>Description</th>
<th>Approved</th>
<th>Email</th>
</tr>
</thead>
......@@ -173,9 +52,7 @@
<tr>
<td><input type="checkbox" name="username" value={{ blockedUsers.user }}></td>
<td>{{ blockedUsers.user }}</td>
<td>{{ blockedUsers.full_name }}</td>
<td>{{ blockedUsers.description|default:"No description provided" }}</td>
<td>{{ blockedUsers.approved }}</td>
<td>{{ blockedUsers.user.email }}</td>
</tr>
<!-- unless there are no blocked users -->
{% empty %}
......@@ -183,8 +60,6 @@
<td>none</td>
<td>none</td>
<td>none</td>
<td>none</td>
<td>none</td>
</tr>
{% endfor %}
</tbody>
......@@ -219,7 +94,7 @@
</div>
</div>
<!-- Table 3 -->
<!-- Table 2 -->
<!-- define the table of current users -->
<div class="row">
<div class="col-md-12">
......@@ -234,9 +109,8 @@
<thead>
<tr>
<th>Selected</th>
<th>Username</th>
<th>Full Name</th>
<th>Description</th>
<th>Username</th>
<th>Email</th>
</tr>
</thead>
......@@ -246,15 +120,13 @@
{% for currentUsers in current_users %}
<tr>
<td><input type="checkbox" name="username" value={{ currentUsers.user }}></td>
<td>{{ currentUsers.user }}</td>
<td>{{ currentUsers.full_name }}</td>
<td>{{ currentUsers.description|default:"No description provided" }}</td>
<td>{{ currentUsers.user }}</td>
<td>{{ currentUsers.user.email }}</td>
</tr>
<!-- unless there are none -->
{% empty %}
<tr>
<td>none</td>
<td>none</td>
<td>none</td>
<td>none</td>
<td>none</td>
</tr>
......@@ -315,4 +187,4 @@
<script src="{% static "js/useradmin.js" %}"></script>
{% endblock %}
\ No newline at end of file
{% endblock %}
......@@ -26,13 +26,7 @@
<!-- only show if the current user is authenticated -->
{% if user.is_authenticated %}
<!-- only if they are an approved user -->
{% if user.registereduser.approved %}
<li><a href="{% url 'new_link' %}">New Link</a></li>
{% endif %}
<!-- only if the current user has not registered to use go -->
{% if not user.registereduser.registered %}
<li><a href="{% url 'signup' %}">Register</a></li>
{% endif %}
<li><a href="{% url 'new_link' %}">New Link</a></li>
{% endif %}
</ul>
......
......@@ -90,7 +90,7 @@
<strong>Target URL:</strong>
<a href="{{url.target}}" target="_blank">{{url.target}}</a>
<br />
{% if url.owner == request.user.registereduser and request.user.registereduser.approved == True %}
{% if url.owner == request.user.registereduser %}
<strong>Clicks:</strong> {{url.clicks}}
<br />
......@@ -208,4 +208,4 @@
e.clearSelection();
$('#copy-button-{{url.short}}').attr('data-original-title', 'Copied!').tooltip('show');
});
</script>
\ No newline at end of file
</script>
......@@ -81,12 +81,7 @@ SRCT Go &bull; Welcome
<h3>How do I access Go?</h3>
<legend></legend>
<p>
In order to prevent abuse of the URL shortener, access to Go
is moderated by SRCT administrators. New users will need to fill out
a registration form in order to become an approved user.
<br></br>
Additionally, only users with a current, valid Mason username and password
may request to be approved.
All users with a current, valid GMU username and password can access Go.
</p>
</div>
</div>
......@@ -96,13 +91,9 @@ SRCT Go &bull; Welcome
<!-- carefully formatted login and signup buttons -->
<!-- https://files.slack.com/files-pri/T025B796J-F0KPTCTD3/spin-dhaynes-buttons.gif -->
<div class="fix row">
<div class="col-md-offset-1 col-md-4">
<div class="col">
<a href="{% url 'go_login' %}" class="btn btn-primary btn-block">Log In</a>
</div>
<div class="col-md-offset-1 col-md-4">
<a href="{% url 'signup' %}" class="btn btn-primary btn-block">Sign Up</a>
</div>
</div>
{% endblock %}
......@@ -2,10 +2,6 @@
go/templatetags/test_go_extras.py
"""
# Future Imports
from __future__ import (absolute_import, division, print_function,
unicode_literals)
# Django Imports
from django.contrib.auth.models import User
from django.test import TestCase
......
# Future Imports
from __future__ import (absolute_import, division, print_function,
unicode_literals)
# Django Imports
from django.test import TestCase
# App Imports
from .cas_callbacks import pfparse, pfinfo
class CasCallbacksTest(TestCase):
"""
Test cases for the functions in call_callbacks.
"""
def test_pf_peoplefinder_method(self):
"""
Presently enrolled student who has been added to peoplefinder
"""
actual = pfinfo('dhaynes3')
expected = ['David', 'Haynes']
self.assertEqual(expected, actual)
def test_pfparse_peoplefinder_method(self):
"""
Test the parsing method to ensure that first and last names are seperated
accordingly and correctly.
"""
actual = pfparse("Haynes, David M")
expected = ['David', 'Haynes']
self.assertEqual(expected, actual)
def test_pfinfo_ldap_method(self):
"""
student no longer in peoplefinder, or who hasn't yet been added
"""
actual = pfinfo('lfaraone')
expected = ['Luke W', 'Faraone']
self.assertEqual(expected, actual)
def test_pfinfo_employee_method(self):
"""
student employees will have their staff info return before their student info
"""
actual = pfinfo('nander13')
expected = ['Nicholas J', 'Anderson']
self.assertEqual(expected, actual)
def test_pfinfo_dne(self):
"""
a name not found for either (should never happen, but gracefully handle anyway)
"""
actual = pfinfo('bobama')
expected = ['', '']
self.assertEqual(expected, actual)
......@@ -5,10 +5,6 @@ References:
- http://stackoverflow.com/a/7304658
"""
# Future Imports
from __future__ import (absolute_import, division, print_function,
unicode_literals)
# Python stdlib Imports
from datetime import datetime, timedelta