onboard fix? many improvements for the game

parent 2371e034
import logging
from django.contrib import admin
from import_export.admin import ImportExportModelAdmin
from jet.admin import CompactInline
from failmap.game.forms import TeamForm
from failmap.game.models import Contest, Submission, Team
from failmap.game.models import Contest, OrganizationSubmission, Team, UrlSubmission
from failmap.organizations.models import Url
log = logging.getLogger(__package__)
class TeamInline(CompactInline):
......@@ -13,11 +17,11 @@ class TeamInline(CompactInline):
ordering = ["name"]
class SubmissionInline(CompactInline):
model = Submission
class OrganizationSubmissionInline(CompactInline):
model = OrganizationSubmission
extra = 0
can_delete = False
ordering = ["url"]
ordering = ["organization_name"]
# Register your models here.
......@@ -38,8 +42,6 @@ class ContestAdmin(ImportExportModelAdmin, admin.ModelAdmin):
inlines = [TeamInline]
form = TeamForm
# todo: submissioninline, read only... there are going to be MANY new things...
class TeamAdmin(ImportExportModelAdmin, admin.ModelAdmin):
......@@ -57,20 +59,63 @@ class TeamAdmin(ImportExportModelAdmin, admin.ModelAdmin):
}),
)
inlines = [SubmissionInline]
inlines = [OrganizationSubmissionInline]
class UrlSubmissionAdmin(ImportExportModelAdmin, admin.ModelAdmin):
list_display = ('added_by_team', 'for_organization', 'url', 'has_been_accepted', 'added_on')
search_fields = ('added_by_team__name', 'organization_name', 'url')
list_filter = ('has_been_accepted', 'added_by_team__name', 'added_by_team__participating_in_contest__name')
fields = ('added_by_team', 'for_organization', 'url', 'url_in_system', 'has_been_accepted', 'added_on')
ordering = ('for_organization', 'url')
actions = []
def accept(self, request, queryset):
for urlsubmission in queryset:
# don't add the same thing over and over, allows to re-select the ones already added without a problem
if urlsubmission.has_been_accepted:
continue
try:
url = Url.objects.all().get(url=urlsubmission.url)
except Url.DoesNotExist:
# if it already exists, then add the url to the organization.
url = Url(url=urlsubmission.url)
url.save()
# organization might also be added... that not really a problem.
try:
url.organization.add(urlsubmission.for_organization)
url.save()
except Exception as e:
log.error(e)
urlsubmission.url_in_system = url
urlsubmission.has_been_accepted = True
urlsubmission.save()
self.message_user(request, "Urls have been accepted and added to the system.")
accept.short_description = "✅ Accept"
actions.append('accept')
class SubmissionAdmin(ImportExportModelAdmin, admin.ModelAdmin):
list_display = ('added_by_team', 'has_been_accepted', 'url', 'organization_name', 'added_on')
search_fields = ('url', 'added_by_team__name', 'organization_name', 'organization_type_name')
class OrganizationSubmissionAdmin(ImportExportModelAdmin, admin.ModelAdmin):
list_display = ('added_by_team', 'organization_name', 'has_been_accepted', 'added_on')
search_fields = ('added_by_team__name', 'organization_name', 'organization_type_name')
list_filter = ('added_by_team__name', 'has_been_accepted', 'added_by_team__participating_in_contest__name')
fields = ('added_by_team', 'organization_country', 'organization_type_name', 'organization_name',
'organization_address', 'organization_address_geocoded', 'url', 'url_in_system', 'has_been_accepted',
'organization_address', 'organization_address_geocoded', 'url_in_system', 'has_been_accepted',
'added_on')
admin.site.register(Contest, ContestAdmin)
admin.site.register(Team, TeamAdmin)
admin.site.register(Submission, SubmissionAdmin)
admin.site.register(UrlSubmission, UrlSubmissionAdmin)
admin.site.register(OrganizationSubmission, OrganizationSubmissionAdmin)
from django import forms
import logging
import time
from failmap.game.models import Team
import tldextract
from dal import autocomplete
from django.contrib.gis import forms
from django.db import transaction
from django.forms import ValidationError
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django_countries.fields import CountryField
from mapwidgets.widgets import GooglePointFieldWidget
from failmap.game.models import Contest, OrganizationSubmission, Team, UrlSubmission
from failmap.organizations.models import Organization, OrganizationType, Url
from failmap.scanners.scanner_http import resolves
# todo: callback on edit address, put result in leaflet:
log = logging.getLogger(__package__)
# todo: this doesn't work yet
# don't show the secret (only in the source)
# should this be in forms.py or in admin.py?
# https://stackoverflow.com/questions/17523263/how-to-create-password-field-in-model-django
class TeamForm(forms.ModelForm):
class TeamForm(forms.Form):
field_order = ('team', 'secret')
secret = forms.CharField(widget=forms.PasswordInput)
CHOICES = Team.objects.all().filter(allowed_to_submit_things=True,
participating_in_contest=Contest.objects.get(pk=1)).values_list('pk', 'name')
team = forms.ChoiceField(widget=forms.RadioSelect, choices=CHOICES)
def clean(self):
cleaned_data = super().clean()
team = cleaned_data.get("team")
secret = cleaned_data.get("secret")
log.error("%s %s", team, secret)
# validate secret, add some timing...
time.sleep(1) # wait a second to deter brute force attacks (you can still do them)
try:
team = Team.objects.all().get(id=team, secret=secret)
except Team.DoesNotExist:
raise ValidationError(
_('Incorrect secret or team. Try again!'),
code='invalid',
)
class Meta:
# model = UrlSubmission # not bound to a model, we have to write save ourselves since we want to do
# a bit of dirty hacks (to prevent more N-N fields).
fields = ('team', 'secret', )
# http://django-autocomplete-light.readthedocs.io/en/master/tutorial.html
class OrganisationSubmissionForm(forms.ModelForm):
# todo: filter based on country and organization type.
# todo: but how to suggest new organizations?
organization_type_name = forms.ModelChoiceField(
queryset=OrganizationType.objects.all(),
widget=autocomplete.ModelSelect2(url='/game/autocomplete/organization-type-autocomplete/')
)
organization_name = forms.ModelChoiceField(
queryset=Organization.objects.all(),
widget=autocomplete.ModelSelect2Multiple(url='/game/autocomplete/organization-autocomplete/',
forward=['organization_type_name'])
)
class Meta:
model = OrganizationSubmission
# todo: show map, view only.
fields = ('organization_type_name', 'organization_name', 'organization_address_geocoded',)
widgets = {'organization_address_geocoded': GooglePointFieldWidget}
class UrlSubmissionForm(forms.Form):
field_order = ('country', 'organization_type_name', 'for_organization', 'url',)
country = CountryField().formfield(
required=False
)
organization_type_name = forms.ModelChoiceField(
queryset=OrganizationType.objects.all(),
widget=autocomplete.ModelSelect2(url='/game/autocomplete/organization-type-autocomplete/',
forward=['country']),
required=False
)
for_organization = forms.ModelMultipleChoiceField(
queryset=Organization.objects.all(),
widget=autocomplete.ModelSelect2Multiple(url='/game/autocomplete/organization-autocomplete/',
forward=['organization_type_name', 'country'])
)
url = forms.CharField()
def clean_url(self):
url = self.cleaned_data['url']
extract = tldextract.extract(url)
if not extract.suffix:
raise ValidationError(
_('Invalid or missing suffix (like .com etc): %(url)s'),
code='invalid',
params={'url': url},
)
# see if the URL resolves at all:
if not resolves(url):
raise ValidationError(
_('URL does not resolve (anymore): %(url)s.'),
code='does_not_resolve',
params={'url': url},
)
return url
# clean moet NA de velden... niet VOOR de velden...
def clean(self):
# this is a (i think) dirty hack. The clean should be called after all fields have been validated.
# but this is run even though there are ValidationErrors on fields.
cleaned_data = super().clean()
organisations = cleaned_data.get("for_organization")
url = cleaned_data.get("url")
if not organisations or not url:
raise forms.ValidationError(
"Fix the errors."
)
log.info(organisations)
# todo: raise multiple validation errors, so you can delete multiple organizations in the list
# perhaps, use the add error.
for organization in organisations:
if Url.objects.all().filter(url=url, organization=organization).exists():
raise ValidationError(
_('This URL %(url)s is already in the production data for organization %(organization)s'),
code='invalid',
params={'url': url, 'organization': organization},
)
# See if the URL is already suggested for these organizations
if UrlSubmission.objects.all().filter(url=url, for_organization=organization).exists():
raise ValidationError(
_('This URL %(url)s is already suggested for organization %(organization)s'),
code='invalid',
params={'url': url, 'organization': organization},
)
# See if the URL already exists, for these organizations
# todo: check if the team belongs to the contest... (elsewhere)
@transaction.atomic
def save(self, team):
# validate again to prevent race conditions
self.clean_url()
self.clean()
organizations = self.cleaned_data.get('for_organization', None)
url = self.cleaned_data.get('url', None)
# a horrible error...
if not organizations or not url:
raise forms.ValidationError(
"Race condition error: while form was submitted duplicated where created. Try again to see duplicated."
)
for organization in organizations:
submission = UrlSubmission(
added_by_team=Team.objects.get(pk=team),
for_organization=organization,
url=url,
added_on=timezone.now(),
has_been_accepted=False,
)
submission.save()
class Meta:
model = Team
fields = ('name', 'secret', 'participating_in_contest', 'allowed_to_submit_things')
# model = UrlSubmission # not bound to a model, we have to write save ourselves since we want to do
# a bit of dirty hacks (to prevent more N-N fields).
fields = ('url', 'for_organization', )
# Generated by Django 2.0.4 on 2018-04-07 16:11
import django.db.models.deletion
import django_countries.fields
import djgeojson.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('organizations', '0030_auto_20180403_1547'),
('game', '0002_auto_20180404_1908'),
]
operations = [
migrations.CreateModel(
name='OrganizationSubmission',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('organization_country', django_countries.fields.CountryField(max_length=2)),
('organization_type_name', models.CharField(default='unknown',
help_text='The contest the team is participating in.', max_length=42)),
('organization_name', models.CharField(default='unknown',
help_text='The contest the team is participating in.', max_length=42)),
('organization_address', models.CharField(default='unknown',
help_text='The address of the (main location) of the organization. This will be used for geocoding.', max_length=600)),
('organization_address_geocoded', djgeojson.fields.GeoJSONField(blank=True,
help_text='Automatic geocoded organization address.', max_length=5000, null=True)),
('has_been_accepted', models.BooleanField(default=False,
help_text='If the admin likes it, they can accept the submission to be part of the real system')),
('added_on', models.DateTimeField(blank=True,
help_text='Automatically filled when creating a new submission.', null=True)),
('added_by_team', models.ForeignKey(blank=True, null=True,
on_delete=django.db.models.deletion.CASCADE, to='game.Team')),
('organisation_in_system', models.ForeignKey(blank=True, help_text='This reference will be used to calculate the score and to track imports.',
null=True, on_delete=django.db.models.deletion.CASCADE, to='organizations.Organization')),
],
options={
'verbose_name': 'organisation submission',
'verbose_name_plural': 'organisation submissions',
},
),
migrations.CreateModel(
name='UrlSubmission',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('url', models.CharField(help_text='The URL the team has submitted, for review before acceptance.', max_length=500)),
('has_been_accepted', models.BooleanField(default=False,
help_text='If the admin likes it, they can accept the submission to be part of the real system')),
('added_on', models.DateTimeField(blank=True,
help_text='Automatically filled when creating a new submission.', null=True)),
('added_by_team', models.ForeignKey(blank=True, null=True,
on_delete=django.db.models.deletion.CASCADE, to='game.Team')),
('for_organization', models.ForeignKey(blank=True, null=True,
on_delete=django.db.models.deletion.CASCADE, to='organizations.Organization')),
('url_in_system', models.ForeignKey(blank=True, help_text='This reference will be used to calculate the score and to track imports.',
null=True, on_delete=django.db.models.deletion.CASCADE, to='organizations.Url')),
],
options={
'verbose_name': 'url submission',
'verbose_name_plural': 'url submissions',
},
),
migrations.RemoveField(
model_name='submission',
name='added_by_team',
),
migrations.RemoveField(
model_name='submission',
name='url_in_system',
),
migrations.DeleteModel(
name='Submission',
),
]
......@@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _
from django_countries.fields import CountryField
from djgeojson.fields import GeoJSONField
from failmap.organizations.models import Url
from failmap.organizations.models import Organization, Url
# Highest level adding:
......@@ -12,6 +12,7 @@ from failmap.organizations.models import Url
class Contest(models.Model):
name = models.CharField(
verbose_name=_("Contest name"),
max_length=42,
help_text="Whatever name the team wants. Must be at least PEGI 88."
)
......@@ -56,6 +57,7 @@ class Team(models.Model):
"""
name = models.CharField(
verbose_name=_("Team name"),
max_length=42,
help_text="Whatever name the team wants. Must be at least PEGI 88."
)
......@@ -85,15 +87,9 @@ class Team(models.Model):
return "%s/%s" % (self.participating_in_contest, self.name)
class Submission(models.Model):
"""
Submissions are suggestions of urls to add. They are not directly added to the system.
The admin of the system is the consensus algorithm.
class OrganizationSubmission(models.Model):
The admin can do "imports" on these submissions if they think it's a good one.
Todo: create admin action.
"""
organization_country = CountryField()
added_by_team = models.ForeignKey(
Team,
......@@ -102,8 +98,6 @@ class Submission(models.Model):
on_delete=models.CASCADE
)
organization_country = CountryField()
# Organization types are managed by the admin, so informed decisions are made.
# the type is not really important, that will be managed anyway. It's more a suggestion.
organization_type_name = models.CharField(
......@@ -133,6 +127,60 @@ class Submission(models.Model):
help_text="Automatic geocoded organization address."
)
organisation_in_system = models.ForeignKey(
Organization,
null=True,
help_text="This reference will be used to calculate the score and to track imports.",
blank=True,
on_delete=models.CASCADE
)
has_been_accepted = models.BooleanField(
default=False,
help_text="If the admin likes it, they can accept the submission to be part of the real system"
)
added_on = models.DateTimeField(
blank=True,
null=True,
help_text="Automatically filled when creating a new submission."
)
def __str__(self):
if self.has_been_accepted:
return "OK: %s" % self.organization_name
else:
return self.organization_name
class Meta:
verbose_name = _('organisation submission')
verbose_name_plural = _('organisation submissions')
class UrlSubmission(models.Model):
"""
Submissions are suggestions of urls to add. They are not directly added to the system.
The admin of the system is the consensus algorithm.
The admin can do "imports" on these submissions if they think it's a good one.
Todo: create admin action.
"""
added_by_team = models.ForeignKey(
Team,
null=True,
blank=True,
on_delete=models.CASCADE
)
for_organization = models.ForeignKey(
Organization,
null=True,
blank=True,
on_delete=models.CASCADE
)
url = models.CharField(
max_length=500,
help_text="The URL the team has submitted, for review before acceptance."
......@@ -164,5 +212,5 @@ class Submission(models.Model):
return self.url
class Meta:
verbose_name = _('submission')
verbose_name_plural = _('submissions')
verbose_name = _('url submission')
verbose_name_plural = _('url submissions')
{% load static %} {% load i18n %} {% load leaflet_tags %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
{% leaflet_js plugins="forms" %}
{% leaflet_css plugins="forms" %}
<script type="text/javascript" src="{% static 'js/vendor/jquery-3.2.1.js' %}"></script>
<link rel="stylesheet" type="text/css" href="{% static 'css/vendor/leaflet.css' %}"/>
<link rel="stylesheet" type="text/css" href="{% static 'css/vendor/bootstrap_v3.3.6.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'css/vendor/bootstrap-theme_v3.3.6.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'css/vendor/leaflet.fullscreen.css' %}">
{{ form.media }}
</head>
<body>
<nav class="navbar navbar-default navbar-static-top">
<div class="container">
<div class="navbar-collapse">
<ul class="nav navbar-nav navbar-center">
<li><a href="/game/scores/">Scores</a></li>
<li><a href="/game/team/">Select Team</a></li>
<li><a href="/game/submit_url/">Submit Url</a></li>
<li><a href="/game/submit_organization/">Submit Organization</a></li>
</ul>
</div>
</div>
</nav>
<div class="container">
{% block content %}
{% endblock %}
</div>
</body>
</html>
{% extends 'game/base.html' %}
{% block content %}
<h1>Scores</h1>
<p>
Jouw Team: {{ team.name }}
</p>
<table class="table">
<thead>
<tr>
<th>Rank</th>
<th>Team</th>
<th>High</th>
<th>Medium</th>
<th>Low</th>
</tr>
</thead>
<tbody>
{% for score in scores %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ score.team }}</td>
<td>{{ score.high }}</td>
<td>{{ score.medium }}</td>
<td>{{ score.low }}</td>
</tr>
{% empty %}
<tr><td colspan="5">-</td></tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
{% extends 'game/base.html' %}
{% block content %}
<h1>Suggest a new organisation</h1>
<form method="POST" class="uniForm">{% csrf_token %}
{{ form | crispy }}
<button type="submit" class="save btn btn-lg btn-primary">Save</button>
</form>
{% endblock %}
{% extends 'game/base.html' %}
{% load crispy_forms_tags %}
{% block content %}
<h1>Suggest a new url</h1>
<form method="POST" class="uniForm">{% csrf_token %}
{{ form | crispy }}
<br />
<button type="submit" class="save btn btn-lg btn-primary">DO THE THING</button>
</form>
{% endblock %}
{% extends 'game/base.html' %}
{% load crispy_forms_tags %}
{% block content %}
{% if success %}
<div class="alert alert-success" role="alert">
<strong>URL Toegevoegd!</strong> {{ url }} is toegevoegd!
</div>
{% endif %}
{% if error %}
<div class="alert alert-danger" role="alert">
<strong>Fout!</strong> {{ error }}
</div>
{% endif %}
<h1>Suggest a new url</h1>
<form method="POST" class="uniForm">{% csrf_token %}
{{ form | crispy }}
<br />
<button type="submit" class="save btn btn-lg btn-primary">DO THE THING</button>
</form>
{% endblock %}
# urls for scanners, maybe in their own url files
from django.conf.urls import url
from failmap.game.views import (OrganizationAutocomplete, OrganizationTypeAutocomplete, scores,
submit_organisation, submit_url, teams)
urlpatterns = [
url(r'^game/$', scores, name='scores'),
url(r'^game/scores/$', scores, name='scores'),
url(r'^game/team/$', teams, name='teams'),
url(r'^game/submit_url/$', submit_url, name='submit url'),
url(r'^game/submit_organisation/$', submit_organisation, name='submit url'),
url(r'^game/autocomplete/organization-autocomplete/$', OrganizationAutocomplete.as_view(),
name='organization-autocomplete'),
url(r'^game/autocomplete/organization-type-autocomplete/$', OrganizationTypeAutocomplete.as_view(),
name='organization-type-autocomplete'),
]
import logging
from dal import autocomplete
from django.shortcuts import redirect, render
from django.views.decorators.cache import cache_page
from failmap.game.forms import OrganisationSubmissionForm, TeamForm, UrlSubmissionForm
from failmap.game.models import Contest, Team
from failmap.map.calculate import get_calculation