Initial version
This commit is contained in:
commit
3b6bb33505
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
*.pyc
|
||||||
|
env_db.txt
|
||||||
|
env_email_key.txt
|
||||||
|
env_secret_key.txt
|
||||||
|
/static
|
||||||
|
!.gitignore
|
||||||
|
gunicorn.conf.py
|
0
geogame/__init__.py
Normal file
0
geogame/__init__.py
Normal file
0
geogame/main/__init__.py
Normal file
0
geogame/main/__init__.py
Normal file
20
geogame/main/admin.py
Normal file
20
geogame/main/admin.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
|
||||||
|
from .forms import CustomUserCreationForm, CustomUserChangeForm
|
||||||
|
from .models import User, Coord
|
||||||
|
|
||||||
|
@admin.register(Coord)
|
||||||
|
class CoordAdmin(admin.ModelAdmin):
|
||||||
|
list_display = 'id', 'country',
|
||||||
|
search_fields = 'country',
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUserAdmin(UserAdmin):
|
||||||
|
add_form = CustomUserCreationForm
|
||||||
|
form = CustomUserChangeForm
|
||||||
|
model = User
|
||||||
|
list_display = ['email', 'username',]
|
||||||
|
|
||||||
|
admin.site.register(User, CustomUserAdmin)
|
64
geogame/main/forms.py
Normal file
64
geogame/main/forms.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.conf import settings
|
||||||
|
from django.forms import widgets
|
||||||
|
from django.forms.utils import to_current_timezone
|
||||||
|
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
|
||||||
|
|
||||||
|
from geogame.main.models import (
|
||||||
|
Coord, User, GameRound
|
||||||
|
)
|
||||||
|
|
||||||
|
from dal import autocomplete
|
||||||
|
#from django_starfield import Stars
|
||||||
|
|
||||||
|
|
||||||
|
class CoordForm(forms.ModelForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Coord
|
||||||
|
fields = ('lat', 'lng', 'country')
|
||||||
|
widgets = {'country': autocomplete.ModelSelect2(url='game:country-autocomplete')}
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super(CoordForm, self).clean()
|
||||||
|
lat = cleaned_data.get('lat')
|
||||||
|
lng = cleaned_data.get('lng')
|
||||||
|
|
||||||
|
|
||||||
|
class GuessForm(forms.ModelForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = GameRound
|
||||||
|
fields = ('guess_lat', 'guess_lng',)
|
||||||
|
widgets = {
|
||||||
|
'guess_lat': forms.HiddenInput(),
|
||||||
|
'guess_lng': forms.HiddenInput(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super(GuessForm, self).clean()
|
||||||
|
lat = cleaned_data.get('lat')
|
||||||
|
lng = cleaned_data.get('lng')
|
||||||
|
|
||||||
|
|
||||||
|
class APIForm(forms.ModelForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ('api_key',)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUserCreationForm(UserCreationForm):
|
||||||
|
|
||||||
|
class Meta(UserCreationForm):
|
||||||
|
model = User
|
||||||
|
fields = ('username', 'email')
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUserChangeForm(UserChangeForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ('username', 'email')
|
33
geogame/main/management/commands/seed_coords.py
Normal file
33
geogame/main/management/commands/seed_coords.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import random
|
||||||
|
import string
|
||||||
|
import csv
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
from django.db.models import Max
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from geogame.main.models import Coord, User, Country
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Seed coord data'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('--dry-run', action='store_true')
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
|
||||||
|
country = Country.objects.get(country='United Kingdom')
|
||||||
|
user = User.objects.first()
|
||||||
|
with open('/home/ubuntu/lat_lng.csv', 'r') as csvfile:
|
||||||
|
datareader = csv.reader(csvfile)
|
||||||
|
for row in datareader:
|
||||||
|
if row:
|
||||||
|
Coord.objects.create(lat=row[0], lng=row[1], country=country, user=user)
|
||||||
|
|
||||||
|
if options['dry_run']:
|
||||||
|
transaction.set_rollback(True)
|
||||||
|
self.stdout.write('Coord data seeded')
|
93
geogame/main/migrations/0001_initial.py
Normal file
93
geogame/main/migrations/0001_initial.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
# Generated by Django 2.2.4 on 2019-08-21 15:51
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
import django.contrib.auth.models
|
||||||
|
import django.contrib.auth.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('auth', '0011_update_proxy_permissions'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='User',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
|
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||||
|
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||||
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||||
|
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||||
|
('first_name', models.CharField(blank=True, db_index=True, max_length=50, verbose_name='first name')),
|
||||||
|
('last_name', models.CharField(blank=True, db_index=True, max_length=50, verbose_name='last name')),
|
||||||
|
('email', models.EmailField(db_index=True, max_length=254, verbose_name='email address')),
|
||||||
|
('api_key', models.CharField(blank=True, db_index=True, max_length=255, verbose_name='google maps api key')),
|
||||||
|
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
|
||||||
|
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name_plural': 'users',
|
||||||
|
'verbose_name': 'user',
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
managers=[
|
||||||
|
('objects', django.contrib.auth.models.UserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Coord',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('lng', models.CharField(max_length=50, verbose_name='longitude')),
|
||||||
|
('lat', models.CharField(max_length=50, verbose_name='latitude')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Country',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('country', models.CharField(max_length=255, verbose_name='Country')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Game',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('start', models.DateTimeField()),
|
||||||
|
('score', models.PositiveIntegerField()),
|
||||||
|
('active', models.BooleanField()),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='game_user', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='GameRound',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('order', models.IntegerField(verbose_name='round order')),
|
||||||
|
('guess_lat', models.CharField(blank=True, max_length=50, null=True, verbose_name='latitude')),
|
||||||
|
('guess_lng', models.CharField(blank=True, max_length=50, null=True, verbose_name='longitude')),
|
||||||
|
('coord', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='round_coord', to='main.Coord')),
|
||||||
|
('game', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='round_game', to='main.Game')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='coord',
|
||||||
|
name='country',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='coord_country', to='main.Country'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='coord',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='coord_user', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
19
geogame/main/migrations/0002_game_country.py
Normal file
19
geogame/main/migrations/0002_game_country.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 2.2.4 on 2019-08-22 21:56
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='game',
|
||||||
|
name='country',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='game_country', to='main.Country'),
|
||||||
|
),
|
||||||
|
]
|
19
geogame/main/migrations/0003_auto_20190822_2204.py
Normal file
19
geogame/main/migrations/0003_auto_20190822_2204.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 2.2.4 on 2019-08-22 22:04
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0002_game_country'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='gameround',
|
||||||
|
name='coord',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='round_coord', to='main.Coord'),
|
||||||
|
),
|
||||||
|
]
|
0
geogame/main/migrations/__init__.py
Normal file
0
geogame/main/migrations/__init__.py
Normal file
104
geogame/main/models.py
Normal file
104
geogame/main/models.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import os
|
||||||
|
import random
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.contrib.postgres.search import SearchVectorField, SearchQuery, SearchVector
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models, transaction, IntegrityError
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.validators import FileExtensionValidator
|
||||||
|
from django.shortcuts import reverse
|
||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
|
from django_countries.fields import CountryField
|
||||||
|
from geopy import distance
|
||||||
|
|
||||||
|
|
||||||
|
class User(AbstractUser):
|
||||||
|
first_name = models.CharField(_('first name'), max_length=50, blank=True, db_index=True)
|
||||||
|
last_name = models.CharField(_('last name'), max_length=50, blank=True, db_index=True)
|
||||||
|
email = models.EmailField(_('email address'), blank=False, null=False, db_index=True)
|
||||||
|
api_key = models.CharField(_('google maps api key'), max_length=255, blank=True, db_index=True)
|
||||||
|
|
||||||
|
def generate_new_game(self, country=None):
|
||||||
|
game = Game.objects.create(
|
||||||
|
start=timezone.now(),
|
||||||
|
user=self,
|
||||||
|
score=0,
|
||||||
|
active=True,
|
||||||
|
)
|
||||||
|
qs = Coord.objects.all()
|
||||||
|
if country:
|
||||||
|
qs = qs.filter(country=country)
|
||||||
|
coords = qs.order_by('?')[:5]
|
||||||
|
for i, coord in enumerate(coords):
|
||||||
|
round = GameRound.objects.create(
|
||||||
|
game=game,
|
||||||
|
coord=coord,
|
||||||
|
order=i,
|
||||||
|
)
|
||||||
|
if i == 0:
|
||||||
|
round_id = round.id
|
||||||
|
return game.id, round_id
|
||||||
|
|
||||||
|
def get_active_game(self):
|
||||||
|
return Game.objects.filter(user=self, active=True).last()
|
||||||
|
|
||||||
|
def deactive_games(self):
|
||||||
|
return Game.objects.filter(user=self).update(active=False)
|
||||||
|
|
||||||
|
|
||||||
|
class Country(models.Model):
|
||||||
|
country = models.CharField(_('Country'), max_length=255, null=False, blank=False)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.country
|
||||||
|
|
||||||
|
|
||||||
|
class Coord(models.Model):
|
||||||
|
lng = models.CharField(_('longitude'), max_length=50, null=False, blank=False)
|
||||||
|
lat = models.CharField(_('latitude'), max_length=50, null=False, blank=False)
|
||||||
|
country = models.ForeignKey(Country, models.PROTECT,
|
||||||
|
related_name='coord_country',
|
||||||
|
null=False, blank=False)
|
||||||
|
user = models.ForeignKey(User, models.PROTECT,
|
||||||
|
related_name='coord_user',
|
||||||
|
null=False, blank=False)
|
||||||
|
|
||||||
|
|
||||||
|
class Game(models.Model):
|
||||||
|
start = models.DateTimeField()
|
||||||
|
user = models.ForeignKey(User, models.PROTECT,
|
||||||
|
related_name='game_user',
|
||||||
|
null=False, blank=False)
|
||||||
|
score = models.PositiveIntegerField()
|
||||||
|
active = models.BooleanField()
|
||||||
|
country = models.ForeignKey(Country, models.PROTECT,
|
||||||
|
related_name='game_country',
|
||||||
|
null=True, blank=True)
|
||||||
|
|
||||||
|
def get_rounds(self):
|
||||||
|
return GameRound.objects.filter(game=self)
|
||||||
|
|
||||||
|
|
||||||
|
class GameRound(models.Model):
|
||||||
|
game = models.ForeignKey(Game, models.PROTECT,
|
||||||
|
related_name='round_game',
|
||||||
|
null=False, blank=False)
|
||||||
|
coord = models.ForeignKey(Coord, models.CASCADE,
|
||||||
|
related_name='round_coord',
|
||||||
|
null=False, blank=False)
|
||||||
|
order = models.IntegerField(_('round order'))
|
||||||
|
guess_lat = models.CharField(_('latitude'), max_length=50, blank=True, null=True)
|
||||||
|
guess_lng = models.CharField(_('longitude'), max_length=50, blank=True, null=True)
|
||||||
|
|
||||||
|
def get_distance(self):
|
||||||
|
if not self.guess_lat or not self.guess_lng:
|
||||||
|
return 0
|
||||||
|
actual_coord = (self.coord.lat, self.coord.lng,)
|
||||||
|
guess_coord = (self.guess_lat, self.guess_lng,)
|
||||||
|
return distance.distance(actual_coord, guess_coord).km
|
3
geogame/main/tests.py
Normal file
3
geogame/main/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
19
geogame/main/urls.py
Normal file
19
geogame/main/urls.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from django.conf.urls import url
|
||||||
|
from django.contrib.auth.decorators import permission_required
|
||||||
|
|
||||||
|
from geogame.main.views import (
|
||||||
|
RoundView, RoundRecapView, GameRecapView, NewGameView,
|
||||||
|
ContributeView, CountryAutocomplete, RemoveCoordView
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^new-game/$', NewGameView.as_view(), name="new-game"),
|
||||||
|
url(r'^round/(?P<game_pk>\d+)/(?P<round_pk>\d+)/$', RoundView.as_view(), name="round-view"),
|
||||||
|
url(r'^round-recap/(?P<game_pk>\d+)/(?P<round_pk>\d+)/$', RoundRecapView.as_view(), name="round-recap-view"),
|
||||||
|
url(r'^remove-coord/(?P<game_pk>\d+)/(?P<round_pk>\d+)/$', RemoveCoordView.as_view(), name="remove-coord"),
|
||||||
|
url(r'^end-recap/(?P<game_pk>\d+)/$', GameRecapView.as_view(), name="end-recap-view"),
|
||||||
|
url(r'^contribute/$', ContributeView.as_view(), name="contribute"),
|
||||||
|
url(r'^country-autocomplete/$', CountryAutocomplete.as_view(), name='country-autocomplete',
|
||||||
|
),
|
||||||
|
]
|
231
geogame/main/views.py
Normal file
231
geogame/main/views.py
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
|
from django.views.generic import TemplateView, ListView
|
||||||
|
from django.views.generic.edit import CreateView, DeleteView, UpdateView
|
||||||
|
from django.views.generic.detail import DetailView
|
||||||
|
from django.views import View
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
|
||||||
|
from braces import views
|
||||||
|
|
||||||
|
from geogame.main.models import (
|
||||||
|
Game, GameRound, Coord, User, Country
|
||||||
|
)
|
||||||
|
from dal import autocomplete
|
||||||
|
from geogame.main.forms import GuessForm, CoordForm, APIForm
|
||||||
|
|
||||||
|
|
||||||
|
class CountryAutocomplete(autocomplete.Select2QuerySetView):
|
||||||
|
def get_queryset(self):
|
||||||
|
# Don't forget to filter out results depending on the visitor !
|
||||||
|
if not self.request.user.is_authenticated:
|
||||||
|
return Country.objects.none()
|
||||||
|
|
||||||
|
qs = Country.objects.all().order_by('country')
|
||||||
|
|
||||||
|
if self.q:
|
||||||
|
qs = qs.filter(country__icontains=self.q)
|
||||||
|
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
|
class HomePageView(TemplateView):
|
||||||
|
template_name = 'main/homepage.html'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(HomePageView, self).get_context_data(**kwargs)
|
||||||
|
user = self.request.user
|
||||||
|
if user.is_authenticated:
|
||||||
|
if user.api_key:
|
||||||
|
context['has_api_key'] = True
|
||||||
|
game = Game.objects.filter(user=user, active=True)
|
||||||
|
if game and game.exists():
|
||||||
|
game = game.first()
|
||||||
|
if not GameRound.objects.get(game=game, order=4).guess_lat:
|
||||||
|
context['existing_game'] = game
|
||||||
|
rounds = GameRound.objects.filter(game=game).order_by('order')
|
||||||
|
for round in rounds:
|
||||||
|
if not round.guess_lat or not round.guess_lng:
|
||||||
|
context['existing_round'] = round
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
context['has_api_key'] = False
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class ProfilePageView(views.LoginRequiredMixin, TemplateView):
|
||||||
|
template_name = 'main/profile.html'
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateAPIView(views.LoginRequiredMixin, UpdateView):
|
||||||
|
model = User
|
||||||
|
form_class = APIForm
|
||||||
|
template_name = 'main/api_form.html'
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse_lazy('profile')
|
||||||
|
|
||||||
|
|
||||||
|
class ContributeView(views.LoginRequiredMixin, CreateView):
|
||||||
|
model = Coord
|
||||||
|
form_class = CoordForm
|
||||||
|
template_name = 'main/contribute_form.html'
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse_lazy('game:contribute')
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
self.object = form.save(commit=False)
|
||||||
|
self.object.user = self.request.user
|
||||||
|
self.object.save()
|
||||||
|
messages.success(self.request, "Thank you so much for helping the site, you coordinates have been added.")
|
||||||
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
|
|
||||||
|
class NewGameView(views.LoginRequiredMixin, View):
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
user = self.request.user
|
||||||
|
user.deactive_games()
|
||||||
|
game_pk, round_pk = user.generate_new_game()
|
||||||
|
|
||||||
|
return redirect(
|
||||||
|
reverse_lazy(
|
||||||
|
'game:round-view',
|
||||||
|
kwargs={
|
||||||
|
'game_pk': game_pk,
|
||||||
|
'round_pk': round_pk,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RoundView(views.LoginRequiredMixin, UpdateView):
|
||||||
|
model = GameRound
|
||||||
|
form_class = GuessForm
|
||||||
|
template_name = 'main/round.html'
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
round_id = self.kwargs.get('round_pk', 0)
|
||||||
|
return get_object_or_404(GameRound, pk=round_id)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(RoundView, self).get_context_data(**kwargs)
|
||||||
|
user = self.request.user
|
||||||
|
round_id = self.kwargs.get('round_pk', 0)
|
||||||
|
round = get_object_or_404(GameRound, pk=round_id)
|
||||||
|
if round.guess_lat:
|
||||||
|
#user has already played this round, so something has gone wrong
|
||||||
|
messages.warning(self.request, 'You have already played this round, something went wrong. Hit "Continue Last Game" to try again.')
|
||||||
|
return redirect(reverse_lazy('home'))
|
||||||
|
|
||||||
|
context['api_key'] = user.api_key
|
||||||
|
context['lat'] = round.coord.lat
|
||||||
|
context['lng'] = round.coord.lng
|
||||||
|
context['game_pk'] = round.game.pk
|
||||||
|
context['round_pk'] = round.pk
|
||||||
|
return context
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
self.object = form.save(commit=False)
|
||||||
|
if not self.object.guess_lat or not self.object.guess_lng:
|
||||||
|
self.object.guess_lat = 0
|
||||||
|
self.object.guess_lng = 0
|
||||||
|
self.object.save()
|
||||||
|
round = self.get_object()
|
||||||
|
return redirect(
|
||||||
|
reverse_lazy(
|
||||||
|
'game:round-recap-view',
|
||||||
|
kwargs={
|
||||||
|
'game_pk': round.game.pk,
|
||||||
|
'round_pk': round.pk,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
class RemoveCoordView(View):
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
round = get_object_or_404(GameRound, pk=self.kwargs.get('round_pk', 0))
|
||||||
|
game = get_object_or_404(Game, pk=self.kwargs.get('game_pk', 0))
|
||||||
|
qs = Coord.objects.all()
|
||||||
|
if round.game.country:
|
||||||
|
qs = qs.filter(country=round.game.country)
|
||||||
|
coords = qs.order_by('?')[:5]
|
||||||
|
|
||||||
|
current_coords = GameRound.objects.filter(game=game).exclude(id=round.id).values_list('coord__id', flat=True)
|
||||||
|
for coord in coords:
|
||||||
|
if coord.id not in current_coords:
|
||||||
|
round.coord = coord
|
||||||
|
round.save()
|
||||||
|
break
|
||||||
|
|
||||||
|
return redirect(
|
||||||
|
reverse_lazy(
|
||||||
|
'game:round-view',
|
||||||
|
kwargs={
|
||||||
|
'game_pk': round.game.pk,
|
||||||
|
'round_pk': round.pk,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class RoundRecapView(views.UserPassesTestMixin, TemplateView):
|
||||||
|
template_name = 'main/round_recap.html'
|
||||||
|
|
||||||
|
def test_func(self, *args, **kwargs):
|
||||||
|
return self.request.user == get_object_or_404(Game, pk=self.kwargs.get('game_pk', 0)).user
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(RoundRecapView, self).get_context_data(**kwargs)
|
||||||
|
user = self.request.user
|
||||||
|
round_id = self.kwargs.get('round_pk', 0)
|
||||||
|
round = get_object_or_404(GameRound, pk=round_id)
|
||||||
|
|
||||||
|
context['api_key'] = user.api_key
|
||||||
|
context['lat'] = round.coord.lat
|
||||||
|
context['lng'] = round.coord.lng
|
||||||
|
context['guess_lat'] = round.guess_lat
|
||||||
|
context['guess_lng'] = round.guess_lng
|
||||||
|
context['game_id'] = round.game.id
|
||||||
|
context['distance'] = "{0:.3f}".format(round.get_distance())
|
||||||
|
|
||||||
|
if round.order == 4:
|
||||||
|
context['last_round'] = True
|
||||||
|
else:
|
||||||
|
next_round = GameRound.objects.get(
|
||||||
|
game=round.game,
|
||||||
|
order=round.order+1
|
||||||
|
)
|
||||||
|
context['next_round_id'] = next_round.id
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class GameRecapView(views.UserPassesTestMixin, TemplateView):
|
||||||
|
template_name = 'main/game_recap.html'
|
||||||
|
|
||||||
|
def test_func(self, *args, **kwargs):
|
||||||
|
return self.request.user == get_object_or_404(Game, pk=self.kwargs.get('game_pk', 0)).user
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(GameRecapView, self).get_context_data(**kwargs)
|
||||||
|
user = self.request.user
|
||||||
|
game_id = self.kwargs.get('game_pk', 0)
|
||||||
|
game = get_object_or_404(Game, pk=game_id)
|
||||||
|
|
||||||
|
coord_results = []
|
||||||
|
distance_total = 0
|
||||||
|
for round in GameRound.objects.filter(game=game).select_related('coord'):
|
||||||
|
coord_results.append(
|
||||||
|
[
|
||||||
|
[round.coord.lat, round.coord.lng],
|
||||||
|
[round.guess_lat, round.guess_lng]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
distance_total += round.get_distance()
|
||||||
|
|
||||||
|
context['average_distance'] = "{0:.3f}".format(distance_total / 5)
|
||||||
|
context['results'] = coord_results
|
||||||
|
return context
|
174
geogame/settings.py
Normal file
174
geogame/settings.py
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
"""
|
||||||
|
Django settings for geogame project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 2.2.4.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/2.2/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/2.2/ref/settings/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||||
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
with open(os.path.join(BASE_DIR, "env_secret_key.txt")) as secret_key:
|
||||||
|
SECRET_KEY = secret_key.read().strip()
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = ['*']
|
||||||
|
|
||||||
|
AUTH_USER_MODEL = 'main.User'
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
'django.contrib.sites',
|
||||||
|
|
||||||
|
'allauth',
|
||||||
|
'allauth.account',
|
||||||
|
'allauth.socialaccount',
|
||||||
|
'django_extensions',
|
||||||
|
'dal',
|
||||||
|
'dal_select2',
|
||||||
|
|
||||||
|
'geogame.main',
|
||||||
|
'django_countries',
|
||||||
|
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'geogame.urls'
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [os.path.join(BASE_DIR, "geogame/templates"),],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
'django.template.context_processors.debug',
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = 'geogame.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
|
||||||
|
|
||||||
|
with open(os.path.join(BASE_DIR, "env_db.txt")) as db_pw:
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||||
|
'NAME': 'geogame',
|
||||||
|
'USER': 'geogame',
|
||||||
|
'PASSWORD': db_pw.read().strip(),
|
||||||
|
'HOST': 'localhost',
|
||||||
|
'PORT': '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/2.2/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
|
TIME_ZONE = 'UTC'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_L10N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/2.2/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = '/static/'
|
||||||
|
STATIC_ROOT = os.path.join(BASE_DIR, "static")
|
||||||
|
|
||||||
|
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
|
||||||
|
MEDIA_URL = '/attachments/'
|
||||||
|
|
||||||
|
|
||||||
|
with open(os.path.join(BASE_DIR, "env_email_key.txt")) as email_key:
|
||||||
|
SENDGRID_API_KEY = email_key.read().strip()
|
||||||
|
EMAIL_HOST = 'smtp.sendgrid.net'
|
||||||
|
EMAIL_HOST_USER = 'apikey'
|
||||||
|
EMAIL_HOST_PASSWORD = SENDGRID_API_KEY
|
||||||
|
EMAIL_PORT = 587
|
||||||
|
EMAIL_USE_TLS = True
|
||||||
|
DEFAULT_FROM_EMAIL = "admin@peakdistrictwalks.org.uk"
|
||||||
|
|
||||||
|
|
||||||
|
# AUTH
|
||||||
|
|
||||||
|
AUTHENTICATION_BACKENDS = (
|
||||||
|
# Needed to login by username in Django admin, regardless of `allauth`
|
||||||
|
"django.contrib.auth.backends.ModelBackend",
|
||||||
|
# `allauth` specific authentication methods, such as login by e-mail
|
||||||
|
"allauth.account.auth_backends.AuthenticationBackend",
|
||||||
|
)
|
||||||
|
|
||||||
|
SITE_ID = 1
|
||||||
|
|
||||||
|
ACCOUNT_EMAIL_REQUIRED = True
|
||||||
|
ACCOUNT_USERNAME_REQUIRED = False
|
||||||
|
ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE = False
|
||||||
|
ACCOUNT_SESSION_REMEMBER = True
|
||||||
|
ACCOUNT_AUTHENTICATION_METHOD = 'email'
|
||||||
|
ACCOUNT_UNIQUE_EMAIL = True
|
||||||
|
ACCOUNT_EMAIL_VERIFICATION = 'none'
|
||||||
|
LOGIN_REDIRECT_URL = 'home'
|
||||||
|
ACCOUNT_LOGOUT_REDIRECT_URL = 'home'
|
0
geogame/templates/account/messages/logged_in.txt
Normal file
0
geogame/templates/account/messages/logged_in.txt
Normal file
64
geogame/templates/base.html
Normal file
64
geogame/templates/base.html
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
{% load static %}
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="initial-scale=1.0, width=device-width">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Abril+Fatface&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static 'main.css' %}">
|
||||||
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
|
||||||
|
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" type="text/css" rel="stylesheet">
|
||||||
|
<link rel="shortcut icon" type="image/png" href="{% static 'favicon.ico' %}"/>
|
||||||
|
<title>Geogame</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||||
|
<img id="logo" src="{% static 'brand.png' %}"></img>
|
||||||
|
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||||
|
<ul class="navbar-nav mr-auto">
|
||||||
|
<li class="nav-item {% if request.path == "/" %} active {% endif %}">
|
||||||
|
<a href="{% url 'home' %}" class="nav-link">Home</a>
|
||||||
|
</li>
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{% url 'profile' %}" class="nav-link">Profile</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{% url 'game:contribute' %}" class="nav-link">Contribute</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{% url 'account_logout' %}" class="nav-link">Logout</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{% url 'account_login' %}" class="nav-link">Login</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
{% if messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{ message.tags }}">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-10 offset-lg-1 content">
|
||||||
|
{% block content %}
|
||||||
|
{% endblock content %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
|
||||||
|
</body>
|
60
geogame/templates/game_base.html
Normal file
60
geogame/templates/game_base.html
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
{% load static %}
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="initial-scale=1.0, width=device-width">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Abril+Fatface&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static 'main.css' %}">
|
||||||
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
|
||||||
|
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" type="text/css" rel="stylesheet">
|
||||||
|
<link rel="shortcut icon" type="image/png" href="{% static 'favicon.ico' %}"/>
|
||||||
|
<title>Geogame</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||||
|
<img id="logo" src="{% static 'brand.png' %}"></img>
|
||||||
|
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||||
|
<ul class="navbar-nav mr-auto">
|
||||||
|
<li class="nav-item {% if request.path == "/" %} active {% endif %}">
|
||||||
|
<a href="{% url 'home' %}" class="nav-link">Home</a>
|
||||||
|
</li>
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{% url 'profile' %}" class="nav-link">Profile</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{% url 'game:contribute' %}" class="nav-link">Contribute</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{% url 'account_logout' %}" class="nav-link">Logout</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{% url 'account_login' %}" class="nav-link">Login</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{% if messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{ message.tags }}">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% block content %}
|
||||||
|
{% endblock content %}
|
||||||
|
|
||||||
|
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
|
||||||
|
</body>
|
24
geogame/templates/main/api_form.html
Normal file
24
geogame/templates/main/api_form.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{% extends 'game_base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-10 offset-lg-1 content">
|
||||||
|
<h1 class="section-header">API key for for {{user.email}}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-10 offset-lg-1 content">
|
||||||
|
<form action="" method="POST">
|
||||||
|
{{ form.non_field_errors }}
|
||||||
|
{% csrf_token %}
|
||||||
|
{{form.as_p}}
|
||||||
|
<button type="submit" class="btn btn-primary">Update API Key</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
25
geogame/templates/main/contribute_form.html
Normal file
25
geogame/templates/main/contribute_form.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{% extends 'game_base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-10 offset-lg-1 content">
|
||||||
|
<h1 class="section-header">Coordinate Submission</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-10 offset-lg-1 content">
|
||||||
|
<form action="" method="POST">
|
||||||
|
{{ form.non_field_errors }}
|
||||||
|
{% csrf_token %}
|
||||||
|
{{form.as_p}}
|
||||||
|
{{ form.media }}
|
||||||
|
<button type="submit" class="btn btn-primary">Submit Coordinates</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
74
geogame/templates/main/game_recap.html
Normal file
74
geogame/templates/main/game_recap.html
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
{% extends 'game_base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12 pt-4 pb-4">
|
||||||
|
{% if messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{message.tags}}">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
Your average guess was {{average_distance}}km away.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="map" class="map"></div>
|
||||||
|
|
||||||
|
<a class="btn btn-success btn-lg btn-block" href="{% url 'game:new-game' %}" role="button">New Game</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function initializeMaps() {
|
||||||
|
|
||||||
|
var map = new google.maps.Map(document.getElementById('map'), {
|
||||||
|
zoom: 2,
|
||||||
|
center: {lat: 30, lng: 0}
|
||||||
|
});
|
||||||
|
|
||||||
|
{% for result in results %}
|
||||||
|
var actual_coord{{forloop.counter}} = {lat: {{result.0.0}}, lng: {{result.0.1}}};
|
||||||
|
var guess_coord{{forloop.counter}} = {lat: {{result.1.0}}, lng: {{result.1.1}}};
|
||||||
|
|
||||||
|
var actual_marker{{forloop.counter}} = new google.maps.Marker({
|
||||||
|
position: actual_coord{{forloop.counter}},
|
||||||
|
map: map,
|
||||||
|
title: 'Actual Position',
|
||||||
|
icon: 'http://maps.google.com/mapfiles/ms/micons/green.png',
|
||||||
|
draggable:false,
|
||||||
|
});
|
||||||
|
|
||||||
|
var guess_marker{{forloop.counter}} = new google.maps.Marker({
|
||||||
|
position: guess_coord{{forloop.counter}},
|
||||||
|
map: map,
|
||||||
|
title: 'Your Guess',
|
||||||
|
draggable:false,
|
||||||
|
});
|
||||||
|
|
||||||
|
var line{{forloop.counter}} = new google.maps.Polyline({
|
||||||
|
path: [
|
||||||
|
new google.maps.LatLng({{result.0.0}}, {{result.0.1}}),
|
||||||
|
new google.maps.LatLng({{result.1.0}}, {{result.1.1}})
|
||||||
|
],
|
||||||
|
strokeColor: "#FF0000",
|
||||||
|
strokeOpacity: 0.8,
|
||||||
|
strokeWeight: 5,
|
||||||
|
map: map
|
||||||
|
});
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script async defer
|
||||||
|
src="https://maps.googleapis.com/maps/api/js?key={{user.api_key}}&callback=initializeMaps">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
78
geogame/templates/main/homepage.html
Normal file
78
geogame/templates/main/homepage.html
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
{% extends 'game_base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-10 offset-lg-1 content">
|
||||||
|
<h1 class="section-header">Welcome to Geogame</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-10 offset-lg-1 content">
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
{% if has_api_key %}
|
||||||
|
{% if existing_game %}
|
||||||
|
<a class="btn btn-info btn-lg btn-block" href="{% url 'game:round-view' game_pk=existing_game.id round_pk=existing_round.id %}" role="button">Continue Last Game</a>
|
||||||
|
<a class="btn btn-success btn-lg btn-block" href="{% url 'game:new-game' %}" role="button">New Game</a>
|
||||||
|
{% else %}
|
||||||
|
<button type="button" class="btn btn-info btn-lg btn-block" disabled>Continue Last Game</button>
|
||||||
|
<a class="btn btn-success btn-lg btn-block" href="{% url 'game:new-game' %}" role="button">New Game</a>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
You must set an api key on your profile page to play the game. Please read the FAQ for more info.
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-info btn-lg btn-block" disabled>Continue Last Game</button>
|
||||||
|
<button type="button" class="btn btn-success btn-lg btn-block" disabled>New Game</button>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
You must be logged in to play the game. Please read the FAQ for more info.
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-info btn-lg btn-block" disabled>Continue Last Game</button>
|
||||||
|
<button type="button" class="btn btn-success btn-lg btn-block" disabled>New Game</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-10 offset-lg-1 content">
|
||||||
|
<h3 class="section-header">FAQ</h3>
|
||||||
|
<p><strong>What is this?</strong></p>
|
||||||
|
<p>This is a game that uses <a href="http://www.geoguessr.com">Geoguessr</a> as inspiration.</p>
|
||||||
|
<p><strong>Why does this exist?</strong></p>
|
||||||
|
<p>Sadly, because of Google's predatory pricing strategy for their Maps api, geoguessr have had to modify their game in a detrimental way to cut costs.</p>
|
||||||
|
<p>I created this little site to allow people to play the game how it used to be (or as close to it).</p>
|
||||||
|
<p><strong>Why do I need an api key?</strong></p>
|
||||||
|
<p>To get around the aforementioned api pricing strategy I require you to upload your own api key.</p>
|
||||||
|
<p>Your api key will be used only for games that you play, each key has $200/month free usage, which is plenty for all but the most avid players.</p>
|
||||||
|
<p><strong>How do I get an api key?</strong></p>
|
||||||
|
<p>Go <a href="https://developers.google.com/maps/documentation/javascript/get-api-key">here</a> and register to get an api key.</p>
|
||||||
|
<p>Once you have a key save it against your profile <a href="{% url 'profile' %}">here</a>.
|
||||||
|
<p><strong>Is this safe?</strong></p>
|
||||||
|
<p>All I can do is offer assurances that I won't use the api keys for anything other than their stated purpose: namely, for each individual's games on this site.</p>
|
||||||
|
<p>This site uses django's built in authentication framework, and I have taken every care to look after your personal details.</p>
|
||||||
|
<p><strong>What does the "Broken Streetview" button do?</strong></p>
|
||||||
|
<p>Sometimes a coordinate with no valid streetview is selected for you, you will just get a black screen.</p>
|
||||||
|
<p>If this occurs please click the "Broken Streetview" button and you will be provided with a new scene and the faulty coordinate will be removed from the database.</p>
|
||||||
|
<p><strong>There isn't a great variety of countries, why?</strong></p>
|
||||||
|
<p>I am relying on crowd sourcing of playable scenes, please consider helping out by <a href="{% url 'game:contribute' %}">adding a few coordinates of your own</a>.</p>
|
||||||
|
<p>You get to keep track of the coordinates you add, and see how well (or badly) other people do on them!</p>
|
||||||
|
<p>For now I have managed to find some data on uk coordinates that I can use, but its not so easy getting data for other countries.</p>
|
||||||
|
<p><strong>I didn't receive my email reset password, what gives?</strong></p>
|
||||||
|
<p>I am using a free mail server to send password reset emails, the limit is 100/day.</p>
|
||||||
|
<p>If you require assisstance accessing your account, please email edward.wilding3@gmail.com</p>
|
||||||
|
<p><strong>This site is atrocious and you should feel bad</strong></p>
|
||||||
|
<p>Firstly that isn't a question, secondly that hurts my feelings, thirdly I built it in 2 days after geoguessr changed their site.</p>
|
||||||
|
<p><strong>This site is amazing, how can I contact you to discuss this, or another, site?</strong></p>
|
||||||
|
<p>Thank you! Please get in touch: edward.wilding3@gmail.com</p>
|
||||||
|
|
||||||
|
<h4>Now go out there and have some fun exploring the world!</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
41
geogame/templates/main/profile.html
Normal file
41
geogame/templates/main/profile.html
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{% extends 'game_base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-10 offset-lg-1 content">
|
||||||
|
<h1 class="section-header">Your Profile</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-10 offset-lg-1 content">
|
||||||
|
<p>Logged in as {{user.email}}</p>
|
||||||
|
<p>
|
||||||
|
<a class="btn btn-primary btn-lg" style="margin-top:16px" href="{% url 'account_change_password' %}">Change Password</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a class="btn btn-primary btn-lg" href="{% url 'api-key-view' pk=user.pk %}">API key</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-10 offset-lg-1 content">
|
||||||
|
<h3 class="section-header">Played Games</h3>
|
||||||
|
<p>Coming soon</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-10 offset-lg-1 content">
|
||||||
|
<h3 class="section-header">Your Contributions</h3>
|
||||||
|
<p>Coming soon</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
89
geogame/templates/main/round.html
Normal file
89
geogame/templates/main/round.html
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
{% extends 'game_base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
{% if messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{message.tags}}">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
<div id="map" class="map"></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
|
||||||
|
<div id="map2" class="map"></div>
|
||||||
|
|
||||||
|
<form action="{% url 'game:round-view' game_pk=game_pk round_pk=round_pk %}" method="POST">
|
||||||
|
{{ form.non_field_errors }}
|
||||||
|
{% csrf_token %}
|
||||||
|
{{form.as_p}}
|
||||||
|
<button type="submit" class="btn btn-primary btn-block">Make Guess</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form action="{% url 'game:remove-coord' game_pk=game_pk round_pk=round_pk %}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-warning btn-block">Broken Streetview</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function initializeMaps() {
|
||||||
|
var panorama = new google.maps.StreetViewPanorama(
|
||||||
|
document.getElementById('map'), {
|
||||||
|
position: {lat: {{lat}}, lng: {{lng}}},
|
||||||
|
addressControl: false,
|
||||||
|
linksControl: true,
|
||||||
|
enableCloseButton: false,
|
||||||
|
showRoadLabels: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
var myLatLng = {lat: 0, lng: 0};
|
||||||
|
var centreLatLng = {lat: 30, lng: 0};
|
||||||
|
|
||||||
|
var map2 = new google.maps.Map(document.getElementById('map2'), {
|
||||||
|
zoom: 2,
|
||||||
|
center: centreLatLng
|
||||||
|
});
|
||||||
|
|
||||||
|
marker = new google.maps.Marker({
|
||||||
|
position: myLatLng,
|
||||||
|
map: map2,
|
||||||
|
title: 'Your Guess',
|
||||||
|
draggable:false,
|
||||||
|
});
|
||||||
|
|
||||||
|
google.maps.event.addListener(map2, 'click', function(event) {
|
||||||
|
placeMarker(event.latLng);
|
||||||
|
$("#id_guess_lat").val(event.latLng.lat());
|
||||||
|
$("#id_guess_lng").val(event.latLng.lng());
|
||||||
|
});
|
||||||
|
|
||||||
|
function placeMarker(location) {
|
||||||
|
marker.setMap(null);
|
||||||
|
marker = new google.maps.Marker({
|
||||||
|
position: location,
|
||||||
|
map: map2
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script async defer
|
||||||
|
src="https://maps.googleapis.com/maps/api/js?key={{user.api_key}}&callback=initializeMaps">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
63
geogame/templates/main/round_recap.html
Normal file
63
geogame/templates/main/round_recap.html
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
{% extends 'game_base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12 pt-4 pb-4">
|
||||||
|
{% if messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{message.tags}}">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
Your guess was {{distance}}km away.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="map" class="map"></div>
|
||||||
|
|
||||||
|
{% if last_round %}
|
||||||
|
<a class="btn btn-success btn-lg btn-block" href="{% url 'game:end-recap-view' game_pk=game_id %}" role="button">Game Recap</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn btn-success btn-lg btn-block" href="{% url 'game:round-view' game_pk=game_id round_pk=next_round_id %}" role="button">Next Round</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function initializeMaps() {
|
||||||
|
var actual_coord = {lat: {{lat}}, lng: {{lng}}};
|
||||||
|
var guess_coord = {lat: {{guess_lat}}, lng: {{guess_lng}}};
|
||||||
|
|
||||||
|
var map = new google.maps.Map(document.getElementById('map'), {
|
||||||
|
zoom: 4,
|
||||||
|
center: actual_coord
|
||||||
|
});
|
||||||
|
|
||||||
|
var marker = new google.maps.Marker({
|
||||||
|
position: actual_coord,
|
||||||
|
map: map,
|
||||||
|
title: 'Actual Position',
|
||||||
|
icon: 'http://maps.google.com/mapfiles/ms/icons/green-dot.png',
|
||||||
|
draggable:false,
|
||||||
|
});
|
||||||
|
|
||||||
|
var marker2 = new google.maps.Marker({
|
||||||
|
position: guess_coord,
|
||||||
|
map: map,
|
||||||
|
title: 'Your Guess',
|
||||||
|
draggable:false,
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script async defer
|
||||||
|
src="https://maps.googleapis.com/maps/api/js?key={{user.api_key}}&callback=initializeMaps">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
33
geogame/urls.py
Normal file
33
geogame/urls.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
"""geogame 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.contrib import admin
|
||||||
|
from django.conf.urls import url
|
||||||
|
from django.urls import path, include
|
||||||
|
from django.contrib.auth import views as auth_views
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
from django.conf import settings
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
|
||||||
|
from geogame.main.views import HomePageView, ProfilePageView, UpdateAPIView
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('admin/', admin.site.urls),
|
||||||
|
path('accounts/', include('allauth.urls')),
|
||||||
|
url(r'play/', include(('geogame.main.urls', 'game'), namespace="game")),
|
||||||
|
url(r'profile/$', ProfilePageView.as_view(), name="profile"),
|
||||||
|
url(r'api-form/(?P<pk>\d+)/$', UpdateAPIView.as_view(), name="api-key-view"),
|
||||||
|
url(r'', HomePageView.as_view(), name="home"),
|
||||||
|
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
16
geogame/wsgi.py
Normal file
16
geogame/wsgi.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
WSGI config for geogame project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'geogame.settings')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
21
manage.py
Executable file
21
manage.py
Executable file
@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'geogame.settings')
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
Loading…
Reference in New Issue
Block a user