Initial version

This commit is contained in:
root 2019-08-23 10:07:26 +02:00
commit 3b6bb33505
29 changed files with 1374 additions and 0 deletions

7
.gitignore vendored Normal file
View 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
View File

0
geogame/main/__init__.py Normal file
View File

20
geogame/main/admin.py Normal file
View 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
View 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')

View 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')

View 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),
),
]

View 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'),
),
]

View 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'),
),
]

View File

104
geogame/main/models.py Normal file
View 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
View File

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

19
geogame/main/urls.py Normal file
View 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
View 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
View 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'

View 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>

View 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>

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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
View 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
View 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
View 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()