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