Browse Source

Initial commit

Thomas Ross 4 years ago
commit
db40332082
72 changed files with 7635 additions and 0 deletions
  1. 95
    0
      .gitignore
  2. 0
    0
      FRCScoutWeb/__init__.py
  3. 3
    0
      FRCScoutWeb/config.py
  4. 15
    0
      FRCScoutWeb/forms.py
  5. 95
    0
      FRCScoutWeb/jinja2/base.html
  6. 38
    0
      FRCScoutWeb/jinja2/registration/login.html
  7. 16
    0
      FRCScoutWeb/jinja_environment.py
  8. 130
    0
      FRCScoutWeb/settings.py
  9. 5705
    0
      FRCScoutWeb/static/material-components-web.js
  10. 6
    0
      FRCScoutWeb/static/material-components-web.min.css
  11. 36
    0
      FRCScoutWeb/static/notifications.js
  12. 116
    0
      FRCScoutWeb/static/style.css
  13. 17
    0
      FRCScoutWeb/urls.py
  14. 129
    0
      FRCScoutWeb/widgets.py
  15. 16
    0
      FRCScoutWeb/wsgi.py
  16. 0
    0
      api/__init__.py
  17. 5
    0
      api/admin.py
  18. 5
    0
      api/apps.py
  19. 23
    0
      api/middleware.py
  20. 27
    0
      api/migrations/0001_initial.py
  21. 0
    0
      api/migrations/__init__.py
  22. 12
    0
      api/models.py
  23. 94
    0
      api/tests.py
  24. 9
    0
      api/urls.py
  25. 71
    0
      api/views.py
  26. 0
    0
      games/__init__.py
  27. 3
    0
      games/admin.py
  28. 5
    0
      games/apps.py
  29. 4
    0
      games/jinja2/games/index.html
  30. 0
    0
      games/migrations/__init__.py
  31. 3
    0
      games/models.py
  32. 3
    0
      games/tests.py
  33. 10
    0
      games/urls.py
  34. 11
    0
      games/views.py
  35. 0
    0
      home/__init__.py
  36. 3
    0
      home/admin.py
  37. 5
    0
      home/apps.py
  38. 124
    0
      home/jinja2/home/index.html
  39. 0
    0
      home/migrations/__init__.py
  40. 3
    0
      home/models.py
  41. 3
    0
      home/tests.py
  42. 8
    0
      home/urls.py
  43. 19
    0
      home/views.py
  44. 22
    0
      manage.py
  45. 0
    0
      tasks/__init__.py
  46. 5
    0
      tasks/admin.py
  47. 5
    0
      tasks/apps.py
  48. 146
    0
      tasks/fixtures/tasks.json
  49. 4
    0
      tasks/jinja2/tasks/index.html
  50. 24
    0
      tasks/migrations/0001_initial.py
  51. 0
    0
      tasks/migrations/__init__.py
  52. 10
    0
      tasks/models.py
  53. 3
    0
      tasks/tests.py
  54. 8
    0
      tasks/urls.py
  55. 5
    0
      tasks/views.py
  56. 0
    0
      teams/__init__.py
  57. 4
    0
      teams/admin.py
  58. 5
    0
      teams/apps.py
  59. 54
    0
      teams/forms.py
  60. 80
    0
      teams/jinja2/teams/edit_team.html
  61. 78
    0
      teams/jinja2/teams/new_team.html
  62. 28
    0
      teams/migrations/0001_initial.py
  63. 25
    0
      teams/migrations/0002_auto_20170207_1813.py
  64. 20
    0
      teams/migrations/0003_team_favorite.py
  65. 19
    0
      teams/migrations/0004_auto_20170210_0004.py
  66. 20
    0
      teams/migrations/0005_auto_20170210_0005.py
  67. 20
    0
      teams/migrations/0006_auto_20170212_0429.py
  68. 0
    0
      teams/migrations/__init__.py
  69. 22
    0
      teams/models.py
  70. 3
    0
      teams/tests.py
  71. 10
    0
      teams/urls.py
  72. 148
    0
      teams/views.py

+ 95
- 0
.gitignore View File

@@ -0,0 +1,95 @@
1
+# Byte-compiled / optimized / DLL files
2
+__pycache__/
3
+*.py[cod]
4
+*$py.class
5
+
6
+# C extensions
7
+*.so
8
+
9
+# Distribution / packaging
10
+.Python
11
+env/
12
+build/
13
+develop-eggs/
14
+dist/
15
+downloads/
16
+eggs/
17
+.eggs/
18
+lib/
19
+lib64/
20
+parts/
21
+sdist/
22
+var/
23
+*.egg-info/
24
+.installed.cfg
25
+*.egg
26
+
27
+# PyInstaller
28
+#  Usually these files are written by a python script from a template
29
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
30
+*.manifest
31
+*.spec
32
+
33
+# Installer logs
34
+pip-log.txt
35
+pip-delete-this-directory.txt
36
+
37
+# Unit test / coverage reports
38
+htmlcov/
39
+.tox/
40
+.coverage
41
+.coverage.*
42
+.cache
43
+nosetests.xml
44
+coverage.xml
45
+*,cover
46
+.hypothesis/
47
+
48
+# Translations
49
+*.mo
50
+*.pot
51
+
52
+# Django stuff:
53
+*.log
54
+local_settings.py
55
+
56
+# Flask stuff:
57
+instance/
58
+.webassets-cache
59
+
60
+# Scrapy stuff:
61
+.scrapy
62
+
63
+# Sphinx documentation
64
+docs/_build/
65
+
66
+# PyBuilder
67
+target/
68
+
69
+# IPython Notebook
70
+.ipynb_checkpoints
71
+
72
+# pyenv
73
+.python-version
74
+
75
+# celery beat schedule file
76
+celerybeat-schedule
77
+
78
+# dotenv
79
+.env
80
+
81
+# virtualenv
82
+venv/
83
+ENV/
84
+
85
+# Spyder project settings
86
+.spyderproject
87
+
88
+# Rope project settings
89
+.ropeproject
90
+
91
+# PyCharm
92
+.idea
93
+
94
+# Django dev database
95
+db.sqlite3

+ 0
- 0
FRCScoutWeb/__init__.py View File


+ 3
- 0
FRCScoutWeb/config.py View File

@@ -0,0 +1,3 @@
1
+CURRENT_FRC_YEAR = 2017
2
+
3
+ALLOWED_YEARS = [2016, 2017]

+ 15
- 0
FRCScoutWeb/forms.py View File

@@ -0,0 +1,15 @@
1
+from django import forms
2
+from django.contrib.auth.forms import AuthenticationForm, UsernameField
3
+from django.utils.translation import ugettext_lazy as _
4
+
5
+
6
+class FSWAuthForm(AuthenticationForm):
7
+    username = UsernameField(
8
+        max_length=254,
9
+        widget=forms.TextInput(attrs={"autofocus": "", "required": "",
10
+                                      "class": "mdc-textfield__input mdc-typography"}))
11
+    password = forms.CharField(
12
+        label=_("Password"),
13
+        strip=False,
14
+        widget=forms.PasswordInput({"class": "mdc-textfield__input mdc-typography",
15
+                                    "required": ""}))

+ 95
- 0
FRCScoutWeb/jinja2/base.html View File

@@ -0,0 +1,95 @@
1
+<!DOCTYPE html>
2
+<html lang="en">
3
+    <head>
4
+        <meta charset="UTF-8">
5
+        <title>FRC Scout</title>
6
+
7
+        <meta name="viewport" content="width=device-width, initial-scale=1">
8
+
9
+        <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
10
+        <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500">
11
+        <link rel="stylesheet" href="{{ static("material-components-web.min.css") }}">
12
+        {#<link rel="stylesheet"#}
13
+        {#      href="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.css">#}
14
+        <link rel="stylesheet" type="text/css" href="{{ static("style.css") }}"/>
15
+
16
+        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
17
+        <script src="{{ static("notifications.js") }}"></script>
18
+
19
+        {% block head %}
20
+        {% endblock %}
21
+    </head>
22
+    <body class="mdc-typography" style="background-color: #F5F5F5;">
23
+        <div class="temp-toolbar mdc-theme--primary-bg mdc-theme--text-primary-on-primary mdc-typography--title mdc-elevation--z4">
24
+            <button class="temp-toolbar-menu material-icons">menu</button>
25
+            {% if request.user.is_authenticated %}
26
+                <a href="{{ url("logout") }}?next={{ request.path|urlencode }}"
27
+                   class="toolbar-right-aligned-link">Logout</a>
28
+            {% else %}
29
+                <a href="{{ url("login") }}?next={{ request.path|urlencode }}"
30
+                   class="toolbar-right-aligned-link">Login</a>
31
+            {% endif %}
32
+        </div>
33
+
34
+        <aside class="mdc-temporary-drawer">
35
+            <nav class="mdc-temporary-drawer__drawer">
36
+                <header class="mdc-temporary-drawer__header">
37
+                    <div class="mdc-temporary-drawer__header-content mdc-theme--primary-bg mdc-theme--text-primary-on-primary">
38
+                        FRC Scout
39
+                    </div>
40
+                </header>
41
+                <nav class="mdc-temporary-drawer__content mdc-list-group">
42
+                    <div class="mdc-list">
43
+                        {#                        mdc-temporary-drawer--selected#}
44
+                        <a class="mdc-list-item" href="/">
45
+                            <i class="material-icons mdc-list-item__start-detail" aria-hidden="true">home</i>
46
+                            Home
47
+                        </a>
48
+                        <a class="mdc-list-item" href="{{ url("tasks:index") }}">
49
+                            <i class="material-icons mdc-list-item__start-detail" aria-hidden="true">done_all</i>
50
+                            Tasks
51
+                        </a>
52
+                        <a class="mdc-list-item" href="{{ url("games:index") }}">
53
+                            <i class="material-icons mdc-list-item__start-detail" aria-hidden="true">gamepad</i>
54
+                            Games
55
+                        </a>
56
+                    </div>
57
+                    {#<hr class="mdc-list-divider">#}
58
+                </nav>
59
+            </nav>
60
+        </aside>
61
+
62
+        <main>
63
+            {% block body %}
64
+            {% endblock %}
65
+        </main>
66
+
67
+        <div class="mdc-snackbar" aria-live="assertive" aria-atomic="true" aria-hidden="true">
68
+            <div class="mdc-snackbar__text"></div>
69
+            <div class="mdc-snackbar__action-wrapper">
70
+                <button type="button" class="mdc-button mdc-snackbar__action-button"></button>
71
+            </div>
72
+        </div>
73
+
74
+        {% block outside_content %}
75
+        {% endblock %}
76
+
77
+        <script src="{{ static("material-components-web.js") }}"></script>
78
+        <script>
79
+            $(window).on("load", function()
80
+            {
81
+                window.mdc.autoInit();
82
+
83
+                var drawer = new mdc.drawer.MDCTemporaryDrawer(document.querySelector(".mdc-temporary-drawer"));
84
+                $(".temp-toolbar-menu").click(function()
85
+                {
86
+                    drawer.open = true;
87
+                });
88
+
89
+                window.snackbar = new window.mdc.snackbar.MDCSnackbar(document.querySelector(".mdc-snackbar"));
90
+
91
+                window.run_notifications();
92
+            });
93
+        </script>
94
+    </body>
95
+</html>

+ 38
- 0
FRCScoutWeb/jinja2/registration/login.html View File

@@ -0,0 +1,38 @@
1
+{% extends "base.html" %}
2
+{% block body %}
3
+    <div class="mdc-layout-grid">
4
+        <div class="mdc-card mdc-layout-grid__cell mdc-layout-grid__cell--span-2 mdc-layout-grid__cell--span-3-phone"
5
+             style="margin: 0 auto;">
6
+            <form method="POST" action="{{ url("login") }}">
7
+                <section class="mdc-card__primary mdc-theme--primary-bg">
8
+                    <h1 class="mdc-card__title mdc-card__title--large mdc-theme--text-primary-on-primary">Log in</h1>
9
+                </section>
10
+                <section class="mdc-card__supporting-text" style="padding-top: 1em;">
11
+                    <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
12
+                    <input type="hidden" name="next" value="{{ next }}"/>
13
+                    {% for hidden in form.hidden_fields() %}
14
+                        {{ hidden }}
15
+                    {% endfor %}
16
+
17
+                    {% for error in form.non_field_errors() %}
18
+                        <span class="field-error">{{ error }}</span>
19
+                    {% endfor %}
20
+
21
+                    {% for field in form.visible_fields() %}
22
+                        <div class="mdc-textfield mdc-form-field" data-mdc-auto-init="MDCTextfield">
23
+                            {{ field }}
24
+                            <label class="mdc-textfield__label" for="{{ field.id_For_label }}">{{ field.label }}</label>
25
+                        </div>
26
+                        {% for error in field.errors %}
27
+                            <span class="field-error">{{ error }}</span>
28
+                        {% endfor %}
29
+                    {% endfor %}
30
+                </section>
31
+                <section class="mdc-card__actions">
32
+                    <input class="mdc-button mdc-button--compact mdc-button--accent mdc-card__action"
33
+                           data-mdc-auto-init="MDCRipple" type="submit" value="Log in"/>
34
+                </section>
35
+            </form>
36
+        </div>
37
+    </div>
38
+{% endblock %}

+ 16
- 0
FRCScoutWeb/jinja_environment.py View File

@@ -0,0 +1,16 @@
1
+from django.contrib.staticfiles.storage import staticfiles_storage
2
+from django.core.urlresolvers import reverse, reverse_lazy
3
+
4
+from jinja2 import Environment
5
+
6
+
7
+def environment(**options):
8
+    env = Environment(**options, trim_blocks=True, lstrip_blocks=True)
9
+
10
+    env.globals.update({
11
+        "static": staticfiles_storage.url,
12
+        "url": reverse,
13
+        "url_lazy": reverse_lazy
14
+    })
15
+
16
+    return env

+ 130
- 0
FRCScoutWeb/settings.py View File

@@ -0,0 +1,130 @@
1
+import os
2
+
3
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
4
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
5
+MAIN_APP_DIR = os.path.join(BASE_DIR, "FRCScoutWeb")
6
+
7
+# Quick-start development settings - unsuitable for production
8
+# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
9
+
10
+# SECURITY WARNING: keep the secret key used in production secret!
11
+SECRET_KEY = "dk==i@ta-$e)%e#zd$v+ar_8#i57y_ss^08)uls#_&ov6*ezj0"
12
+
13
+# SECURITY WARNING: don"t run with debug turned on in production!
14
+DEBUG = True
15
+
16
+ALLOWED_HOSTS = []
17
+
18
+
19
+# Application definition
20
+
21
+INSTALLED_APPS = [
22
+    "home",
23
+    "teams",
24
+    "tasks",
25
+    "games",
26
+    "api",
27
+    "django.contrib.admin",
28
+    "django.contrib.auth",
29
+    "django.contrib.contenttypes",
30
+    "django.contrib.sessions",
31
+    "django.contrib.messages",
32
+    "django.contrib.staticfiles",
33
+]
34
+
35
+MIDDLEWARE = [
36
+    "api.middleware.api_middleware",
37
+    "django.middleware.security.SecurityMiddleware",
38
+    "django.contrib.sessions.middleware.SessionMiddleware",
39
+    "django.middleware.common.CommonMiddleware",
40
+    "django.middleware.csrf.CsrfViewMiddleware",
41
+    "django.contrib.auth.middleware.AuthenticationMiddleware",
42
+    "django.contrib.messages.middleware.MessageMiddleware",
43
+    "django.middleware.clickjacking.XFrameOptionsMiddleware"
44
+]
45
+
46
+ROOT_URLCONF = "FRCScoutWeb.urls"
47
+
48
+TEMPLATES = [
49
+    {
50
+        "BACKEND": "django.template.backends.jinja2.Jinja2",
51
+        "DIRS": [os.path.join(MAIN_APP_DIR, "jinja2")],
52
+        "APP_DIRS": True,
53
+        "OPTIONS": {
54
+            "environment": "FRCScoutWeb.jinja_environment.environment"
55
+        }
56
+    },
57
+    {
58
+        "BACKEND": "django.template.backends.django.DjangoTemplates",
59
+        "DIRS": [],
60
+        "APP_DIRS": True,
61
+        "OPTIONS": {
62
+            "context_processors": [
63
+                "django.template.context_processors.debug",
64
+                "django.template.context_processors.request",
65
+                "django.contrib.auth.context_processors.auth",
66
+                "django.contrib.messages.context_processors.messages",
67
+            ],
68
+        },
69
+    }
70
+]
71
+
72
+STATICFILES_DIRS = [
73
+    os.path.join(MAIN_APP_DIR, "static")
74
+]
75
+
76
+WSGI_APPLICATION = "FRCScoutWeb.wsgi.application"
77
+
78
+
79
+# Database
80
+# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
81
+
82
+DATABASES = {
83
+    "default": {
84
+        "ENGINE": "django.db.backends.sqlite3",
85
+        "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
86
+    }
87
+}
88
+
89
+
90
+# Password validation
91
+# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
92
+
93
+AUTH_PASSWORD_VALIDATORS = [
94
+    {
95
+        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
96
+    },
97
+    {
98
+        "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
99
+    },
100
+    {
101
+        "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
102
+    },
103
+    {
104
+        "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
105
+    },
106
+]
107
+
108
+
109
+# Internationalization
110
+# https://docs.djangoproject.com/en/1.10/topics/i18n/
111
+
112
+LANGUAGE_CODE = "en-us"
113
+
114
+TIME_ZONE = "UTC"
115
+
116
+USE_I18N = True
117
+
118
+USE_L10N = True
119
+
120
+USE_TZ = True
121
+
122
+
123
+# Static files (CSS, JavaScript, Images)
124
+# https://docs.djangoproject.com/en/1.10/howto/static-files/
125
+
126
+STATIC_URL = "/static/"
127
+
128
+LOGIN_URL = "/login/"
129
+LOGIN_REDIRECT_URL = "/"
130
+LOGOUT_REDIRECT_URL = "/"

+ 5705
- 0
FRCScoutWeb/static/material-components-web.js
File diff suppressed because it is too large
View File


+ 6
- 0
FRCScoutWeb/static/material-components-web.min.css
File diff suppressed because it is too large
View File


+ 36
- 0
FRCScoutWeb/static/notifications.js View File

@@ -0,0 +1,36 @@
1
+function getQueryParams()
2
+{
3
+    var queryString = window.location.search.substr(1);
4
+
5
+    var queryParams = queryString.split("&");
6
+
7
+    var rtn = {};
8
+    for (var i = 0; i < queryParams.length; i++)
9
+    {
10
+        var param = queryParams[i].split("=");
11
+        rtn[param[0]] = param[1];
12
+    }
13
+
14
+    return rtn;
15
+}
16
+
17
+window.run_notifications = function()
18
+{
19
+    var queryParams = getQueryParams();
20
+
21
+    if ("add_team_success" in queryParams && queryParams["add_team_success"].toLowerCase() == "true" &&
22
+        "add_team_team_number" in queryParams)
23
+    {
24
+        window.snackbar.show({
25
+            message: "Successfully added team " + queryParams["add_team_team_number"]
26
+        });
27
+    }
28
+
29
+    if ("edit_team_success" in queryParams && queryParams["edit_team_success"].toLowerCase() == "true" &&
30
+        "edit_team_team_number" in queryParams)
31
+    {
32
+        window.snackbar.show({
33
+            message: "Successfully modified team " + queryParams["edit_team_team_number"]
34
+        });
35
+    }
36
+};

+ 116
- 0
FRCScoutWeb/static/style.css View File

@@ -0,0 +1,116 @@
1
+/* Base styles */
2
+body
3
+{
4
+    padding: 0;
5
+    margin: 0;
6
+    box-sizing: border-box;
7
+}
8
+
9
+.temp-toolbar
10
+{
11
+    width: 100%;
12
+    height: 56px;
13
+    display: flex;
14
+    flex-direction: row;
15
+    align-items: center;
16
+    padding: 0 16px;
17
+    box-sizing: border-box;
18
+}
19
+
20
+@media (min-width: 600px)
21
+{
22
+    .temp-toolbar
23
+    {
24
+        height: 64px;
25
+    }
26
+}
27
+
28
+.toolbar-right-aligned-link
29
+{
30
+    margin: 0 0 0 auto;
31
+    font-size: 14px;
32
+    font-weight: 400;
33
+    color: #FFFFFF;
34
+    text-decoration: none;
35
+    letter-spacing: 0;
36
+    padding: 24px;
37
+}
38
+
39
+.temp-toolbar-menu
40
+{
41
+    background: none;
42
+    border: none;
43
+    width: 24px;
44
+    height: 24px;
45
+    padding: 0;
46
+    margin: 0;
47
+    color: #FFFFFF;
48
+    box-sizing: border-box;
49
+}
50
+
51
+/* MDC Overrides */
52
+.mdc-card
53
+{
54
+    background-color: #FFFFFF;
55
+}
56
+
57
+a.mdc-fab
58
+{
59
+    text-decoration: none;
60
+}
61
+
62
+/* Custom Styles */
63
+.text-bold
64
+{
65
+    font-weight: bold;
66
+}
67
+
68
+.emptystate
69
+{
70
+    text-align: center;
71
+    color: #616161;
72
+}
73
+
74
+.emptystate i
75
+{
76
+    font-size: 10em;
77
+}
78
+
79
+.field-column
80
+{
81
+    flex-direction: column;
82
+    align-items: flex-start;
83
+}
84
+
85
+.field-error
86
+{
87
+    color: #D50000;
88
+    font-size: 12px;
89
+    margin-top: 3px;
90
+    display: block;
91
+}
92
+
93
+.field-helptext
94
+{
95
+    color: var(--mdc-theme-text-hint-on-light, rgba(0, 0, 0, .38));
96
+    margin: 0;
97
+    font-size: .75rem;
98
+    opacity: 1;
99
+}
100
+
101
+.field-label
102
+{
103
+    margin-top: 1.5em;
104
+    margin-left: 0 !important;
105
+    margin-bottom: .5em;
106
+    font-size: 1rem;
107
+}
108
+
109
+.floating-bottom-right-fab
110
+{
111
+    color: var(--mdc-theme-text-primary-on-primary);
112
+    position: fixed;
113
+    bottom: 2em;
114
+    right: 2em;
115
+    z-index: 99;
116
+}

+ 17
- 0
FRCScoutWeb/urls.py View File

@@ -0,0 +1,17 @@
1
+from django.conf.urls import url, include
2
+from django.contrib import admin
3
+from django.contrib.auth.views import login, logout
4
+
5
+from FRCScoutWeb.forms import FSWAuthForm
6
+
7
+urlpatterns = [
8
+    url(r"", include("home.urls")),
9
+    url(r"^login/", login, {"authentication_form": FSWAuthForm}, name="login"),
10
+    url(r'^logout/', logout, name="logout"),
11
+    # TODO: other auth views
12
+    url(r"^teams/", include("teams.urls")),
13
+    url(r"^tasks/", include("tasks.urls")),
14
+    url(r"^games/", include("games.urls")),
15
+    url(r"^api/", include("api.urls")),
16
+    url(r"^admin/", admin.site.urls)
17
+]

+ 129
- 0
FRCScoutWeb/widgets.py View File

@@ -0,0 +1,129 @@
1
+import copy
2
+
3
+from django.forms import Widget, CheckboxInput
4
+from django.forms.utils import flatatt
5
+from django.utils.datastructures import MultiValueDict
6
+
7
+from django.utils.encoding import force_text
8
+
9
+from django.utils.html import format_html
10
+from django.utils.safestring import mark_safe
11
+
12
+
13
+class MultipleCheckboxes(Widget):
14
+    dont_use_model_field_default_for_empty_data = True
15
+
16
+    def __init__(self, attrs=None, choices=()):
17
+        super(MultipleCheckboxes, self).__init__(attrs)
18
+
19
+        self.choices = list(choices)
20
+
21
+    def __deepcopy__(self, memo):
22
+        obj = copy.copy(self)
23
+        obj.attrs = self.attrs.copy()
24
+        obj.choices = copy.copy(self.choices)
25
+        memo[id(self)] = obj
26
+        return obj
27
+
28
+    def render(self, name, value, attrs=None):
29
+        if value is None:
30
+            value = []
31
+        final_attrs = self.build_attrs(attrs, name=name)
32
+        output = [format_html("<div {}>", flatatt(final_attrs))]
33
+        checkboxes = self.render_checkboxes(value, name)
34
+        if checkboxes:
35
+            output.append(checkboxes)
36
+        output.append("</div>")
37
+        return mark_safe("\n".join(output))
38
+
39
+    @staticmethod
40
+    def render_checkbox(selected_choices, checkbox_value, checkbox_label, name):
41
+        if checkbox_value is None:
42
+            checkbox_value = ""
43
+
44
+        checkbox_value = force_text(checkbox_value)
45
+        if checkbox_value in selected_choices:
46
+            selected_html = mark_safe("checked=\"checked\"")
47
+        else:
48
+            selected_html = ""
49
+
50
+        output = [
51
+            "<div class=\"mdc-form-field\">"
52
+            "<div class=\"mdc-checkbox\" data-mdc-auto-init=\"MDCCheckbox\">",
53
+            format_html("<input type=\"checkbox\" id=\"checkbox-{}\" name=\"{}\" {} value=\"{}\" "
54
+                        "class=\"mdc-checkbox__native-control\">",
55
+                        checkbox_value, name, selected_html, checkbox_value),
56
+            "<div class=\"mdc-checkbox__background\">",
57
+            """
58
+                    <svg version="1.1"
59
+                        class="mdc-checkbox__checkmark"
60
+                        xmlns="http://www.w3.org/2000/svg"
61
+                        viewBox="0 0 24 24"
62
+                        xml:space="preserve">
63
+                    <path class="mdc-checkbox__checkmark__path"
64
+                        fill="none"
65
+                        stroke="white"
66
+                        d="M1.73,12.91 8.1,19.28 22.79,4.59"/>
67
+                    </svg>
68
+                    """,
69
+            "<div class=\"mdc-checkbox__mixedmark\"></div>",
70
+            "</div>",
71
+            "</div>",
72
+            format_html("<label for=\"{}\">{}</label></div><br>", checkbox_value, force_text(checkbox_label))
73
+        ]
74
+
75
+        return "\n".join(output)
76
+
77
+    def render_checkboxes(self, selected_choices, name):
78
+        selected_choices = set(force_text(v) for v in selected_choices)
79
+        output = []
80
+        for option_value, option_label in self.choices:
81
+            output.append(self.render_checkbox(selected_choices, option_value, option_label, name))
82
+        return "\n".join(output)
83
+
84
+    def value_from_datadict(self, data, files, name):
85
+        if isinstance(data, MultiValueDict):
86
+            return data.getlist(name)
87
+        return data.get(name)
88
+
89
+
90
+class MaterialCheckboxInput(CheckboxInput):
91
+    def render(self, name, value, attrs=None):
92
+        final_attrs = self.build_attrs(attrs, type="checkbox", name=name)
93
+        if self.check_test(value):
94
+            final_attrs["checked"] = "checked"
95
+        if not (value is True or value is False or value is None or value == ""):
96
+            final_attrs["value"] = force_text(value)
97
+
98
+        output = [
99
+            "<br><div class=\"mdc-checkbox\" data-mdc-auto-init=\"MDCCheckbox\">",
100
+            format_html("<input class=\"mdc-checkbox__native-control\" {}>", flatatt(final_attrs)),
101
+            "<div class=\"mdc-checkbox__background\">",
102
+            """
103
+                    <svg version="1.1"
104
+                        class="mdc-checkbox__checkmark"
105
+                        xmlns="http://www.w3.org/2000/svg"
106
+                        viewBox="0 0 24 24"
107
+                        xml:space="preserve">
108
+                    <path class="mdc-checkbox__checkmark__path"
109
+                        fill="none"
110
+                        stroke="white"
111
+                        d="M1.73,12.91 8.1,19.28 22.79,4.59"/>
112
+                    </svg>
113
+                    """,
114
+            "<div class=\"mdc-checkbox__mixedmark\"></div>",
115
+            "</div></div>",
116
+        ]
117
+
118
+        # output = [
119
+        #     "<div class=\"mdc-switch\">",
120
+        #     format_html("<input class=\"mdc-checkbox__native-control\" {}>", flatatt(final_attrs)),
121
+        #     "<div class=\"mdc-switch__background\">",
122
+        #     "<div class=\"mdc-switch__knob\"></div>",
123
+        #     "</div>",
124
+        #     "</div></div>",
125
+        # ]
126
+
127
+        return "\n".join(output)
128
+
129
+        # return format_html('<input{} />', flatatt(final_attrs))

+ 16
- 0
FRCScoutWeb/wsgi.py View File

@@ -0,0 +1,16 @@
1
+"""
2
+WSGI config for FRCScoutWeb project.
3
+
4
+It exposes the WSGI callable as a module-level variable named ``application``.
5
+
6
+For more information on this file, see
7
+https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
8
+"""
9
+
10
+import os
11
+
12
+from django.core.wsgi import get_wsgi_application
13
+
14
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "FRCScoutWeb.settings")
15
+
16
+application = get_wsgi_application()

+ 0
- 0
api/__init__.py View File


+ 5
- 0
api/admin.py View File

@@ -0,0 +1,5 @@
1
+from django.contrib import admin
2
+
3
+from .models import APIKey
4
+
5
+admin.site.register(APIKey)

+ 5
- 0
api/apps.py View File

@@ -0,0 +1,5 @@
1
+from django.apps import AppConfig
2
+
3
+
4
+class ApiConfig(AppConfig):
5
+    name = 'api'

+ 23
- 0
api/middleware.py View File

@@ -0,0 +1,23 @@
1
+from django.core.exceptions import ObjectDoesNotExist
2
+
3
+from api.models import APIKey
4
+
5
+
6
+def api_middleware(get_response):
7
+
8
+    def middleware(request):
9
+
10
+        if "api_key" in request.POST:
11
+            api_key_param = request.POST["api_key"]
12
+            try:
13
+                api_key = APIKey.objects.get(pk=api_key_param)
14
+            except ObjectDoesNotExist:
15
+                return
16
+            else:
17
+                request.api_key = api_key
18
+
19
+        response = get_response(request)
20
+
21
+        return response
22
+
23
+    return middleware

+ 27
- 0
api/migrations/0001_initial.py View File

@@ -0,0 +1,27 @@
1
+# -*- coding: utf-8 -*-
2
+# Generated by Django 1.10.1 on 2017-02-15 23:58
3
+from __future__ import unicode_literals
4
+
5
+from django.conf import settings
6
+from django.db import migrations, models
7
+import django.db.models.deletion
8
+
9
+
10
+class Migration(migrations.Migration):
11
+
12
+    initial = True
13
+
14
+    dependencies = [
15
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
16
+    ]
17
+
18
+    operations = [
19
+        migrations.CreateModel(
20
+            name='APIKey',
21
+            fields=[
22
+                ('key', models.TextField(max_length=50, primary_key=True, serialize=False, unique=True)),
23
+                ('app_name', models.TextField(max_length=255)),
24
+                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
25
+            ],
26
+        ),
27
+    ]

+ 0
- 0
api/migrations/__init__.py View File


+ 12
- 0
api/models.py View File

@@ -0,0 +1,12 @@
1
+from django.db import models
2
+
3
+from django.conf import settings
4
+
5
+
6
+class APIKey(models.Model):
7
+    key = models.TextField(max_length=50, unique=True, primary_key=True)
8
+
9
+    user = models.ForeignKey(settings.AUTH_USER_MODEL,
10
+                             on_delete=models.CASCADE)
11
+
12
+    app_name = models.TextField(max_length=255)

+ 94
- 0
api/tests.py View File

@@ -0,0 +1,94 @@
1
+import json
2
+
3
+from django.contrib.auth.models import User
4
+from django.test import Client
5
+from django.test import TestCase
6
+from django.urls import reverse
7
+
8
+from api.models import APIKey
9
+from tasks.models import Task
10
+from teams.models import Team
11
+
12
+
13
+class GenerateAPIKeyTests(TestCase):
14
+    def setUp(self):
15
+        user = User.objects.create(username="test_user")
16
+        user.save()
17
+
18
+        self.client = Client()
19
+        self.client.force_login(user)
20
+
21
+    def test_generate_api_key_success(self):
22
+        response = self.client.get(reverse("api:generate_api_key") + "?app_name=Test")
23
+        self.assertEqual(response.status_code, 200)
24
+
25
+    def test_generate_api_key_no_appname(self):
26
+        response = self.client.get(reverse("api:generate_api_key"))
27
+        self.assertEqual(response.status_code, 400)
28
+
29
+
30
+class GetTeamTest(TestCase):
31
+    def setUp(self):
32
+        task1 = Task(codeyear="task-1")
33
+        task1.name = "Task"
34
+        task1.year = 1
35
+        task1.save()
36
+
37
+        task2 = Task(codeyear="othertask-1")
38
+        task2.name = "Other Task"
39
+        task2.year = 1
40
+        task2.save()
41
+
42
+        team1 = Team(team_number=1, year=1)
43
+        team1.name = "1"
44
+        team1.auto_points = 100
45
+        team1.favorite = False
46
+        team1.save()
47
+        team1.tasks.add(task1)
48
+
49
+        user = User.objects.create(username="test_user")
50
+        user.save()
51
+
52
+        key = APIKey(key="test_key")
53
+        key.app_name = "django_test"
54
+        key.user = user
55
+        key.save()
56
+
57
+        self.client = Client()
58
+
59
+    def test_get_team_success(self):
60
+        response = self.client.get(reverse("api:get_team", args=[1, 1]) + "?key=test_key")
61
+
62
+        self.assertEqual(response.status_code, 200)
63
+
64
+        parsed_response = json.loads(str(response.content.decode("UTF-8")))
65
+
66
+        self.assertEqual(parsed_response["status"], 0)
67
+        self.assertEqual(parsed_response["name"], "1")
68
+        self.assertEqual(parsed_response["auto_points"], 100)
69
+        self.assertEqual(parsed_response["favorite"], False)
70
+        self.assertTrue("tasks" in parsed_response)
71
+
72
+        task1_expected = {
73
+            "name": "Task",
74
+            "team_able": True
75
+        }
76
+
77
+        task2_expected = {
78
+            "name": "Other Task",
79
+            "team_able": False
80
+        }
81
+
82
+        tasks_expected = {
83
+            "task-1": task1_expected,
84
+            "othertask-1": task2_expected
85
+        }
86
+
87
+        self.assertEqual(parsed_response["tasks"], tasks_expected)
88
+
89
+    def test_get_team_no_or_wrong_key(self):
90
+        response = self.client.get(reverse("api:get_team", args=[1, 1]))
91
+        self.assertEqual(response.status_code, 401)
92
+
93
+        response = self.client.get(reverse("api:get_team", args=[1, 1]) + "?key=garbage_key")
94
+        self.assertEqual(response.status_code, 401)

+ 9
- 0
api/urls.py View File

@@ -0,0 +1,9 @@
1
+from django.conf.urls import url
2
+
3
+from . import views
4
+
5
+app_name = "api"
6
+urlpatterns = [
7
+    url(r"^generate_api_key/$", views.generate_api_key, name="generate_api_key"),
8
+    url(r"^get_team/(?P<year>[0-9]+)/(?P<team_number>[0-9]+)/$", views.get_team, name="get_team")
9
+]

+ 71
- 0
api/views.py View File

@@ -0,0 +1,71 @@
1
+import json
2
+import string
3
+
4
+from django.contrib.auth.decorators import login_required
5
+from django.http import HttpResponse
6
+from django.utils.crypto import get_random_string
7
+
8
+from api.models import APIKey
9
+from tasks.models import Task
10
+from teams.models import Team
11
+
12
+
13
+@login_required
14
+def generate_api_key(request):
15
+    def get_unique_key():
16
+        rtn_key = get_random_string(length=32, allowed_chars=string.ascii_letters + string.digits)
17
+
18
+        if APIKey.objects.filter(key=rtn_key).exists():
19
+            return get_unique_key()
20
+        else:
21
+            return rtn_key
22
+
23
+    key = get_unique_key()
24
+
25
+    if "app_name" not in request.GET:
26
+        return HttpResponse("{\"status\": 1}", status=400)
27
+
28
+    api_key = APIKey(key=key)
29
+    api_key.user = request.user
30
+    api_key.app_name = request.GET["app_name"]
31
+    api_key.save()
32
+
33
+    content = json.dumps({
34
+        "status": 0,
35
+        "key": key
36
+    })
37
+
38
+    return HttpResponse(content, status=200)
39
+
40
+
41
+def get_team(request, year, team_number):
42
+    key = request.GET.get("key", "")
43
+
44
+    if not APIKey.objects.filter(key=key).exists():
45
+        return HttpResponse("{\"status\": 1}", status=401)
46
+
47
+    try:
48
+        team = Team.objects.get(team_number=team_number, year=year)
49
+    except Team.DoesNotExist:
50
+        return HttpResponse("{\"status\": 1}", status=400)
51
+
52
+    tasks_obj = {}
53
+    team_tasks = team.tasks.all()
54
+    for task in Task.objects.filter(year=year):
55
+        tasks_obj[task.codeyear] = {
56
+            "name": task.name,
57
+            "team_able": task in team_tasks
58
+        }
59
+
60
+    content = json.dumps({
61
+        "status": 0,
62
+        "key": key,
63
+        "team_number": team.team_number,
64
+        "name": team.name,
65
+        "tasks": tasks_obj,
66
+        "auto_points": team.auto_points,
67
+        "year": team.year,
68
+        "favorite": team.favorite
69
+    })
70
+
71
+    return HttpResponse(content, status=200)

+ 0
- 0
games/__init__.py View File


+ 3
- 0
games/admin.py View File

@@ -0,0 +1,3 @@
1
+from django.contrib import admin
2
+
3
+# Register your models here.

+ 5
- 0
games/apps.py View File

@@ -0,0 +1,5 @@
1
+from django.apps import AppConfig
2
+
3
+
4
+class GamesConfig(AppConfig):
5
+    name = 'games'

+ 4
- 0
games/jinja2/games/index.html View File

@@ -0,0 +1,4 @@
1
+{% extends "base.html" %}
2
+{% block body %}
3
+    <p>GAMES</p>
4
+{% endblock %}

+ 0
- 0
games/migrations/__init__.py View File


+ 3
- 0
games/models.py View File

@@ -0,0 +1,3 @@
1
+from django.db import models
2
+
3
+# Create your models here.

+ 3
- 0
games/tests.py View File

@@ -0,0 +1,3 @@
1
+from django.test import TestCase
2
+
3
+# Create your tests here.

+ 10
- 0
games/urls.py View File

@@ -0,0 +1,10 @@
1
+from django.conf.urls import url
2
+
3
+from . import views
4
+
5
+app_name = "games"
6
+urlpatterns = [
7
+    url(r"^$", views.index, name="index"),
8
+    # FIXME: home:index hardcodes this URL pattern
9
+    url(r"^select/(?P<new_year>[0-9]+)/$", views.select_year, name="select_year")
10
+]

+ 11
- 0
games/views.py View File

@@ -0,0 +1,11 @@
1
+from django.shortcuts import render, redirect
2
+
3
+
4
+def index(request):
5
+    return render(request, "games/index.html")
6
+
7
+
8
+def select_year(request, new_year):
9
+    request.session["user_selected_year"] = new_year
10
+
11
+    return redirect(request.GET.get("next", "/"))

+ 0
- 0
home/__init__.py View File


+ 3
- 0
home/admin.py View File

@@ -0,0 +1,3 @@
1
+from django.contrib import admin
2
+
3
+# Register your models here.

+ 5
- 0
home/apps.py View File

@@ -0,0 +1,5 @@
1
+from django.apps import AppConfig
2
+
3
+
4
+class HomeConfig(AppConfig):
5
+    name = 'home'

+ 124
- 0
home/jinja2/home/index.html View File

@@ -0,0 +1,124 @@
1
+{% extends "base.html" %}
2
+{% block head %}
3
+    <script>
4
+        $(window).on("load", function()
5
+        {
6
+            var yearSelectBox = new window.mdc.select.MDCSelect(document.querySelector("#year-select-box"));
7
+            yearSelectBox.listen("MDCSelect:change", function()
8
+            {
9
+                {# FIXME: Don't hardcode this URL! #}
10
+                window.location = "/games/select/" + yearSelectBox.selectedOptions[0].id + "/?next={{ request.path|urlencode }}";
11
+            });
12
+
13
+            $(".team-favorite-button").each(function()
14
+            {
15
+                var $this = $(this);
16
+                $this.click(function()
17
+                {
18
+                    var teamNumber = $this.attr("data-team-number");
19
+                    var year = $this.attr("data-year");
20
+
21
+                    $.ajax({
22
+                        url: "{{ url("teams:toggle_favorite") }}",
23
+                        type: "POST",
24
+                        data: {
25
+                            "csrfmiddlewaretoken": "{{ csrf_token }}",
26
+                            "team_number": teamNumber,
27
+                            "year": year
28
+                        },
29
+                        success: function(data)
30
+                        {
31
+                            var message = "Team " + teamNumber + " " + (data == "True" ? "favorited" : "unfavorited");
32
+                            window.snackbar.show({
33
+                                message: message,
34
+                                timeout: 2000,
35
+                                actionHandler: function()
36
+                                {
37
+                                    $this.click();
38
+                                },
39
+                                actionText: "Undo"
40
+                            });
41
+
42
+                            $this.find(".material-icons").text(data == "True" ? "favorite" : "favorite_border");
43
+                        },
44
+                        error: function(jqXHR)
45
+                        {
46
+                            var errorMessage = "Could not toggle favorite on team " + teamNumber + ". ";
47
+
48
+                            if (jqXHR.status == 404)
49
+                            {
50
+                                errorMessage += "Team not found.";
51
+                            }
52
+                            else if (jqXHR.status == 401)
53
+                            {
54
+                                errorMessage += "You need to be logged in to do that."
55
+                            }
56
+                            else
57
+                            {
58
+                                errorMessage += "Code: " + jqXHR.status;
59
+                            }
60
+
61
+                            var notification = document.querySelector('.mdl-js-snackbar');
62
+                            window.snackbar.show({
63
+                                message: errorMessage
64
+                            });
65
+                        }
66
+                    });
67
+                });
68
+            });
69
+        });
70
+    </script>
71
+{% endblock %}
72
+{% block body %}
73
+    <div class="mdc-select" role="listbox" tabindex="0" id="year-select-box"
74
+         style="margin-left: 16px; margin-top: 1em;">
75
+        <span class="mdc-select__selected-text">{{ request.session.user_selected_year or FRCScoutWeb.config.CURRENT_FRC_YEAR }}</span>
76
+        <div class="mdc-simple-menu mdc-select__menu">
77
+            <ul class="mdc-list mdc-simple-menu__items">
78
+                <li class="mdc-list-item" role="option" id="2017" tabindex="0">
79
+                    2017
80
+                </li>
81
+                <li class="mdc-list-item" role="option" id="2016" tabindex="0">
82
+                    2016
83
+                </li>
84
+            </ul>
85
+        </div>
86
+    </div>
87
+    {% if teams %}
88
+        <div class="mdc-layout-grid">
89
+            {% for team in teams %}
90
+                <div class="mdc-card mdc-layout-grid__cell mdc-layout-grid__cell--span-2 mdc-layout-grid__cell--span-3-tablet mdc-layout-grid__cell--span-4-phone">
91
+                    <section class="mdc-card__primary">
92
+                        <h1 class="mdc-card__title mdc-card__title--large">{{ team.team_number }}</h1>
93
+                        <h2 class="mdc-card__subtitle">{{ team.name }}</h2>
94
+                    </section>
95
+                    <section class="mdc-card__supporting-text">
96
+                        Tasks: {{ team.tasks.all()|length }} / {{ num_tasks }}<br>
97
+                        Autonomous Points: {{ team.auto_points }}
98
+                    </section>
99
+
100
+                    <section class="mdc-card__actions">
101
+                        <a href="{{ url("teams:edit_team", args=[team.team_number]) }}" class="mdc-button mdc-button--compact mdc-button--accent mdc-card__action"
102
+                           data-mdc-auto-init="MDCRipple">Edit</a>
103
+                        <a href="https://www.thebluealliance.com/team/{{ team.team_number }}" target="_blank"
104
+                           rel="noreferrer noopener"
105
+                           class="mdc-button mdc-button--compact mdc-button--accent mdc-card__action"
106
+                           data-mdc-auto-init="MDCRipple">TBA</a>
107
+                    </section>
108
+                </div>
109
+            {% endfor %}
110
+        </div>
111
+    {% else %}
112
+        <div class="emptystate" role="presentation">
113
+            <i class="material-icons">group_work</i>
114
+            <p>No teams for this year.</p>
115
+        </div>
116
+    {% endif %}
117
+{% endblock %}
118
+{% block outside_content %}
119
+    <a href="{{ url("teams:new_team") }}?next={{ request.path|urlencode }}"
120
+       class="mdc-fab material-icons floating-bottom-right-fab" aria-label="Add Team" data-mdc-auto-init="MDCRipple"
121
+       tabindex="0">
122
+        <span class="mdc-fab__icon">add</span>
123
+    </a>
124
+{% endblock %}

+ 0
- 0
home/migrations/__init__.py View File


+ 3
- 0
home/models.py View File

@@ -0,0 +1,3 @@
1
+from django.db import models
2
+
3
+# Create your models here.

+ 3
- 0
home/tests.py View File

@@ -0,0 +1,3 @@
1
+from django.test import TestCase
2
+
3
+# Create your tests here.

+ 8
- 0
home/urls.py View File

@@ -0,0 +1,8 @@
1
+from django.conf.urls import url
2
+
3
+from . import views
4
+
5
+app_name = "home"
6
+urlpatterns = [
7
+    url(r"^$", views.index, name="index")
8
+]

+ 19
- 0
home/views.py View File

@@ -0,0 +1,19 @@
1
+from django.shortcuts import render
2
+
3
+from teams.models import Team
4
+from tasks.models import Task
5
+
6
+from FRCScoutWeb.config import CURRENT_FRC_YEAR, ALLOWED_YEARS
7
+
8
+
9
+def index(request):
10
+    user_selected_year = request.session.get("user_selected_year")
11
+    if not user_selected_year or int(user_selected_year) not in ALLOWED_YEARS:
12
+        request.session["user_selected_year"] = CURRENT_FRC_YEAR
13
+        user_selected_year = CURRENT_FRC_YEAR
14
+
15
+    teams = Team.objects.filter(year=user_selected_year)
16
+
17
+    num_tasks = len(Task.objects.filter(year=user_selected_year))
18
+
19
+    return render(request, "home/index.html", {"teams": teams, "num_tasks": num_tasks})

+ 22
- 0
manage.py View File

@@ -0,0 +1,22 @@
1
+#!/usr/bin/env python
2
+import os
3
+import sys
4
+
5
+if __name__ == "__main__":
6
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "FRCScoutWeb.settings")
7
+    try:
8
+        from django.core.management import execute_from_command_line
9
+    except ImportError:
10
+        # The above import may fail for some other reason. Ensure that the
11
+        # issue is really that Django is missing to avoid masking other
12
+        # exceptions on Python 2.
13
+        try:
14
+            import django
15
+        except ImportError:
16
+            raise ImportError(
17
+                "Couldn't import Django. Are you sure it's installed and "
18
+                "available on your PYTHONPATH environment variable? Did you "
19
+                "forget to activate a virtual environment?"
20
+            )
21
+        raise
22
+    execute_from_command_line(sys.argv)

+ 0
- 0
tasks/__init__.py View File


+ 5
- 0
tasks/admin.py View File

@@ -0,0 +1,5 @@
1
+from django.contrib import admin
2
+from .models import Task
3
+
4
+
5
+admin.site.register(Task)

+ 5
- 0
tasks/apps.py View File

@@ -0,0 +1,5 @@
1
+from django.apps import AppConfig
2
+
3
+
4
+class TasksConfig(AppConfig):
5
+    name = 'tasks'

+ 146
- 0
tasks/fixtures/tasks.json View File

@@ -0,0 +1,146 @@
1
+[
2
+  {
3
+    "model": "tasks.task",
4
+    "pk": "cheval-2016",
5
+    "fields": {
6
+      "name": "Cheval de Frise",
7
+      "year": 2016
8
+    }
9
+  },
10
+  {
11
+    "model": "tasks.task",
12
+    "pk": "drawbridge-2016",
13
+    "fields": {
14
+      "name": "Drawbridge",
15
+      "year": 2016
16
+    }
17
+  },
18
+  {
19
+    "model": "tasks.task",
20
+    "pk": "gears-2017",
21
+    "fields": {
22
+      "name": "Gears",
23
+      "year": 2017
24
+    }
25
+  },
26
+  {
27
+    "model": "tasks.task",
28
+    "pk": "hang-2017",
29
+    "fields": {
30
+      "name": "Hang",
31
+      "year": 2017
32
+    }
33
+  },
34
+  {
35
+    "model": "tasks.task",
36
+    "pk": "highgoal-2016",
37
+    "fields": {
38
+      "name": "High Goal",
39
+      "year": 2016
40
+    }
41
+  },
42
+  {
43
+    "model": "tasks.task",
44
+    "pk": "highgoal-2017",
45
+    "fields": {
46
+      "name": "High Goal",
47
+      "year": 2017
48
+    }
49
+  },
50
+  {
51
+    "model": "tasks.task",
52
+    "pk": "lowbar-2016",
53
+    "fields": {
54
+      "name": "Low Bar",
55
+      "year": 2016
56
+    }
57
+  },
58
+  {
59
+    "model": "tasks.task",
60
+    "pk": "lowgoal-2016",
61
+    "fields": {
62
+      "name": "Low Goal",
63
+      "year": 2016
64
+    }
65
+  },
66
+  {
67
+    "model": "tasks.task",
68
+    "pk": "lowgoal-2017",
69
+    "fields": {
70
+      "name": "Low Goal",
71
+      "year": 2017
72
+    }
73
+  },
74
+  {
75
+    "model": "tasks.task",
76
+    "pk": "moat-2016",
77
+    "fields": {
78
+      "name": "Moat",
79
+      "year": 2016
80
+    }
81
+  },
82
+  {
83
+    "model": "tasks.task",
84
+    "pk": "pickup-ground-2017",
85
+    "fields": {
86
+      "name": "Ground Fuel Pick up",
87
+      "year": 2017
88
+    }
89
+  },
90
+  {
91
+    "model": "tasks.task",
92
+    "pk": "pickup-hopper-2017",
93
+    "fields": {
94
+      "name": "Hopper Fuel Pick up",
95
+      "year": 2017
96
+    }
97
+  },
98
+  {
99
+    "model": "tasks.task",
100
+    "pk": "portcullis-2016",
101
+    "fields": {
102
+      "name": "Portcullis",
103
+      "year": 2016
104
+    }
105
+  },
106
+  {
107
+    "model": "tasks.task",
108
+    "pk": "ramparts-2016",
109
+    "fields": {
110
+      "name": "Ramparts",
111
+      "year": 2016
112
+    }
113
+  },
114
+  {
115
+    "model": "tasks.task",
116
+    "pk": "rockwall-2016",
117
+    "fields": {
118
+      "name": "Rock Wall",
119
+      "year": 2016
120
+    }
121
+  },
122
+  {
123
+    "model": "tasks.task",
124
+    "pk": "roughterrain-2016",
125
+    "fields": {
126
+      "name": "Rough Terrain",
127
+      "year": 2016
128
+    }
129
+  },
130
+  {
131
+    "model": "tasks.task",
132
+    "pk": "sallyport-2016",
133
+    "fields": {
134
+      "name": "Sally Port",
135
+      "year": 2016
136
+    }
137
+  },
138
+  {
139
+    "model": "tasks.task",
140
+    "pk": "scaletower-2016",
141
+    "fields": {
142
+      "name": "Scale Tower",
143
+      "year": 2016
144
+    }
145
+  }
146
+]

+ 4
- 0
tasks/jinja2/tasks/index.html View File

@@ -0,0 +1,4 @@
1
+{% extends "base.html" %}
2
+{% block body %}
3
+    <p>Tasks</p>
4
+{% endblock %}

+ 24
- 0
tasks/migrations/0001_initial.py View File

@@ -0,0 +1,24 @@
1
+# -*- coding: utf-8 -*-
2
+# Generated by Django 1.10.1 on 2017-02-04 05:52
3
+from __future__ import unicode_literals
4
+
5
+from django.db import migrations, models
6
+
7
+
8
+class Migration(migrations.Migration):
9
+
10
+    initial = True
11
+
12
+    dependencies = [
13
+    ]
14
+
15
+    operations = [
16
+        migrations.CreateModel(
17
+            name='Task',
18
+            fields=[
19
+                ('codeyear', models.CharField(max_length=50, primary_key=True, serialize=False, unique=True)),
20
+                ('name', models.CharField(max_length=100)),
21
+                ('year', models.IntegerField()),
22
+            ],
23
+        ),
24
+    ]

+ 0
- 0
tasks/migrations/__init__.py View File


+ 10
- 0
tasks/models.py View File

@@ -0,0 +1,10 @@
1
+from django.db import models
2
+
3
+
4
+class Task(models.Model):
5
+    codeyear = models.CharField(primary_key=True, unique=True, max_length=50)
6
+    name = models.CharField(max_length=100)
7
+    year = models.IntegerField()
8
+
9
+    def __str__(self):
10
+        return self.name

+ 3
- 0
tasks/tests.py View File

@@ -0,0 +1,3 @@
1
+from django.test import TestCase
2
+
3
+# Create your tests here.

+ 8
- 0
tasks/urls.py View File

@@ -0,0 +1,8 @@
1
+from django.conf.urls import url
2
+
3
+from . import views
4
+
5
+app_name = "tasks"
6
+urlpatterns = [
7
+    url(r"^$", views.index, name="index")
8
+]

+ 5
- 0
tasks/views.py View File

@@ -0,0 +1,5 @@
1
+from django.shortcuts import render
2
+
3
+
4
+def index(request):
5
+    return render(request, "tasks/index.html")

+ 0
- 0
teams/__init__.py View File


+ 4
- 0
teams/admin.py View File

@@ -0,0 +1,4 @@
1
+from django.contrib import admin
2
+from .models import Team
3
+
4
+admin.site.register(Team)

+ 5
- 0
teams/apps.py View File

@@ -0,0 +1,5 @@
1
+from django.apps import AppConfig
2
+
3
+
4
+class TeamsConfig(AppConfig):
5
+    name = 'teams'

+ 54
- 0
teams/forms.py View File

@@ -0,0 +1,54 @@
1
+from django import forms
2
+
3
+from FRCScoutWeb.config import CURRENT_FRC_YEAR
4
+from FRCScoutWeb.widgets import MultipleCheckboxes, MaterialCheckboxInput
5
+
6
+from tasks.models import Task
7
+
8
+
9
+class NewTeamForm(forms.Form):
10
+    team_number = forms.IntegerField(label="Team Number",
11
+                                     widget=forms.NumberInput(attrs={"autofocus": "",
12
+                                                                     "class": "mdc-textfield__input mdc-typography"}))
13
+
14
+    team_name = forms.CharField(label="Team Name",
15
+                                required=False,
16
+                                widget=forms.TextInput(attrs={"class": "mdc-textfield__input mdc-typography"}))
17
+
18
+    auto_points = forms.IntegerField(label="Autonomous Points",
19
+                                     required=False,
20
+                                     widget=forms.NumberInput(attrs={"autofocus": "",
21
+                                                                     "class": "mdc-textfield__input mdc-typography"}))
22
+
23
+    tasks = forms.ModelMultipleChoiceField(queryset=Task.objects.all(),
24
+                                           # TODO: Maybe this can be replaced with forms.CheckboxSelectMultiple
25
+                                           widget=MultipleCheckboxes,
26
+                                           required=False)
27
+
28
+    favorite = forms.BooleanField(label="Favorite", widget=MaterialCheckboxInput, required=False)
29
+
30
+    def prepare_tasks(self, year=None):
31
+        year = year or CURRENT_FRC_YEAR
32
+        self.fields["tasks"].queryset = Task.objects.filter(year=year)
33
+
34
+
35
+class EditTeamForm(forms.Form):
36
+    team_name = forms.CharField(label="Team Name",
37
+                                required=False,
38
+                                widget=forms.TextInput(attrs={"class": "mdc-textfield__input mdc-typography"}))
39
+
40
+    auto_points = forms.IntegerField(label="Autonomous Points",
41
+                                     required=False,
42
+                                     widget=forms.NumberInput(attrs={"autofocus": "",
43
+                                                                     "class": "mdc-textfield__input mdc-typography"}))
44
+
45
+    tasks = forms.ModelMultipleChoiceField(queryset=Task.objects.all(),
46
+                                           # TODO: Maybe this can be replaced with forms.CheckboxSelectMultiple
47
+                                           widget=MultipleCheckboxes,
48
+                                           required=False)
49
+
50
+    favorite = forms.BooleanField(label="Favorite", widget=MaterialCheckboxInput, required=False)
51
+
52
+    def prepare_tasks(self, year=None):
53
+        year = year or CURRENT_FRC_YEAR
54
+        self.fields["tasks"].queryset = Task.objects.filter(year=year)

+ 80
- 0
teams/jinja2/teams/edit_team.html View File

@@ -0,0 +1,80 @@
1
+{% extends "base.html" %}
2
+
3
+{# FIXME: This whole form feels horribly hack-y. #}
4
+{% block body %}
5
+    <div class="mdc-layout-grid">
6
+        <div class="mdc-card mdc-layout-grid__cell mdc-layout-grid__cell--span-2 mdc-layout-grid__cell--span-3-phone"
7
+             style="margin: 0 auto;">
8
+            <form method="POST" action="{{ url("teams:edit_team", args=[team_number]) }}">
9
+                <section class="mdc-card__primary mdc-theme--primary-bg">
10
+                    <h1 class="mdc-card__title mdc-card__title--large mdc-theme--text-primary-on-primary">
11
+                        Editing team {{ team_number }}
12
+                    </h1>
13
+                </section>
14
+                <section class="mdc-card__supporting-text">
15
+                    <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
16
+
17
+                    {% for hidden in form.hidden_fields() %}
18
+                        {{ hidden }}
19
+                    {% endfor %}
20
+
21
+                    {% for error in form.non_field_errors() %}
22
+                        <span class="field-error">{{ error }}</span>
23
+                    {% endfor %}
24
+
25
+                    {% for field in form.visible_fields() %}
26
+                        {% set widget_type = field.field.widget.__class__.__name__ %}
27
+                        {% if widget_type == "NumberInput" or widget_type == "TextInput" %}
28
+                            {% set div_attrs %}
29
+                                class="mdc-textfield" data-mdc-auto-init="MDCTextfield"
30
+                            {% endset %}
31
+                            {% set label_class = "mdc-textfield__label" %}
32
+
33
+                            {% if field.value() != "" and field.value() != None %}
34
+                                {% set label_class = label_class + " mdc-textfield__label--float-above" %}
35
+                            {% endif %}
36
+
37
+                            {% set helptext_class = "mdc-textfield-helptext" %}
38
+                        {% elif widget_type == "MultipleCheckboxes" %}
39
+                            {% set div_attrs %}
40
+                                class="mdc-form-field mdc-form-field--align-end field-column"
41
+                            {% endset %}
42
+                            {% set label_class = "field-label" %}
43
+                            {% set helptext_class = "field-helptext" %}
44
+                        {% elif widget_type == "MaterialCheckboxInput" %}
45
+                            {# FIXME: This is really hack-y #}
46
+                            <hr style="margin-top: 1em; border: 1px solid var(--mdc-theme-text-hint-on-light, rgba(0,0,0,.38));">
47
+                            {% set div_attrs %}
48
+                                class="mdc-form-field"
49
+                            {% endset %}
50
+                            {% set label_class = "mdc-switch-label" %}
51
+                            {% set helptext_class = "field-helptext" %}
52
+                        {% else %}
53
+                            {% set div_attrs %}
54
+                                class="mdc-form-field"
55
+                            {% endset %}
56
+                            {% set label_class = "field-label" %}
57
+                            {% set helptext_class = "field-helptext" %}
58
+                        {% endif %}
59
+
60
+                        <div {{ div_attrs|safe }}>
61
+                            {{ field }}
62
+                            <label class="{{ label_class|safe }}"
63
+                                   for="{{ field.id_for_label }}">{{ field.label }}</label>
64
+                        </div>
65
+                        {% for error in field.errors %}
66
+                            <span class="field-error">{{ error }}</span>
67
+                        {% endfor %}
68
+                        {% if field.help_text %}
69
+                            <p class="{{ helptext_class|safe }}" aria-hidden="true">{{ field.help_text }}</p>
70
+                        {% endif %}
71
+                    {% endfor %}
72
+                </section>
73
+                <section class="mdc-card__actions">
74
+                    <input class="mdc-button mdc-button--compact mdc-button--accent mdc-card__action"
75
+                           data-mdc-auto-init="MDCRipple" type="submit" value="Submit"/>
76
+                </section>
77
+            </form>
78
+        </div>
79
+    </div>
80
+{% endblock %}

+ 78
- 0
teams/jinja2/teams/new_team.html View File

@@ -0,0 +1,78 @@
1
+{% extends "base.html" %}
2
+
3
+{# FIXME: This whole form feels horribly hack-y. #}
4
+{% block body %}
5
+    <div class="mdc-layout-grid">
6
+        <div class="mdc-card mdc-layout-grid__cell mdc-layout-grid__cell--span-2 mdc-layout-grid__cell--span-3-phone"
7
+             style="margin: 0 auto;">
8
+            <form method="POST" action="{{ url("teams:new_team") }}">
9
+                <section class="mdc-card__primary mdc-theme--primary-bg">
10
+                    <h1 class="mdc-card__title mdc-card__title--large mdc-theme--text-primary-on-primary">Add team</h1>
11
+                </section>
12
+                <section class="mdc-card__supporting-text">
13
+                    <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
14
+
15
+                    {% for hidden in form.hidden_fields() %}
16
+                        {{ hidden }}
17
+                    {% endfor %}
18
+
19
+                    {% for error in form.non_field_errors() %}
20
+                        <span class="field-error">{{ error }}</span>
21
+                    {% endfor %}
22
+
23
+                    {% for field in form.visible_fields() %}
24
+                        {% set widget_type = field.field.widget.__class__.__name__ %}
25
+                        {% if widget_type == "NumberInput" or widget_type == "TextInput" %}
26
+                            {% set div_attrs %}
27
+                                class="mdc-textfield" data-mdc-auto-init="MDCTextfield"
28
+                            {% endset %}
29
+                            {% set label_class = "mdc-textfield__label" %}
30
+
31
+                            {% if field.value() != "" and field.value() != None %}
32
+                                {% set label_class = label_class + " mdc-textfield__label--float-above" %}
33
+                            {% endif %}
34
+
35
+                            {% set helptext_class = "mdc-textfield-helptext" %}
36
+                        {% elif widget_type == "MultipleCheckboxes" %}
37
+                            {% set div_attrs %}
38
+                                class="mdc-form-field mdc-form-field--align-end field-column"
39
+                            {% endset %}
40
+                            {% set label_class = "field-label" %}
41
+                            {% set helptext_class = "field-helptext" %}
42
+                        {% elif widget_type == "MaterialCheckboxInput" %}
43
+                            {# FIXME: This is really hack-y #}
44
+                            <hr style="margin-top: 1em; border: 1px solid var(--mdc-theme-text-hint-on-light, rgba(0,0,0,.38));">
45
+                            {% set div_attrs %}
46
+                                class="mdc-form-field"
47
+                            {% endset %}
48
+                            {% set label_class = "mdc-switch-label" %}
49
+                            {% set helptext_class = "field-helptext" %}
50
+                        {% else %}
51
+                            {% set div_attrs %}
52
+                                class="mdc-form-field"
53
+                            {% endset %}
54
+                            {% set label_class = "field-label" %}
55
+                            {% set helptext_class = "field-helptext" %}
56
+                        {% endif %}
57
+
58
+                        <div {{ div_attrs|safe }}>
59
+                            {{ field }}
60
+                            <label class="{{ label_class|safe }}"
61
+                                   for="{{ field.id_for_label }}">{{ field.label }}</label>
62
+                        </div>
63
+                        {% for error in field.errors %}
64
+                            <span class="field-error">{{ error }}</span>
65
+                        {% endfor %}
66
+                        {% if field.help_text %}
67
+                            <p class="{{ helptext_class|safe }}" aria-hidden="true">{{ field.help_text }}</p>
68
+                        {% endif %}
69
+                    {% endfor %}
70
+                </section>
71
+                <section class="mdc-card__actions">
72
+                    <input class="mdc-button mdc-button--compact mdc-button--accent mdc-card__action"
73
+                           data-mdc-auto-init="MDCRipple" type="submit" value="Submit"/>
74
+                </section>
75
+            </form>
76
+        </div>
77
+    </div>
78
+{% endblock %}

+ 28
- 0
teams/migrations/0001_initial.py View File

@@ -0,0 +1,28 @@
1
+# -*- coding: utf-8 -*-
2
+# Generated by Django 1.10.1 on 2017-02-04 05:52
3
+from __future__ import unicode_literals
4
+
5
+from django.db import migrations, models
6
+
7
+
8
+class Migration(migrations.Migration):
9
+
10
+    initial = True
11
+
12
+    dependencies = [
13
+        ('tasks', '0001_initial'),
14
+    ]
15
+
16
+    operations = [
17
+        migrations.CreateModel(
18
+            name='Team',
19
+            fields=[
20
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21
+                ('teamNumber', models.IntegerField()),
22
+                ('name', models.CharField(max_length=100)),
23
+                ('autoPoints', models.IntegerField()),
24
+                ('year', models.IntegerField(default=2017)),
25
+                ('tasks', models.ManyToManyField(to='tasks.Task')),
26
+            ],
27
+        ),
28
+    ]

+ 25
- 0
teams/migrations/0002_auto_20170207_1813.py View File

@@ -0,0 +1,25 @@
1
+# -*- coding: utf-8 -*-
2
+# Generated by Django 1.10.1 on 2017-02-07 18:13
3
+from __future__ import unicode_literals
4
+
5
+from django.db import migrations
6
+
7
+
8
+class Migration(migrations.Migration):
9
+
10
+    dependencies = [
11
+        ('teams', '0001_initial'),
12
+    ]
13
+
14
+    operations = [
15
+        migrations.RenameField(
16
+            model_name='team',
17
+            old_name='autoPoints',
18
+            new_name='auto_points',
19
+        ),
20
+        migrations.RenameField(
21
+            model_name='team',
22
+            old_name='teamNumber',
23
+            new_name='team_number',
24
+        ),
25
+    ]

+ 20
- 0
teams/migrations/0003_team_favorite.py View File

@@ -0,0 +1,20 @@
1
+# -*- coding: utf-8 -*-
2
+# Generated by Django 1.10.1 on 2017-02-07 19:14
3
+from __future__ import unicode_literals
4
+
5
+from django.db import migrations, models
6
+
7
+
8
+class Migration(migrations.Migration):
9
+
10
+    dependencies = [
11
+        ('teams', '0002_auto_20170207_1813'),
12
+    ]
13
+
14
+    operations = [
15
+        migrations.AddField(
16
+            model_name='team',
17
+            name='favorite',
18
+            field=models.BooleanField(default=False),
19
+        ),
20
+    ]

+ 19
- 0
teams/migrations/0004_auto_20170210_0004.py View File

@@ -0,0 +1,19 @@
1
+# -*- coding: utf-8 -*-
2
+# Generated by Django 1.10.1 on 2017-02-10 00:04
3
+from __future__ import unicode_literals
4
+
5
+from django.db import migrations
6
+
7
+
8
+class Migration(migrations.Migration):
9
+
10
+    dependencies = [
11
+        ('teams', '0003_team_favorite'),
12
+    ]
13
+
14
+    operations = [
15
+        migrations.AlterUniqueTogether(
16
+            name='team',
17
+            unique_together=set([('team_number', 'year')]),
18
+        ),
19
+    ]

+ 20
- 0
teams/migrations/0005_auto_20170210_0005.py View File

@@ -0,0 +1,20 @@
1
+# -*- coding: utf-8 -*-
2
+# Generated by Django 1.10.1 on 2017-02-10 00:05
3
+from __future__ import unicode_literals
4
+
5
+from django.db import migrations, models
6
+
7
+
8
+class Migration(migrations.Migration):
9
+
10
+    dependencies = [
11
+        ('teams', '0004_auto_20170210_0004'),
12
+    ]
13
+
14
+    operations = [
15
+        migrations.AlterField(
16
+            model_name='team',
17
+            name='tasks',
18
+            field=models.ManyToManyField(blank=True, to='tasks.Task'),
19
+        ),
20
+    ]

+ 20
- 0
teams/migrations/0006_auto_20170212_0429.py View File

@@ -0,0 +1,20 @@
1
+# -*- coding: utf-8 -*-
2
+# Generated by Django 1.10.1 on 2017-02-12 04:29
3
+from __future__ import unicode_literals
4
+
5
+from django.db import migrations, models
6
+
7
+
8
+class Migration(migrations.Migration):
9
+
10
+    dependencies = [
11
+        ('teams', '0005_auto_20170210_0005'),
12
+    ]
13
+
14
+    operations = [
15
+        migrations.AlterField(
16
+            model_name='team',
17
+            name='auto_points',
18
+            field=models.IntegerField(default=0),
19
+        ),
20
+    ]

+ 0
- 0
teams/migrations/__init__.py View File


+ 22
- 0
teams/models.py View File

@@ -0,0 +1,22 @@
1
+from django.db import models
2
+from tasks.models import Task
3
+
4
+from FRCScoutWeb.config import CURRENT_FRC_YEAR
5
+
6
+
7
+class Team(models.Model):
8
+    team_number = models.IntegerField()
9
+    name = models.CharField(max_length=100)
10
+
11
+    tasks = models.ManyToManyField(Task, blank=True)
12
+    auto_points = models.IntegerField(default=0)
13
+
14
+    year = models.IntegerField(default=CURRENT_FRC_YEAR)
15
+
16
+    favorite = models.BooleanField(default=False)
17
+
18
+    class Meta:
19
+        unique_together = ("team_number", "year")
20
+
21
+    def __str__(self):
22
+        return "{}".format(self.team_number, self.name, self.year)

+ 3
- 0
teams/tests.py View File

@@ -0,0 +1,3 @@
1
+from django.test import TestCase
2
+
3
+# Create your tests here.

+ 10
- 0
teams/urls.py View File

@@ -0,0 +1,10 @@
1
+from django.conf.urls import url
2
+
3
+from . import views
4
+
5
+app_name = "teams"
6
+urlpatterns = [
7
+    url(r"^new_team/", views.new_team, name="new_team"),
8
+    url(r"^edit_team/(?P<team_number>[0-9]+)/", views.edit_team, name="edit_team"),
9
+    url(r"^toggle_favorite/", views.toggle_favorite, name="toggle_favorite")
10
+]

+ 148
- 0
teams/views.py View File

@@ -0,0 +1,148 @@
1
+from urllib.parse import urlparse, urlunparse
2
+
3
+from django.contrib.auth.decorators import login_required
4
+from django.http import HttpResponse, QueryDict
5
+from django.shortcuts import render, get_object_or_404, redirect
6
+from django.urls import reverse
7
+
8
+from FRCScoutWeb.config import CURRENT_FRC_YEAR, ALLOWED_YEARS
9
+from teams.models import Team
10
+from .forms import NewTeamForm, EditTeamForm
11
+
12
+
13
+@login_required
14
+def new_team(request):
15
+    user_selected_year = request.session.get("user_selected_year")
16
+    if not user_selected_year or int(user_selected_year) not in ALLOWED_YEARS:
17
+        request.session["user_selected_year"] = CURRENT_FRC_YEAR
18
+        user_selected_year = CURRENT_FRC_YEAR
19
+
20
+    if request.method == "POST":
21
+        form = NewTeamForm(request.POST)
22
+        form.prepare_tasks(user_selected_year)
23
+
24
+        if form.is_valid():
25
+            do_continue = True
26
+
27
+            if not form.cleaned_data["team_number"] or form.cleaned_data["team_number"] < 0:
28
+                form.add_error("team_number", "Invalid team number.")
29
+                do_continue = False
30
+
31
+            if do_continue:
32
+                team_number = form.cleaned_data["team_number"]
33
+                name = form.cleaned_data["team_name"] or "Team #{}".format(team_number)
34
+
35
+                auto_points = form.cleaned_data["auto_points"] or 0
36
+                tasks = form.cleaned_data["tasks"]
37
+
38
+                favorite = form.cleaned_data["favorite"] or False
39
+
40
+                team = Team(team_number=team_number)
41
+
42
+                team.name = name
43
+                team.auto_points = auto_points
44
+                team.favorite = favorite
45
+
46
+                team.save()
47
+
48
+                for task in tasks:
49
+                    team.tasks.add(task)
50
+
51
+                if "next" in request.GET:
52
+                    parsed_url = urlparse(request.GET["next"])
53
+                    query = QueryDict(parsed_url.query, True)
54
+                    query["add_team_success"] = True
55
+                    query["add_team_team_number"] = form.cleaned_data.get("team_number")
56
+                    next_url = urlunparse(
57
+                        (parsed_url.scheme, parsed_url.netloc, parsed_url.path, parsed_url.params, query.urlencode(),
58
+                         parsed_url.fragment))
59
+                else:
60
+                    query = QueryDict(mutable=True)
61
+                    query["add_team_success"] = True
62
+                    query["add_team_team_number"] = form.cleaned_data.get("team_number")
63
+                    next_url = reverse("teams:new_team") + "?" + query.urlencode()
64
+
65
+                return redirect(next_url)
66
+
67
+    else:
68
+        form = NewTeamForm()
69
+        form.prepare_tasks(user_selected_year)
70
+
71
+    return render(request, "teams/new_team.html", {"form": form})
72
+
73
+
74
+@login_required
75
+def edit_team(request, team_number):
76
+    user_selected_year = request.session.get("user_selected_year")
77
+    if not user_selected_year or int(user_selected_year) not in ALLOWED_YEARS:
78
+        request.session["user_selected_year"] = CURRENT_FRC_YEAR
79
+        user_selected_year = CURRENT_FRC_YEAR
80
+
81
+    team = get_object_or_404(Team, team_number=team_number)
82
+
83
+    if request.method == "POST":
84
+        form = EditTeamForm(request.POST)
85
+        form.prepare_tasks(user_selected_year)
86
+
87
+        if form.is_valid():
88
+            name = form.cleaned_data["team_name"] or "Team #{}".format(team_number)
89
+
90
+            auto_points = form.cleaned_data["auto_points"] or 0
91
+            tasks = form.cleaned_data["tasks"]
92
+
93
+            favorite = form.cleaned_data["favorite"] or False
94
+
95
+            team.name = name
96
+            team.auto_points = auto_points
97
+            team.favorite = favorite
98
+
99
+            team.save()
100
+
101
+            team.tasks.clear()
102
+            for task in tasks:
103
+                team.tasks.add(task)
104
+
105
+            if "next" in request.GET:
106
+                parsed_url = urlparse(request.GET["next"])
107
+                query = QueryDict(parsed_url.query, True)
108
+                query["edit_team_success"] = True
109
+                query["edit_team_team_number"] = form.cleaned_data.get("team_number")
110
+                next_url = urlunparse(
111
+                    (parsed_url.scheme, parsed_url.netloc, parsed_url.path, parsed_url.params, query.urlencode(),
112
+                     parsed_url.fragment))
113
+            else:
114
+                query = QueryDict(mutable=True)
115
+                query["edit_team_success"] = True
116
+                query["edit_team_team_number"] = form.cleaned_data.get("team_number")
117
+                next_url = reverse("home:index") + "?" + query.urlencode()
118
+
119
+            return redirect(next_url)
120
+
121
+    else:
122
+        # FIXME: This should probably be a ModelForm
123
+        initial = {"team_name": team.name,
124
+                   "auto_points": team.auto_points,
125
+                   "tasks": team.tasks.all(),
126
+                   "favorite": team.favorite}
127
+
128
+        form = EditTeamForm(initial)
129
+
130
+        form.prepare_tasks(user_selected_year)
131
+
132
+    return render(request, "teams/edit_team.html", {"team_number": team_number, "form": form})
133
+
134
+
135
+def toggle_favorite(request):
136
+    if not request.user.is_authenticated:
137
+        return HttpResponse(status=401)
138
+    if "team_number" not in request.POST or "year" not in request.POST:
139
+        return HttpResponse(status=404)
140
+
141
+    team_number = request.POST.get("team_number")
142
+    year = request.POST.get("year")
143
+
144
+    team = get_object_or_404(Team, team_number=team_number, year=year)
145
+    team.favorite = not team.favorite
146
+    team.save()
147
+
148
+    return HttpResponse(content=str(team.favorite))