From c194b9220e56d99d6fcd0da1c8802c170856ee56 Mon Sep 17 00:00:00 2001 From: "Peter J. Holzer" Date: Mon, 15 Apr 2019 00:24:46 +0200 Subject: [PATCH] Compare pictures and display top 10 --- .gitignore | 5 ++ manage.py | 22 +++++++ picture_tournament/__init__.py | 0 picture_tournament/settings.py | 103 +++++++++++++++++++++++++++++++++ picture_tournament/urls.py | 31 ++++++++++ picture_tournament/wsgi.py | 16 +++++ pt/__init__.py | 0 pt/admin.py | 3 + pt/apps.py | 5 ++ pt/migrations/0001_initial.py | 24 ++++++++ pt/migrations/__init__.py | 0 pt/models.py | 9 +++ pt/static/style.css | 85 +++++++++++++++++++++++++++ pt/templates/pt/compare.html | 37 ++++++++++++ pt/templates/pt/top.html | 28 +++++++++ pt/tests.py | 3 + pt/views.py | 83 ++++++++++++++++++++++++++ 17 files changed, 454 insertions(+) create mode 100644 .gitignore create mode 100755 manage.py create mode 100644 picture_tournament/__init__.py create mode 100644 picture_tournament/settings.py create mode 100644 picture_tournament/urls.py create mode 100644 picture_tournament/wsgi.py create mode 100644 pt/__init__.py create mode 100644 pt/admin.py create mode 100644 pt/apps.py create mode 100644 pt/migrations/0001_initial.py create mode 100644 pt/migrations/__init__.py create mode 100644 pt/models.py create mode 100644 pt/static/style.css create mode 100644 pt/templates/pt/compare.html create mode 100644 pt/templates/pt/top.html create mode 100644 pt/tests.py create mode 100644 pt/views.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..26393d2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.pyc +.*.swp +__pycache__ +media +settings_local.py diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..6e9d7df --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "picture_tournament.settings") + try: + from django.core.management import execute_from_command_line + except ImportError: + # The above import may fail for some other reason. Ensure that the + # issue is really that Django is missing to avoid masking other + # exceptions on Python 2. + try: + import django + except ImportError: + 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?" + ) + raise + execute_from_command_line(sys.argv) diff --git a/picture_tournament/__init__.py b/picture_tournament/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/picture_tournament/settings.py b/picture_tournament/settings.py new file mode 100644 index 0000000..8c6f6d8 --- /dev/null +++ b/picture_tournament/settings.py @@ -0,0 +1,103 @@ +""" +Django settings for picture_tournament project. + +Generated by 'django-admin startproject' using Django 1.10.7. + +For more information on this file, see +https://docs.djangoproject.com/en/1.10/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.10/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__))) + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'pt', +] + +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 = 'picture_tournament.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + '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 = 'picture_tournament.wsgi.application' + + +# Password validation +# https://docs.djangoproject.com/en/1.10/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/1.10/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/1.10/howto/static-files/ + +STATIC_URL = '/static/' + +MEDIA_URL = '/media/' + + +from .settings_local import * diff --git a/picture_tournament/urls.py b/picture_tournament/urls.py new file mode 100644 index 0000000..a5a9818 --- /dev/null +++ b/picture_tournament/urls.py @@ -0,0 +1,31 @@ +"""picture_tournament URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.10/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.conf.urls import url, include + 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) +""" +from django.conf import settings +from django.conf.urls import url +from django.conf.urls.static import static +from django.contrib import admin + +from pt.views import compare, left_wins, right_wins, refresh, top + +urlpatterns = [ + url(r'^admin/', admin.site.urls), + url(r'^cmp/$', compare, name='compare'), + url(r'^cmp/left$', left_wins, name='left-wins'), + url(r'^cmp/right$', right_wins, name='right-wins'), + url(r'^cmp/refresh$', refresh, name='refresh'), + url(r'^top$', top, name='top'), +] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + diff --git a/picture_tournament/wsgi.py b/picture_tournament/wsgi.py new file mode 100644 index 0000000..b36e638 --- /dev/null +++ b/picture_tournament/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for picture_tournament 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/1.10/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "picture_tournament.settings") + +application = get_wsgi_application() diff --git a/pt/__init__.py b/pt/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pt/admin.py b/pt/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/pt/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/pt/apps.py b/pt/apps.py new file mode 100644 index 0000000..03534a6 --- /dev/null +++ b/pt/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class PtConfig(AppConfig): + name = 'pt' diff --git a/pt/migrations/0001_initial.py b/pt/migrations/0001_initial.py new file mode 100644 index 0000000..d60171c --- /dev/null +++ b/pt/migrations/0001_initial.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2019-04-14 17:35 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Picture', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('url', models.CharField(max_length=200)), + ('elo', models.FloatField(default=1000)), + ], + ), + ] diff --git a/pt/migrations/__init__.py b/pt/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pt/models.py b/pt/models.py new file mode 100644 index 0000000..655e4eb --- /dev/null +++ b/pt/models.py @@ -0,0 +1,9 @@ +from django.db import models + +# Create your models here. +class Picture(models.Model): + url = models.CharField(max_length=200) + elo = models.FloatField(default=1000) + + def __str__(self): + return "#%d: %s: %f" % (self.id, self.url, self.elo) diff --git a/pt/static/style.css b/pt/static/style.css new file mode 100644 index 0000000..e72f667 --- /dev/null +++ b/pt/static/style.css @@ -0,0 +1,85 @@ +/* You can add global styles to this file, and also import other style files */ +body { + background-color: #000; + color: #FFF; +} + +h1 { + margin-top: 0; + text-align: center; + margin-bottom: 0; +} + + +#page { + background-color: #222; + color: #FFF; + display: grid; + grid-template-columns: auto auto; + grid-template-rows: 1fr auto; +} + +#hdr { grid-column: 1 / span 2; grid-row: 1; } +#view-left { grid-column: 1; grid-row: 2; } +#view-right { grid-column: 2; grid-row: 2; } + +header { + display: flex; + flex-direction: row; + justify-content: space-between; +} +.imgviewer { + border-width: 1px; + border-style: none; + border-color: #888; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.up { + color: #080; + font-size: 2rem; + transform: scale(2,1); + text-align: center; +} + +.up a { + color: #080; + text-decoration: none; +} + +.down { + font-size: 2rem; + color: #C00; + transform: scale(2,1); + text-align: center; +} + +.down a { + color: #C00; + text-decoration: none; +} + +.nav { + font-size: 2rem; +} +.nav a { + color: #CC0; + text-decoration: none; +} + +.pic { + border-color: #000; + border-style: none; + border-width: 1px; + margin-left: auto; + margin-right: auto; +} + +.pic img { + text-align: center; + vertical-align: middle; + max-width: 48vw; + max-height: 48vw; +} diff --git a/pt/templates/pt/compare.html b/pt/templates/pt/compare.html new file mode 100644 index 0000000..b01fbe0 --- /dev/null +++ b/pt/templates/pt/compare.html @@ -0,0 +1,37 @@ +{% load staticfiles %} + + + + + Picture Tournament + + + +
+
+ +

Picture Tournament

+ +
+
+ +
+
{{left.elo | floatformat:"1"}} +
+ +
+ +
+ +
+
{{right.elo | floatformat:"1"}} +
+ +
+ +
+ + +{% comment %} + vim: sw=2 tw=79 +{% endcomment %} diff --git a/pt/templates/pt/top.html b/pt/templates/pt/top.html new file mode 100644 index 0000000..0575d85 --- /dev/null +++ b/pt/templates/pt/top.html @@ -0,0 +1,28 @@ +{% load staticfiles %} + + + + + Picture Tournament + + + +
+
+ +

Picture Tournament - Top

+ +
+ {% for pic in top %} +
+
{{pic.elo | floatformat:"1"}} +
+ {% endfor %} + +
+ + +{% comment %} + vim: sw=2 tw=79 +{% endcomment %} + diff --git a/pt/tests.py b/pt/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/pt/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/pt/views.py b/pt/views.py new file mode 100644 index 0000000..3b5e67d --- /dev/null +++ b/pt/views.py @@ -0,0 +1,83 @@ +import logging +import random + +from django.core.urlresolvers import reverse +from django.db.models import Max +from django.http import HttpResponse, HttpResponseRedirect +from django.shortcuts import render + +from . import models + +log = logging.getLogger('picture_tournament') + +# Create your views here. +def compare(request): + if "left" not in request.session: + left_pic = random_pic() + request.session["left"] = left_pic.id + if "right" not in request.session: + right_pic = random_pic() + request.session["right"] = right_pic.id + left_pic = models.Picture.objects.get(id=request.session["left"]) + right_pic = models.Picture.objects.get(id=request.session["right"]) + context = { + "left": left_pic, + "right": right_pic, + } + return HttpResponse(render(request, 'pt/compare.html', context)) + +def random_pic(): + max_id = models.Picture.objects.all().aggregate(Max('id'))["id__max"] + log.debug("max_id = %s", max_id) + while True: + id = random.randint(1, max_id) + pics = models.Picture.objects.filter(id=id) + if len(pics) == 1: + return pics[0] + + +def left_wins(request): + left = models.Picture.objects.get(id=request.session["left"]) + right = models.Picture.objects.get(id=request.session["right"]) + + expected = 1 / (1 + 10**((right.elo - left.elo) / 400)) + log.info("before: left=%f, right=%f, expected=%f", left.elo, right.elo, expected) + adjust = (1 - expected) * 20 + left.elo += adjust + left.save() + right.elo -= adjust + right.save() + log.info("after: left=%f, right=%f", left.elo, right.elo) + del request.session["right"] + + return HttpResponseRedirect(reverse('compare')) + + +def right_wins(request): + left = models.Picture.objects.get(id=request.session["left"]) + right = models.Picture.objects.get(id=request.session["right"]) + + expected = 1 / (1 + 10**((left.elo - right.elo) / 400)) + log.info("before: left=%f, right=%f, expected=%f", left.elo, right.elo, expected) + adjust = (1 - expected) * 20 + left.elo -= adjust + left.save() + right.elo += adjust + right.save() + log.info("after: left=%f, right=%f", left.elo, right.elo) + del request.session["left"] + + return HttpResponseRedirect(reverse('compare')) + + +def refresh(request): + del request.session["left"] + del request.session["right"] + + return HttpResponseRedirect(reverse('compare')) + + +def top(request): + top = models.Picture.objects.all().order_by("-elo")[0:9] + context = { "top": top } + return HttpResponse(render(request, 'pt/top.html', context))