From 3b6bb335054fc9e98ee522c462c7839084120f8a Mon Sep 17 00:00:00 2001 From: root Date: Fri, 23 Aug 2019 10:07:26 +0200 Subject: [PATCH] Initial version --- .gitignore | 7 + geogame/__init__.py | 0 geogame/main/__init__.py | 0 geogame/main/admin.py | 20 ++ geogame/main/forms.py | 64 +++++ .../main/management/commands/seed_coords.py | 33 +++ geogame/main/migrations/0001_initial.py | 93 +++++++ geogame/main/migrations/0002_game_country.py | 19 ++ .../migrations/0003_auto_20190822_2204.py | 19 ++ geogame/main/migrations/__init__.py | 0 geogame/main/models.py | 104 ++++++++ geogame/main/tests.py | 3 + geogame/main/urls.py | 19 ++ geogame/main/views.py | 231 ++++++++++++++++++ geogame/settings.py | 174 +++++++++++++ .../messages/email_confirmation_sent.txt | 0 .../templates/account/messages/logged_in.txt | 0 geogame/templates/base.html | 64 +++++ geogame/templates/game_base.html | 60 +++++ geogame/templates/main/api_form.html | 24 ++ geogame/templates/main/contribute_form.html | 25 ++ geogame/templates/main/game_recap.html | 74 ++++++ geogame/templates/main/homepage.html | 78 ++++++ geogame/templates/main/profile.html | 41 ++++ geogame/templates/main/round.html | 89 +++++++ geogame/templates/main/round_recap.html | 63 +++++ geogame/urls.py | 33 +++ geogame/wsgi.py | 16 ++ manage.py | 21 ++ 29 files changed, 1374 insertions(+) create mode 100644 .gitignore create mode 100644 geogame/__init__.py create mode 100644 geogame/main/__init__.py create mode 100644 geogame/main/admin.py create mode 100644 geogame/main/forms.py create mode 100644 geogame/main/management/commands/seed_coords.py create mode 100644 geogame/main/migrations/0001_initial.py create mode 100644 geogame/main/migrations/0002_game_country.py create mode 100644 geogame/main/migrations/0003_auto_20190822_2204.py create mode 100644 geogame/main/migrations/__init__.py create mode 100644 geogame/main/models.py create mode 100644 geogame/main/tests.py create mode 100644 geogame/main/urls.py create mode 100644 geogame/main/views.py create mode 100644 geogame/settings.py create mode 100644 geogame/templates/account/messages/email_confirmation_sent.txt create mode 100644 geogame/templates/account/messages/logged_in.txt create mode 100644 geogame/templates/base.html create mode 100644 geogame/templates/game_base.html create mode 100644 geogame/templates/main/api_form.html create mode 100644 geogame/templates/main/contribute_form.html create mode 100644 geogame/templates/main/game_recap.html create mode 100644 geogame/templates/main/homepage.html create mode 100644 geogame/templates/main/profile.html create mode 100644 geogame/templates/main/round.html create mode 100644 geogame/templates/main/round_recap.html create mode 100644 geogame/urls.py create mode 100644 geogame/wsgi.py create mode 100755 manage.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..98cd6fb --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*.pyc +env_db.txt +env_email_key.txt +env_secret_key.txt +/static +!.gitignore +gunicorn.conf.py diff --git a/geogame/__init__.py b/geogame/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geogame/main/__init__.py b/geogame/main/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geogame/main/admin.py b/geogame/main/admin.py new file mode 100644 index 0000000..bce62cd --- /dev/null +++ b/geogame/main/admin.py @@ -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) diff --git a/geogame/main/forms.py b/geogame/main/forms.py new file mode 100644 index 0000000..629c9bb --- /dev/null +++ b/geogame/main/forms.py @@ -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') diff --git a/geogame/main/management/commands/seed_coords.py b/geogame/main/management/commands/seed_coords.py new file mode 100644 index 0000000..cd3df13 --- /dev/null +++ b/geogame/main/management/commands/seed_coords.py @@ -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') diff --git a/geogame/main/migrations/0001_initial.py b/geogame/main/migrations/0001_initial.py new file mode 100644 index 0000000..669b286 --- /dev/null +++ b/geogame/main/migrations/0001_initial.py @@ -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), + ), + ] diff --git a/geogame/main/migrations/0002_game_country.py b/geogame/main/migrations/0002_game_country.py new file mode 100644 index 0000000..76252da --- /dev/null +++ b/geogame/main/migrations/0002_game_country.py @@ -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'), + ), + ] diff --git a/geogame/main/migrations/0003_auto_20190822_2204.py b/geogame/main/migrations/0003_auto_20190822_2204.py new file mode 100644 index 0000000..3b50451 --- /dev/null +++ b/geogame/main/migrations/0003_auto_20190822_2204.py @@ -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'), + ), + ] diff --git a/geogame/main/migrations/__init__.py b/geogame/main/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geogame/main/models.py b/geogame/main/models.py new file mode 100644 index 0000000..73d8938 --- /dev/null +++ b/geogame/main/models.py @@ -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 diff --git a/geogame/main/tests.py b/geogame/main/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/geogame/main/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/geogame/main/urls.py b/geogame/main/urls.py new file mode 100644 index 0000000..68c1f03 --- /dev/null +++ b/geogame/main/urls.py @@ -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\d+)/(?P\d+)/$', RoundView.as_view(), name="round-view"), + url(r'^round-recap/(?P\d+)/(?P\d+)/$', RoundRecapView.as_view(), name="round-recap-view"), + url(r'^remove-coord/(?P\d+)/(?P\d+)/$', RemoveCoordView.as_view(), name="remove-coord"), + url(r'^end-recap/(?P\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', + ), +] diff --git a/geogame/main/views.py b/geogame/main/views.py new file mode 100644 index 0000000..e338d06 --- /dev/null +++ b/geogame/main/views.py @@ -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 diff --git a/geogame/settings.py b/geogame/settings.py new file mode 100644 index 0000000..4dd9bdd --- /dev/null +++ b/geogame/settings.py @@ -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' diff --git a/geogame/templates/account/messages/email_confirmation_sent.txt b/geogame/templates/account/messages/email_confirmation_sent.txt new file mode 100644 index 0000000..e69de29 diff --git a/geogame/templates/account/messages/logged_in.txt b/geogame/templates/account/messages/logged_in.txt new file mode 100644 index 0000000..e69de29 diff --git a/geogame/templates/base.html b/geogame/templates/base.html new file mode 100644 index 0000000..184d4ec --- /dev/null +++ b/geogame/templates/base.html @@ -0,0 +1,64 @@ +{% load static %} + + + + + + + + + + + Geogame + + + + + {% if messages %} + {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} +
+
+
+ {% block content %} + {% endblock content %} +
+
+
+ + + + diff --git a/geogame/templates/game_base.html b/geogame/templates/game_base.html new file mode 100644 index 0000000..3c0910c --- /dev/null +++ b/geogame/templates/game_base.html @@ -0,0 +1,60 @@ +{% load static %} + + + + + + + + + + + Geogame + + + + + + {% if messages %} + {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} + {% block content %} + {% endblock content %} + + + + + diff --git a/geogame/templates/main/api_form.html b/geogame/templates/main/api_form.html new file mode 100644 index 0000000..ec65e87 --- /dev/null +++ b/geogame/templates/main/api_form.html @@ -0,0 +1,24 @@ +{% extends 'game_base.html' %} + +{% block content %} +
+
+
+

API key for for {{user.email}}

+
+
+
+
+
+
+
+ {{ form.non_field_errors }} + {% csrf_token %} + {{form.as_p}} + +
+
+
+
+ +{% endblock %} diff --git a/geogame/templates/main/contribute_form.html b/geogame/templates/main/contribute_form.html new file mode 100644 index 0000000..ca9c7b8 --- /dev/null +++ b/geogame/templates/main/contribute_form.html @@ -0,0 +1,25 @@ +{% extends 'game_base.html' %} + +{% block content %} +
+
+
+

Coordinate Submission

+
+
+
+
+
+
+
+ {{ form.non_field_errors }} + {% csrf_token %} + {{form.as_p}} + {{ form.media }} + +
+
+
+
+ +{% endblock %} diff --git a/geogame/templates/main/game_recap.html b/geogame/templates/main/game_recap.html new file mode 100644 index 0000000..55a18e6 --- /dev/null +++ b/geogame/templates/main/game_recap.html @@ -0,0 +1,74 @@ +{% extends 'game_base.html' %} + +{% block content %} + +
+
+
+ {% if messages %} + {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} + +
+ Your average guess was {{average_distance}}km away. +
+ +
+ + New Game + +
+
+
+ + + + + +{% endblock %} diff --git a/geogame/templates/main/homepage.html b/geogame/templates/main/homepage.html new file mode 100644 index 0000000..d5ef4a9 --- /dev/null +++ b/geogame/templates/main/homepage.html @@ -0,0 +1,78 @@ +{% extends 'game_base.html' %} + +{% block content %} +
+
+
+

Welcome to Geogame

+
+
+
+
+
+
+ {% if user.is_authenticated %} + {% if has_api_key %} + {% if existing_game %} + Continue Last Game + New Game + {% else %} + + New Game + {% endif %} + {% else %} +
+ You must set an api key on your profile page to play the game. Please read the FAQ for more info. +
+ + + {% endif %} + + {% else %} +
+ You must be logged in to play the game. Please read the FAQ for more info. +
+ + + {% endif %} +
+
+
+
+
+
+

FAQ

+

What is this?

+

This is a game that uses Geoguessr as inspiration.

+

Why does this exist?

+

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.

+

I created this little site to allow people to play the game how it used to be (or as close to it).

+

Why do I need an api key?

+

To get around the aforementioned api pricing strategy I require you to upload your own api key.

+

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.

+

How do I get an api key?

+

Go here and register to get an api key.

+

Once you have a key save it against your profile here. +

Is this safe?

+

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.

+

This site uses django's built in authentication framework, and I have taken every care to look after your personal details.

+

What does the "Broken Streetview" button do?

+

Sometimes a coordinate with no valid streetview is selected for you, you will just get a black screen.

+

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.

+

There isn't a great variety of countries, why?

+

I am relying on crowd sourcing of playable scenes, please consider helping out by adding a few coordinates of your own.

+

You get to keep track of the coordinates you add, and see how well (or badly) other people do on them!

+

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.

+

I didn't receive my email reset password, what gives?

+

I am using a free mail server to send password reset emails, the limit is 100/day.

+

If you require assisstance accessing your account, please email edward.wilding3@gmail.com

+

This site is atrocious and you should feel bad

+

Firstly that isn't a question, secondly that hurts my feelings, thirdly I built it in 2 days after geoguessr changed their site.

+

This site is amazing, how can I contact you to discuss this, or another, site?

+

Thank you! Please get in touch: edward.wilding3@gmail.com

+ +

Now go out there and have some fun exploring the world!

+
+
+
+{% endblock %} diff --git a/geogame/templates/main/profile.html b/geogame/templates/main/profile.html new file mode 100644 index 0000000..6f24c21 --- /dev/null +++ b/geogame/templates/main/profile.html @@ -0,0 +1,41 @@ +{% extends 'game_base.html' %} + +{% block content %} +
+
+
+

Your Profile

+
+
+
+
+
+
+

Logged in as {{user.email}}

+

+ Change Password +

+

+ API key +

+
+
+
+
+
+
+

Played Games

+

Coming soon

+
+
+
+
+
+
+

Your Contributions

+

Coming soon

+
+
+
+ +{% endblock %} diff --git a/geogame/templates/main/round.html b/geogame/templates/main/round.html new file mode 100644 index 0000000..99bfaab --- /dev/null +++ b/geogame/templates/main/round.html @@ -0,0 +1,89 @@ +{% extends 'game_base.html' %} + +{% block content %} + +
+
+
+ {% if messages %} + {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} + + +
+ +
+
+
+
+ +
+ +
+ {{ form.non_field_errors }} + {% csrf_token %} + {{form.as_p}} + +
+ +
+ {% csrf_token %} + +
+ +
+
+
+ + + + + +{% endblock %} diff --git a/geogame/templates/main/round_recap.html b/geogame/templates/main/round_recap.html new file mode 100644 index 0000000..0d07dd4 --- /dev/null +++ b/geogame/templates/main/round_recap.html @@ -0,0 +1,63 @@ +{% extends 'game_base.html' %} + +{% block content %} + +
+
+
+ {% if messages %} + {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} + +
+ Your guess was {{distance}}km away. +
+ +
+ + {% if last_round %} + Game Recap + {% else %} + Next Round + {% endif %} +
+
+
+ + + + + +{% endblock %} diff --git a/geogame/urls.py b/geogame/urls.py new file mode 100644 index 0000000..5473f0d --- /dev/null +++ b/geogame/urls.py @@ -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\d+)/$', UpdateAPIView.as_view(), name="api-key-view"), + url(r'', HomePageView.as_view(), name="home"), +] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/geogame/wsgi.py b/geogame/wsgi.py new file mode 100644 index 0000000..1d49c82 --- /dev/null +++ b/geogame/wsgi.py @@ -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() diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..c9541fa --- /dev/null +++ b/manage.py @@ -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()