Compare commits
76 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ef4aa5e9f | |||
| 54d556f565 | |||
| b0d049555c | |||
| ffa49895e7 | |||
| b76a9c15c6 | |||
| 4cb149f279 | |||
| a39e51e1f5 | |||
| cbabafb492 | |||
| 12e6edc3dc | |||
| 76ef47caaf | |||
| 69902022f7 | |||
| 6c7c5ad188 | |||
| 931fd77ac2 | |||
| 4ed203aa23 | |||
| 3ec387babb | |||
| 5be429a113 | |||
| 403a4f1a6d | |||
| 64f38f3aa4 | |||
| 9037540988 | |||
| c27269f406 | |||
| 3be7669343 | |||
| 3b0c16665f | |||
| c696038321 | |||
| b815899101 | |||
| 298530599e | |||
| acf802c7fa | |||
| fbd52bbbf7 | |||
| a7e1c87e82 | |||
| df75d7ffc1 | |||
| 549c152c1d | |||
| 58e3b16455 | |||
| 63a28a6813 | |||
| 25bf93b1bc | |||
| b134a20813 | |||
| 5f06b0c9be | |||
| 8aabb4f46d | |||
| d9d0057fa4 | |||
| 6cd4bb0075 | |||
| c9f9af6103 | |||
| e5549cfc24 | |||
| 7666bd3973 | |||
| cc18ace04b | |||
| 68ea97070b | |||
| 8d2ca26860 | |||
| 820ba17513 | |||
| 0849cfa25a | |||
| ea9341f583 | |||
| f01eb322aa | |||
| d52d2ad78b | |||
| ddcc6af4bd | |||
| 60b9ea5cdf | |||
| 67fd153f2c | |||
| 8bdc94be04 | |||
| 7a7186c9ee | |||
| 5ff0814b85 | |||
| ee2ca7eef6 | |||
| 8a46f34190 | |||
| 9ff327a641 | |||
| 5bbfcf79bf | |||
| 03a3c5a35b | |||
| 583276ec9b | |||
| 1be0d9d5c6 | |||
| 3661c4e60f | |||
| 2f31bf39e6 | |||
| 6610a5ff28 | |||
| 45c27edf74 | |||
| 1b96b62173 | |||
| 4f9c1b5aec | |||
| 01e404923a | |||
| 5503ee1bd7 | |||
| 3cc29e3ad6 | |||
| f3ea1d1d62 | |||
| 44bda77674 | |||
| 03741a236b | |||
| 02bb53d84e | |||
| a6441ffb63 |
2
.dockerignore
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
venv
|
||||
.env
|
||||
6
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
*.html linguist-detectable=false
|
||||
*.js linguist-detectable=false
|
||||
*.css linguist-detectable=false
|
||||
|
||||
static/* linguist-vendored
|
||||
static/src/*
|
||||
30
.github/release-drafter.yml
vendored
Normal file
|
|
@ -0,0 +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'
|
||||
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
|
||||
template: |
|
||||
# Changes
|
||||
$CHANGES
|
||||
55
.github/workflows/build-deploy.yml
vendored
Normal 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 }}
|
||||
12
.gitignore
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
13
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"emmet.includeLanguages": {
|
||||
"jinja-html": "html"
|
||||
},
|
||||
"editor.defaultFormatter": null,
|
||||
"[dockercompose]": {
|
||||
"editor.defaultFormatter": "ms-azuretools.vscode-docker"
|
||||
},
|
||||
"lldb.displayFormat": "auto",
|
||||
"lldb.showDisassembly": "auto",
|
||||
"lldb.dereferencePointers": true,
|
||||
"lldb.consoleMode": "commands"
|
||||
}
|
||||
1
CollegiateHouseWars/settings/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from .local import *
|
||||
|
|
@ -1,34 +1,11 @@
|
|||
"""
|
||||
Django settings for CollegiateHouseWars project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 4.0.4.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.0/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/4.0/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from django.contrib.messages import constants
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
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!
|
||||
SECRET_KEY = 'django-insecure-+m#ore4ojw1s_+yo+kutzpi^asg2dg9+f3+rf(hr#o)73ifo_1'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
||||
# Application definition
|
||||
SECURE_SSL_REDIRECT = False
|
||||
SESSION_COOKIE_SECURE = False
|
||||
CSRF_COOKIE_SECURE = False
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
|
|
@ -38,11 +15,15 @@ INSTALLED_APPS = [
|
|||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
|
||||
'housewars.apps.HousewarsConfig'
|
||||
'smart_selects',
|
||||
'formtools',
|
||||
|
||||
'housewars',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
|
|
@ -56,7 +37,7 @@ ROOT_URLCONF = 'CollegiateHouseWars.urls'
|
|||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'DIRS': [BASE_DIR / 'templates'],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
|
|
@ -71,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': BASE_DIR / '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',
|
||||
|
|
@ -101,10 +67,6 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/4.0/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
|
@ -113,13 +75,34 @@ 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 = BASE_DIR / "staticfiles"
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
|
||||
STATICFILES_DIRS = [
|
||||
BASE_DIR / "static"
|
||||
]
|
||||
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'handlers': {
|
||||
'console': {
|
||||
'class': 'logging.StreamHandler',
|
||||
},
|
||||
},
|
||||
'root': {
|
||||
'handlers': ['console'],
|
||||
'level': 'WARNING',
|
||||
},
|
||||
}
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
|
||||
MESSAGE_TAGS = {constants.DEBUG: 'debug',
|
||||
constants.INFO: 'info',
|
||||
constants.SUCCESS: 'success',
|
||||
constants.WARNING: 'warning',
|
||||
constants.ERROR: 'danger', }
|
||||
|
||||
USE_DJANGO_JQUERY = True
|
||||
12
CollegiateHouseWars/settings/docker.py
Normal 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'
|
||||
}
|
||||
}
|
||||
17
CollegiateHouseWars/settings/local.py
Normal 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',
|
||||
}
|
||||
}
|
||||
29
CollegiateHouseWars/settings/prod.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import os
|
||||
|
||||
from .defaults import *
|
||||
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY')
|
||||
|
||||
DEBUG = False
|
||||
|
||||
ALLOWED_HOSTS = [
|
||||
'csmb-housewars.c4patino.com',
|
||||
'collegiate-housewars-02810c3f29dc.herokuapp.com',
|
||||
]
|
||||
|
||||
CSRF_TRUSTED_ORIGINS = [
|
||||
'http://collegiate-housewars-02810c3f29dc.herokuapp.com/'
|
||||
]
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
'NAME': os.environ.get('DATABASE_NAME'),
|
||||
'USER': os.environ.get('DATABASE_USERNAME'),
|
||||
'PASSWORD': os.environ.get('DATABASE_PASSWORD'),
|
||||
'HOST': os.environ.get('DATABASE_HOST'),
|
||||
'PORT': '3306'
|
||||
}
|
||||
}
|
||||
|
||||
STATICFILES_STORAGE = "whitenoise.storage.CompressedStaticFilesStorage"
|
||||
|
|
@ -15,9 +15,12 @@ Including another URLconf
|
|||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
20
Dockerfile
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
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 && \
|
||||
pip install gunicorn
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN python manage.py collectstatic --noinput
|
||||
|
||||
EXPOSE 8000/tcp
|
||||
|
||||
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "CollegiateHouseWars.wsgi:application"]
|
||||
17
Dockerfile.dev
Normal 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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
web: gunicorn CollegiateHouseWars.wsgi
|
||||
86
README.md
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
# Collegiate Housewars
|
||||
|
||||
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) |
|
||||
| [Setup](#setup) |
|
||||
| [Hosting](#hosting) |
|
||||
| [Contributing](#contributing) |
|
||||
|
||||
## Usage Instructions
|
||||
So far there are three primary pages on this site. They include the:
|
||||
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.
|
||||
|
||||
## 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)
|
||||
|
||||
### 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. 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.
|
||||
|
||||
#### 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 the IP address of your production database.
|
||||
|
||||
## Contributing
|
||||
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 [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 (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.
|
||||
27
docker-compose.prod.yml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
version: '3.8'
|
||||
|
||||
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:
|
||||
- 127.0.0.1:8000:8000
|
||||
networks:
|
||||
- housewars
|
||||
- common-network
|
||||
external_links:
|
||||
- mysqldb
|
||||
|
||||
networks:
|
||||
common-network:
|
||||
external: true
|
||||
housewars: {}
|
||||
44
docker-compose.yml
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
app:
|
||||
restart: unless-stopped
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.dev
|
||||
ports:
|
||||
- 8000:8000
|
||||
volumes:
|
||||
- ./:/app
|
||||
networks:
|
||||
- housewars
|
||||
environment:
|
||||
DJANGO_SETTINGS_MODULE: "CollegiateHouseWars.settings.docker"
|
||||
depends_on:
|
||||
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:8.3
|
||||
ports:
|
||||
- 3306:3306
|
||||
volumes:
|
||||
- housewars-mysql:/var/lib/mysql
|
||||
networks:
|
||||
- housewars
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: "passw0rd"
|
||||
MYSQL_DATABASE: dev
|
||||
healthcheck:
|
||||
test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ]
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
volumes:
|
||||
housewars-mysql:
|
||||
|
||||
|
||||
networks:
|
||||
housewars: {}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
from django.contrib import admin
|
||||
from .models import Activity, Entry
|
||||
|
||||
# Register your models here.
|
||||
admin.site.register(Activity)
|
||||
admin.site.register(Entry)
|
||||
145
housewars/admin/Activity.py
Normal 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
|
||||
15
housewars/admin/Facilitator.py
Normal 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
|
|
@ -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
|
||||
32
housewars/admin/PointsEntry.py
Normal 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
|
||||
87
housewars/admin/Teacher.py
Normal 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
|
||||
103
housewars/admin/UserEntry.py
Normal 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')
|
||||
6
housewars/admin/__init__.py
Normal 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
|
||||
35
housewars/forms/ActivityForm.py
Normal 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.")
|
||||
26
housewars/forms/FacilitatorForm.py
Normal 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'
|
||||
}),
|
||||
}
|
||||
26
housewars/forms/PointsEntryForm.py
Normal 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'})
|
||||
45
housewars/forms/UserEntryForm.py
Normal 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
|
||||
4
housewars/forms/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from .ActivityForm import ActivityForm
|
||||
from .FacilitatorForm import FacilitatorForm
|
||||
from .PointsEntryForm import PointsEntryForm
|
||||
from .UserEntryForm import UserEntryForm
|
||||
|
|
@ -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')},
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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')},
|
||||
),
|
||||
]
|
||||
|
|
@ -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,
|
||||
),
|
||||
]
|
||||
18
housewars/migrations/0003_alter_activity_time.py
Normal 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')]),
|
||||
),
|
||||
]
|
||||
|
|
@ -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')]),
|
||||
),
|
||||
]
|
||||
|
|
@ -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(),
|
||||
),
|
||||
]
|
||||
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
18
housewars/migrations/0005_alter_teacher_grade.py
Normal 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),
|
||||
),
|
||||
]
|
||||
23
housewars/migrations/0006_facilitator.py
Normal 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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
19
housewars/migrations/0007_alter_facilitator_activity.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
14
housewars/migrations/0008_merge_20220907_2259.py
Normal 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 = [
|
||||
]
|
||||
18
housewars/migrations/0009_alter_activity_room_number.py
Normal 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),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
from django.db import models
|
||||
from django.forms import ValidationError
|
||||
|
||||
# Create your models here.
|
||||
|
||||
|
||||
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 Entry(models.Model):
|
||||
class Meta:
|
||||
verbose_name_plural = "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)
|
||||
grade = models.IntegerField(choices=GradeChoices)
|
||||
activity1 = models.ForeignKey(
|
||||
Activity, related_name='activity1', on_delete=models.CASCADE)
|
||||
activity2 = models.ForeignKey(
|
||||
Activity, related_name='activity2', on_delete=models.CASCADE)
|
||||
|
||||
def clean(self):
|
||||
if self.activity1.time + self.activity2.time > 60:
|
||||
raise ValidationError(
|
||||
"Please only pick activities adding up to one hour.")
|
||||
else:
|
||||
super().clean()
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.first_name} {self.last_name}"
|
||||
26
housewars/models/Activity.py
Normal 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
|
|
@ -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)
|
||||
13
housewars/models/Facilitator.py
Normal 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
|
|
@ -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)
|
||||
22
housewars/models/PointsEntry.py
Normal 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
|
|
@ -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)
|
||||
30
housewars/models/Teacher.py
Normal 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)
|
||||
47
housewars/models/UserEntry.py
Normal 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)
|
||||
8
housewars/models/__init__.py
Normal 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
|
||||
77
housewars/static/css/main.css
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
html, body {
|
||||
height: 100%;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.btn-50 {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.btn-100 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.max-height {
|
||||
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 {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
.steps-form .steps-row:before {
|
||||
top: 14px;
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
content: " ";
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
.steps-form .steps-row .steps-step {
|
||||
display: table-cell;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.steps-form .steps-row .steps-step p {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.steps-form .steps-row .steps-step button[disabled] {
|
||||
opacity: 1 !important;
|
||||
filter: alpha(opacity=100) !important;
|
||||
}
|
||||
|
||||
.steps-form .steps-row .steps-step .btn-circle {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
text-align: center;
|
||||
padding: 6px 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.428571429;
|
||||
border-radius: 15px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
#banner {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
40
housewars/templates/housewars/activity_form.html
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% 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 }}
|
||||
|
||||
<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 id='submit' class="btn btn-outline-primary btn-50" type="submit" value="Submit" />
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
@ -1,42 +1,51 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="ISO-8859-1" />
|
||||
<meta name="description" content="A website made by C4 Patino" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% if title %}
|
||||
<title>Django Blog - {{title}}</title>
|
||||
{% else %}
|
||||
<title>Django Blog</title>
|
||||
{% endif %}
|
||||
</head>
|
||||
<body>
|
||||
<main role="main" class="container">
|
||||
<div class="content-section">
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<fieldset class="form-group">
|
||||
<legend class="border-bottom mb-4">Blog Post</legend>
|
||||
{{ form }}
|
||||
</fieldset>
|
||||
<div>
|
||||
<button class="btn btn-outline-info" type="submit">
|
||||
Post
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-gtEjrD/SeCtmISkJkNUaaKMoLD0//ElJ19smozuHV6z3Iehds+3Ulb9Bn9Plx0x4"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
{% 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 }}
|
||||
|
||||
<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>
|
||||
</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 %}
|
||||
14
housewars/templates/housewars/general_form.html
Normal 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 %}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
0
housewars/tests/__init__.py
Normal file
0
housewars/tests/forms/__init__.py
Normal file
33
housewars/tests/forms/test_activity_form.py
Normal 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)
|
||||
24
housewars/tests/forms/test_facilitator_form.py
Normal 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)
|
||||
27
housewars/tests/forms/test_points_entry_form.py
Normal 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)
|
||||
38
housewars/tests/forms/test_user_entry_form.py
Normal 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)
|
||||
0
housewars/tests/models/__init__.py
Normal file
23
housewars/tests/models/test_house.py
Normal 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)
|
||||
18
housewars/tests/models/test_user_entry.py
Normal 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)
|
||||
14
housewars/tests/test_urls.py
Normal 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)
|
||||
0
housewars/tests/utils/__init__.py
Normal file
13
housewars/tests/utils/test_get_random_string.py
Normal 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)
|
||||
20
housewars/tests/utils/test_load_pdf.py
Normal 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)
|
||||
26
housewars/tests/utils/test_unpack_values.py
Normal 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)
|
||||
0
housewars/tests/views/__init__.py
Normal file
33
housewars/tests/views/test_facilitator_create.py
Normal 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)
|
||||
35
housewars/tests/views/test_points_entry_create.py
Normal 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)
|
||||
62
housewars/tests/views/test_user_entry_create.py
Normal 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)
|
||||
|
|
@ -14,11 +14,13 @@ Including another URLconf
|
|||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = 'housewars'
|
||||
urlpatterns = [
|
||||
path('', views.EntryCreateView.as_view(), name='entry_create'),
|
||||
path('', views.EntryCreateView.as_view(), name='signup'),
|
||||
path('points/', views.PointsEntryCreateView.as_view(), name='add_points'),
|
||||
path('facilitator/', views.FacilitatorCreateView.as_view(), name='facilitator')
|
||||
]
|
||||
|
|
|
|||
3
housewars/utils/__init__.py
Normal 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
|
||||
14
housewars/utils/get_random_string.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import random
|
||||
import string
|
||||
|
||||
|
||||
def get_random_string(length: int) -> str:
|
||||
"""Generates a random string of length n
|
||||
|
||||
:param length int: The desired length of the returned string
|
||||
:return: A random string of length n
|
||||
"""
|
||||
|
||||
letters = string.ascii_lowercase
|
||||
result_str = ''.join(random.choice(letters) for i in range(length))
|
||||
return result_str
|
||||
44
housewars/utils/load_pdf.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
from reportlab.platypus import Table, TableStyle, SimpleDocTemplate, Paragraph
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.lib.colors import black
|
||||
|
||||
import io
|
||||
|
||||
from housewars.utils import unpack_values
|
||||
|
||||
|
||||
def load_pdf(data, headers, title=""):
|
||||
"""Loads data from queryset into a pdf file
|
||||
|
||||
:param queryset: The queryset that will be unpacked
|
||||
:param headers: A list of the desired headers of the queryset
|
||||
:returns A bytestream of a pdf file
|
||||
"""
|
||||
|
||||
# Initialize pdf object
|
||||
buffer = io.BytesIO()
|
||||
doc = SimpleDocTemplate(buffer, pagesize=letter)
|
||||
elements = []
|
||||
|
||||
# Create the page title
|
||||
title = Paragraph(title)
|
||||
|
||||
# Load title into PDF file
|
||||
elements.append(title)
|
||||
|
||||
# Unpack queryset data
|
||||
data = unpack_values(data, headers)
|
||||
|
||||
# Load data into table and set styles
|
||||
table = Table(data, repeatRows=1)
|
||||
table.setStyle(TableStyle(
|
||||
[('INNERGRID', (0, 0), (-1, -1), 0.25, black), ('BOX', (0, 0), (-1, -1), 0.25, black)]))
|
||||
|
||||
# Load table into PDF file
|
||||
elements.append(table)
|
||||
|
||||
# Build document
|
||||
doc.build(elements)
|
||||
buffer.seek(0)
|
||||
|
||||
return buffer
|
||||
24
housewars/utils/unpack_values.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
from django.db.models import QuerySet
|
||||
from typing import List
|
||||
|
||||
|
||||
def unpack_values(queryset: QuerySet, headers: List[str]) -> List[List]:
|
||||
"""Unpacks a queryset into a 2D array
|
||||
|
||||
:param queryset: The queryset that will be unpacked
|
||||
:param headers: A list of the desired headers of the queryset
|
||||
:returns A 2D array of headers, then rows
|
||||
"""
|
||||
|
||||
data = []
|
||||
data.append(headers)
|
||||
for row in queryset:
|
||||
values = []
|
||||
for field in headers:
|
||||
value = getattr(row, field)
|
||||
if value is None:
|
||||
value = ''
|
||||
values.append(value)
|
||||
data.append(values)
|
||||
|
||||
return data
|
||||
|
|
@ -1,10 +1,135 @@
|
|||
from django.shortcuts import render
|
||||
from django.contrib import messages
|
||||
from django.db.models import Count, F, Q, Subquery, OuterRef
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.views.generic import CreateView
|
||||
from formtools.wizard.views import SessionWizardView
|
||||
|
||||
from .models import Entry
|
||||
from housewars.forms import ActivityForm, PointsEntryForm, UserEntryForm, FacilitatorForm
|
||||
from housewars.models import Activity, Quota, PointsEntry, UserEntry, Facilitator
|
||||
|
||||
FORMS = [("user", UserEntryForm),
|
||||
("activity", ActivityForm)]
|
||||
|
||||
TEMPLATES = {"user": "housewars/entry_form.html",
|
||||
"activity": "housewars/activity_form.html"}
|
||||
|
||||
|
||||
class EntryCreateView(CreateView):
|
||||
model = Entry
|
||||
fields = ['first_name', 'last_name', 'email',
|
||||
'grade', 'activity1', 'activity2']
|
||||
class EntryCreateView(SessionWizardView):
|
||||
form_list = FORMS
|
||||
|
||||
def get_template_names(self):
|
||||
return [TEMPLATES[self.steps.current]]
|
||||
|
||||
def get_form(self, step=None, data=None, files=None):
|
||||
form = super().get_form(step, data, files)
|
||||
|
||||
if step == 'activity':
|
||||
cd = self.storage.get_step_data('user')
|
||||
|
||||
# Retrieve custom quotas for house activities
|
||||
quota = Quota.objects.filter(
|
||||
house=cd.get('user-house'), activity=OuterRef('id')).values('quota')
|
||||
|
||||
# Set quota to custom quota or default, then set count to the total signups for each activity. Filter where quota is less than house signups.
|
||||
filtered1 = Activity.objects.annotate(final_quota=Coalesce(Subquery(quota[:1]), F('default_quota'))).annotate(count=Count(
|
||||
'activity1', filter=Q(activity1__house=cd.get('user-house')))).filter(final_quota__gt=F('count'))
|
||||
|
||||
filtered2 = Activity.objects.annotate(final_quota=Coalesce(Subquery(quota[:1]), F('default_quota'))).annotate(count=Count(
|
||||
'activity2', filter=Q(activity2__house=cd.get('user-house')))).filter(final_quota__gt=F('count'), time=30)
|
||||
|
||||
# Render filtered as choices in activity select step
|
||||
form.fields['activity1'].queryset = filtered1
|
||||
form.fields['activity2'].queryset = filtered2
|
||||
|
||||
return form
|
||||
|
||||
def done(self, form_list, **kwargs):
|
||||
cleaned_data = self.get_all_cleaned_data()
|
||||
UserEntry.objects.create(**cleaned_data)
|
||||
|
||||
activity1 = cleaned_data['activity1']
|
||||
activity2 = cleaned_data['activity2']
|
||||
|
||||
# Build the response string.
|
||||
a1_string = f'You are in {activity1}'
|
||||
activity1_room = activity1.room_number
|
||||
|
||||
if (activity1_room != None):
|
||||
a1_string += f' in {activity1_room}'
|
||||
|
||||
if (hasattr(activity1, 'teacher')):
|
||||
activity1_teacher = activity1.teacher
|
||||
a1_string += f' with {activity1_teacher}.'
|
||||
else:
|
||||
a1_string += '.'
|
||||
|
||||
a2_string = ''
|
||||
if (activity2 != None):
|
||||
a2_string = f'You are in {activity2}'
|
||||
|
||||
activity2_room = activity2.room_number
|
||||
if (activity2_room != None):
|
||||
a2_string += f' in {activity2_room}'
|
||||
if (hasattr(activity2, 'teacher')):
|
||||
activity2_teacher = activity2.teacher.last_name
|
||||
a2_string += f' with {activity2_teacher}.'
|
||||
else:
|
||||
a2_string += '.'
|
||||
|
||||
messages.success(self.request, 'Your signup has been submitted.')
|
||||
messages.success(self.request, a1_string)
|
||||
messages.success(self.request, a2_string)
|
||||
|
||||
return redirect('housewars:signup')
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('housewars:signup')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['title'] = 'Signup'
|
||||
context['formtitle'] = 'House Wars Signup'
|
||||
return context
|
||||
|
||||
|
||||
class PointsEntryCreateView(CreateView):
|
||||
model = PointsEntry
|
||||
form_class = PointsEntryForm
|
||||
template_name = 'housewars/general_form.html'
|
||||
|
||||
def form_valid(self, form):
|
||||
messages.success(self.request, 'Your points entry has been submitted.')
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('housewars:add_points')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['title'] = 'Add Points'
|
||||
context['formtitle'] = 'Add House Points'
|
||||
return context
|
||||
|
||||
|
||||
class FacilitatorCreateView(CreateView):
|
||||
model = Facilitator
|
||||
form_class = FacilitatorForm
|
||||
template_name = 'housewars/general_form.html'
|
||||
|
||||
def form_valid(self, form):
|
||||
messages.success(
|
||||
self.request, 'Your facilitator signup has been submitted.')
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('housewars:add_points')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['title'] = 'Facilitator'
|
||||
context['formtitle'] = 'Facilitator Signup'
|
||||
return context
|
||||
|
|
|
|||
|
|
@ -3,10 +3,16 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from pathlib import Path
|
||||
|
||||
load_dotenv(Path('.env'))
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'CollegiateHouseWars.settings')
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE',
|
||||
'CollegiateHouseWars.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,30 @@
|
|||
asgiref==3.5.2
|
||||
autopep8==1.6.0
|
||||
Django==4.0.4
|
||||
pycodestyle==2.8.0
|
||||
sqlparse==0.4.2
|
||||
toml==0.10.2
|
||||
asgiref==3.7.2
|
||||
attrs==23.2.0
|
||||
certifi==2024.2.2
|
||||
chardet==5.2.0
|
||||
charset-normalizer==3.3.2
|
||||
Django==4.2.10
|
||||
django-formtools==2.5.1
|
||||
django-smart-selects==1.6.0
|
||||
gunicorn==21.2.0
|
||||
h11==0.14.0
|
||||
idna==3.6
|
||||
mysqlclient==2.2.4
|
||||
outcome==1.3.0.post0
|
||||
packaging==23.2
|
||||
pillow==10.2.0
|
||||
PySocks==1.7.1
|
||||
python-dotenv==1.0.1
|
||||
reportlab==4.1.0
|
||||
requests==2.31.0
|
||||
selenium==4.4.3
|
||||
sniffio==1.3.0
|
||||
sortedcontainers==2.4.0
|
||||
sqlparse==0.4.4
|
||||
trio==0.24.0
|
||||
trio-websocket==0.11.1
|
||||
typing_extensions==4.9.0
|
||||
urllib3==1.26.18
|
||||
webdriver-manager==4.0.1
|
||||
whitenoise==6.6.0
|
||||
wsproto==1.2.0
|
||||
|
|
|
|||
BIN
static/favicon.ico
vendored
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
static/img/IMG_5817.JPG
Normal file
|
After Width: | Height: | Size: 5.4 MiB |
BIN
static/img/IMG_5818.JPG
Normal file
|
After Width: | Height: | Size: 4.9 MiB |
BIN
static/img/IMG_5819.JPG
Normal file
|
After Width: | Height: | Size: 5 MiB |
BIN
static/img/IMG_5821.JPG
Normal file
|
After Width: | Height: | Size: 5.7 MiB |
BIN
static/img/IMG_5823.JPG
Normal file
|
After Width: | Height: | Size: 4.2 MiB |
BIN
static/img/IMG_5826.JPG
Normal file
|
After Width: | Height: | Size: 5.7 MiB |
BIN
static/img/IMG_5849.JPG
Normal file
|
After Width: | Height: | Size: 4.9 MiB |
BIN
static/img/IMG_5856.JPG
Normal file
|
After Width: | Height: | Size: 5.2 MiB |
2
static/jquery-3.6.1.min.js
vendored
Normal file
17
templates/admin/base_site.html
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{% extends "admin/base_site.html" %}
|
||||
|
||||
{% block title %}
|
||||
{% if subtitle %}
|
||||
{{ subtitle }} |
|
||||
{% endif %}
|
||||
{{ title }} | House Wars Admin
|
||||
{% endblock %}
|
||||
|
||||
{% block branding %}
|
||||
<h1 id="site-name">
|
||||
<a href="{% url 'admin:index' %}">House Wars Administration</a>
|
||||
</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block nav-global %}
|
||||
{% endblock %}
|
||||
98
templates/base.html
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="ISO-8859-1" />
|
||||
<meta name="description" content="A website made by C4 Patino" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="{% static 'jquery-3.6.1.min.js' %}"></script>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href='{% static "css/main.css" %}' />
|
||||
<link rel="icon" type="image/x-icon" href='{% static "favicon.ico" %}'>
|
||||
|
||||
<script src="{% static 'smart-selects/admin/js/chainedfk.js' %}"></script>
|
||||
<script src="{% static 'smart-selects/admin/js/chainedm2m.js' %}"></script>
|
||||
|
||||
{% block title %}
|
||||
<title>
|
||||
{% if title %}{{ title }}{% endif %} | House Wars
|
||||
</title>
|
||||
{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main role="main" class="container-fluid h-100 m-0 p-0">
|
||||
<div class="row h-100 m-0 p-0">
|
||||
<div class="col-lg-4 col-md-12 p-5">
|
||||
<div id="step-counter" class="mb-5">
|
||||
{% block step-counter %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<h2 id="header" class="mt-2 mb-4">{% if formtitle %}{{ formtitle }}{% endif %}</h2>
|
||||
|
||||
<!-- Main content -->
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
<!-- Message content and form errors -->
|
||||
<div class="mt-2">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if form.errors %}
|
||||
{% for field in form %}
|
||||
{% for error in field.errors %}
|
||||
<div class="alert alert-danger">{{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
||||
{% for error in form.non_field_errors %}
|
||||
<div class="alert alert-danger">{{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div id="banner" class="col row align-items-center m-0 pe-5">
|
||||
<div id="carousel" class="carousel slide m-0 p-0" data-bs-ride="carousel">
|
||||
<div class="carousel-inner">
|
||||
<div class="carousel-item active">
|
||||
<img class="d-block w-100" src='{% static "img/IMG_5817.JPG" %}'>
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
<img class="d-block w-100" src='{% static "img/IMG_5818.JPG" %}'>
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
<img class="d-block w-100" src='{% static "img/IMG_5819.JPG" %}'>
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
<img class="d-block w-100" src='{% static "img/IMG_5821.JPG" %}'>
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
<img class="d-block w-100" src='{% static "img/IMG_5823.JPG" %}'>
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
<img class="d-block w-100" src='{% static "img/IMG_5826.JPG" %}'>
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
<img class="d-block w-100" src='{% static "img/IMG_5849.JPG" %}'>
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
<img class="d-block w-100" src='{% static "img/IMG_5856.JPG" %}'>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
0
tests/__init__.py
Normal file
50
tests/test_facilitator.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
from django.urls import reverse
|
||||
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
|
||||
|
||||
from selenium.webdriver import Chrome, ChromeOptions
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.wait import WebDriverWait
|
||||
from selenium.webdriver.support.select import Select
|
||||
from selenium.webdriver.support.expected_conditions import visibility_of
|
||||
from webdriver_manager.chrome import ChromeDriverManager
|
||||
|
||||
from housewars.models import Activity
|
||||
|
||||
|
||||
class FacilitatorEntry(StaticLiveServerTestCase):
|
||||
def setUp(self):
|
||||
# Headless chrome driver test setup
|
||||
options = ChromeOptions()
|
||||
options.add_argument('--headless')
|
||||
options.add_argument('--start-maximized')
|
||||
options.add_argument('--disable-gpu')
|
||||
self.browser = Chrome(
|
||||
ChromeDriverManager().install(), chrome_options=options)
|
||||
self.url = self.live_server_url + reverse('housewars:facilitator')
|
||||
|
||||
# Initial data setup
|
||||
self.activity = Activity.objects.create(
|
||||
name='Dodgeball', time=30)
|
||||
|
||||
def tearDown(self):
|
||||
self.browser.close()
|
||||
|
||||
def test_form_submission(self):
|
||||
self.browser.get(self.url)
|
||||
|
||||
# Wait until browser loads page before continuing
|
||||
WebDriverWait(self.browser, timeout=5).until(
|
||||
visibility_of(self.browser.find_element(By.ID, 'header')))
|
||||
|
||||
self.browser.find_element(By.ID, 'first-name').send_keys('Test')
|
||||
self.browser.find_element(By.ID, 'last-name').send_keys('User')
|
||||
Select(self.browser.find_element(By.ID, 'activity')
|
||||
).select_by_visible_text('Dodgeball - 30 min')
|
||||
self.browser.find_element(By.ID, 'submit').click()
|
||||
|
||||
# Assert that signup is successful
|
||||
success_text = self.browser.find_element(
|
||||
By.CSS_SELECTOR, 'div.alert.alert-success').text
|
||||
|
||||
self.assertEquals(
|
||||
success_text, 'Your facilitator signup has been submitted.')
|
||||