Compare commits

..

76 commits

Author SHA1 Message Date
1ef4aa5e9f Merge branch 'main' of github.com:C4theBomb/csmb-housewars 2024-02-26 21:00:02 -06:00
54d556f565 Modified settings to work with heroku 2024-02-26 20:59:57 -06:00
b0d049555c Modified settings to work with heroku 2024-02-26 20:55:02 -06:00
ffa49895e7 Update production settings and docker-compose.yml 2024-02-19 23:31:51 -06:00
b76a9c15c6 Add Docker credentials and fix CSRF trusted origin 2024-02-18 19:29:42 -06:00
4cb149f279 Update Docker build and push workflow and Dockerfile 2024-02-18 19:14:50 -06:00
a39e51e1f5 Updated test suites 2024-02-18 19:12:29 -06:00
cbabafb492 Added new admin commands to make next housewars easier 2024-02-18 19:12:29 -06:00
12e6edc3dc Turn debug off 2024-02-18 19:12:25 -06:00
76ef47caaf Added the ability to mass export activity attendance sheets 2024-02-18 19:12:25 -06:00
69902022f7 Changed activity listing and return format 2024-02-18 19:12:25 -06:00
6c7c5ad188 Updated csv magic button 2024-02-18 19:12:25 -06:00
931fd77ac2 Added staticfiles to gitignore 2024-02-18 19:12:25 -06:00
4ed203aa23 Added test for activities allowed time 2024-02-18 19:12:24 -06:00
3ec387babb Added non-field errors to form alerts 2024-02-18 19:12:24 -06:00
5be429a113 Updated UI to include new slideshow 2024-02-18 19:12:24 -06:00
403a4f1a6d Updated tests to disable GPU and not use static screen sizes 2024-02-18 19:12:24 -06:00
64f38f3aa4 Added mobile layout to new components 2024-02-18 19:12:24 -06:00
9037540988 Updated dev and prod to use static secret keys across dev environments 2024-02-18 19:12:24 -06:00
c27269f406 Updated forms to banner type 2024-02-18 19:12:24 -06:00
3be7669343 Updated tests to use static window sizes 2024-02-18 19:12:24 -06:00
3b0c16665f Update LICENSE 2024-02-18 19:12:24 -06:00
c696038321 Fixed new tests to pass with new implementations 2024-02-18 19:12:24 -06:00
b815899101 Added facilitator form 2024-02-18 19:12:01 -06:00
298530599e Fixed site to use simpler templates 2024-02-18 19:12:01 -06:00
acf802c7fa Generalized template titles and forms 2024-02-18 19:12:01 -06:00
fbd52bbbf7 Added room number to return 2024-02-18 19:12:01 -06:00
a7e1c87e82 Added room numbers to export to pdf/csv 2024-02-18 19:12:01 -06:00
df75d7ffc1 Added activity return to views page 2024-02-18 19:12:01 -06:00
549c152c1d Added teachers and room # to success strings 2024-02-18 19:12:01 -06:00
58e3b16455 Wrote full test suite for housewars app 2024-02-18 19:12:01 -06:00
63a28a6813 Added selenium 2024-02-18 19:12:01 -06:00
25bf93b1bc Modified form comments 2024-02-18 19:12:01 -06:00
b134a20813 Changed documentation to reStructuredText strings 2024-02-18 19:12:01 -06:00
5f06b0c9be Removed unique_together from points entries 2024-02-18 19:12:01 -06:00
8aabb4f46d Updated award field 2024-02-18 19:12:01 -06:00
d9d0057fa4 Added ability to see current house points 2024-02-18 19:12:01 -06:00
6cd4bb0075 Updated github workflows 2024-02-18 19:12:01 -06:00
c9f9af6103 Added function to export to pdf 2024-02-18 19:12:01 -06:00
e5549cfc24 More dynamic forms and templates 2024-02-18 19:12:01 -06:00
7666bd3973 Updated models with new requirements 2024-02-18 19:12:01 -06:00
cc18ace04b Added smart-selects 2024-02-18 19:12:01 -06:00
68ea97070b Added model based enforcements 2024-02-18 19:12:01 -06:00
8d2ca26860 Remove null activities from form options 2024-02-18 19:12:01 -06:00
820ba17513 Isolated individual admin files 2024-02-18 19:12:01 -06:00
0849cfa25a Added functionality to finalize house points 2024-02-18 19:12:01 -06:00
ea9341f583 Changed admin site title 2024-02-18 19:12:01 -06:00
f01eb322aa Updated docker compose to use .env file 2024-02-18 19:12:01 -06:00
d52d2ad78b Updated admin with new filters 2024-02-18 19:12:01 -06:00
ddcc6af4bd Updated models to include new fields 2024-02-18 19:12:01 -06:00
60b9ea5cdf Added healthcheck to docker compose 2024-02-18 19:12:01 -06:00
67fd153f2c Modified host value of database 2024-02-18 19:12:01 -06:00
8bdc94be04 Added Procfile 2024-02-18 19:12:01 -06:00
7a7186c9ee Fixed model route 2024-02-18 19:12:01 -06:00
5ff0814b85 Modified models and added full_clean to save 2024-02-18 19:12:01 -06:00
ee2ca7eef6 Removed verbosity from test script 2024-02-18 19:12:01 -06:00
8a46f34190 Separated models 2024-02-18 19:12:01 -06:00
9ff327a641 Added test (test) suite 2024-02-18 19:12:01 -06:00
5bbfcf79bf Updated docker settings 2024-02-18 19:12:01 -06:00
03a3c5a35b Added test suite to main app 2024-02-18 19:12:01 -06:00
583276ec9b Implemented github actions for CI/CD 2024-02-18 19:12:01 -06:00
1be0d9d5c6 Fixed test dockerfile 2024-02-18 19:12:01 -06:00
3661c4e60f Implemented docker to work for better CI/CD 2024-02-18 19:11:46 -06:00
2f31bf39e6 Removed static folder 2024-02-18 19:11:46 -06:00
6610a5ff28 Added .env section to README 2024-02-18 19:11:41 -06:00
45c27edf74 Added functionality for test database. 2024-02-18 19:11:41 -06:00
1b96b62173 Added README with installation and usage instructions 2024-02-18 19:11:41 -06:00
4f9c1b5aec Created .gitattributes file 2024-02-18 19:11:41 -06:00
01e404923a Added form to allow incrementation of house points 2024-02-18 19:11:41 -06:00
5503ee1bd7 Added House and Points Entry models 2024-02-18 19:11:41 -06:00
3cc29e3ad6 Basic styling on all forms 2024-02-18 19:11:41 -06:00
f3ea1d1d62 Updated project dependencies 2024-02-18 19:11:41 -06:00
44bda77674 Added deployment settings to settings.py 2024-02-18 19:11:41 -06:00
03741a236b Generated static files and modified settings.py 2024-02-18 19:11:23 -06:00
02bb53d84e Working model logic and views 2024-02-18 19:11:23 -06:00
a6441ffb63 Mutated database tables to have activity1 and activity2 2024-02-18 19:11:23 -06:00
120 changed files with 2544 additions and 1396 deletions

View file

@ -1 +1,2 @@
venv
venv
.env

View file

@ -1,30 +1,30 @@
name-template: 'v$RESOLVED_VERSION'
tag-template: 'v$RESOLVED_VERSION'
categories:
- title: 'Features'
labels:
- 'feature'
- 'enhancement'
- title: 'Bug Fixes'
labels:
- 'fix'
- 'bugfix'
- 'bug'
- title: 'Maintenance'
label: 'chore'
- title: 'Features'
labels:
- 'feature'
- 'enhancement'
- title: 'Bug Fixes'
labels:
- 'fix'
- 'bugfix'
- 'bug'
- title: 'Maintenance'
label: 'chore'
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
change-title-escapes: '\<*_&#@' # You can add # and @ to disable mentions, and add ` to disable code blocks.
version-resolver:
major:
labels:
- 'major'
minor:
labels:
- 'minor'
patch:
labels:
- 'patch'
default: patch
major:
labels:
- 'major'
minor:
labels:
- 'minor'
patch:
labels:
- 'patch'
default: patch
template: |
# Changes
$CHANGES
# Changes
$CHANGES

55
.github/workflows/build-deploy.yml vendored Normal file
View file

@ -0,0 +1,55 @@
name: Docker Build and Push
on:
push:
branches:
- main
tags:
- v*
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Setup Chrome
uses: nanasess/setup-chromedriver@v2
- name: Install dependencies
run: python -m pip install -r requirements.txt
- name: Test build
run: python manage.py test
build-and-push:
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Build & push Docker image
uses: mr-smithers-excellent/docker-build-push@v6
with:
image: c4thebomb/csmb-housewars
registry: docker.io
addLatest: true
multiPlatform: true
platform: linux/amd64,linux/arm64/v8
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
draft:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v3
- name: Draft release
uses: release-drafter/release-drafter@v5
id: release-draft
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -1,42 +0,0 @@
name: Commit docker image
on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:
jobs:
build:
environment: production
runs-on: ubuntu-latest
steps:
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Checkout
uses: actions/checkout@v2
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
builder: ${{ steps.buildx.outputs.name }}
push: true
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/collegiate-housewars:latest
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}

View file

@ -1,18 +0,0 @@
name: Create release draft
on:
push:
branches: [development]
workflow_dispatch:
jobs:
draft:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Draft release
uses: release-drafter/release-drafter@v5
id: release-draft
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -1,27 +0,0 @@
name: Test build
on:
push:
branches: [development, main]
pull_request:
workflow_dispatch:
jobs:
test:
environment: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Write .env to build
env:
DOTENV: '${{ secrets.DOTENV }}'
run: echo "$DOTENV" >> .env
- name: Setup python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: python -m pip install -r requirements.txt
- name: Test build
run: python manage.py test

12
.gitignore vendored
View file

@ -1,4 +1,4 @@
staticfiles/
# Created by https://www.toptal.com/developers/gitignore/api/django,python,pydev,venv
# Edit at https://www.toptal.com/developers/gitignore?templates=django,python,pydev,venv
@ -260,14 +260,6 @@ cython_debug/
### venv ###
# Virtualenv
# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
[Bb]in
[Ii]nclude
[Ll]ib
[Ll]ib64
[Ll]ocal
[Ss]cripts
pyvenv.cfg
pip-selfcheck.json
venv
# End of https://www.toptal.com/developers/gitignore/api/django,python,pydev,venv

View file

@ -2,5 +2,12 @@
"emmet.includeLanguages": {
"jinja-html": "html"
},
"editor.defaultFormatter": null
"editor.defaultFormatter": null,
"[dockercompose]": {
"editor.defaultFormatter": "ms-azuretools.vscode-docker"
},
"lldb.displayFormat": "auto",
"lldb.showDisassembly": "auto",
"lldb.dereferencePointers": true,
"lldb.consoleMode": "commands"
}

View file

@ -1,3 +1 @@
from . import dev
from . import prod
from . import test
from .local import *

View file

@ -1,31 +1,12 @@
from pathlib import Path
import os
from django.contrib.messages import constants
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
DOTENV_FILE = BASE_DIR / '.env'
SECRET_KEY = os.environ.get('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
SECURE_SSL_REDIRECT = False
SESSION_COOKIE_SECURE = False
CSRF_COOKIE_SECURE = False
ALLOWED_HOSTS = [
'localhost',
'127.0.0.1',
]
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
@ -34,13 +15,15 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'smart_selects',
'formtools',
'housewars.apps.HousewarsConfig'
'housewars',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
@ -69,21 +52,6 @@ TEMPLATES = [
WSGI_APPLICATION = 'CollegiateHouseWars.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': '/var/www/housewars/db/db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
@ -99,10 +67,6 @@ AUTH_PASSWORD_VALIDATORS = [
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
@ -111,20 +75,26 @@ USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/
STATIC_URL = 'static/'
STATIC_ROOT = '/var/www/collegiate-housewars/static/'
STATIC_ROOT = BASE_DIR / "staticfiles"
STATICFILES_DIRS = [
BASE_DIR / "static"
]
# Default primary key field type
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'root': {
'handlers': ['console'],
'level': 'WARNING',
},
}
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
@ -134,3 +104,5 @@ MESSAGE_TAGS = {constants.DEBUG: 'debug',
constants.SUCCESS: 'success',
constants.WARNING: 'warning',
constants.ERROR: 'danger', }
USE_DJANGO_JQUERY = True

View file

@ -1,141 +0,0 @@
from pathlib import Path
import os
from django.contrib.messages import constants
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
DOTENV_FILE = BASE_DIR / '.env'
SECRET_KEY = os.environ.get('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
SECURE_SSL_REDIRECT = False
SESSION_COOKIE_SECURE = False
CSRF_COOKIE_SECURE = False
ALLOWED_HOSTS = [
'localhost',
'127.0.0.1',
]
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'formtools',
'housewars.apps.HousewarsConfig'
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'CollegiateHouseWars.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'CollegiateHouseWars.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'dev',
'USER': os.environ.get('DATABASE_USERNAME'),
'PASSWORD': os.environ.get('DATABASE_PASSWORD'),
'HOST': 'mysqldb',
'PORT': '3306'
}
}
# Password validation
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/
STATIC_URL = 'static/'
STATIC_ROOT = '/var/www/collegiate-housewars/static/'
STATICFILES_DIRS = [
BASE_DIR / "static"
]
# Default primary key field type
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
MESSAGE_TAGS = {constants.DEBUG: 'debug',
constants.INFO: 'info',
constants.SUCCESS: 'success',
constants.WARNING: 'warning',
constants.ERROR: 'danger', }

View file

@ -0,0 +1,12 @@
from .local import *
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'dev',
'USER': 'root',
'PASSWORD': 'passw0rd',
'HOST': 'mysqldb',
'PORT': '3306'
}
}

View file

@ -0,0 +1,17 @@
from .defaults import *
SECRET_KEY = '*9r50c74r58p#*tnfpadsw*cp&$(^sj585w1u@!y9*eowmwl1*'
DEBUG = True
ALLOWED_HOSTS = [
'localhost',
'127.0.0.1',
]
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}

View file

@ -1,141 +1,29 @@
from pathlib import Path
import os
from django.contrib.messages import constants
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent.parent
from .defaults import *
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
DOTENV_FILE = BASE_DIR / '.env'
SECRET_KEY = os.environ.get('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
SECURE_SSL_REDIRECT = False
SESSION_COOKIE_SECURE = False
CSRF_COOKIE_SECURE = False
ALLOWED_HOSTS = [
'localhost',
'127.0.0.1',
'csmb-housewars.c4patino.com',
'collegiate-housewars-02810c3f29dc.herokuapp.com',
]
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'formtools',
'housewars.apps.HousewarsConfig'
CSRF_TRUSTED_ORIGINS = [
'http://collegiate-housewars-02810c3f29dc.herokuapp.com/'
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'CollegiateHouseWars.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'CollegiateHouseWars.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'prod',
'NAME': os.environ.get('DATABASE_NAME'),
'USER': os.environ.get('DATABASE_USERNAME'),
'PASSWORD': os.environ.get('DATABASE_PASSWORD'),
'HOST': 'mysqldb',
'HOST': os.environ.get('DATABASE_HOST'),
'PORT': '3306'
}
}
# Password validation
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/
STATIC_URL = 'static/'
STATIC_ROOT = '/var/www/housewars/static/'
STATICFILES_DIRS = [
BASE_DIR / "static"
]
# Default primary key field type
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
MESSAGE_TAGS = {constants.DEBUG: 'debug',
constants.INFO: 'info',
constants.SUCCESS: 'success',
constants.WARNING: 'warning',
constants.ERROR: 'danger', }
STATICFILES_STORAGE = "whitenoise.storage.CompressedStaticFilesStorage"

View file

@ -21,5 +21,6 @@ from django.conf.urls.static import static
urlpatterns = [
path('', include('housewars.urls')),
path('chaining/', include('smart_selects.urls')),
path('admin/', admin.site.urls),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

View file

@ -10,7 +10,9 @@ https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/
import os
from django.core.wsgi import get_wsgi_application
from whitenoise import WhiteNoise
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'CollegiateHouseWars.settings')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'CollegiateHouseWars.settings.prod')
application = get_wsgi_application()
application = WhiteNoise(application)

View file

@ -1,9 +1,20 @@
# syntax=docker/dockerfile:1
FROM python:3.10.6-slim-buster
RUN apt update && apt install build-essential default-libmysqlclient-dev -y
FROM python:3.10
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
WORKDIR /app
COPY requirements.txt requirements.txt
RUN python -m pip install -r requirements.txt
RUN pip install --upgrade pip && \
pip install -r requirements.txt && \
pip install gunicorn
COPY . .
RUN python manage.py collectstatic --noinput
EXPOSE 8000/tcp
CMD python manage.py runserver 0.0.0.0:8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "CollegiateHouseWars.wsgi:application"]

17
Dockerfile.dev Normal file
View file

@ -0,0 +1,17 @@
FROM python:3.10
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
WORKDIR /app
COPY requirements.txt requirements.txt
RUN pip install --upgrade pip && \
pip install -r requirements.txt
COPY . .
EXPOSE 8000/tcp
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

214
LICENSE
View file

@ -1,21 +1,201 @@
MIT License
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Copyright (c) 2022 C4 Patino
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
1. Definitions.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2022 C4 Patino
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

1
Procfile Normal file
View file

@ -0,0 +1 @@
web: gunicorn CollegiateHouseWars.wsgi

View file

@ -1,50 +1,86 @@
# Collegiate Housewars
Welcome to the repository for the Collegiate School of Medicine and Bioscience Housewars site. This repository is open-source, so feel free to download, play around, or contribute. Below is a guide detailing the basic usage of the site and instructions for installation of a local server. Enjoy!
Welcome to the repository for the Collegiate School of Medicine and Bioscience House Wars site. This is a project that is completely made in the Python Django framework, and should contain all the features that are necessary to run House Wars (if it doesn't please let us know). This repository is open-source, so feel free to download, play around, and/or contribute. Below is a guide detailing the basic usage of the site and instructions for installation of a local development server. Enjoy!
| Table of Contents |
| ------- |
| [Usage](#Usage-Instructions) |
| [Installation](#Installation) |
| [Defining .env](#Defining-env) |
| [Contributing](#Contributing) |
| [Usage](#usage-instructions) |
| [Setup](#setup) |
| [Hosting](#hosting) |
| [Contributing](#contributing) |
## Usage Instructions
So far there are three primary pages on this site. They include the:
1. Admin page - this is accessed through `/admin`, and contains the function for managing the activities in the latest housewars and managing points totals and student signups.
2. Student signup form - this is accessed at `/`, and contains the form needed for students to signup for housewars activities.
3. Teacher points form - this is accessed at `/points` and contains the form needed for teachers to add points to houses for activities.
1. Student signup form - this is accessed at `/`, and contains the form for students to signup for House Wars activities.
2. Teacher points form - this is accessed at `/points` and contains the form to award points for activities.
3. Facilitator signup form - this is accessed at `/facilitator` and contains the form for volunteers to signup to facilitate activities.
4. Admin - this is accessed through `/admin`, and contains the function for managing everything related to House Wars that the general population should not be able to see.
## Installation
Welcome to the installation section of the guide. This will walk you through installing the site and spinning up a local development server.
## Setup
Welcome to the installation section of the guide. This will walk you through installing the site and spinning up a local development server. There are two methods to settings up the development server. One uses docker and the other just boots up a local development server. I would highly recommend setting up docker for contributing, it is used by very many other projects and is a great tool for development.
- [With Docker](#setup-with-docker)
- [Without Docker](#setup-without-docker)
### Prerequisites:
- git - You can test if you have git installed using the command `git -v`, which should output a version number. If you do not have it installed you can install it [here](https://git-scm.com/downloads).
- python - You can test if you have python installed by typing `python -v` in the terminal. The resulting output should be a version number. If you do not have python installed, you can download it [here](https://www.python.org/downloads/).
- python-pip - Test if you have it installed by typing `python -m pip --version`. You should see a version number for the current version of pip. There are two ways to download it if you do not have it. You can either run the command `python -m ensurepip --upgrade` if your python installation contains the ensurepip module. Otherwise, you will have to download the [get-pip.py](https://bootstrap.pypa.io/get-pip.py) file from online and run that.
- virtualenv - You will need to install virtualenv by using the command `python -m virtualenv <virtualenv name> <root project directory>`.
### Setup with Docker:
#### Prerequisites:
- `git` - You can test if you have git installed using the command `git -v`, which should output a version number. If you do not have git installed you can download it [here](https://git-scm.com/downloads).
- `docker` - You can test if you have docker installed by typing `docker -v` in the terminal. The resulting output should be a version number. If you do not have docker installed, you can download it [here](https://www.docker.com/get-started/).
### Instructions
1. Clone the repository into the desired directory using `git clone https://github.com/C4theBomb/collegiate-housewars.git`
2. Navigate into the repository using `cd collegiate-housewars`
3. Run the command `python -m virtualenv <virtual env name>`, then activate the environment using `./<virtual env name>/Scripts/activate`.
4. Install all the required packages using `pip install -r requirements.txt`.
5. Run migrations and create a development sqlite3 database using `python manage.py migrate`.
6. Server should be available to run. You can activate the server using `python manage.py runserver`. The site will be accessible using the [default url](http://localhost:8000).
#### Instructions
1. Clone the repository into the desired directory using `git clone https://github.com/C4theBomb/collegiate-housewars.git`.
2. Navigate into the repository using `cd collegiate-housewars`.
3. Open Docker and run the command `docker compose up -d` in a shell.
4. In Docker, open the shell of the `app-1`.
5. Server should be running automatically. The site will be accessible using the [default url](http://localhost:8000). Any changes made in the filesystem will update live on the local server.
## Defining .env
The `.env` file is used to secure data and keep it from entering a cloud, open-acccess environment. Because of this, you will need to define a `.env` file yourself. The parameters that you will need are:
- SECRET_KEY - This will contain the server key of your django local server. It can be generated using the command `python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'`.
#### Relevant Commands:
- `docker compose up`: This will run the docker server on the default url.
- `-d`: Removed terminal output from the command (disconnected mode). AKA docker doesn't steal your terminal
- `docker compose down`: This will stop the currently running containers and free up other ports for other apps (if you have them).
- `-v`: Removes volumes as well. Docker volumes can get very large and this will wipe all data contained in your database. WARNING: IRREVERSIBLE.
### Setup without Docker:
#### Prerequisites:
- `git` - You can test if you have git installed using the command `git -v`, which should output a version number. If you do not have git installed you can download it [here](https://git-scm.com/downloads).
- `python` - You can test if you have docker installed by typing `docker -v` in the terminal. The resulting output should be a version number. If you do not have docker installed, you can download it [here](https://www.docker.com/get-started/).
- `pip` - You can test if you have pip installed using `python -m pip --version`. If you do not have it installed then installation instructions can be found [here](https://pip.pypa.io/en/stable/installation/).
#### Instructions:
1. Clone the repository into the desired directory using `git clone https://github.com/C4theBomb/collegiate-housewars.git`.
2. Navigate into the repository using `cd collegiate-housewars`.
3. Install virtualenv by running `python -m pip install virtualenv`.
4. Create a virtualenv using the command `python -m virtualenv venv`.
5. Activate the virtualenvironment using the command for your os:
- Linux/Mac: `source ./venv/bin/activate`
- Windows: `.\venv\Scripts\activate`
6. Install dependencies using the command `pip install -r requirements.txt`.
7. Run migrations using the command `python manage.py migrate`.
8. Run the local development server using `python manage.py runserver`.
### Relevant Commands:
- `python manage.py test`: Runs all tests for current features of the site. If something fails, your broke something, please fix it. If you think its not your fault, it probably is. However, if your REALLY think its not your fault, create an issue and we will try our best to resolve it as soon as possible.
- `python manage.py makemigrations`: If you end up making any changes to the files in `housewars/models`, please run this command to log your changes. This will allow us to make the relevant changes to our production database and display the new features on the site.
## Hosting
### Options
There are several options that you can use for hosting the site on a production server. My personal recommendation would either be Heroku or Linode, since it comes pre-configured with the website. Linode is very cheap at only $5 a month, while Heroku is completely free (albeit more restricted).
### Environment Variables
The server environment variables are used to secure data and keep it from entering a cloud, open-acccess environment. Because of this, you will need to define a them file yourself. These are only needed in production and are completely unnecessary if you are using a local development server. The varaibles that you will need are:
- SECRET_KEY - This will contain the secret encryption key of your django server. It can be generated using the command `python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'`.
- DATABASE_USERNAME - This will be the username of your production database.
- DATABASE_PASSWORD - This will be the password of your production database.
- DATABASE_HOST - This will be IPv4/IPv6 address of your production database.
- DATABASE_NAME - This will be the name of your production database.
- DATABASE_HOST - This will be the IP address of your production database.
## Contributing
As an open-source school repository we welcome all contributors willing to help make our website better.
As an open-source school repository we welcome all contributors willing to help make the House Wars website better (by hacking it or otherwise).
### Instructions
1. Follow the [installation instructions above](#Installation) to install the required packages and run the local server. You will want to fork the repository before cloning it.
1. Follow the [setup instructions above](#setup) to install the required dependencies and run the local server. You will want to fork the repository and clone your own repository.
2. Commit your new features to your new forked repository.
3. Create a pull request that details the changes/improvements that you have made.
4. Wait for @C4theBomb to open discussion/merge your pull request.
4. Wait for @C4theBomb (C4 Patino), @gywn9081 (Henry Bloch), or @Slayer121 (Dylan Fritz) to open discussion or merge your pull request.
5. After your pull request is merged, it will automatically be uploaded into production code and will show up on the website.

View file

@ -1,35 +1,27 @@
version: '3.8'
name: 'collegiate-housewars'
services:
app:
restart: unless-stopped
build:
context: .
dockerfile: Dockerfile
platforms:
- linux/amd64
- linux/arm64/v8
image: c4thebomb/csmb-housewars:${TAG:-latest}
env_file:
- path: .env
required: true
ports:
- 8000:8000
volumes:
- ./:/app
- 127.0.0.1:8000:8000
networks:
- housewars
env_file: .env
environment:
DJANGO_SETTINGS_MODULE: "CollegiateHouseWars.settings.prod"
depends_on:
- common-network
external_links:
- mysqldb
mysqldb:
restart: unless-stopped
image: mysql:5.7
ports:
- 3306:3306
volumes:
- housewars-mysql:/var/lib/mysql
networks:
- housewars
environment:
MYSQL_ROOT_PASSWORD: "${DATABASE_PASSWORD}"
MYSQL_DATABASE: prod
volumes:
housewars-mysql:
networks:
common-network:
external: true
housewars: {}

View file

@ -1,18 +0,0 @@
version: '3.8'
name: 'collegiate-housewars'
services:
app:
build:
context: .
ports:
- 8000:8000
volumes:
- ./:/app
- housewars-sqlite3:/var/www/housewars/db
env_file: .env
environment:
DJANGO_SETTINGS_MODULE: "CollegiateHouseWars.settings.test"
command: python manage.py test
volumes:
housewars-sqlite3:

View file

@ -1,25 +1,27 @@
version: '3.8'
name: 'collegiate-housewars'
services:
app:
restart: unless-stopped
build:
context: .
dockerfile: Dockerfile.dev
ports:
- 8000:8000
volumes:
- ./:/app
networks:
- housewars
env_file: .env
environment:
DJANGO_SETTINGS_MODULE: "CollegiateHouseWars.settings.dev"
DJANGO_SETTINGS_MODULE: "CollegiateHouseWars.settings.docker"
depends_on:
- mysqldb
mysqldb:
condition: service_healthy
command: >
bash -c "python manage.py migrate && python manage.py runserver 0.0.0.0:8000"
mysqldb:
restart: unless-stopped
image: mysql:5.7
image: mysql:8.3
ports:
- 3306:3306
volumes:
@ -27,9 +29,16 @@ services:
networks:
- housewars
environment:
MYSQL_ROOT_PASSWORD: "${DATABASE_PASSWORD}"
MYSQL_ROOT_PASSWORD: "passw0rd"
MYSQL_DATABASE: dev
healthcheck:
test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ]
timeout: 5s
retries: 10
volumes:
housewars-mysql:
networks:
housewars: {}

View file

@ -1,66 +0,0 @@
from django.contrib import admin
from django.db.models import Sum
from django.db.models.functions import Coalesce
from .models import Activity, House, PointsEntry, UserEntry
@admin.register(Activity)
class ActivityAdmin(admin.ModelAdmin):
fieldsets = [
(None, {'fields': ['name']}),
('Activity Information', {
'fields': ['quota', 'time']}),
]
list_display = ['name', 'time', 'quota', 'hawk_signups',
'great_grey_signups', 'snowy_signups', 'eagle_signups']
@admin.display(description='Hawk Signups')
def hawk_signups(self, obj):
return str(UserEntry.hawk.filterActivity1(obj).count()) + " / " + str((UserEntry.hawk.filterActivity2(obj).count(), "N/A")[obj.time == 60])
@admin.display(description='Great Grey Signups')
def great_grey_signups(self, obj):
return str(UserEntry.great_grey.filterActivity1(obj).count()) + " / " + str((UserEntry.great_grey.filterActivity2(obj).count(), "N/A")[obj.time == 60])
@admin.display(description='Snowy Signups')
def snowy_signups(self, obj):
return str(UserEntry.snowy.filterActivity1(obj).count()) + " / " + str((UserEntry.snowy.filterActivity2(obj).count(), "N/A")[obj.time == 60])
@admin.display(description='Eagle Signups')
def eagle_signups(self, obj):
return str(UserEntry.eagle.filterActivity1(obj).count()) + " / " + str((UserEntry.eagle.filterActivity2(obj).count(), "N/A")[obj.time == 60])
@admin.register(House)
class HouseAdmin(admin.ModelAdmin):
list_display = ('name', 'points', 'current_points', 'total_points')
@admin.display(description='Current Housewars Points')
def current_points(self, obj):
return PointsEntry.objects.filter(house__name=obj.name).aggregate(points__sum=Coalesce(Sum('points'), 0)).get('points__sum')
@admin.display(description='Total Points')
def total_points(self, obj):
return self.current_points(obj) + obj.points
@admin.register(UserEntry)
class EntryAdmin(admin.ModelAdmin):
fieldsets = [
('Personal Information', {'fields': [
'first_name', 'last_name', 'email', 'grade']}),
('House Wars Information', {
'fields': ['house', 'activity1', 'activity2']}),
]
list_display = ('first_name', 'last_name',
'house', 'activity1', 'activity2')
list_filter = ['house', 'grade', 'activity1', 'activity2']
@admin.register(PointsEntry)
class PointsEntryAdmin(admin.ModelAdmin):
list_display = ('activity', 'name', 'house', 'points')

145
housewars/admin/Activity.py Normal file
View file

@ -0,0 +1,145 @@
from django.http import HttpResponse
from django.contrib import admin, messages
from django.db.models import F
from django.utils.translation import ngettext
import io
import zipfile
from housewars.utils import get_random_string, load_pdf
from housewars.models import Activity, Award, Quota, UserEntry
class QuotaInline(admin.TabularInline):
model = Quota
extra = 0
class AwardsInline(admin.TabularInline):
model = Award
extra = 0
@admin.register(Activity)
class ActivityAdmin(admin.ModelAdmin):
fieldsets = [
(None, {'fields': ['name', 'room_number']}),
('Activity Information', {
'fields': ['default_quota', 'time', 'password']}),
]
list_display = ['name', 'time', 'room_number', 'teacher', 'password',
'default_quota', 'hawk_signups', 'great_grey_signups', 'snowy_signups', 'eagle_signups']
inlines = [QuotaInline, AwardsInline]
actions = ['generate_passwords', 'remove_room_number',
'export_a1_to_pdf', 'export_a2_to_pdf']
@admin.display(description='Hawk')
def hawk_signups(self, obj):
if (obj.time == None):
return "- | -"
return str(UserEntry.objects.filter(house__name='Hawk', activity1=obj).count()) + ' | ' + str((UserEntry.objects.filter(house__name='Hawk', activity2=obj).count(), '-')[obj.time == 60])
@admin.display(description='Great Grey')
def great_grey_signups(self, obj):
if (obj.time == None):
return "- | -"
return str(UserEntry.objects.filter(house__name='Great Grey', activity1=obj).count()) + ' | ' + str((UserEntry.objects.filter(house__name='Great Grey', activity2=obj).count(), '-')[obj.time == 60])
@admin.display(description='Snowy')
def snowy_signups(self, obj):
if (obj.time == None):
return "- | -"
return str(UserEntry.objects.filter(house__name='Snowy', activity1=obj).count()) + ' | ' + str((UserEntry.objects.filter(house__name='Snowy', activity2=obj).count(), '-')[obj.time == 60])
@admin.display(description='Eagle')
def eagle_signups(self, obj):
if (obj.time == None):
return "- | -"
return str(UserEntry.objects.filter(house__name='Eagle', activity1=obj).count()) + ' | ' + str((UserEntry.objects.filter(house__name='Eagle', activity2=obj).count(), '-')[obj.time == 60])
@admin.action(description='Generate passwords for selected activities')
def generate_passwords(self, request, queryset):
for activity in queryset:
activity.password = get_random_string(8)
activity.save()
self.message_user(request, ngettext(
'Generated password for %d activity.',
'Generated passwords for %d activities.',
queryset.count(),
) % queryset.count(), messages.SUCCESS)
@admin.action(description='Remove room number from selected activities')
def remove_room_number(self, request, queryset):
queryset.update(room_number=None)
self.message_user(request, ngettext(
'Removed room number for %d activity.',
'Removed room numbers for %d activities.',
queryset.count(),
) % queryset.count(), messages.SUCCESS)
@admin.action(description='Export activity 1 attendance to pdf')
def export_a1_to_pdf(self, request, queryset):
file_list = {}
for activity in queryset:
user_entries = activity.activity1
# Augment the queryset with extra data
user_entries = user_entries.annotate(
a1_room=F('activity1__room_number')).annotate(a2_room=F('activity2__room_number'))
# Select headers that are to be unpacked
headers = ['first_name', 'last_name', 'grade', 'house',
'activity1', 'a1_room', 'activity2', 'a2_room']
file = load_pdf(user_entries, headers, f"{activity.name} - 1")
file_list[f"{activity.name} - 1"] = file
outfile = io.BytesIO()
with zipfile.ZipFile(outfile, 'w') as zf:
for name, file in file_list.items():
zf.writestr(f"{name}.pdf", file.getvalue())
outfile = outfile.getvalue()
response = HttpResponse(
outfile, content_type='application/octet-stream')
response['Content-Disposition'] = 'attachment; filename=activity1_attendance.zip'
return response
@admin.action(description='Export activity 2 attendance to pdf')
def export_a2_to_pdf(self, request, queryset):
file_list = {}
for activity in queryset:
user_entries = activity.activity2
# Augment the queryset with extra data
user_entries = user_entries.annotate(
a1_room=F('activity1__room_number')).annotate(a2_room=F('activity2__room_number'))
# Select headers that are to be unpacked
headers = ['first_name', 'last_name', 'grade', 'house',
'activity1', 'a1_room', 'activity2', 'a2_room']
file = load_pdf(user_entries, headers, f"{activity.name} - 2")
file_list[f"{activity.name} - 2"] = file
outfile = io.BytesIO()
with zipfile.ZipFile(outfile, 'w') as zf:
for name, file in file_list.items():
zf.writestr(f"{name}.pdf", file.getvalue())
outfile = outfile.getvalue()
response = HttpResponse(
outfile, content_type='application/octet-stream')
response['Content-Disposition'] = 'attachment; filename=activity2_attendance.zip'
return response

View file

@ -0,0 +1,15 @@
from django.contrib import admin
from housewars.models import Facilitator
@admin.register(Facilitator)
class FacilitatorAdmin(admin.ModelAdmin):
list_display = ('first_name', 'last_name', 'activity')
fieldsets = [
('Personal Information', {'fields': [
'first_name', 'last_name']}),
('House Wars Information', {
'fields': ['activity']}),
]

16
housewars/admin/House.py Normal file
View file

@ -0,0 +1,16 @@
from django.contrib import admin
from housewars.models import House
@admin.register(House)
class HouseAdmin(admin.ModelAdmin):
list_display = ('name', 'points', 'current_points', 'total_points')
@admin.display(description='Current Housewars Points')
def current_points(self, obj):
return obj.current_points
@admin.display(description='Total Points')
def total_points(self, obj):
return obj.total_points

View file

@ -0,0 +1,32 @@
from django.contrib import admin
from django.db.models import F
from housewars.models import PointsEntry
class ValidatedPasswordFilter(admin.SimpleListFilter):
title = 'valid password'
parameter_name = 'valid'
def lookups(self, request, model_admin):
return (('True', True), ('False', False))
def queryset(self, request, queryset):
if (self.value() == 'True'):
return queryset.filter(password=F('activity__password'))
elif (self.value() == 'False'):
return queryset.exclude(password=F('activity__password'))
else:
return queryset
@admin.register(PointsEntry)
class PointsEntryAdmin(admin.ModelAdmin):
list_display = ('activity', 'award', 'house', 'validated')
list_filter = ('activity', ValidatedPasswordFilter)
@admin.display(description='Validated', boolean=True)
def validated(self, obj):
return obj.password == obj.activity.password

View file

@ -0,0 +1,87 @@
from django.contrib import admin, messages
from django.http import HttpResponse
from django.db.models import F
from django.utils.translation import ngettext
import io
import zipfile
from housewars.utils import load_pdf
from housewars.models import Teacher, UserEntry
@admin.register(Teacher)
class TeacherAdmin(admin.ModelAdmin):
fieldsets = [
('Personal Information', {'fields': [
'first_name', 'last_name']}),
('House Wars Information', {
'fields': ['grade', 'house', 'activity']}),
]
list_display = ('first_name', 'last_name', 'house', 'grade', 'activity')
actions = ['export_mentor_to_pdf']
@admin.action(description='Remove activity from selected teachers')
def remove_activity(self, request, queryset):
queryset.update(activity=None)
self.message_user(request, ngettext(
'Removed activity for %d teacher.',
'Removed activities for %d teachers.',
queryset.count(),
) % queryset.count(), messages.SUCCESS)
@admin.action(description='Remove house from selected teachers')
def remove_activity(self, request, queryset):
queryset.update(house=None)
self.message_user(request, ngettext(
'Removed house for %d teacher.',
'Removed houses for %d teachers.',
queryset.count(),
) % queryset.count(), messages.SUCCESS)
@admin.action(description='Remove grade from selected teachers')
def remove_activity(self, request, queryset):
queryset.update(grade=None)
self.message_user(request, ngettext(
'Removed grade for %d teacher.',
'Removed grades for %d teachers.',
queryset.count(),
) % queryset.count(), messages.SUCCESS)
@admin.action(description='Export mentor to pdf')
def export_mentor_to_pdf(self, request, queryset):
file_list = {}
for teacher in queryset:
user_entries = UserEntry.objects.filter(
grade=teacher.grade, house=teacher.house)
# Augment the queryset with extra data
user_entries = user_entries.annotate(
a1_room=F('activity1__room_number')).annotate(a2_room=F('activity2__room_number'))
# Select headers that are to be unpacked
headers = ['first_name', 'last_name', 'grade', 'house',
'activity1', 'a1_room', 'activity2', 'a2_room']
file = load_pdf(user_entries, headers,
f"{teacher.last_name}, {teacher.first_name}")
file_list[f"{teacher.last_name}, {teacher.first_name}"] = file
outfile = io.BytesIO()
with zipfile.ZipFile(outfile, 'w') as zf:
for name, file in file_list.items():
zf.writestr(f"{name}.pdf", file.getvalue())
outfile = outfile.getvalue()
response = HttpResponse(
outfile, content_type='application/octet-stream')
response['Content-Disposition'] = 'attachment; filename=mentor.zip'
return response

View file

@ -0,0 +1,103 @@
from django.contrib import admin
from django.db.models import F, Value
from django.db.models.functions import Concat
from django.http import HttpResponse, FileResponse
import csv
from housewars.models import UserEntry
from housewars.utils import unpack_values, load_pdf
class MentorListFilter(admin.SimpleListFilter):
title = 'mentor teacher'
parameter_name = 'mentor'
def lookups(self, request, model_admin):
qs = model_admin.get_queryset(request).annotate(full_name=Concat(F(
'house__teacher__first_name'), Value(' '), F('house__teacher__last_name'))).values_list('house__teacher', 'full_name').distinct()
return qs
def queryset(self, request, queryset):
if (self.value() == None):
return queryset
return queryset.filter(house__teacher=self.value(), grade=F('house__teacher__grade'))
@admin.register(UserEntry)
class UserEntryAdmin(admin.ModelAdmin):
fieldsets = [
('Personal Information', {'fields': [
'first_name', 'last_name', 'email', 'grade']}),
('House Wars Information', {
'fields': ['house', 'activity1', 'activity2']}),
]
list_display = ('first_name', 'last_name',
'house', 'mentor_teacher', 'activity1', 'activity1_teacher', 'activity2', 'activity2_teacher')
list_filter = ['house', 'grade', MentorListFilter, 'activity1',
'activity1__teacher', 'activity2', 'activity2__teacher']
search_fields = ['first_name', 'last_name']
actions = ['export_to_csv', 'export_to_pdf']
@admin.display(description='Activity 1 Teacher')
def activity1_teacher(self, obj):
if (obj.activity1 == None):
return None
return obj.activity1.teacher
@admin.display(description='Mentor Teacher')
def mentor_teacher(self, obj):
return obj.mentor
@admin.display(description='Activity 2 Teacher')
def activity2_teacher(self, obj):
if (obj.activity2 == None):
return None
return obj.activity2.teacher
@admin.action(description='Export selected to csv')
def export_to_csv(self, request, queryset):
# Augment the querset with extra data
queryset = queryset.annotate(
a1_room=F('activity1__room_number')).annotate(a2_room=F('activity1__room_number'))
# Initialize csv object writers
response = HttpResponse(content_type='text/csv', headers={
'Content-Disposition': 'attachment; filename="export.csv"'},)
writer = csv.writer(response, delimiter=";")
# Select headers that are to be unpacked
headers = ['first_name', 'last_name', 'grade', 'house',
'activity1', 'a1_room', 'activity2', 'a2_room']
# Unpack queryset data
data = unpack_values(queryset, headers)
# Write data into tables
writer.writerows(data)
return response
@admin.action(description='Export selected to pdf')
def export_to_pdf(self, request, queryset):
# Augment the querset with extra data
queryset = queryset.annotate(
a1_room=F('activity1__room_number')).annotate(a2_room=F('activity2__room_number'))
# Select headers that are to be unpacked
headers = ['first_name', 'last_name', 'grade', 'house',
'activity1', 'a1_room', 'activity2', 'a2_room']
# Unpack queryset data
file = load_pdf(queryset, headers)
return FileResponse(file, as_attachment=True, filename='export.pdf')

View file

@ -0,0 +1,6 @@
from .Activity import ActivityAdmin
from .Facilitator import Facilitator
from .House import House
from .PointsEntry import PointsEntry
from .Teacher import Teacher
from .UserEntry import UserEntry

View file

@ -1,98 +0,0 @@
from django.forms import (CharField, ChoiceField, EmailField, EmailInput, Form,
ModelChoiceField, ModelForm, NumberInput, Select,
Textarea, TextInput, ValidationError)
from .models import Activity, House, PointsEntry, UserEntry
class UserEntryForm(Form):
first_name = CharField(widget=TextInput(attrs={
'class': 'form-control',
'id': 'first-name',
'placeholder': 'First Name'
}))
last_name = CharField(widget=TextInput(attrs={
'class': "form-control",
'id': 'last-name',
'placeholder': 'Last Name'
}))
email = EmailField(widget=EmailInput(attrs={
'class': "form-control",
'id': 'email',
'placeholder': 'Email'
}))
grade = ChoiceField(choices=UserEntry.GradeChoices, widget=Select(attrs={
'class': "form-select",
'id': 'grade',
'placeholder': 'Grade'
}))
house = ModelChoiceField(House.objects.all(), widget=Select(attrs={
'class': "form-select",
'id': 'house',
'placeholder': 'House'
}))
class ActivityForm(Form):
activity1 = ModelChoiceField(Activity.objects.all(), widget=Select(attrs={
'class': "form-select",
'id': 'activity1',
'placeholder': 'Activity 1'
}))
activity2 = ModelChoiceField(Activity.objects.all(), required=False, widget=Select(attrs={
'class': "form-select",
'id': 'activity2',
'placeholder': 'Activity 2'
}))
def clean(self):
data = self.cleaned_data
activity1 = data.get('activity1')
activity2 = data.get('activity2')
if activity1:
if activity2 and activity1.time + activity2.time != 60:
raise ValidationError(
"Please pick activities adding up to one hour.")
elif not activity2 and activity1.time != 60:
raise ValidationError(
"Please pick activities adding up to one hour.")
else:
super().clean()
else:
raise ValidationError("Please select an activity.")
class PointsEntryForm(ModelForm):
class Meta:
model = PointsEntry
fields = '__all__'
widgets = {
'name': TextInput(attrs={
'class': 'form-control',
'id': 'first-name',
'placeholder': 'Full Name'
}),
'activity': Select(attrs={
'class': "form-select",
'id': 'house',
'placeholder': 'Activity'
}),
'house': Select(attrs={
'class': "form-select",
'id': 'house',
'placeholder': 'House'
}),
'points': NumberInput(attrs={
'class': "form-control",
'id': 'last-name',
'placeholder': 'Number of Points'
}),
'comment': Textarea(attrs={
'class': 'form-control',
'id': 'first-name',
'placeholder': 'Comments',
'style': 'height:10vh;'
})
}

View file

@ -0,0 +1,35 @@
from django.forms import Form, ModelChoiceField, Select, ValidationError
from housewars.models import Activity
class ActivityForm(Form):
activity1 = ModelChoiceField(Activity.objects.all(), widget=Select(attrs={
'class': "form-select",
'id': 'activity1',
'placeholder': 'Activity 1'
}))
activity2 = ModelChoiceField(Activity.objects.all(), required=False, widget=Select(attrs={
'class': "form-select",
'id': 'activity2',
'placeholder': 'Activity 2'
}))
def clean(self):
data = self.cleaned_data
activity1 = data.get('activity1')
activity2 = data.get('activity2')
# Verify that there are 60 minutes of activities selected
if activity1:
if activity2 and activity1.time + activity2.time != 60:
raise ValidationError(
"Please pick activities adding up to one hour.")
elif not activity2 and activity1.time != 60:
raise ValidationError(
"Please pick activities adding up to one hour.")
else:
super().clean()
else:
raise ValidationError("Please select an activity.")

View file

@ -0,0 +1,26 @@
from django.forms import ModelForm, Select, TextInput
from housewars.models import Facilitator
class FacilitatorForm(ModelForm):
class Meta:
model = Facilitator
fields = '__all__'
widgets = {
'first_name': TextInput(attrs={
'class': "form-control",
'id': 'first-name',
'placeholder': 'First Name'
}),
'last_name': TextInput(attrs={
'class': 'form-control',
'id': 'last-name',
'placeholder': 'Last Name',
}),
'activity': Select(attrs={
'class': 'form-select',
'id': 'activity',
'placeholder': 'activity'
}),
}

View file

@ -0,0 +1,26 @@
from django.forms import ModelForm, Select, TextInput
from housewars.models import PointsEntry
class PointsEntryForm(ModelForm):
class Meta:
model = PointsEntry
fields = '__all__'
widgets = {
'house': Select(attrs={
'class': "form-select",
'id': 'house',
'placeholder': 'House'
}),
'password': TextInput(attrs={
'class': 'form-control',
'id': 'first-name',
'placeholder': 'Password',
'type': 'password'
})
}
def __init__(self, *args, **kwargs):
super(PointsEntryForm, self).__init__(*args, **kwargs)
self.fields['activity'].widget.attrs.update({'class': 'form-select'})
self.fields['award'].widget.attrs.update({'class': 'form-select'})

View file

@ -0,0 +1,45 @@
from django.forms import (CharField, ChoiceField, EmailField, EmailInput,
Form, ModelChoiceField, Select, TextInput, ValidationError)
from housewars.models import House, UserEntry
class UserEntryForm(Form):
first_name = CharField(widget=TextInput(attrs={
'class': 'form-control',
'id': 'first-name',
'placeholder': 'First'
}))
last_name = CharField(widget=TextInput(attrs={
'class': "form-control",
'id': 'last-name',
'placeholder': 'Last'
}))
email = EmailField(widget=EmailInput(attrs={
'class': "form-control",
'id': 'email',
'placeholder': 'Email'
}))
grade = ChoiceField(choices=UserEntry.GradeChoices, widget=Select(attrs={
'class': "form-select",
'id': 'grade',
'placeholder': 'Grade'
}))
house = ModelChoiceField(House.objects.all(), widget=Select(attrs={
'class': "form-select",
'id': 'house',
'placeholder': 'House'
}))
def clean_email(self):
email = self.cleaned_data.get('email')
# Verify that email domain matches
if ('@slps.org' not in email):
raise ValidationError("Please use your school email.")
# Verify that the email is unique
if UserEntry.objects.filter(email=email).exists():
raise ValidationError(
"A signup with this email already exists, if you want to change your signups, please email ipatino3341@slps.org.")
return email

View file

@ -0,0 +1,4 @@
from .ActivityForm import ActivityForm
from .FacilitatorForm import FacilitatorForm
from .PointsEntryForm import PointsEntryForm
from .UserEntryForm import UserEntryForm

View file

@ -1,29 +0,0 @@
from django.db.models import Manager
class EntryManager(Manager):
def filterActivity1(self, act):
return self.filter(activity1=act)
def filterActivity2(self, act):
return self.filter(activity2=act)
class HawkEntryManager(EntryManager):
def get_queryset(self):
return super().get_queryset().filter(house__name='Hawk')
class EagleEntryManager(EntryManager):
def get_queryset(self):
return super().get_queryset().filter(house__name='Eagle')
class GreatGreyEntryManager(EntryManager):
def get_queryset(self):
return super().get_queryset().filter(house__name='Great Grey')
class SnowyEntryManager(EntryManager):
def get_queryset(self):
return super().get_queryset().filter(house__name='Snowy')

View file

@ -1,7 +1,8 @@
# Generated by Django 4.0.4 on 2022-05-16 23:29
# Generated by Django 4.0.4 on 2022-09-03 16:15
from django.db import migrations, models
import django.db.models.deletion
import smart_selects.db_fields
class Migration(migrations.Migration):
@ -17,20 +18,84 @@ class Migration(migrations.Migration):
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('quota', models.IntegerField()),
('time', models.IntegerField(choices=[(30, '30'), (60, '60')])),
('default_quota', models.IntegerField(default=0)),
('time', models.IntegerField(blank=True, choices=[(30, '30'), (60, '60')], null=True)),
('password', models.CharField(blank=True, default=None, max_length=100, null=True)),
],
options={
'verbose_name_plural': 'Activities',
},
),
migrations.CreateModel(
name='Award',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=15)),
('points', models.IntegerField(default=0)),
('activity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='housewars.activity')),
],
),
migrations.CreateModel(
name='Entry',
name='House',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('points', models.IntegerField(default=0)),
],
),
migrations.CreateModel(
name='UserEntry',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(max_length=100)),
('last_name', models.CharField(max_length=100)),
('email', models.EmailField(max_length=100)),
('grade', models.CharField(choices=[('9th', '9Th'), ('10th', '10Th'), ('11th', '11Th'), ('12th', '12Th')], max_length=5)),
('email', models.EmailField(max_length=100, unique=True)),
('grade', models.IntegerField(choices=[(9, '9th/Freshman'), (10, '10th/Sophomore'), (11, '11th/Junior'), (12, '12th/Senior')])),
('activity1', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activity1', to='housewars.activity')),
('activity2', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activity2', to='housewars.activity')),
('activity2', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='activity2', to='housewars.activity')),
('house', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='housewars.house')),
],
options={
'verbose_name_plural': 'User Entries',
},
),
migrations.CreateModel(
name='PointsEntry',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(blank=True, max_length=15)),
('activity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='Activity', to='housewars.activity')),
('award', smart_selects.db_fields.ChainedForeignKey(chained_field='activity', chained_model_field='activity', on_delete=django.db.models.deletion.CASCADE, to='housewars.award')),
('house', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='housewars.house')),
],
options={
'verbose_name_plural': 'Point Entries',
},
),
migrations.CreateModel(
name='Teacher',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(max_length=100)),
('last_name', models.CharField(max_length=100)),
('grade', models.IntegerField(choices=[(9, '9th/Freshman'), (10, '10th/Sophomore'), (11, '11th/Junior'), (12, '12th/Senior')])),
('activity', models.OneToOneField(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='teacher', to='housewars.activity')),
('house', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='teacher', to='housewars.house')),
],
options={
'unique_together': {('house', 'grade')},
},
),
migrations.CreateModel(
name='Quota',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quota', models.IntegerField(default=0)),
('activity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='housewars.activity')),
('house', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='housewars.house')),
],
options={
'unique_together': {('activity', 'house')},
},
),
]

View file

@ -1,4 +1,4 @@
# Generated by Django 4.0.4 on 2022-05-16 23:37
# Generated by Django 4.0.4 on 2022-09-03 16:59
from django.db import migrations
@ -10,8 +10,8 @@ class Migration(migrations.Migration):
]
operations = [
migrations.AlterModelTable(
name='activity',
table='Activities',
migrations.AlterUniqueTogether(
name='pointsentry',
unique_together={('activity', 'award')},
),
]

View file

@ -1,25 +0,0 @@
# Generated by Django 4.0.4 on 2022-05-16 23:38
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('housewars', '0002_alter_activity_table'),
]
operations = [
migrations.AlterModelOptions(
name='activity',
options={'verbose_name_plural': 'Activities'},
),
migrations.AlterModelOptions(
name='entry',
options={'verbose_name_plural': 'Entries'},
),
migrations.AlterModelTable(
name='activity',
table=None,
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 4.0.4 on 2022-09-03 19:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('housewars', '0002_alter_pointsentry_unique_together'),
]
operations = [
migrations.AlterField(
model_name='activity',
name='time',
field=models.IntegerField(choices=[(30, '30'), (60, '60')]),
),
]

View file

@ -1,18 +0,0 @@
# Generated by Django 4.0.4 on 2022-05-17 02:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('housewars', '0003_alter_activity_options_alter_entry_options_and_more'),
]
operations = [
migrations.AlterField(
model_name='entry',
name='grade',
field=models.IntegerField(choices=[(9, '9th/Freshman'), (10, '10th/Sophomore'), (11, '11th/Junior'), (12, '12th/Senior')]),
),
]

View file

@ -0,0 +1,17 @@
# Generated by Django 4.0.4 on 2022-09-05 18:38
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('housewars', '0003_alter_activity_time'),
]
operations = [
migrations.AlterUniqueTogether(
name='pointsentry',
unique_together=set(),
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 4.0.4 on 2022-09-07 12:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('housewars', '0004_alter_pointsentry_unique_together'),
]
operations = [
migrations.AddField(
model_name='activity',
name='room_number',
field=models.IntegerField(blank=True, default=None, null=True),
),
migrations.AlterField(
model_name='teacher',
name='grade',
field=models.IntegerField(blank=True, choices=[(9, '9th/Freshman'), (10, '10th/Sophomore'), (11, '11th/Junior'), (12, '12th/Senior')], default=None, null=True),
),
]

View file

@ -1,19 +0,0 @@
# Generated by Django 4.0.4 on 2022-05-17 12:43
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('housewars', '0004_alter_entry_grade'),
]
operations = [
migrations.AlterField(
model_name='entry',
name='activity2',
field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='activity2', to='housewars.activity'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 4.0.4 on 2022-09-06 13:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('housewars', '0004_alter_pointsentry_unique_together'),
]
operations = [
migrations.AlterField(
model_name='teacher',
name='grade',
field=models.IntegerField(blank=True, choices=[(9, '9th/Freshman'), (10, '10th/Sophomore'), (11, '11th/Junior'), (12, '12th/Senior')], default=None, null=True),
),
]

View file

@ -1,19 +0,0 @@
# Generated by Django 4.0.4 on 2022-05-17 13:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('housewars', '0005_alter_entry_activity2'),
]
operations = [
migrations.AddField(
model_name='entry',
name='house',
field=models.CharField(choices=[('HAWK', 'Hawk'), ('GREATGREY', 'Great Grey'), ('EAGLE', 'Eagle'), ('SNOWY', 'Snowy')], default='HAWK', max_length=15),
preserve_default=False,
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 4.0.4 on 2022-09-07 21:22
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('housewars', '0005_activity_room_number_alter_teacher_grade'),
]
operations = [
migrations.CreateModel(
name='Facilitator',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(max_length=100)),
('last_name', models.CharField(max_length=100)),
('activity', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='housewars.activity')),
],
),
]

View file

@ -1,19 +0,0 @@
# Generated by Django 4.0.4 on 2022-05-17 14:28
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('housewars', '0006_entry_house'),
]
operations = [
migrations.AlterField(
model_name='entry',
name='activity2',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='activity2', to='housewars.activity'),
),
]

View file

@ -0,0 +1,19 @@
# Generated by Django 4.0.4 on 2022-09-07 22:30
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('housewars', '0006_facilitator'),
]
operations = [
migrations.AlterField(
model_name='facilitator',
name='activity',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='housewars.activity'),
),
]

View file

@ -1,25 +0,0 @@
# Generated by Django 4.0.4 on 2022-05-17 15:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('housewars', '0007_alter_entry_activity2'),
]
operations = [
migrations.AddField(
model_name='activity',
name='session1_signups',
field=models.IntegerField(default=10),
preserve_default=False,
),
migrations.AddField(
model_name='activity',
name='session2_signups',
field=models.IntegerField(default=10),
preserve_default=False,
),
]

View file

@ -0,0 +1,14 @@
# Generated by Django 4.0.4 on 2022-09-07 22:59
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('housewars', '0005_alter_teacher_grade'),
('housewars', '0007_alter_facilitator_activity'),
]
operations = [
]

View file

@ -0,0 +1,18 @@
# Generated by Django 4.0.4 on 2023-05-16 03:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('housewars', '0008_merge_20220907_2259'),
]
operations = [
migrations.AlterField(
model_name='activity',
name='room_number',
field=models.CharField(blank=True, default=None, max_length=100, null=True),
),
]

View file

@ -1,23 +0,0 @@
# Generated by Django 4.0.4 on 2022-05-17 15:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('housewars', '0008_activity_session1_signups_activity_session2_signups'),
]
operations = [
migrations.AlterField(
model_name='activity',
name='session1_signups',
field=models.IntegerField(default=0),
),
migrations.AlterField(
model_name='activity',
name='session2_signups',
field=models.IntegerField(default=0),
),
]

View file

@ -1,21 +0,0 @@
# Generated by Django 4.0.4 on 2022-05-17 22:36
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('housewars', '0009_alter_activity_session1_signups_and_more'),
]
operations = [
migrations.RemoveField(
model_name='activity',
name='session1_signups',
),
migrations.RemoveField(
model_name='activity',
name='session2_signups',
),
]

View file

@ -1,33 +0,0 @@
# Generated by Django 4.0.4 on 2022-05-18 22:10
from django.db import migrations, models
import django.db.models.deletion
import django.db.models.manager
class Migration(migrations.Migration):
dependencies = [
('housewars', '0010_remove_activity_session1_signups_and_more'),
]
operations = [
migrations.CreateModel(
name='House',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
],
),
migrations.AlterModelManagers(
name='activity',
managers=[
('session1', django.db.models.manager.Manager()),
],
),
migrations.AlterField(
model_name='entry',
name='house',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='housewars.house'),
),
]

View file

@ -1,26 +0,0 @@
# Generated by Django 4.0.4 on 2022-05-19 00:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('housewars', '0011_house_alter_activity_managers_alter_entry_house'),
]
operations = [
migrations.AlterModelManagers(
name='activity',
managers=[
],
),
migrations.AlterField(
model_name='entry',
name='house',
field=models.CharField(choices=[('HAWK', 'Hawk'), ('GREATGREY', 'Great Grey'), ('EAGLE', 'Eagle'), ('SNOWY', 'Snowy')], max_length=15),
),
migrations.DeleteModel(
name='House',
),
]

View file

@ -1,59 +0,0 @@
# Generated by Django 4.0.4 on 2022-05-21 18:15
from django.db import migrations, models
import django.db.models.deletion
import django.db.models.manager
class Migration(migrations.Migration):
dependencies = [
('housewars', '0012_alter_activity_managers_alter_entry_house_and_more'),
]
operations = [
migrations.CreateModel(
name='House',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('points', models.IntegerField()),
],
),
migrations.CreateModel(
name='PointsEntry',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('points', models.IntegerField()),
('reason', models.TextField()),
('activity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='housewars.activity')),
('house', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='housewars.house')),
],
options={
'verbose_name_plural': 'Point Entries',
},
),
migrations.CreateModel(
name='UserEntry',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(max_length=100)),
('last_name', models.CharField(max_length=100)),
('email', models.EmailField(max_length=100)),
('grade', models.IntegerField(choices=[(9, '9th/Freshman'), (10, '10th/Sophomore'), (11, '11th/Junior'), (12, '12th/Senior')])),
('activity1', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activity1', to='housewars.activity')),
('activity2', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='activity2', to='housewars.activity')),
('house', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='housewars.house')),
],
options={
'verbose_name_plural': 'User Entries',
},
managers=[
('hawk', django.db.models.manager.Manager()),
],
),
migrations.DeleteModel(
name='Entry',
),
]

View file

@ -1,20 +0,0 @@
# Generated by Django 4.0.4 on 2022-05-21 18:17
from django.db import migrations
import django.db.models.manager
class Migration(migrations.Migration):
dependencies = [
('housewars', '0013_house_pointsentry_userentry_delete_entry'),
]
operations = [
migrations.AlterModelManagers(
name='pointsentry',
managers=[
('hawk', django.db.models.manager.Manager()),
],
),
]

View file

@ -1,18 +0,0 @@
# Generated by Django 4.0.4 on 2022-05-21 18:33
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('housewars', '0014_alter_pointsentry_managers'),
]
operations = [
migrations.AlterModelManagers(
name='pointsentry',
managers=[
],
),
]

View file

@ -1,23 +0,0 @@
# Generated by Django 4.0.4 on 2022-05-21 20:18
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('housewars', '0015_alter_pointsentry_managers'),
]
operations = [
migrations.AlterModelManagers(
name='userentry',
managers=[
],
),
migrations.RenameField(
model_name='pointsentry',
old_name='reason',
new_name='comment',
),
]

View file

@ -1,23 +0,0 @@
# Generated by Django 4.0.4 on 2022-05-21 20:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('housewars', '0016_alter_userentry_managers_and_more'),
]
operations = [
migrations.AlterField(
model_name='house',
name='points',
field=models.IntegerField(default=0),
),
migrations.AlterField(
model_name='pointsentry',
name='points',
field=models.IntegerField(default=0),
),
]

View file

@ -1,72 +0,0 @@
from django.db import models
from .managers import (EagleEntryManager, EntryManager, GreatGreyEntryManager,
HawkEntryManager, SnowyEntryManager)
class Activity(models.Model):
class Meta:
verbose_name_plural = "Activities"
TimeslotChoices = [
(30, '30'),
(60, '60')
]
name = models.CharField(max_length=100)
quota = models.IntegerField()
time = models.IntegerField(choices=TimeslotChoices)
def __str__(self):
return f"{self.name} ({self.time})"
class House(models.Model):
name = models.CharField(max_length=100)
points = models.IntegerField(default=0)
def __str__(self):
return f"{self.name}"
class UserEntry(models.Model):
class Meta:
verbose_name_plural = "User Entries"
objects = EntryManager()
hawk = HawkEntryManager()
great_grey = GreatGreyEntryManager()
snowy = SnowyEntryManager()
eagle = EagleEntryManager()
GradeChoices = (
(9, '9th/Freshman'),
(10, '10th/Sophomore'),
(11, '11th/Junior'),
(12, '12th/Senior'),
)
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
email = models.EmailField(max_length=100)
grade = models.IntegerField(choices=GradeChoices)
house = models.ForeignKey(
House, on_delete=models.CASCADE)
activity1 = models.ForeignKey(
Activity, related_name='activity1', on_delete=models.CASCADE)
activity2 = models.ForeignKey(
Activity, related_name='activity2', on_delete=models.CASCADE, blank=True, null=True)
def __str__(self):
return f"{self.first_name} {self.last_name}"
class PointsEntry(models.Model):
class Meta:
verbose_name_plural = "Point Entries"
name = models.CharField(max_length=100)
activity = models.ForeignKey(Activity, on_delete=models.CASCADE)
house = models.ForeignKey(House, on_delete=models.CASCADE)
points = models.IntegerField(default=0)
comment = models.TextField(blank=False)

View file

@ -0,0 +1,26 @@
from django.db import models
class Activity(models.Model):
class Meta:
verbose_name_plural = "Activities"
TimeslotChoices = [
(30, '30'),
(60, '60')
]
name = models.CharField(max_length=100)
default_quota = models.IntegerField(default=0)
time = models.IntegerField(choices=TimeslotChoices)
room_number = models.CharField(
max_length=100, blank=True, null=True, default=None)
password = models.CharField(
max_length=100, blank=True, null=True, default=None)
def __str__(self):
return f"{self.name} - {self.time} min"
def save(self, *args, **kwargs):
self.full_clean()
return super().save(*args, **kwargs)

15
housewars/models/Award.py Normal file
View file

@ -0,0 +1,15 @@
from django.db import models
class Award(models.Model):
activity = models.ForeignKey(
'Activity', on_delete=models.CASCADE)
name = models.CharField(max_length=15)
points = models.IntegerField(default=0)
def __str__(self):
return f"{self.name}"
def save(self, *args, **kwargs):
self.full_clean()
return super().save(*args, **kwargs)

View file

@ -0,0 +1,13 @@
from django.db import models
from . import Activity
class Facilitator(models.Model):
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
activity = models.ForeignKey(
Activity, on_delete=models.CASCADE)
def save(self, *args, **kwargs):
self.full_clean()
return super().save(*args, **kwargs)

23
housewars/models/House.py Normal file
View file

@ -0,0 +1,23 @@
from django.db import models
from django.db.models import Sum
from django.db.models.functions import Coalesce
class House(models.Model):
name = models.CharField(max_length=100)
points = models.IntegerField(default=0)
@property
def current_points(self):
return self.pointsentry_set.aggregate(sum=Coalesce(Sum('award__points'), 0)).get('sum')
@property
def total_points(self):
return self.current_points + self.points
def __str__(self):
return f"{self.name}"
def save(self, *args, **kwargs):
self.full_clean()
return super().save(*args, **kwargs)

View file

@ -0,0 +1,22 @@
from django.db.models import Model, ForeignKey, CharField, CASCADE, Q, F
from smart_selects.db_fields import ChainedForeignKey
from . import Award
class PointsEntry(Model):
class Meta:
verbose_name_plural = "Point Entries"
activity = ForeignKey(
'Activity', related_name='Activity', on_delete=CASCADE)
house = ForeignKey('House', on_delete=CASCADE)
award = ChainedForeignKey(Award, chained_field="activity",
chained_model_field="activity", on_delete=CASCADE)
password = CharField(max_length=15, blank=True)
def __str__(self):
return f"{self.activity}"
def save(self, *args, **kwargs):
self.full_clean()
return super().save(*args, **kwargs)

17
housewars/models/Quota.py Normal file
View file

@ -0,0 +1,17 @@
from django.db import models
class Quota(models.Model):
class Meta:
unique_together = ('activity', 'house')
activity = models.ForeignKey('Activity', on_delete=models.CASCADE)
house = models.ForeignKey('House', on_delete=models.CASCADE)
quota = models.IntegerField(default=0)
def __str__(self):
return f"{self.house}"
def save(self, *args, **kwargs):
self.full_clean()
return super().save(*args, **kwargs)

View file

@ -0,0 +1,30 @@
from django.db import models
from . import House, Activity
class Teacher(models.Model):
class Meta:
unique_together = ('house', 'grade')
GradeChoices = (
(9, '9th/Freshman'),
(10, '10th/Sophomore'),
(11, '11th/Junior'),
(12, '12th/Senior'),
)
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
house = models.ForeignKey(
House, related_name='teacher', default=None, blank=True, null=True, on_delete=models.SET_NULL)
grade = models.IntegerField(
choices=GradeChoices, default=None, blank=True, null=True)
activity = models.OneToOneField(
Activity, related_name='teacher', default=None, blank=True, null=True, on_delete=models.SET_NULL)
def __str__(self):
return f"{self.first_name} {self.last_name}"
def save(self, *args, **kwargs):
self.full_clean()
return super().save(*args, **kwargs)

View file

@ -0,0 +1,47 @@
from django.db import models
from django.core.exceptions import ValidationError
from . import House, Activity
class UserEntry(models.Model):
class Meta:
verbose_name_plural = "User Entries"
GradeChoices = (
(9, '9th/Freshman'),
(10, '10th/Sophomore'),
(11, '11th/Junior'),
(12, '12th/Senior'),
)
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
email = models.EmailField(max_length=100, unique=True)
grade = models.IntegerField(choices=GradeChoices)
house = models.ForeignKey(
House, on_delete=models.CASCADE)
activity1 = models.ForeignKey(
Activity, related_name='activity1', on_delete=models.CASCADE)
activity2 = models.ForeignKey(
Activity, related_name='activity2', on_delete=models.CASCADE, blank=True, null=True)
@property
def mentor(self):
return self.house.teacher.get(grade=self.grade)
def __str__(self):
return f"{self.first_name} {self.last_name}"
def clean(self):
# Check to verify that there are 60 minutes of activities
if self.activity2 and self.activity1.time + self.activity2.time != 60:
raise ValidationError(
"Please pick activities adding up to one hour.")
elif not self.activity2 and self.activity1.time != 60:
raise ValidationError(
"Please pick activities adding up to one hour.")
def save(self, *args, **kwargs):
self.full_clean()
return super().save(*args, **kwargs)

View file

@ -0,0 +1,8 @@
from .Activity import Activity
from .Award import Award
from .Facilitator import Facilitator
from .House import House
from .PointsEntry import PointsEntry
from .Quota import Quota
from .Teacher import Teacher
from .UserEntry import UserEntry

View file

@ -15,10 +15,19 @@ html, body {
height: 100%;
}
#banner {
background-color: #fff;
}
#step-counter {
height: 10%
}
.steps-form {
display: table;
width: 100%;
position: relative;
margin-bottom: 1rem;
}
.steps-form .steps-row {
@ -59,4 +68,10 @@ html, body {
line-height: 1.428571429;
border-radius: 15px;
margin-top: 0;
}
@media only screen and (max-width: 992px) {
#banner {
display: none;
}
}

View file

@ -1,54 +1,40 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}
<title>House Wars - Signup</title>
{% endblock %}
{% block formtitle %}
<h1 class="text-center mt-1 mb-4">Signup</h1>
{% block step-counter %}
<div class="steps-form">
<div class="steps-row setup-panel">
<div class="steps-step">
<button name="wizard_goto_step" class="btn btn-secondary btn-circle" type="submit" value="user">1</button>
<p>Info</p>
</div>
<div class="steps-step">
<button name="wizard_goto_step" class="btn btn-primary btn-circle" type="disabled"
value="activity">2</button>
<p>Activities</p>
</div>
</div>
</div>
{% endblock %}
{% block content %}
<form method="POST">
{% csrf_token %}
{{ wizard.management_form }}
<form method="POST">
{% csrf_token %}
{{ wizard.management_form }}
<div class="steps-form">
<div class="steps-row setup-panel">
<div class="steps-step">
<button name="wizard_goto_step" class="btn btn-secondary btn-circle" type="submit" value="user">1</button>
<p>Student Information</p>
</div>
<div class="steps-step">
<button name="wizard_goto_step" class="btn btn-primary btn-circle" type="disabled"
value="activity">2</button>
<p>Activity Information</p>
</div>
</div>
</div>
<div class="mb-3">
<label for="activity1" class="form-label">Activity 1:</label>
{{ form.activity1 }}
</div>
<div class="mb-3">
<label for="activity2" class="form-label">Activity 2:</label>
{{ form.activity2 }}
</div>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
<div class="mb-3">
<label for="activity1" class="form-label">Activity 1:</label>
{{ form.activity1 }}
</div>
<div class="mb-3">
<label for="activity2" class="form-label">Activity 2:</label>
{{ form.activity2 }}
</div>
<div class="form-group d-flex justify-content-center">
<button name="wizard_goto_step" class="btn btn-outline-primary btn-50" type="submit" value="{{ wizard.steps.prev }}">Previous Step</button>
<input class="btn btn-outline-primary btn-50" type="submit" value="Submit" />
</div>
</form>
<div class="form-group d-flex justify-content-center">
<button name="wizard_goto_step" class="btn btn-outline-primary btn-50" type="submit"
value="{{ wizard.steps.prev }}">Previous Step</button>
<input id='submit' class="btn btn-outline-primary btn-50" type="submit" value="Submit" />
</div>
</form>
{% endblock %}

View file

@ -1,64 +1,51 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}
<title>House Wars - Signup</title>
{% endblock %}
{% block formtitle %}
<h1 class="text-center mt-1 mb-4">Signup</h1>
{% block step-counter %}
<div class="steps-form">
<div class="steps-row setup-panel">
<div class="steps-step">
<button name="wizard_goto_step" class="btn btn-primary btn-circle" type="submit" value="user">1</button>
<p>Student Info</p>
</div>
<div class="steps-step">
<button name="wizard_goto_step" class="btn btn-secondary btn-circle" type="disabled"
value="activity">2</button>
<p>Activity Signup</p>
</div>
</div>
</div>
{% endblock %}
{% block content %}
<form method="POST">
{% csrf_token %}
{{ wizard.management_form }}
<div class="steps-form">
<div class="steps-row setup-panel">
<div class="steps-step">
<button name="wizard_goto_step" class="btn btn-primary btn-circle" type="disabled" value="user">1</button>
<p>Student Information</p>
</div>
<div class="steps-step">
<button name="wizard_goto_step" class="btn btn-secondary btn-circle" type="submit"
value="activity">2</button>
<p>Activity Information</p>
</div>
</div>
</div>
<form method="POST">
{% csrf_token %}
{{ wizard.management_form }}
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
<label for="name" class="form-label">Name:</label>
<div class="row mb-3" id="name">
<div class="col">
{{ form.first_name }}
</div>
<div class="col">
{{ form.last_name }}
</div>
<label for="name" class="form-label">Name:</label>
<div class="row mb-3" id="name">
<div class="col">
{{ form.first_name }}
</div>
<div class="mb-3">
<label for="email" class="form-label">Email:</label>
{{ form.email }}
<div class="col">
{{ form.last_name }}
</div>
<div class="mb-3">
<label for="grade" class="form-label">Grade:</label>
{{ form.grade }}
</div>
<div class="mb-3">
<label for="house" class="form-label">House:</label>
{{ form.house }}
</div>
<div class="form-group d-flex justify-content-center">
<input class="btn btn-outline-primary btn-100" type="submit" value="Submit" />
</div>
</form>
{% endblock %}
</div>
<div class="mb-3">
<label for="email" class="form-label">Email:</label>
{{ form.email }}
</div>
<div class="mb-3">
<label for="grade" class="form-label">Grade:</label>
{{ form.grade }}
</div>
<div class="mb-3">
<label for="house" class="form-label">House:</label>
{{ form.house }}
</div>
<div class="form-group d-flex justify-content-center">
<input id='submit' class="btn btn-outline-primary btn-100" type="submit" value="Submit" />
</div>
</form>
{% endblock %}

View file

@ -0,0 +1,14 @@
{% extends 'base.html' %}
{% block content %}
<form method="POST">
{% csrf_token %}
{{ form.media.js }}
{{ form.as_p }}
<div class="form-group d-flex justify-content-center">
<input id='submit' class="btn btn-outline-primary btn-100" type="submit" value="Submit" />
</div>
</form>
{% endblock %}

View file

@ -1,50 +0,0 @@
{% extends 'base.html' %}
{% block title %}
<title>House Wars - Add House Points</title>
{% endblock %}
{% block formtitle %}
<h1 class="text-center mt-1 mb-4">Add House Points</h1>
{% endblock %}
{% block content %}
<form method="POST">
{% csrf_token %}
<div class="mb-3">
<label for="name" class="name">Name:</label>
{{ form.name }}
</div>
<div class="mb-3">
<label for="activity" class="form-label">Activity:</label>
{{ form.activity }}
</div>
<div class="mb-3">
<label for="house" class="form-label">House:</label>
{{ form.house }}
</div>
<div class="mb-3">
<label for="points" class="form-label">Points:</label>
{{ form.points }}
</div>
<div class="mb-3">
<label for="comment" class="form-label">Comments:</label>
{{ form.comment }}
</div>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
<div class="form-group d-flex justify-content-center">
<input class="btn btn-outline-primary btn-100" type="submit" value="Submit" />
</div>
</form>
{% endblock %}

View file

@ -1 +0,0 @@
from django.test import TestCase

View file

View file

View file

@ -0,0 +1,33 @@
from django.test import TestCase
from housewars.forms import ActivityForm
from housewars.models import Activity, House
class ActivityFormTest(TestCase):
def setUp(self):
self.activity30 = Activity.objects.create(name='Dodgeball', time=30)
self.activity60 = Activity.objects.create(name='Dodgeball', time=60)
def test_valid_time(self):
form = ActivityForm(data={
'activity1': self.activity30.id,
'activity2': self.activity30.id,
})
self.assertTrue(form.is_valid())
def test_invalid_time(self):
form = ActivityForm(data={
'activity1': self.activity60.id,
'activity2': self.activity30.id,
})
self.assertFalse(form.is_valid())
self.assertEquals(len(form.errors), 1)
def test_no_data(self):
form = ActivityForm(data={})
self.assertFalse(form.is_valid())
self.assertEquals(len(form.errors), 2)

View file

@ -0,0 +1,24 @@
from django.test import TestCase
from housewars.forms import FacilitatorForm
from housewars.models import Activity
class FacilitatorFormTest(TestCase):
def setUp(self):
self.activity = Activity.objects.create(name='Dodgeball', time=30)
def test_valid_input(self):
form = FacilitatorForm(data={
'first_name': 'Test',
'last_name': 'User',
'activity': self.activity.id
})
self.assertTrue(form.is_valid())
def test_no_data(self):
form = FacilitatorForm(data={})
self.assertFalse(form.is_valid())
self.assertEquals(len(form.errors), 3)

View file

@ -0,0 +1,27 @@
from django.test import TestCase
from housewars.forms import PointsEntryForm
from housewars.models import Activity, House, Award
class PointsEntryFormTest(TestCase):
def setUp(self):
self.house = House.objects.create(name='Hawk')
self.activity = Activity.objects.create(name='Dodgeball', time=30)
self.activity2 = Activity.objects.create(name='Dodgeball', time=30)
self.award = Award.objects.create(name='1st', activity=self.activity)
def test_valid_time(self):
form = PointsEntryForm(data={
'house': self.house.id,
'activity': self.activity.id,
'award': self.award.id
})
self.assertTrue(form.is_valid())
def test_no_data(self):
form = PointsEntryForm(data={})
self.assertFalse(form.is_valid())
self.assertEquals(len(form.errors), 3)

View file

@ -0,0 +1,38 @@
from django.test import TestCase
from housewars.forms import UserEntryForm
from housewars.models import House
class UserEntryFormTest(TestCase):
def setUp(self):
self.house = House.objects.create(name='Hawk', points=30)
def test_valid(self):
form = UserEntryForm(data={
'first_name': 'Test',
'last_name': 'User',
'email': 'test.user@slps.org',
'grade': 9,
'house': self.house.id
})
self.assertTrue(form.is_valid())
def test_invalid_email(self):
form = UserEntryForm(data={
'first_name': 'Test',
'last_name': 'User',
'email': 'test.user@gmail.com',
'grade': 9,
'house': self.house.id
})
self.assertFalse(form.is_valid())
self.assertEqual(len(form.errors), 1)
def test_no_data(self):
form = UserEntryForm(data={})
self.assertFalse(form.is_valid())
self.assertEquals(len(form.errors), 5)

View file

View file

@ -0,0 +1,23 @@
from django.test import TestCase
from housewars.models import Activity, House, Award, PointsEntry
class HouseModelTest(TestCase):
def setUp(self):
self.house = House.objects.create(name='Hawk', points=30)
self.activity = Activity.objects.create(name='Dodgeball', time=30)
self.award = Award.objects.create(
activity=self.activity, name='1st', points=30)
def test_current_points(self):
PointsEntry.objects.create(
activity=self.activity, house=self.house, award=self.award)
self.assertEquals(self.house.current_points, 30)
def test_total_points(self):
PointsEntry.objects.create(
activity=self.activity, house=self.house, award=self.award)
self.assertEquals(self.house.total_points, 60)

View file

@ -0,0 +1,18 @@
from django.test import TestCase, Client
from django.urls import reverse
from housewars.models import Activity, House, Teacher, UserEntry
class UserEntryTest(TestCase):
def setUp(self):
self.client = Client()
self.house = House.objects.create(name='Hawk', points=30)
self.activity = Activity.objects.create(name='Dodgeball', time=30)
self.entry = UserEntry.objects.create(
first_name='Test', last_name='User', email='test.user@slps.org', grade=9, house=self.house, activity1=self.activity, activity2=self.activity)
self.teacher = Teacher.objects.create(
first_name='Test', last_name='Teacher', grade=9, house=self.house)
def test_mentor(self):
self.assertEquals(self.entry.mentor, self.teacher)

View file

@ -0,0 +1,14 @@
from django.test import TestCase
from django.urls import reverse, resolve
from housewars.views import EntryCreateView, PointsEntryCreateView
class TestUrls(TestCase):
def test_entry_create_is_resolved(self):
url = reverse('housewars:signup')
self.assertEquals(resolve(url).func.view_class, EntryCreateView)
def test_points_entry_create_is_resolved(self):
url = reverse('housewars:add_points')
self.assertEquals(resolve(url).func.view_class, PointsEntryCreateView)

View file

View file

@ -0,0 +1,13 @@
from django.test import TestCase
from housewars.utils import get_random_string
class GetRandomStringTest(TestCase):
def test_string_of_length_8(self):
string = get_random_string(8)
self.assertEqual(len(string), 8)
def test_string_of_length_6(self):
string = get_random_string(6)
self.assertEqual(len(string), 6)

View file

@ -0,0 +1,20 @@
from django.test import TestCase
import io
from housewars.utils import load_pdf
from housewars.models import House
class UnpackValuesTest(TestCase):
def setUp(self):
House.objects.create(name="Hawk")
House.objects.create(name="Snowy")
self.test_queryset = House.objects.all()
def test_matching_object_instance(self):
headers = [field.name for field in House._meta.fields]
self.assertIsInstance(
load_pdf(self.test_queryset, headers), io.BytesIO)

View file

@ -0,0 +1,26 @@
from django.test import TestCase
from housewars.utils import unpack_values
from housewars.models import House
class UnpackValuesTest(TestCase):
def setUp(self):
House.objects.create(name="Hawk")
House.objects.create(name="Snowy")
self.test_queryset = House.objects.all()
def test_valid_row_headers(self):
headers = [field.name for field in House._meta.fields]
values = unpack_values(self.test_queryset, headers)
self.assertEqual(
values[0], headers)
def test_complete_values(self):
headers = [field.name for field in House._meta.fields]
values = unpack_values(self.test_queryset, headers)
queryset = list(self.test_queryset.values_list())
for row in queryset:
self.assertIn(list(row), values)

View file

View file

@ -0,0 +1,33 @@
from django.test import TestCase, Client
from django.urls import reverse
from housewars.models import Activity
class FacilitatorCreateViewTest(TestCase):
def setUp(self):
self.client = Client()
self.url = reverse('housewars:facilitator')
self.activity = Activity.objects.create(name='Dodgeball', time=30)
def test_GET(self):
response = self.client.get(self.url)
self.assertEquals(response.status_code, 200)
self.assertTemplateUsed(response, 'housewars/general_form.html')
def test_POST(self):
response = self.client.post(self.url, {
'first_name': 'Test',
'last_name': 'User',
'activity': self.activity.id
})
self.assertEquals(response.status_code, 302)
self.assertEquals(self.activity.facilitator_set.count(), 1)
def test_failed_POST(self):
response = self.client.post(self.url)
self.assertEquals(response.status_code, 200)
self.assertEquals(self.activity.facilitator_set.count(), 0)

View file

@ -0,0 +1,35 @@
from django.test import TestCase, Client
from django.urls import reverse
from housewars.models import Activity, House, Award
class PointsEntryCreateViewTest(TestCase):
def setUp(self):
self.client = Client()
self.url = reverse('housewars:add_points')
self.house = House.objects.create(name='Hawk')
self.activity = Activity.objects.create(name='Dodgeball', time=30)
self.award = Award.objects.create(activity=self.activity, name='1st')
def test_GET(self):
response = self.client.get(self.url)
self.assertEquals(response.status_code, 200)
self.assertTemplateUsed(response, 'housewars/general_form.html')
def test_POST(self):
response = self.client.post(self.url, {
'house': self.house.id,
'activity': self.activity.id,
'award': self.award.id
})
self.assertEquals(response.status_code, 302)
self.assertEquals(self.house.pointsentry_set.count(), 1)
def test_failed_POST(self):
response = self.client.post(self.url)
self.assertEquals(response.status_code, 200)
self.assertEquals(self.house.pointsentry_set.count(), 0)

View file

@ -0,0 +1,62 @@
import enum
from django.test import TestCase, Client
from django.urls import reverse
from housewars.models import Activity, House
class UserEntryCreateViewTest(TestCase):
def setUp(self):
self.client = Client()
self.url = reverse('housewars:signup')
self.house = House.objects.create(name='Hawk')
self.activity = Activity.objects.create(
name='Dodgeball', time=30, default_quota=5)
self.full_activity = Activity.objects.create(
name='Dodgeball', time=30)
def test_POST(self):
user_info_form = {
'user-first_name': 'Test',
'user-last_name': 'User',
'user-email': 'asdfasdf@slps.org',
'user-grade': 9,
'user-house': self.house.id,
'entry_create_view-current_step': 'user'
}
activity_form = {
'activity-activity1': self.activity.id,
'activity-activity2': self.activity.id,
'entry_create_view-current_step': 'activity'
}
steps_data = [user_info_form, activity_form]
for data in steps_data:
self.client.post(self.url, data)
self.assertEquals(self.house.userentry_set.count(), 1)
def test_full_activity_POST(self):
user_info_form = {
'user-first_name': 'Test',
'user-last_name': 'User',
'user-email': 'asdfasdf@slps.org',
'user-grade': 9,
'user-house': self.house.id,
'entry_create_view-current_step': 'user'
}
activity_form = {
'activity-activity1': self.full_activity.id,
'activity-activity2': self.full_activity.id,
'entry_create_view-current_step': 'activity'
}
steps_data = [user_info_form, activity_form]
for data in steps_data:
self.client.post(self.url, data)
self.assertEquals(self.house.userentry_set.count(), 0)

View file

@ -21,5 +21,6 @@ from . import views
app_name = 'housewars'
urlpatterns = [
path('', views.EntryCreateView.as_view(), name='signup'),
path('points/', views.CreatePointsEntry.as_view(), name='add_points')
path('points/', views.PointsEntryCreateView.as_view(), name='add_points'),
path('facilitator/', views.FacilitatorCreateView.as_view(), name='facilitator')
]

View file

@ -0,0 +1,3 @@
from .get_random_string import get_random_string
from .unpack_values import unpack_values
from .load_pdf import load_pdf

Some files were not shown because too many files have changed in this diff Show more