@@ -1,3 +0,0 @@ | |||
from django.contrib import admin | |||
# Register your models here. |
@@ -1,5 +0,0 @@ | |||
from django.apps import AppConfig | |||
class FearTrackerConfig(AppConfig): | |||
name = 'fear_tracker' |
@@ -0,0 +1,55 @@ | |||
from django import forms | |||
from .models import Game, Player | |||
class PlayerForm(forms.Form): | |||
name = forms.CharField(max_length=80, label="Name", required=False) | |||
spirit = forms.TypedChoiceField(label="Spirit", | |||
choices=enumerate(Player.SPIRIT_NAMES), | |||
empty_value=None, coerce=int) | |||
class BasePlayerFormSet(forms.BaseFormSet): | |||
def clean(self): | |||
if any(self.errors): | |||
return | |||
names = set() | |||
for form in self.forms: | |||
if self.can_delete and self._should_delete_form(form): | |||
continue | |||
name = form.cleaned_data.get('name') | |||
if name: | |||
if name in names: | |||
raise forms.ValidationError( | |||
"Players must have distinct names.") | |||
names.add(name) | |||
if not names: | |||
raise forms.ValidationError("Must have at least one player.") | |||
PlayerFormSet = forms.formset_factory(PlayerForm, formset=BasePlayerFormSet) | |||
class NewGameForm(forms.Form): | |||
combined_growth_spirit = forms.BooleanField( | |||
required=False, initial=True, | |||
label="Combine Growth and Spirit phases into a single phase") | |||
england_build = forms.BooleanField( | |||
required=False, initial=False, | |||
label="High Immigration (extra build phase for England level 3+)") | |||
fear_per_player = forms.IntegerField( | |||
label="Fear per player", initial=4, min_value=1, max_value=99) | |||
class JoinGameForm(forms.Form): | |||
game = forms.CharField(label='Access code', | |||
max_length=Game.ACCESS_CODE_LENGTH) | |||
def clean_game(self): | |||
data = self.cleaned_data['game'] | |||
try: | |||
return Game.objects.get(access_code=data.lower()) | |||
except Game.DoesNotExist: | |||
raise forms.ValidationError("Invalid access code.") |
@@ -1,3 +1,252 @@ | |||
import random | |||
import string | |||
from django.db import models | |||
from django.db.models import Sum | |||
from django.utils import timezone | |||
def generate_code(length): | |||
return "".join([random.choice(string.ascii_lowercase) | |||
for i in range(length)]) | |||
class Game(models.Model): | |||
ACCESS_CODE_LENGTH = 6 | |||
access_code = models.CharField(db_index=True, unique=True, | |||
max_length=ACCESS_CODE_LENGTH) | |||
game_turn = models.IntegerField(default=0) | |||
GAME_PHASE_LOBBY = 0 | |||
GAME_PHASE_GROWTH = 1 | |||
GAME_PHASE_SPIRIT = 2 | |||
GAME_PHASE_GROWTH_SPIRIT = 3 | |||
GAME_PHASE_FAST = 4 | |||
GAME_PHASE_BLIGHTED_ISLAND = 5 | |||
GAME_PHASE_FEAR = 6 | |||
GAME_PHASE_FEAR_CARDS = [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17] | |||
GAME_PHASE_ENGLAND_BUILD = 100 | |||
GAME_PHASE_RAVAGE = 101 | |||
GAME_PHASE_BUILD = 102 | |||
GAME_PHASE_EXPLORE = 103 | |||
GAME_PHASE_SLOW = 104 | |||
GAME_PHASE_END = 120 | |||
game_phase = models.IntegerField(default=GAME_PHASE_LOBBY) | |||
created = models.DateTimeField() | |||
ended = models.DateTimeField(null=True, default=None) | |||
combined_growth_spirit = models.BooleanField(default=False) | |||
england_build = models.BooleanField(default=False) | |||
fear_per_card = models.IntegerField() | |||
# from http://stackoverflow.com/a/11821832 | |||
def save(self, *args, **kwargs): | |||
# object is being created, thus no primary key field yet | |||
if not self.pk: | |||
# Make sure access_code is unique before using it. | |||
access_code = generate_code(Game.ACCESS_CODE_LENGTH) | |||
while Game.objects.filter(access_code=access_code).exists(): | |||
access_code = generate_code(Game.ACCESS_CODE_LENGTH) | |||
self.access_code = access_code | |||
self.created = timezone.now() | |||
if self.ended is None and self.game_phase == Game.GAME_PHASE_END: | |||
self.ended = timezone.now() | |||
super(Game, self).save(*args, **kwargs) | |||
def next_turn_and_phase(self): | |||
if self.game_phase in [Game.GAME_PHASE_LOBBY, Game.GAME_PHASE_SLOW]: | |||
turn = self.game_turn+1 | |||
if self.combined_growth_spirit: | |||
return (turn, Game.GAME_PHASE_GROWTH_SPIRIT) | |||
else: | |||
return (turn, Game.GAME_PHASE_GROWTH) | |||
def next_phase(): | |||
if self.game_phase == Game.GAME_PHASE_SPIRIT: | |||
return Game.GAME_PHASE_FAST | |||
elif self.game_phase == Game.GAME_PHASE_BLIGHTED_ISLAND: | |||
if self.num_available_fear_cards() == 0: | |||
return Game.GAME_PHASE_FEAR | |||
else: | |||
return Game.GAME_PHASE_FEAR_CARDS[0] | |||
elif self.game_phase == Game.GAME_PHASE_FEAR or\ | |||
self.game_phase in Game.GAME_PHASE_FEAR_CARDS: | |||
num_fear = self.fear_cards_in_current_fear_phase() | |||
fear_so_far = self.game_phase - Game.GAME_PHASE_FEAR | |||
if num_fear < fear_so_far: | |||
return self.game_phase + 1 | |||
else: | |||
if self.england_build: | |||
return Game.GAME_PHASE_ENGLAND_BUILD | |||
else: | |||
return Game.GAME_PHASE_RAVAGE | |||
else: | |||
return self.game_phase + 1 | |||
return (self.game_turn, next_phase()) | |||
def get_current_phase(self): | |||
return self.phase_set.filter(game_turn=self.game_turn, | |||
game_phase=self.game_phase).first() | |||
def get_current_total_fear(self): | |||
current_phase = self.get_current_phase() | |||
if current_phase: | |||
return current_phase.starting_fear +\ | |||
current_phase.fear_this_phase() | |||
else: | |||
return 0 | |||
def get_fear_to_next_card(self): | |||
total_fear = self.get_current_total_fear() | |||
leftover_fear = total_fear // self.fear_per_card | |||
return self.fear_per_card - leftover_fear | |||
def _fear_phase(self, previous): | |||
if self.game_phase >= Game.GAME_PHASE_FEAR and not previous: | |||
turn = self.game_turn | |||
else: | |||
turn = self.game_turn - 1 | |||
return self.phase_set.filter( | |||
game_turn=turn, | |||
game_phase__in=[Game.GAME_PHASE_FEAR, | |||
Game.GAME_PHASE_FEAR_CARDS[0]]).first() | |||
def _fear_cards_as_of_fear_phase(self, previous): | |||
fear_phase = self._fear_phase(previous=previous) | |||
if fear_phase: | |||
return fear_phase.initial_fear // self.fear_per_card | |||
else: | |||
return 0 | |||
def fear_cards_in_current_fear_phase(self): | |||
previous_fear_cards = self._fear_cards_as_of_fear_phase(previous=True) | |||
current_fear_cards = self._fear_cards_as_of_fear_phase(previous=False) | |||
return current_fear_cards - previous_fear_cards | |||
def num_available_fear_cards(self): | |||
recent_fear_fear_cards = self._fear_cards_as_of_fear_phase( | |||
previous=False) | |||
current_fear = self.get_current_total_fear() | |||
current_fear_cards = current_fear // self.fear_per_card | |||
return current_fear_cards - recent_fear_fear_cards | |||
def advance_phase(self): | |||
current_phase = self.get_current_phase() | |||
current_time = timezone.now() | |||
if current_phase: | |||
current_phase.ended = current_time | |||
current_phase.save() | |||
initial_fear = current_phase.starting_fear +\ | |||
current_phase.fear_this_phase() | |||
else: | |||
initial_fear = 0 | |||
(next_turn, next_phase) = self.next_turn_and_phase() | |||
self.game_turn = next_turn | |||
self.game_phase = next_phase | |||
self.save() | |||
next_phase = self.get_current_phase() | |||
if next_phase is None: | |||
Phase.objects.create( | |||
game=self, | |||
game_turn=next_turn, | |||
game_phase=next_phase, | |||
initial_fear=initial_fear, | |||
started=current_time) | |||
else: | |||
next_phase.started = current_time | |||
next_phase.initial_fear = initial_fear | |||
next_phase.ended = None | |||
next_phase.save() | |||
@staticmethod | |||
def get_name_for_phase_id(phase): | |||
if phase == Game.GAME_PHASE_LOBBY: | |||
return "Game Setup" | |||
elif phase == Game.GAME_PHASE_GROWTH: | |||
return "Growth" | |||
elif phase == Game.GAME_PHASE_SPIRIT: | |||
return "Spirit" | |||
elif phase == Game.GAME_PHASE_GROWTH_SPIRIT: | |||
return "Growth/Spirit" | |||
elif phase == Game.GAME_PHASE_FAST: | |||
return "Fast Actions" | |||
elif phase == Game.GAME_PHASE_BLIGHTED_ISLAND: | |||
return "Blighted Island" | |||
elif phase == Game.GAME_PHASE_FEAR: | |||
return "Fear (no fear cards)" | |||
elif phase in Game.GAME_PHASE_FEAR_CARDS: | |||
return "Fear Card #" + str(phase-Game.GAME_PHASE_FEAR) | |||
elif phase == Game.GAME_PHASE_ENGLAND_BUILD: | |||
return "England Extra Build" | |||
elif phase == Game.GAME_PHASE_RAVAGE: | |||
return "Ravage" | |||
elif phase == Game.GAME_PHASE_BUILD: | |||
return "Build" | |||
elif phase == Game.GAME_PHASE_EXPLORE: | |||
return "Explore" | |||
elif phase == Game.GAME_PHASE_SLOW: | |||
return "Slow Actions" | |||
elif phase == Game.GAME_PHASE_END: | |||
return "Game Over" | |||
def get_current_phase_name(self): | |||
res = Game.get_name_for_phase_id(self.game_phase) | |||
if self.game_phase in Game.GAME_PHASE_FEAR_CARDS: | |||
res += ' (of %d)' % self.fear_cards_in_current_fear_phase() | |||
return res | |||
class Player(models.Model): | |||
game = models.ForeignKey(Game, on_delete=models.CASCADE, db_index=True) | |||
name = models.CharField(max_length=80) | |||
SPIRIT_NAMES = [ | |||
"Lightning's Swift Strike", | |||
"River Surges in Sunlight", | |||
"Vital Strength of the Earth", | |||
"Shadows Flicker Like Flame", | |||
"Thunderspeaker", | |||
"A Spread of Rampant Green", | |||
"Ocean's Hungry Grasp", | |||
"Bringer of Dreams and Nightmares", | |||
"Keeper of the Forbidden Wilds", | |||
"Sharp Fangs Behind the Leaves", | |||
"Serpent Slumbering Beneath the Island", | |||
"Heart of the Wildfire", | |||
] | |||
spirit = models.IntegerField() | |||
order = models.IntegerField() | |||
ready = models.BooleanField(default=False) | |||
unique_together = (("game", "name"), ("game", "order")) | |||
def get_spirit_name(self): | |||
return Player.SPIRIT_NAMES[self.spirit] | |||
class Phase(models.Model): | |||
game = models.ForeignKey(Game, on_delete=models.CASCADE, db_index=True) | |||
game_turn = models.IntegerField() | |||
game_phase = models.IntegerField() | |||
started = models.DateTimeField() | |||
ended = models.DateTimeField(null=True, default=None) | |||
# fear total at the start of this phase | |||
starting_fear = models.IntegerField(default=0) | |||
unique_together = (("game", "game_turn", "game_phase")) | |||
def fear_this_phase(self): | |||
return self.fear_set.aggregate( | |||
fear=Sum('pure_fear') | |||
+ Sum('towns_destroyed') | |||
+ 2*Sum('cities_destroyed'))['fear'] or 0 | |||
# Create your models here. | |||
class Fear(models.Model): | |||
phase = models.ForeignKey(Phase, on_delete=models.CASCADE, db_index=True) | |||
player = models.ForeignKey(Player, on_delete=models.CASCADE, db_index=True) | |||
effect = models.IntegerField(default=0) | |||
# "pure" = not from destroying a town or city | |||
pure_fear = models.IntegerField() | |||
towns_destroyed = models.IntegerField() | |||
cities_destroyed = models.IntegerField() |
@@ -0,0 +1,67 @@ | |||
footer hr { | |||
border-width: 0; | |||
border-top: 1px solid lightgray; | |||
} | |||
footer p { | |||
text-align: center; | |||
font-size: 0.7em; | |||
margin: 0.4em; | |||
color: darkgrey; | |||
} | |||
footer a, footer a:visited { | |||
color: darkgray; | |||
} | |||
.players-form p { | |||
margin: 0; | |||
} | |||
.player-1 { | |||
background: #f0ba57; | |||
} | |||
.player-2 { | |||
background: #73fcf6; | |||
} | |||
.player-3 { | |||
background: #bc9c8b; | |||
} | |||
.player-4 { | |||
background: #77a0a6; | |||
} | |||
.player-5 { | |||
background: #fff172; | |||
} | |||
.player-6 { | |||
background: #3adb85; | |||
} | |||
.player-7 { | |||
background: #99c8e1; | |||
} | |||
.player-8 { | |||
background: #f73347; | |||
} | |||
.player-9 { | |||
background: #cac186; | |||
} | |||
.player-10 { | |||
background: #8ca684; | |||
} | |||
.player-11 { | |||
background: #f068a9; | |||
} | |||
.player-12 { | |||
background: #ed822b; | |||
} |
@@ -0,0 +1,32 @@ | |||
{% load static %} | |||
<!DOCTYPE html> | |||
<html> | |||
<head> | |||
<meta charset="utf-8"/> | |||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> | |||
<link rel="stylesheet" href="{% static 'css/styles.css' %}" type="text/css"> | |||
<title>Spirit Island Fear Tracker</title> | |||
</head> | |||
<body> | |||
{% block content %}{% endblock %} | |||
<footer> | |||
<hr> | |||
<p> | |||
<a href="https://boardgamegeek.com/boardgame/162886/spirit-island">Spirit | |||
Island</a> | |||
was designed by | |||
<a href="https://boardgamegeek.com/boardgamedesigner/16615/r-eric-reuss">Eric Reuss</a> | |||
and published by | |||
<a href="https://www.greaterthangames.com/">Greater Than Games</a> | |||
</p> | |||
<p> | |||
Spirit Island Fear Tracker was built by | |||
<a href="https://aweirdimagination.net/~perelman/">Daniel Perelman</a> | |||
| | |||
<a href="https://git.aweirdimagination.net/perelman/fear-tracker">source | |||
code</a> licensed under | |||
<a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPLv3+</a> | |||
</p> | |||
</footer> | |||
</body> | |||
</html> |
@@ -0,0 +1,19 @@ | |||
{% extends "base.html" %} | |||
{% block content %} | |||
<h2>Join Game</h2> | |||
<form class="join-game-form" action="{% url 'enter_code' %}" method="post"> | |||
{% csrf_token %} | |||
<table> | |||
{{ form.as_table }} | |||
</table> | |||
<div class="button-container"> | |||
<input type="submit" class="button button-join" value="Join"></input> | |||
</div> | |||
<div class="button-container"> | |||
<a href="{% url 'index' %}" class="button button-main-menu">Back</a> | |||
</div> | |||
</form> | |||
{% endblock %} |
@@ -0,0 +1,40 @@ | |||
{% extends "base.html" %} | |||
{% block content %} | |||
<header> | |||
<div class="phase">{{ phase }} phase of turn #{{ turn }}</div> | |||
<div class="fear_summary"> | |||
<span class="available_fear_cards">{{ available_fear_cards }}</span> | |||
fear cards available; | |||
<span class="fear_to_next_card">{{ fear_to_next_card }}</span> | |||
fear to next fear card; | |||
players have generated | |||
<span class="fear_this_phase">{{ fear_this_phase }}</span> | |||
fear this phase | |||
</div> | |||
<div class="access-code"> | |||
<span class="header">Access Code:</span> <a href="{% url 'qr_code' access_code=access_code %}" target="_blank">{{ access_code }}</a> | |||
</div> | |||
</header> | |||
<div class="players"> | |||
{% for order, player in players.items %} | |||
<div class="player player-{{ player.order }}"> | |||
<div class="player-summary"> | |||
{% if player.ready %}(ready){% else %}(waiting...){% endif %} | |||
(<span class="player-total-fear player-{{ player.order }}-total-fear">{{ player.total_fear }}</span> fear) | |||
{{ player.name }} ({{ player.get_spirit_name }}) | |||
</div> | |||
<div class="player-effects"> | |||
{% for effect_num, effect in player.fear.items %} | |||
<div class="effect effect-{{ effect_num }}"> | |||
Effect #{{ effect_num|add:1 }} | |||
{{ effect.pure_fear }} fear | |||
{{ effect.towns }} towns | |||
{{ effect.cities }} cities | |||
</div> | |||
{% endfor %} | |||
</div> | |||
</div> | |||
{% endfor %} | |||
</div> | |||
{% endblock %} |
@@ -0,0 +1,8 @@ | |||
{% extends "base.html" %} | |||
{% block content %} | |||
<div class="button-container"> | |||
<a href="{% url 'new_game' %}" class="button button-new-game">New Game</a> | |||
<a href="{% url 'enter_code' %}" class="button button-join-game">Join Game</a> | |||
</div> | |||
{% endblock %} |
@@ -0,0 +1,27 @@ | |||
{% extends "base.html" %} | |||
{% block content %} | |||
<h2>New Game</h2> | |||
<form class="new-game-form" action="#" method="post"> | |||
{% csrf_token %} | |||
{{ formset.non_form_errors }} | |||
<table class="new-game-form"> | |||
{{ form.as_table }} | |||
</table> | |||
{{ formset.management_form }} | |||
<table class="players-form"> | |||
{% for form in formset %} | |||
<tr class="player-{{ form.player_id }}"> | |||
<th>Player #{{ form.player_id }}</th> | |||
<td>{{ form.as_p }}</td> | |||
</tr> | |||
{% endfor %} | |||
</table> | |||
<div class="button-container"> | |||
<input type="submit" class="button button-create" value="Create Game"></input> | |||
</div> | |||
<div class="button-container"> | |||
<a href="{% url 'index' %}" class="button button-main-menu">Back</a> | |||
</div> | |||
</form> | |||
{% endblock %} |
@@ -0,0 +1,30 @@ | |||
"""fear_tracker URL Configuration | |||
The `urlpatterns` list routes URLs to views. For more information please see: | |||
https://docs.djangoproject.com/en/2.2/topics/http/urls/ | |||
Examples: | |||
Function views | |||
1. Add an import: from my_app import views | |||
2. Add a URL to urlpatterns: path('', views.home, name='home') | |||
Class-based views | |||
1. Add an import: from other_app.views import Home | |||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') | |||
Including another URLconf | |||
1. Import the include() function: from django.urls import include, path | |||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) | |||
""" | |||
from django.conf.urls import include, url | |||
from django.urls import path | |||
from . import views | |||
urlpatterns = [ | |||
path('', views.index, name='index'), | |||
path('join/', views.enter_code, name='enter_code'), | |||
path('new/', views.new_game, name='new_game'), | |||
url(r'^(?P<access_code>[a-zA-Z]{6})/', include([ | |||
path('', views.game, name='game'), | |||
path('qr/', views.qr_code, name='qr_code'), | |||
path('status/', views.status, name='status'), | |||
])), | |||
] |
@@ -1,3 +1,133 @@ | |||
from django.shortcuts import render | |||
from collections import OrderedDict | |||
from io import BytesIO | |||
import qrcode | |||
# Create your views here. | |||
from django.http import HttpResponse | |||
from django.shortcuts import get_object_or_404, redirect, render | |||
from django.views.decorators.http import require_safe, require_http_methods | |||
from django.urls import reverse | |||
from .forms import NewGameForm, JoinGameForm, PlayerFormSet | |||
from .models import Game, Player | |||
def lookup_access_code(func): | |||
def with_game(request, access_code, *args, **kwargs): | |||
game = get_object_or_404(Game, access_code=access_code.lower()) | |||
return func(request, game, *args, **kwargs) | |||
return with_game | |||
@require_safe | |||
def index(request): | |||
return render(request, 'index.html') | |||
@require_http_methods(["HEAD", "GET", "POST"]) | |||
def enter_code(request): | |||
if request.method == 'POST': | |||
form = JoinGameForm(request.POST) | |||
if form.is_valid(): | |||
game = form.cleaned_data['game'] | |||
return redirect('game', access_code=game.access_code) | |||
else: | |||
form = JoinGameForm() | |||
return render(request, 'enter_code.html', {'form': form}) | |||
@require_http_methods(["HEAD", "GET", "POST"]) | |||
def new_game(request): | |||
if request.method == 'POST': | |||
form = NewGameForm(request.POST) | |||
formset = PlayerFormSet(request.POST) | |||
if form.is_valid() and formset.is_valid(): | |||
player_forms = list(filter(lambda p: p.cleaned_data.get('name'), | |||
formset.forms)) | |||
num_players = len(player_forms) | |||
game = Game() | |||
game.combined_growth_spirit =\ | |||
form.cleaned_data.get('combined_growth_spirit') | |||
game.england_build = form.cleaned_data.get('england_build') | |||
game.fear_per_card =\ | |||
form.cleaned_data.get('fear_per_player') * num_players | |||
game.save() | |||
for player_form in player_forms: | |||
player = Player() | |||
player.order = int(player_form.prefix[5:]) + 1 | |||
player.spirit = player_form.cleaned_data.get('spirit') | |||
player.name = player_form.cleaned_data.get('name') | |||
player.game = game | |||
player.save() | |||
game.advance_phase() | |||
return redirect('game', access_code=game.access_code) | |||
else: | |||
form = NewGameForm() | |||
initial_player_data = [{'spirit': i} | |||
for i in enumerate(Player.SPIRIT_NAMES)] | |||
formset = PlayerFormSet(initial=initial_player_data) | |||
for player_form in formset: | |||
player_form.player_id = int(player_form.prefix[5:]) + 1 | |||
return render(request, 'new_game.html', | |||
{'form': form, 'formset': formset}) | |||
@require_safe | |||
def qr_code(request, access_code): | |||
join_url = reverse('game', kwargs={'access_code': access_code.lower()}) | |||
join_url = request.build_absolute_uri(join_url) | |||
img = qrcode.make(join_url) | |||
output = BytesIO() | |||
img.save(output, "PNG") | |||
return HttpResponse(output.getvalue(), content_type='image/png') | |||
@lookup_access_code | |||
@require_safe | |||
def game(request, game): | |||
players = OrderedDict() | |||
for player in game.player_set.order_by('order').all(): | |||
player.total_fear = 0 | |||
player.fear = OrderedDict() | |||
players[player.order] = player | |||
for fear in game.get_current_phase().fear_set.order_by('effect').all(): | |||
player[fear.player.order].fear[fear.effect] = { | |||
'pure_fear': fear.pure_fear, | |||
'towns': fear.towns_destroyed, | |||
'cities': fear.cities_destroyed, | |||
} | |||
players[fear.player.order].total_fear +=\ | |||
fear.pure_fear + fear.towns_destroyed + 2*fear.cities_destroyed | |||
for player in players.values(): | |||
info = player.fear | |||
if not info: | |||
new_effect = 0 | |||
else: | |||
new_effect = max(info.keys()) + 1 | |||
info[new_effect] = { | |||
'pure_fear': 0, | |||
'towns': 0, | |||
'cities': 0, | |||
} | |||
return render(request, 'game.html', { | |||
'access_code': game.access_code, | |||
'turn': game.game_turn, | |||
'phase': game.get_current_phase_name(), | |||
'available_fear_cards': game.num_available_fear_cards(), | |||
'fear_to_next_card': game.get_fear_to_next_card(), | |||
'fear_this_phase': game.get_current_phase().fear_this_phase(), | |||
'players': players, | |||
}) | |||
@lookup_access_code | |||
@require_safe | |||
def status(request, game): | |||
# TODO status json | |||
pass |
@@ -31,6 +31,7 @@ ALLOWED_HOSTS = [] | |||
# Application definition | |||
INSTALLED_APPS = [ | |||
'fear_tracker', | |||
'django.contrib.admin', | |||
'django.contrib.auth', | |||
'django.contrib.contenttypes', | |||
@@ -13,9 +13,9 @@ Including another URLconf | |||
1. Import the include() function: from django.urls import include, path | |||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) | |||
""" | |||
from django.contrib import admin | |||
from django.conf.urls import include | |||
from django.urls import path | |||
urlpatterns = [ | |||
path('admin/', admin.site.urls), | |||
path('', include('fear_tracker.urls')), | |||
] |