From e2d75c3ea0f473a70398f8f4c3f8e903674e627a Mon Sep 17 00:00:00 2001
From: root
Date: Tue, 26 Nov 2019 02:13:47 +0100
Subject: [PATCH] Added tonnes of features, lets see what breaks.
---
geogame/main/admin.py | 3 +-
geogame/main/forms.py | 30 +--
.../main/management/commands/seed_coords.py | 9 +-
.../migrations/0004_auto_20191125_2344.py | 64 ++++++
geogame/main/models.py | 71 +++++--
geogame/main/urls.py | 11 +-
geogame/main/views.py | 187 +++++++++++++-----
geogame/settings.py | 1 +
geogame/templates/base.html | 9 +-
geogame/templates/game_base.html | 3 -
geogame/templates/main/api_form.html | 4 +-
geogame/templates/main/challenge_list.html | 49 +++++
.../templates/main/create_challenge_form.html | 35 ++++
.../templates/main/edit_challenge_form.html | 92 +++++++++
geogame/templates/main/game_recap.html | 2 +-
geogame/templates/main/homepage.html | 11 +-
geogame/templates/main/profile.html | 21 +-
17 files changed, 500 insertions(+), 102 deletions(-)
create mode 100644 geogame/main/migrations/0004_auto_20191125_2344.py
create mode 100644 geogame/templates/main/challenge_list.html
create mode 100644 geogame/templates/main/create_challenge_form.html
create mode 100644 geogame/templates/main/edit_challenge_form.html
diff --git a/geogame/main/admin.py b/geogame/main/admin.py
index bce62cd..fcdbd37 100644
--- a/geogame/main/admin.py
+++ b/geogame/main/admin.py
@@ -7,8 +7,7 @@ from .models import User, Coord
@admin.register(Coord)
class CoordAdmin(admin.ModelAdmin):
- list_display = 'id', 'country',
- search_fields = 'country',
+ list_display = 'id',
class CustomUserAdmin(UserAdmin):
diff --git a/geogame/main/forms.py b/geogame/main/forms.py
index 629c9bb..0651e62 100644
--- a/geogame/main/forms.py
+++ b/geogame/main/forms.py
@@ -5,26 +5,17 @@ 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 django.forms import modelformset_factory
from geogame.main.models import (
- Coord, User, GameRound
+ Coord, User, GameRound, Challenge
)
-from dal import autocomplete
-#from django_starfield import Stars
-
-
-class CoordForm(forms.ModelForm):
+class ChallengeForm(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')
+ model = Challenge
+ fields = ('name',)
class GuessForm(forms.ModelForm):
@@ -47,7 +38,7 @@ class APIForm(forms.ModelForm):
class Meta:
model = User
- fields = ('api_key',)
+ fields = ('api_key', 'display_name',)
class CustomUserCreationForm(UserCreationForm):
@@ -62,3 +53,12 @@ class CustomUserChangeForm(UserChangeForm):
class Meta:
model = User
fields = ('username', 'email')
+
+
+
+
+ChallengeCoordFormSet = modelformset_factory(
+ Coord,
+ fields=('lat', 'lng',),
+ extra=1,
+)
\ No newline at end of file
diff --git a/geogame/main/management/commands/seed_coords.py b/geogame/main/management/commands/seed_coords.py
index cd3df13..eb7e7dc 100644
--- a/geogame/main/management/commands/seed_coords.py
+++ b/geogame/main/management/commands/seed_coords.py
@@ -20,13 +20,18 @@ class Command(BaseCommand):
with transaction.atomic():
- country = Country.objects.get(country='United Kingdom')
+ country = Country.objects.get(country="United States of America")
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)
+ _,c = Coord.objects.get_or_create(
+ lat=row[0],
+ lng=row[1],
+ country=country,
+ user=user
+ )
if options['dry_run']:
transaction.set_rollback(True)
diff --git a/geogame/main/migrations/0004_auto_20191125_2344.py b/geogame/main/migrations/0004_auto_20191125_2344.py
new file mode 100644
index 0000000..80c5baa
--- /dev/null
+++ b/geogame/main/migrations/0004_auto_20191125_2344.py
@@ -0,0 +1,64 @@
+# Generated by Django 2.2.4 on 2019-11-25 23:44
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('main', '0003_auto_20190822_2204'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Challenge',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(blank=True, db_index=True, max_length=255, verbose_name='Challenge Name')),
+ ('average', models.PositiveIntegerField()),
+ ],
+ ),
+ migrations.RemoveField(
+ model_name='coord',
+ name='country',
+ ),
+ migrations.RemoveField(
+ model_name='game',
+ name='country',
+ ),
+ migrations.AddField(
+ model_name='coord',
+ name='reports',
+ field=models.PositiveIntegerField(default=0),
+ ),
+ migrations.AddField(
+ model_name='gameround',
+ name='result',
+ field=models.PositiveIntegerField(default=0),
+ ),
+ migrations.AddField(
+ model_name='user',
+ name='display_name',
+ field=models.CharField(blank=True, db_index=True, max_length=25, verbose_name='display name'),
+ ),
+ migrations.DeleteModel(
+ name='Country',
+ ),
+ migrations.AddField(
+ model_name='challenge',
+ name='user',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='challenge_user', to=settings.AUTH_USER_MODEL),
+ ),
+ migrations.AddField(
+ model_name='coord',
+ name='challenge',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='coord_challenge', to='main.Challenge'),
+ ),
+ migrations.AddField(
+ model_name='game',
+ name='challenge',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='game_challenge', to='main.Challenge'),
+ ),
+ ]
diff --git a/geogame/main/models.py b/geogame/main/models.py
index 73d8938..c25a264 100644
--- a/geogame/main/models.py
+++ b/geogame/main/models.py
@@ -5,6 +5,7 @@ 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.db.models import Avg
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
@@ -13,8 +14,6 @@ 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
@@ -23,17 +22,16 @@ class User(AbstractUser):
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)
+ display_name = models.CharField(_('display name'), max_length=25, blank=True, db_index=True)
- def generate_new_game(self, country=None):
+ def generate_new_game(self):
game = Game.objects.create(
start=timezone.now(),
user=self,
score=0,
active=True,
)
- qs = Coord.objects.all()
- if country:
- qs = qs.filter(country=country)
+ qs = Coord.objects.filter(reports=0)
coords = qs.order_by('?')[:5]
for i, coord in enumerate(coords):
round = GameRound.objects.create(
@@ -52,34 +50,65 @@ class User(AbstractUser):
return Game.objects.filter(user=self).update(active=False)
-class Country(models.Model):
- country = models.CharField(_('Country'), max_length=255, null=False, blank=False)
+class Challenge(models.Model):
+ name = models.CharField(_('Challenge Name'), max_length=255, blank=True, db_index=True)
+ average = models.PositiveIntegerField(default=0)
+ user = models.ForeignKey(User, models.PROTECT,
+ related_name='challenge_user',
+ null=False, blank=False)
- def __str__(self):
- return self.country
+ def update_average_score(self):
+ average = GameRound.objects.filter(game__challenge=self).aggregate(Avg('result')).get('result__avg', 0)
+ self.average = average
+ self.save()
+ return average
+
+ def setup_challenge(self, user):
+ rounds = Coord.objects.filter(challenge=self)
+ game = Game.objects.create(
+ challenge=self,
+ start=timezone.now(),
+ user=user,
+ score=0,
+ active=True,
+ )
+ for order, coord in enumerate(rounds):
+ round = GameRound.objects.create(
+ game=game,
+ coord=coord,
+ order=order
+ )
+ if order == 0:
+ round_id = round.id
+ return game.id, round_id
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)
+ reports = models.PositiveIntegerField(default=0)
+ challenge = models.ForeignKey(Challenge, models.PROTECT,
+ related_name='coord_challenge',
+ null=True, blank=True)
+
+ def report(self):
+ self.reports = self.reports + 1
+ self.save()
class Game(models.Model):
+ challenge = models.ForeignKey(Challenge, models.PROTECT,
+ related_name='game_challenge',
+ null=True, blank=True)
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)
@@ -95,10 +124,18 @@ class GameRound(models.Model):
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)
+ result = models.PositiveIntegerField(default=0)
def get_distance(self):
if not self.guess_lat or not self.guess_lng:
+ self.distance = 0
+ self.save()
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
+
+ result = distance.distance(actual_coord, guess_coord).km
+ self.result = result
+ self.save()
+ return result
\ No newline at end of file
diff --git a/geogame/main/urls.py b/geogame/main/urls.py
index 68c1f03..dc38f10 100644
--- a/geogame/main/urls.py
+++ b/geogame/main/urls.py
@@ -2,8 +2,8 @@ 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
+ RoundView, RoundRecapView, GameRecapView, NewGameView, ChallengeListView,
+ EditChallengeView, RemoveCoordView, CoordDeleteView, ChallengeCreateView
)
@@ -13,7 +13,8 @@ urlpatterns = [
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',
- ),
+ url(r'^create-challenge/$', ChallengeCreateView.as_view(), name="create-challenge"),
+ url(r'^list-challenge/$', ChallengeListView.as_view(), name="list-challenge"),
+ url(r'^edit-challenge/(?P\d+)/$', EditChallengeView.as_view(), name="edit-challenge"),
+ url(r'^coord/(?P\d+)/delete/$', CoordDeleteView.as_view(), name="coord-delete"),
]
diff --git a/geogame/main/views.py b/geogame/main/views.py
index 14f4cf1..dfb8f3a 100644
--- a/geogame/main/views.py
+++ b/geogame/main/views.py
@@ -1,6 +1,7 @@
from django.conf import settings
from django.shortcuts import get_object_or_404, redirect
from django.views.generic import TemplateView, ListView
+from django.db.models import Avg
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from django.views.generic.detail import DetailView
from django.views import View
@@ -10,25 +11,12 @@ from django.urls import reverse_lazy
from braces import views
from geogame.main.models import (
- Game, GameRound, Coord, User, Country
+ Game, GameRound, Coord, User, Challenge
)
from dal import autocomplete
-from geogame.main.forms import GuessForm, CoordForm, APIForm
+from geogame.main.forms import GuessForm, ChallengeCoordFormSet, APIForm, ChallengeForm
-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'
@@ -60,6 +48,12 @@ class HomePageView(TemplateView):
class ProfilePageView(views.LoginRequiredMixin, TemplateView):
template_name = 'main/profile.html'
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ user = self.request.user
+ context['user_challenges'] = Challenge.objects.filter(user=user)
+ return context
+
class UpdateAPIView(views.LoginRequiredMixin, UpdateView):
model = User
@@ -70,20 +64,75 @@ class UpdateAPIView(views.LoginRequiredMixin, UpdateView):
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')
+class ChallengeCreateView(views.LoginRequiredMixin, CreateView):
+ template_name = 'main/create_challenge_form.html'
+ form_class = ChallengeForm
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.user = self.request.user
self.object.save()
- messages.success(self.request, "Your coordinates have been added.")
- return redirect(self.get_success_url())
+ messages.success(self.request, 'Challenge Created Successfully, add some Co-ords')
+ return redirect(reverse_lazy('game:edit-challenge', args=(self.object.id,)))
+
+
+class EditChallengeView(views.UserPassesTestMixin, TemplateView):
+ template_name = 'main/edit_challenge_form.html'
+
+ def test_func(self, user):
+ challenge = get_object_or_404(Challenge, pk=self.kwargs['pk'])
+ return challenge.user == user
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ challenge = get_object_or_404(Challenge, pk=self.kwargs['pk'])
+ context['challenge'] = challenge
+ context['formset'] = ChallengeCoordFormSet(queryset=Coord.objects.none())
+ context['coords'] = Coord.objects.filter(challenge=challenge)
+ return context
+
+ def post(self, *args, **kwargs):
+ challenge = get_object_or_404(Challenge, pk=self.kwargs['pk'])
+ formset = ChallengeCoordFormSet(self.request.POST)
+
+ if formset.is_valid():
+ for form in formset:
+ obj = form.save(commit=False)
+ obj.challenge = challenge
+ obj.user = self.request.user
+ obj.save()
+ #XX prevent duplicates
+ messages.success(self.request, 'Co-ords Updated Successfully')
+ return redirect(reverse_lazy('game:edit-challenge', args=(challenge.id,)))
+ messages.error(self.request, 'The form was invalid - something has gone wrong')
+ return redirect(reverse_lazy('game:edit-challenge', args=(challenge.id,)))
+
+
+class CoordDeleteView(views.UserPassesTestMixin, DeleteView):
+ model = Coord
+
+ def test_func(self, user):
+ coord = self.get_object()
+ return coord.user == user
+
+ def get_success_url(self):
+ challenge = self.object.challenge
+ messages.success(self.request, 'Coord Removed Successfully')
+ return reverse_lazy('game:edit-challenge',args=(challenge.id,))
+
+
+class ChallengeListView(views.LoginRequiredMixin, ListView):
+ template_name = 'main/challenge_list.html'
+ context_object_name = 'challenges'
+ paginate_by = 50
+
+ def get_queryset(self):
+ ids = Coord.objects.filter(challenge__isnull=False).\
+ order_by().\
+ values('challenge').\
+ distinct()
+
+ return Challenge.objects.filter(id__in=ids).order_by('average')
class NewGameView(views.LoginRequiredMixin, View):
@@ -91,7 +140,11 @@ 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()
+ if request.GET.get('challenge'):
+ challenge = get_object_or_404(Challenge, pk=request.GET.get('challenge'))
+ game_pk, round_pk = challenge.setup_challenge(user)
+ else:
+ game_pk, round_pk = user.generate_new_game()
return redirect(
reverse_lazy(
@@ -151,30 +204,60 @@ 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]
+ dodgy_coord = round.coord
+ dodgy_coord.report()
- 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
+ if game.challenge:
+ round.score = 30000
+ round.save()
+ next = round.order + 1
+ next_round = GameRound.objects.filter(
+ game=game,
+ order=next,
+ ).first()
+ if next_round:
+ return redirect(
+ reverse_lazy(
+ 'game:round-view',
+ kwargs={
+ 'game_pk': game.pk,
+ 'round_pk': next_round.pk,
+ }
+ )
+ )
+ else:
+ return redirect(
+ reverse_lazy(
+ 'game:end-recap-view',
+ kwargs={
+ 'game_pk': game.pk,
+ }
+ )
+ )
+
+ else:
+
+ qs = Coord.objects.filter(reports=0)
+ 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,
+ 'game_pk': game.pk,
'round_pk': round.pk,
}
)
)
-
class RoundRecapView(views.UserPassesTestMixin, TemplateView):
template_name = 'main/round_recap.html'
@@ -195,13 +278,19 @@ class RoundRecapView(views.UserPassesTestMixin, TemplateView):
context['game_id'] = round.game.id
context['distance'] = "{0:.3f}".format(round.get_distance())
- if round.order == 4:
+ next_round = GameRound.objects.filter(
+ game=round.game,
+ order=round.order + 1
+ ).first()
+
+ if not next_round:
+ #not every game is part of a challenge, so try updated the challenge average
+ try:
+ round.game.challenge.update_average_score()
+ except:
+ pass
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
@@ -214,7 +303,6 @@ class GameRecapView(views.UserPassesTestMixin, TemplateView):
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)
@@ -229,6 +317,17 @@ class GameRecapView(views.UserPassesTestMixin, TemplateView):
)
distance_total += round.get_distance()
- context['average_distance'] = "{0:.3f}".format(distance_total / 5)
context['results'] = coord_results
+ context['average_distance'] = GameRound.objects.filter(game=game)\
+ .aggregate(Avg('result'))\
+ .get('result__avg', 0)
+ # not every game is part of a challenge, so keep this in a try
+ try:
+ context['all_average'] = game.challenge.average
+ except:
+ pass
return context
+
+
+
+# handle reports differently if in a challenge (skip with score of 30000 instead of finding a random one)
\ No newline at end of file
diff --git a/geogame/settings.py b/geogame/settings.py
index 4dd9bdd..bdba9c7 100644
--- a/geogame/settings.py
+++ b/geogame/settings.py
@@ -47,6 +47,7 @@ INSTALLED_APPS = [
'django_extensions',
'dal',
'dal_select2',
+ 'dynamic_formsets',
'geogame.main',
'django_countries',
diff --git a/geogame/templates/base.html b/geogame/templates/base.html
index 184d4ec..f9c8bc1 100644
--- a/geogame/templates/base.html
+++ b/geogame/templates/base.html
@@ -11,7 +11,9 @@
Geogame
-
+
+
+
- API key
+ Edit Details
@@ -25,6 +25,7 @@
@@ -33,7 +34,23 @@
-
Coming soon
+
+
+
+ Challenge Name |
+ Average Score |
+ |
+
+ {% for challenge in user_challenges %}
+
+ {{challenge.name}} |
+ {{challenge.average}} |
+ Edit |
+
+ {% endfor %}
+
+
+
Create a New Challenge