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

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