Commit be8d80f4 authored by David Haynes's avatar David Haynes 🙆

Merge branch '2.2-dev' into 'master'

2.2

Closes #97, #91, #92, #90, #88, #99, #100, #78, #75, and #77

See merge request !50
parents ec5b0b71 06ec7ed1
Pipeline #3761 passed with stage
in 1 minute and 6 seconds
......@@ -13,5 +13,4 @@ whats_open/secret_key.py
whats_open/assets/
static/admin/
data
whats-open/api/migrations
.vscode
services:
- mysql:latest
- mysql:5.7
variables:
MYSQL_DATABASE: wopen
......@@ -10,10 +10,13 @@ types:
before_script:
- apt-get update -qy
- apt-get install -y mysql-client libmysqlclient-dev python-mysqldb libgdal1h libproj-dev proj-data proj-bin
- pip install -r requirements/test.txt
- apt-get install -y mysql-client default-libmysqlclient-dev python-mysqldb
gdal-bin libproj-dev proj-data proj-bin binutils
- cd whats-open/
- export WOPEN_SECRET_KEY=$(dd if=/dev/urandom count=100 | tr -dc "A-Za-z0-9" | fold -w 60 | head -n1 2>/dev/null)
- pip install pipenv
- pipenv install --system --deploy
- export WOPEN_SECRET_KEY=$(dd if=/dev/urandom count=100 | tr -dc "A-Za-z0-9"
| fold -w 60 | head -n1 2>/dev/null)
- export WOPEN_EMAIL_DOMAIN="@masonlive.gmu.edu"
- export WOPEN_DB_NAME="wopen"
- export WOPEN_DB_USER="root"
......@@ -21,21 +24,16 @@ before_script:
- export WOPEN_DB_HOST="mysql"
- export WOPEN_DB_PORT=3306
- export WOPEN_SUPERUSER=admin
- export WOPEN_ENV=dev
- python manage.py makemigrations
- python manage.py makemigrations api
- 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
- 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
whats-open-py3.5:
image: library/python:3.5
whats-open-py3.7:
image: library/python:3.7
type: test
script:
- python manage.py test
whats-open-py3.6:
image: library/python:3.6
type: test
script:
# - if pip list --outdated --format=legacy | grep "Latest" | wc -l > 0; then echo "Please update your dependecies!" && pip list --outdated --format=legacy && exit 1; else exit 0; fi
- coverage run --source=api --omit=*migrations/*,*admin.py,*__init__.py,*.pyc manage.py test
- coverage html -i && grep pc_cov htmlcov/index.html | egrep -o "[0-9]+\%" | awk '{ print "covered " $1;}'
- echo "Done 😄"
......@@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [2.2] - 2019-01-29
## Fixed
- Default owner now assigned automatically to the current logged in user / field hidden
- Path buggy schedule checking timezone utilization
- Schedules may now have N open times
## Added
- Admin action to bulk apply schedules to facilities
- Changed default image
- Alert model refactored to support URLs, subjects, and bodies
- Add front royal location as an option
## Removed
- Mason Korea support dropped
- Deprecated previous Alert model
- Drop label support
- Drop Sodoxo classifier
- Drop schedule promotion
- Drop schedule deletion
## [2.1.1] - 2017-01-13
## Fixed
......@@ -31,4 +55,4 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Special Schedules can start at a specific time on a date
[2.1.0]: https://git.gmu.edu/srct/whats-open/compare/2.0...2.1
[2.1.1]: https://git.gmu.edu/srct/whats-open/compare/2.1...2.1.1
\ No newline at end of file
[2.1.1]: https://git.gmu.edu/srct/whats-open/compare/2.1...2.1.1
This diff is collapsed.
......@@ -3,12 +3,19 @@
############################################################
# Set the base image to Ubuntu
FROM python:3.6
FROM python:3.7
ENV PYTHONUNBUFFERED 1
# Update the sources list
RUN apt-get update
RUN apt-get install netcat libgdal1h libproj-dev proj-data proj-bin -y
# Update the sources list and install all packages
# See: https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run
RUN apt-get update && apt-get install -y \
netcat \
libproj-dev \
proj-data \
proj-bin \
binutils \
gdal-bin \
&& rm -rf /var/lib/apt/lists/*
# Copy over all project files into /whats_open
RUN mkdir /whats-open/
......@@ -16,4 +23,5 @@ WORKDIR /whats-open/
ADD . /whats-open/
# Pip install all required dependecies
RUN pip install -r /whats-open/requirements/base.txt
RUN pip install pipenv
RUN pipenv install --system --deploy
\ No newline at end of file
......@@ -186,7 +186,7 @@ Apache License
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2017 George Mason University Student-Run Computing and Technology
Copyright 2018 George Mason University Student-Run Computing and Technology
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
......
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
black = "*"
pylint = "*"
pylint-django = "*"
[packages]
django-autoslug-iplweb = "*"
django-cas-client = "==1.3.0"
djangorestframework = "==3.7.7"
django-model-utils = "==3.0.0"
mysqlclient = "==1.3.12"
django-taggit = "==0.22.2"
django-taggit-serializer = "==0.1.5"
djangorestframework-gis = "==0.12.0"
django-filter = "==1.0.4"
django-crispy-forms = "==1.7.0"
coreapi = "==2.3.3"
urllib3 = "==1.22"
docutils = "==0.13.1"
gunicorn = "*"
Django = "<2.1,>=2.0"
Markdown = "==2.6.10"
[requires]
python_version = "3.7"
[pipenv]
allow_prereleases = true
This diff is collapsed.
......@@ -23,10 +23,11 @@ contribute, so if you are struggling, or just want to learn, then we are
willing to help.
Check out some of the other What's Open projects!
- https://git.gmu.edu/srct/whats-open-android
- https://git.gmu.edu/srct/whats-open-ios
- https://git.gmu.edu/srct/whats-open-web
- https://git.gmu.edu/srct/whats-open-alexa
- https://git.gmu.edu/srct/whats-open-android
- https://git.gmu.edu/srct/whats-open-ios
- https://git.gmu.edu/srct/whats-open-web
- https://git.gmu.edu/srct/whats-open-alexa
# Setup instructions for local development
......@@ -113,9 +114,9 @@ environments across machines.
Installing Docker on your system:
- For macOS: https://docs.docker.com/docker-for-mac/
- For Windows: https://docs.docker.com/docker-for-windows/
- For your specific \*nix distro: https://docs.docker.com/engine/installation/
- For macOS: https://docs.docker.com/docker-for-mac/
- For Windows: https://docs.docker.com/docker-for-windows/
- For your specific \*nix distro: https://docs.docker.com/engine/installation/
Additionally, you will need to install docker-compose: https://docs.docker.com/compose/install/
......@@ -148,9 +149,10 @@ pass: admin
Manual Setup involves all of the same steps as Docker, but just done manually.
First, install python, pip, and virtualenv on your system.
* `python` is the programming language used for Django, the web framework used by whats-open.
* `pip` is the python package manager.
* `virtualenv` allows you to isolate pip packages within virtual environments
- `python` is the programming language used for Django, the web framework used by whats-open.
- `pip` is the python package manager.
- `virtualenv` allows you to isolate pip packages within virtual environments
Open a terminal and run the following command:
......@@ -167,10 +169,10 @@ You will also need the following `gdal` packages for GeoDjango support:
```
sudo add-apt-repository -y ppa:ubuntugis/ubuntugis-unstable
sudo apt update
sudo apt upgrade # if you already have gdal 1.11 installed
sudo apt install gdal-bin python-gdal python3-gdal # if you don't have gdal 1.11 already installed
```
sudo apt update
sudo apt upgrade # if you already have gdal 1.11 installed
sudo apt install gdal-bin python-gdal python3-gdal # if you don't have gdal 1.11 already installed
```
#### Database Setup
......@@ -216,18 +218,17 @@ Run:
GRANT ALL ON test_wopen.* TO 'wopen'@'localhost'; FLUSH PRIVILEGES;
When running test cases, django creates a test database so your 'real' database
doesn't get screwed up. This database is called 'test_' + whatever your normal
doesn't get screwed up. This database is called 'test\_' + whatever your normal
database is named. Note that for permissions it doesn't matter that this database
hasn't yet been created.
The .* is to grant access all tables in the database, and 'flush privileges'
The .\* is to grant access all tables in the database, and 'flush privileges'
reloads privileges to ensure that your user is ready to go.
Exit the mysql shell by typing:
exit
At this point we will need to set some environment variables.
If you are using bash then just copy paste the following into your terminal:
......
version: '3'
version: "3"
services:
db:
image: mysql
deploy:
replicas: 1
restart_policy:
condition: on-failure
networks:
- wopen_net
image: mysql:5.7
command: mysqld --character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
ports:
- "3306:3306"
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
MYSQL_DATABASE: wopen
MYSQL_USER: wopen
MYSQL_PASSWORD: wopen
api:
image: whats-open-api
deploy:
replicas: 1
restart_policy:
condition: on-failure
networks:
- wopen_net
build: .
ports:
- '8000:8000'
command: /bin/bash ./startup.sh
- "8000:8000"
command: ./docker-startup.sh
volumes:
- .:/whats-open
depends_on:
......@@ -39,6 +30,4 @@ services:
- WOPEN_DB_HOST=db
- WOPEN_DB_PORT=3306
- WOPEN_SUPERUSER=admin
networks:
wopen_net:
\ No newline at end of file
- WOPEN_ENV="dev"
......@@ -6,9 +6,8 @@ done
export WOPEN_SECRET_KEY=$(dd if=/dev/urandom count=100 | tr -dc "A-Za-z0-9" | fold -w 60 | head -n1 2>/dev/null)
python whats-open/manage.py flush --no-input
python whats-open/manage.py makemigrations
python whats-open/manage.py makemigrations api
python whats-open/manage.py migrate
echo "from django.contrib.auth.models import User; User.objects.filter(email='$WOPEN_SUPERUSER$WOPEN_EMAIL_DOMAIN').delete(); User.objects.create_superuser('$WOPEN_SUPERUSER$WOPEN_EMAIL_DOMAIN', '$WOPEN_SUPERUSER', 'admin')" | python whats-open/manage.py shell
echo "from django.contrib.auth.models import User; User.objects.filter(username='$WOPEN_SUPERUSER$WOPEN_EMAIL_DOMAIN').delete(); User.objects.create_superuser('$WOPEN_SUPERUSER$WOPEN_EMAIL_DOMAIN', '$WOPEN_SUPERUSER', 'admin')" | python whats-open/manage.py shell
python whats-open/manage.py runserver 0.0.0.0:8000
# This file is here because many Platforms as a Service look for
# requirements.txt in the root directory of a project.
-r requirements/production.txt
Django >= 2.0, < 2.1
django-autoslug-iplweb
django-cas-client==1.3.0
djangorestframework==3.7.7
django-model-utils==3.0.0
mysqlclient==1.3.12
setuptools==36.2.0
django-taggit==0.22.2
django-taggit-serializer==0.1.5
djangorestframework-gis==0.12.0
django-filter==1.0.4
django-crispy-forms==1.7.0
markdown==2.6.10
coreapi==2.3.3
urllib3==1.22
docutils==0.13.1
\ No newline at end of file
# Local development dependencies go here
-r test.txt
# Pro-tip: Try not to put anything here. There should be no dependency in
# production that isn't in development.
-r base.txt
gunicorn
\ No newline at end of file
# Test dependencies go here.
-r base.txt
coverage==4.4.1
......@@ -9,10 +9,16 @@ https://docs.djangoproject.com/en/1.11/ref/contrib/admin/
"""
# Django Imports
from django.contrib import admin
from django.contrib import messages
from django.contrib.gis.admin import OSMGeoAdmin
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpResponseRedirect
from django.shortcuts import render
# App Imports
from .models import Facility, Schedule, OpenTime, Category, Location, Alert
@admin.register(Facility)
class FacilityAdmin(admin.ModelAdmin):
"""
......@@ -20,41 +26,141 @@ class FacilityAdmin(admin.ModelAdmin):
Allows admins to create new facilities through the admin interface.
"""
def drop_special_schedules(self, request, queryset):
num = queryset.count()
for facility in queryset:
facility.special_schedules.clear()
self.message_user(
request,
"Successfully cleared all special schedules for %d facilities." % num,
)
drop_special_schedules.short_description = (
"Clear all special schedules for selected facilities"
)
def assign_bulk_schedules(self, request, queryset):
num = queryset.count()
# all admin actions-related requests are post requests, so we're looking for
# the one that has the associated value with our confirmation input button
if "bulk_schedule" in request.POST:
try:
new_schedule = Schedule.objects.get(pk=request.POST["schedule"])
name = new_schedule.name
for facility in queryset:
facility.main_schedule = new_schedule
facility.save()
self.message_user(
request,
"Set %s as the main schedule for %d facilities." % (name, num),
)
except ObjectDoesNotExist:
self.message_user(
request,
"Unable to set a new main schedule for %d facilities." % num,
level=messages.ERROR,
)
return HttpResponseRedirect(request.get_full_path())
return render(
request,
"bulk_schedules.html",
context={"facilities": queryset, "schedules": Schedule.objects.all()},
)
assign_bulk_schedules.short_description = (
"Set a main schedule for selected facilities"
)
def assign_bulk_special_schedules(self, request, queryset):
num = queryset.count()
if "bulk_special_schedule" in request.POST:
try:
new_special_schedule = Schedule.objects.get(
pk=request.POST["special_schedule"]
)
name = new_special_schedule.name
for facility in queryset:
facility.special_schedules.add(new_special_schedule)
facility.save()
self.message_user(
request,
"Added %s as a special schedule to %d facilities." % (name, num),
)
except ObjectDoesNotExist:
self.message_user(
request,
"Unable to add additional special schedule to %d facilities." % num,
level=messages.ERROR,
)
return HttpResponseRedirect(request.get_full_path())
return render(
request,
"bulk_special_schedules.html",
context={"facilities": queryset, "schedules": Schedule.objects.all()},
)
assign_bulk_special_schedules.short_description = (
"Add a special schedule to selected facilities"
)
# a list of all actions to be added
actions = [
drop_special_schedules,
assign_bulk_schedules,
assign_bulk_special_schedules,
]
# Allow filtering by the following fields
list_filter = ['facility_category', 'facility_location']
list_filter = ["facility_category", "facility_location"]
list_display = ("facility_name", "main_schedule", "modified")
# Modify the rendered layout of the "create a new facility" page
# We are basically reordering things to look nicer to the user here
fieldsets = (
(None, {
'fields': ('facility_name', 'logo', 'facility_category',
'facility_location', 'main_schedule', 'special_schedules',
('facility_product_tags', 'facility_labels',
'facility_classifier'),
'tapingo_url', 'phone_number', 'note', 'owners'),
}),
(
None,
{
"fields": (
"facility_name",
"logo",
"facility_category",
"facility_location",
"main_schedule",
"special_schedules",
("facility_product_tags", "facility_classifier"),
"tapingo_url",
"phone_number",
"note",
)
},
),
)
autocomplete_fields = ["main_schedule", "special_schedules"]
# despite the name of this method, ("change" seems to imply it would affect modify)
# it is called only when initially creating a model
def get_changeform_initial_data(self, request):
initial_data = super(FacilityAdmin, self).get_changeform_initial_data(request)
initial_data["owners"] = [request.user]
return initial_data
class OpenTimeInline(admin.TabularInline):
class OpenTimeInline(admin.StackedInline):
"""
A table of time periods that represent an "open time" for a Facility.
https://docs.djangoproject.com/en/1.11/ref/contrib/admin/#django.contrib.admin.TabularInline
"""
# Columns correspond to each attribute in the OpenTime table
model = OpenTime
# 7 days of the week, so only have 7 rows
max_num = 7
extra = 7
extra = 1
# We are basically reordering things to look nicer to the user here
fieldsets = (
(None, {
'fields': (
('start_day', 'start_time'),
('end_day', 'end_time')
),
}),
(None, {"fields": (("start_day", "start_time"), ("end_day", "end_time"))}),
)
@admin.register(Schedule)
class ScheduleAdmin(admin.ModelAdmin):
"""
......@@ -64,22 +170,28 @@ class ScheduleAdmin(admin.ModelAdmin):
Additionally, we append the OpenTimeInline table to allow for open times to
be defined for the schedule we are creating.
"""
# Allow filtering by the following fields
list_display = ['name', 'modified']
list_display = ["name", "modified"]
# Append the OpenTimeInline table to the end of our admin panel
inlines = [OpenTimeInline, ]
inlines = [OpenTimeInline]
# Modify the rendered layout of the "create a new facility" page
fieldsets = (
(None, {
'fields': ('name',
# Pair valid_start and valid_end together on the same
# line
('valid_start', 'valid_end'),
'twenty_four_hours',
'schedule_for_removal',
'promote_to_main')
}),
(
None,
{
"fields": (
"name",
# Pair valid_start and valid_end together on the same line
("valid_start", "valid_end"),
"twenty_four_hours",
)
},
),
)
search_fields = ["name"] # search terms for autcomplete
ordering = ["name"] # autocomplete ordering
# https://docs.djangoproject.com/en/1.11/ref/contrib/gis/admin/#osmgeoadmin
OSMGeoAdmin.default_lon = -8605757.16502
......@@ -89,3 +201,7 @@ admin.site.register(Location, OSMGeoAdmin)
# Use the default ModelAdmin interface for these
admin.site.register(Category)
admin.site.register(Alert)
admin.site.site_header = "What's Open API"
admin.site.site_title = "What's Open API"
admin.site.index_title = "Admin"
This diff is collapsed.
# Generated by Django 2.0.10 on 2019-01-26 18:06
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('api', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='stupidlabelhack',
name='content_type',
),
migrations.RemoveField(
model_name='stupidlabelhack',
name='tag',
),
migrations.RemoveField(
model_name='facility',
name='facility_labels',
),
migrations.DeleteModel(
name='StupidFacilityLabelHack',
),
migrations.DeleteModel(
name='StupidLabelHack',
),
]
# Generated by Django 2.0.10 on 2019-01-26 18:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0002_auto_20190126_1306'),
]
operations = [
migrations.AlterField(
model_name='facility',
name='facility_classifier',
field=models.CharField(blank=True, choices=[('shopmason', 'shopMason Facility')], help_text="Tag this facility to be shown on the ShopMason What's Open sites.", max_length=100),
),
migrations.AlterField(
model_name='location',
name='campus_region',
field=models.CharField(choices=[('front royal', 'Front Royal'), ('prince william', 'Prince William County Science and Technology'), ('fairfax', 'Fairfax'), ('arlington', 'Arlington')], max_length=100),
),
]
# Generated by Django 2.0.10 on 2019-01-26 20:35
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('api', '0003_auto_20190126_1321'),
]
operations = [
migrations.RemoveField(
model_name='schedule',
name='promote_to_main',
),
migrations.RemoveField(
model_name='schedule',
name='schedule_for_removal',
),
]
# Generated by Django 2.0.10 on 2019-01-29 17:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0004_auto_20190126_1535'),
]
operations = [
migrations.AlterField(
model_name='alert',
name='urgency_tag',
field=models.CharField(choices=[('info', 'Advertising / Announcement'), ('minor', 'Expected Hours Change'), ('major', '(Small Scale) Unexpected Hours Change'), ('emergency', '(University Wide) Unexpected Hours Change')], default='Info', max_length=10),
),
migrations.AlterField(
model_name='alert',
name='url',
field=models.URLField(verbose_name='Reference URL'),
),
]
This diff is collapsed.
......@@ -8,54 +8,70 @@ can then be easily rendered into JSON, XML or other content types.
http://www.django-rest-framework.org/api-guide/serializers
"""
# App Imports
from .models import Category, Facility, Schedule, OpenTime, Location, Alert
# Other Imports
from rest_framework import serializers
from taggit_serializer.serializers import TagListSerializerField
# App Imports
from .models import Category, Facility, Schedule, OpenTime, Location, Alert
class AlertSerializer(serializers.ModelSerializer):
"""
"""
class Meta:
model = Alert
fields = '__all__'
fields = "__all__"
class CategorySerializer(serializers.ModelSerializer):
"""
"""
class Meta:
# Choose the model to be serialized
model = Category
# Serialize all of the fields
fields = '__all__'
fields = "__all__"
class LocationSerializer(serializers.ModelSerializer):
"""
Serializer for the Location model.
"""
class Meta:
# Choose the model to be serialized
model = Location
# Serialize all of the fields
fields = '__all__'
fields = "__all__"
class OpenTimeSerializer(serializers.ModelSerializer):
"""
Serializer for the OpenTime model.
"""
class Meta:
# Choose the model to be serialized
model = OpenTime
# Serialize all of the fields
fields = ('schedule', 'modified',
'start_day', 'end_day', 'start_time', 'end_time')
fields = (
"schedule",
"modified",