Compare commits
80 Commits
Author | SHA1 | Date | |
---|---|---|---|
16deac8849 | |||
56c198f19a | |||
445a769ce5 | |||
c9e9ec46fa | |||
a5bcbf0c7e | |||
286c075ea8 | |||
2e7123989f | |||
2db9340f3d | |||
bb9869df73 | |||
16847d1856 | |||
0c3a9b3be7 | |||
801d26cb4a | |||
52d56926d4 | |||
5e1c32a2ff | |||
bb52c3c19b | |||
00e4b78fe1 | |||
5a2ba5c16a | |||
d43af2278e | |||
00d34f819d | |||
090376f1e3 | |||
110add5b0e | |||
b83f00609a | |||
f60d7d98fd | |||
64ef1b8086 | |||
4dfe7f4285 | |||
083d10fc6e | |||
63bed8818e | |||
eab50caca9 | |||
b0c9f7ebec | |||
a0e365f596 | |||
a86810b451 | |||
c70673b8b1 | |||
f2d7a2dfd8 | |||
91cc57f166 | |||
a6dcad77d9 | |||
50f0361b91 | |||
dcc11b994f | |||
03498d93e7 | |||
bb51eb68cd | |||
c74413e3b4 | |||
20a6d25a8b | |||
0536b16d6d | |||
a5ff2326f7 | |||
619cd8d326 | |||
aa01a0d8db | |||
92ea2f6af7 | |||
95d8b30c3c | |||
1845ae9eac | |||
8dec41073f | |||
b0978034b5 | |||
0380bc9071 | |||
8847c351c1 | |||
ea2d3d0c43 | |||
63e1425b00 | |||
d93a3a024f | |||
1612cdae6e | |||
7d83a4a59f | |||
9b0def75b7 | |||
ff228ab845 | |||
c3491b7975 | |||
88ceb57a09 | |||
be6b7bfc99 | |||
ed9c81f0d9 | |||
ecaf40ba41 | |||
b71c1c7baa | |||
5867d70111 | |||
5ba6193d4b | |||
c55fdf3e38 | |||
6319f24499 | |||
84e1baa64b | |||
99ed5d574c | |||
ae1a3edd99 | |||
96de9e859c | |||
fdc4e36da7 | |||
bd326cd846 | |||
8afd6dec6e | |||
67afac3d45 | |||
00afb10702 | |||
5820bdc63a | |||
04708e6286 |
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
*.mo
|
12
.gitlab-ci.yml
Normal file
12
.gitlab-ci.yml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
image: python:3.6-stretch
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- test
|
||||||
|
|
||||||
|
test-openbackloggery:
|
||||||
|
type: test
|
||||||
|
script:
|
||||||
|
- apt-get update -qy
|
||||||
|
- apt-get install -y python3-dev python3-pip
|
||||||
|
- pip3 install -r requirements.txt
|
||||||
|
- cd collection && python3 manage.py test
|
40
.pre-commit-config.yaml
Normal file
40
.pre-commit-config.yaml
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# See http://pre-commit.com for more information
|
||||||
|
# See http://pre-commit.com/hooks.html for more hooks
|
||||||
|
repos:
|
||||||
|
- repo: git://github.com/asottile/reorder_python_imports
|
||||||
|
sha: v0.3.5
|
||||||
|
hooks:
|
||||||
|
- id: reorder-python-imports
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-yapf
|
||||||
|
sha: v0.18.0
|
||||||
|
hooks:
|
||||||
|
- id: yapf
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
sha: v0.9.2
|
||||||
|
hooks:
|
||||||
|
- id: trailing-whitespace
|
||||||
|
name: Supprime espaces fin de ligne
|
||||||
|
files: .py$
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
name: Règle problème sur les fins de fichiers
|
||||||
|
- id: check-yaml
|
||||||
|
name: Syntaxe des fichiers YAML
|
||||||
|
- id: check-json
|
||||||
|
name: Syntaxe des fichiers JSON
|
||||||
|
- id: debug-statements
|
||||||
|
name: Supprime les PDB restants
|
||||||
|
- id: detect-private-key
|
||||||
|
name: Trouve les clefs privées
|
||||||
|
- id: flake8
|
||||||
|
name: Vérification code Python avec flake8
|
||||||
|
args: ["--exclude=.git, __pycache__"]
|
||||||
|
- id: requirements-txt-fixer
|
||||||
|
name: Réordonne le fichier requirements.txt
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-jshint
|
||||||
|
sha: v2.9.5
|
||||||
|
hooks:
|
||||||
|
- id: jshint
|
||||||
|
- repo: https://github.com/Lucas-C/pre-commit-hooks-safety
|
||||||
|
sha: v1.1.0
|
||||||
|
hooks:
|
||||||
|
- id: python-safety-dependencies-check
|
27
CHANGELOG
Normal file
27
CHANGELOG
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
Current version (0.2) :
|
||||||
|
|
||||||
|
- Nom court pour les plateformes et les collections de figurines
|
||||||
|
- Nouvelle collection de figurines
|
||||||
|
- Tests automatiques chez Gitlab
|
||||||
|
- Bug corrigé : Génération de 2 timelines si on crée un jeu avec l'état "created"
|
||||||
|
- Renommage des consoles en « plateformes »
|
||||||
|
- Activation des requêtes CORS pour permettre à une autre application d'accéder à l'API
|
||||||
|
- MàJ vers Django 1.11.9
|
||||||
|
- Activation d'une API accessible par l'administrateur (avec documentation)
|
||||||
|
- Ajout d'une page d'accueil listant les jeux vidéos en cours, la liste complète et les 5 dernières activités sur ces derniers triées par date
|
||||||
|
- Nouveau champ 'note' pour la progression dans le jeu
|
||||||
|
- Omission de l'état "Nouveau" pour les jeux
|
||||||
|
- Traduction de l'interface en Français
|
||||||
|
- Aide contextuelle sur l'interface admin d'un jeu vidéo
|
||||||
|
|
||||||
|
Version 0.1 :
|
||||||
|
|
||||||
|
- Application de la licence EUPL v1.2
|
||||||
|
- Utilisation possible de l'application par Docker
|
||||||
|
- Interface web d'administration
|
||||||
|
- Liste de jeux joués
|
||||||
|
- Liste de ses jeux souhaités
|
||||||
|
- Historique des états d'un jeu vidéo, y compris sa création
|
||||||
|
- État d'un jeu vidéo parmi : Unfinished, beaten, completed, excluded, mastered, new
|
||||||
|
- Création de jeux vidéos liés à une console
|
||||||
|
- Création de consoles de jeux
|
@ -9,13 +9,16 @@ COPY requirements.txt $APPS_DIR/
|
|||||||
WORKDIR $APPS_DIR
|
WORKDIR $APPS_DIR
|
||||||
|
|
||||||
# uWSGI requires linux-headers
|
# uWSGI requires linux-headers
|
||||||
|
# django-split-settings needs ca-certificates as they changed on PyPi
|
||||||
RUN set -ex \
|
RUN set -ex \
|
||||||
&& buildDeps=' \
|
&& buildDeps=' \
|
||||||
build-base \
|
build-base \
|
||||||
|
ca-certificates \
|
||||||
linux-headers \
|
linux-headers \
|
||||||
python3-dev \
|
python3-dev \
|
||||||
' \
|
' \
|
||||||
&& apk --no-cache --update add \
|
&& apk --no-cache --update add \
|
||||||
|
gettext \
|
||||||
mailcap \
|
mailcap \
|
||||||
python3 \
|
python3 \
|
||||||
$buildDeps \
|
$buildDeps \
|
||||||
|
67
README.md
67
README.md
@ -13,17 +13,54 @@ Logiciel web permettant de :
|
|||||||
[](http://python.org/)
|
[](http://python.org/)
|
||||||
[](http://djangoproject.com/)
|
[](http://djangoproject.com/)
|
||||||
[](http://sqlite.org/)
|
[](http://sqlite.org/)
|
||||||
|
[](http://www.django-rest-framework.org/)
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
En s'appuyant sur [Docker](https://www.docker.com/), l'installation se déroule en plusieurs étapes :
|
Choisissez l'une des méthodes suivantes :
|
||||||
|
|
||||||
* la création d'une image Docker
|
* en utilisant **pip**, le gestionnaire de paquets Python. À utiliser en local ou en utilisant les [fameux environnements virtuels Python](http://sametmax.com/les-environnement-virtuels-python-virtualenv-et-virtualenvwrapper/)
|
||||||
* la création d'un dossier pour la base de données et la génération de cette dernière
|
* ou en s'appuyant sur [Docker](https://www.docker.com/)
|
||||||
* la création d'un utilisateur administrateur de la base de données (l'utilisateur principal)
|
|
||||||
* le lancement d'un conteneur Docker permettant d'accéder à l'interface Web de l'application
|
|
||||||
|
|
||||||
## Création de l'image Docker
|
## En utilisant pip
|
||||||
|
|
||||||
|
Vous devez disposer de **pip** sur votre machine, par exemple sur Debian/Ubuntu, il suffit de lancer la commande suivante :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install python3-pip
|
||||||
|
```
|
||||||
|
|
||||||
|
Ensuite on utilise les dépendances Python propres au projet :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd openbackloggery
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
On génère la base de données et on la rempli :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd collection
|
||||||
|
python3 manage.py migrate
|
||||||
|
python3 manage.py loaddata initial
|
||||||
|
python3 manage.py createsuperuser --user=superadmin --email=superadmin@domaine.tld
|
||||||
|
```
|
||||||
|
|
||||||
|
Remplacez **superadmin** par un identifiant que vous souhaiteriez avoir. Et mettez l'adresse courriel que vous voulez (car pour l'instant l'application n'utilise pas le courriel).
|
||||||
|
|
||||||
|
Il ne vous reste plus qu'à lancer l'application :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 manage.py runserver
|
||||||
|
```
|
||||||
|
|
||||||
|
et d'y accéder à l'adresse suivante : http://127.0.0.1:8000/admin/.
|
||||||
|
|
||||||
|
Pensez à jouer quand même ! ^_^
|
||||||
|
|
||||||
|
## En utilisant Docker
|
||||||
|
|
||||||
|
Avec Docker nous allons procéder en plusieurs étapes :
|
||||||
|
|
||||||
Nous appelerons notre image Docker **openbackloggery**. Nous considéréons que l'application se trouver dans un dossier nommé **openbackloggery** dans lequel se trouve un fichier *Dockerfile*.
|
Nous appelerons notre image Docker **openbackloggery**. Nous considéréons que l'application se trouver dans un dossier nommé **openbackloggery** dans lequel se trouve un fichier *Dockerfile*.
|
||||||
|
|
||||||
@ -32,8 +69,6 @@ cd openbackloggery
|
|||||||
docker build -t openbackloggery:0.1 .
|
docker build -t openbackloggery:0.1 .
|
||||||
```
|
```
|
||||||
|
|
||||||
## Base de données
|
|
||||||
|
|
||||||
Actuellement nous utilisons SQLite3 comme base de données car les fonctionnalités et la quantité de données ne requièrent pas un gestionnaire de base de données plus conséquent.
|
Actuellement nous utilisons SQLite3 comme base de données car les fonctionnalités et la quantité de données ne requièrent pas un gestionnaire de base de données plus conséquent.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -42,9 +77,7 @@ docker run -it --rm -v /openbackloggery_db:/opt/apps/db openbackloggery:0.1 pyth
|
|||||||
docker run -it --rm -v /openbackloggery_db:/opt/apps/db openbackloggery:0.1 python3 manage.py loaddata initial
|
docker run -it --rm -v /openbackloggery_db:/opt/apps/db openbackloggery:0.1 python3 manage.py loaddata initial
|
||||||
```
|
```
|
||||||
|
|
||||||
*loaddata initial* permet de charger quelques données initiales comme une liste de consoles par exemple.
|
*loaddata initial* permet de charger quelques données initiales comme une liste de plateformes par exemple.
|
||||||
|
|
||||||
## Création d'un utilisateur admin
|
|
||||||
|
|
||||||
Adaptez la ligne en remplaçant **admin** par le nom d'utilisateur que vous voulez et **admin@domaine.tld** par l'adresse courriel de votre choix (qu'elle existe ou non importe peu pour l'instant car elle n'est pas utilisée).
|
Adaptez la ligne en remplaçant **admin** par le nom d'utilisateur que vous voulez et **admin@domaine.tld** par l'adresse courriel de votre choix (qu'elle existe ou non importe peu pour l'instant car elle n'est pas utilisée).
|
||||||
|
|
||||||
@ -52,8 +85,6 @@ Adaptez la ligne en remplaçant **admin** par le nom d'utilisateur que vous voul
|
|||||||
docker run -it --rm -v /openbackloggery_db:/opt/apps/db openbackloggery:0.1 python3 manage.py createsuperuser --user=admin --email=admin@domaine.tld
|
docker run -it --rm -v /openbackloggery_db:/opt/apps/db openbackloggery:0.1 python3 manage.py createsuperuser --user=admin --email=admin@domaine.tld
|
||||||
```
|
```
|
||||||
|
|
||||||
## Lancement du conteneur Docker
|
|
||||||
|
|
||||||
Le service se lancera sur le **port 8282** et aura pour clé secrète **abcdefghijk** :
|
Le service se lancera sur le **port 8282** et aura pour clé secrète **abcdefghijk** :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -62,10 +93,16 @@ docker run -d -e SECRET_KEY='abcdefghijk' -v /openbackloggery_db:/opt/apps/db -p
|
|||||||
|
|
||||||
Veillez à modifier la clé secrète par une chaîne de caractère aléatoire assez longue et parsemée de majuscules, minuscules, ponctuation, etc.
|
Veillez à modifier la clé secrète par une chaîne de caractère aléatoire assez longue et parsemée de majuscules, minuscules, ponctuation, etc.
|
||||||
|
|
||||||
## Accès à l'application
|
|
||||||
|
|
||||||
Une fois le conteneur Docker lancé, il suffit d'ouvrir un navigateur Internet et d'y taper l'adresse suivante : http://127.0.0.1:8282/admin/.
|
Une fois le conteneur Docker lancé, il suffit d'ouvrir un navigateur Internet et d'y taper l'adresse suivante : http://127.0.0.1:8282/admin/.
|
||||||
|
|
||||||
|
# Adresses réticulaires disponibles
|
||||||
|
|
||||||
|
Les adresses à connaître :
|
||||||
|
|
||||||
|
* '/admin/' : Interface d'administration
|
||||||
|
* '/api/v1/' : Adresse de l'API RESTful
|
||||||
|
* '/api/v1/docs' : documentation de l'API
|
||||||
|
|
||||||
# Licence
|
# Licence
|
||||||
|
|
||||||
Ce logiciel est concédé sous [licence EUPL, version 1.2 uniquement](https://joinup.ec.europa.eu/community/eupl/og_page/eupl-text-11-12).
|
Ce logiciel est concédé sous [licence EUPL, version 1.2 uniquement](https://joinup.ec.europa.eu/community/eupl/og_page/eupl-text-11-12).
|
||||||
|
47
TODO
47
TODO
@ -1,13 +1,38 @@
|
|||||||
# À faire
|
# À faire
|
||||||
|
|
||||||
* Gérer les figurines (trouver le nom de l'objet qui permet de les regrouper, par exemple Skylanders, Disney Infinity, etc.)
|
* enlever force_authenticate des tests (en dépendances) puisque ça utiliser self.client
|
||||||
* Tests sur Console
|
* Trouver des états supplémentaires aux Figurines (complétée ?)
|
||||||
* Tests sur Game
|
* Ajout des Timeline sur les Figurines
|
||||||
* Données initiales des consoles connues
|
* Figurine API + tests
|
||||||
* API django rest framework
|
* étudier la possibilité à l'utilisateur, via des variables d'environnement, de configurer un email, une autre BDD, etc.
|
||||||
* documentation API
|
|
||||||
|
## Dépôt / code
|
||||||
|
|
||||||
|
* README : Déplacer les informations d'installation de Docker dans une documentation autre
|
||||||
|
|
||||||
|
## Fonctionnalités
|
||||||
|
|
||||||
|
* Ajouter la liste des aptitudes (Ability) d'une figurine
|
||||||
|
* Unplayed ne doit pas s'afficher si on a un état différent de Unfinished
|
||||||
|
* ajouter une date d'obtention du jeu vidéo (par défaut aujourd'hui) => est-ce vraiment utile si on a la Timeline ?
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
* Sur Platform
|
||||||
|
* Sur Game
|
||||||
|
* API : sur Game, pour les champs wish, unplayed, playing et status
|
||||||
* travis.yml to launch test
|
* travis.yml to launch test
|
||||||
* ajouter une date d'obtention du jeu vidéo (par défaut aujourd'hui)
|
* Selenium pour vérifier :
|
||||||
|
* que Nouveau n'apparaisse pas dans la liste des états possibles
|
||||||
|
* que Nouveau n'apparaisse pas dans les filtres
|
||||||
|
|
||||||
|
## Données
|
||||||
|
|
||||||
|
* Données initiales des consoles connues
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
* documentation du projet : installation par Docker, utilisation des variables d'environnement pour changer différentes choses, etc.
|
||||||
|
|
||||||
# Idée
|
# Idée
|
||||||
|
|
||||||
@ -15,3 +40,11 @@
|
|||||||
* Faire une API en vue d'un site web en VueJS ?
|
* Faire une API en vue d'un site web en VueJS ?
|
||||||
* Inclure une récupération sur HowLongToBeat ?
|
* Inclure une récupération sur HowLongToBeat ?
|
||||||
* Gérer plusieurs utilisateurs ? Si oui, comment gérer la liste des jeux par utilisateur ? => à faire dans l'interface web peut-être ?
|
* Gérer plusieurs utilisateurs ? Si oui, comment gérer la liste des jeux par utilisateur ? => à faire dans l'interface web peut-être ?
|
||||||
|
* Proposer de jouer un jeu aléatoire parmi la Liste des jeux jamais joués ou détenus (mais pas dans la wish list)
|
||||||
|
* Proposer un système de gestion de prêt à une personne ? Je prête à quelqu'un, je note la date de début de prêt. Et je note juste quand je l'ai récupéré => historique des prêts
|
||||||
|
* changer de nom :
|
||||||
|
* kolektor (super-héros qui collecte)
|
||||||
|
* Lambda Collection (les collections pour les utilisateurs Lambda)
|
||||||
|
* Kolegeektor (ou collegeektor)
|
||||||
|
* Accumulation, monceau, monticule (en anglais : the hill = petite colline), barda (attirail très lourd sur le dos)
|
||||||
|
* backloggery = la boutique aux backlogs, c'est à dire la boutique des accumulations. Le magasin d'accumulation. C'est notre "Caverne d'Ali Baba". Boutique à souvenirs. La pile de tâches à faire.
|
||||||
|
@ -1 +1 @@
|
|||||||
__version__ = '0.1'
|
__version__ = '0.2-trunk'
|
||||||
|
0
collection/collection/components/__init__.py
Normal file
0
collection/collection/components/__init__.py
Normal file
17
collection/collection/components/api.py
Normal file
17
collection/collection/components/api.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# API - Django Rest Framework
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_PERMISSION_CLASSES': [
|
||||||
|
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly',
|
||||||
|
],
|
||||||
|
'TEST_REQUEST_DEFAULT_FORMAT': 'json',
|
||||||
|
'UNICODE_JSON': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
CORS_ORIGIN_ALLOW_ALL = False # disallow all website for cross site requests
|
||||||
|
|
||||||
|
# Authorized website for cross site requests
|
||||||
|
CORS_ORIGIN_WHITELIST = (
|
||||||
|
'localhost:8000',
|
||||||
|
'127.0.0.1:8000'
|
||||||
|
)
|
98
collection/collection/components/common.py
Normal file
98
collection/collection/components/common.py
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
"""
|
||||||
|
Django settings for collection project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 1.11.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/1.11/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/1.11/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__)))
|
||||||
|
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = (
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
'rest_framework',
|
||||||
|
'corsheaders',
|
||||||
|
'core',
|
||||||
|
'games.apps.GamesConfig',
|
||||||
|
'figurines.apps.FigurinesConfig', )
|
||||||
|
|
||||||
|
MIDDLEWARE = (
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.locale.LocaleMiddleware',
|
||||||
|
'corsheaders.middleware.CorsMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware', )
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'collection.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 = 'collection.wsgi.application'
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/1.11/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',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/1.11/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = '/static/'
|
||||||
|
STATIC_ROOT = os.path.join(os.path.abspath(os.path.curdir), 'static')
|
||||||
|
if os.getenv('STATIC_ROOT', None):
|
||||||
|
STATIC_ROOT = os.path.abspath(os.getenv('STATIC_ROOT'))
|
||||||
|
|
||||||
|
STATICFILES_FINDERS = (
|
||||||
|
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||||
|
'django.contrib.staticfiles.finders.AppDirectoriesFinder', )
|
15
collection/collection/components/database.py
Normal file
15
collection/collection/components/database.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
|
||||||
|
|
||||||
|
DB_DIR = os.getenv('DB_DIR', os.path.join(BASE_DIR, 'db'))
|
||||||
|
|
||||||
|
if not os.path.exists(DB_DIR):
|
||||||
|
os.makedirs(DB_DIR)
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
'NAME': os.path.join(BASE_DIR, 'db', 'db.sqlite3'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
23
collection/collection/components/i18n.py
Normal file
23
collection/collection/components/i18n.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/1.11/topics/i18n/
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
LANGUAGE_CODE = os.getenv('LANGUAGE_CODE', 'fr')
|
||||||
|
|
||||||
|
LANGUAGES = (
|
||||||
|
('fr', _('French')),
|
||||||
|
('en', _('English')), )
|
||||||
|
|
||||||
|
TIME_ZONE = 'UTC'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_L10N = True
|
||||||
|
|
||||||
|
USE_TZ = False
|
||||||
|
|
||||||
|
# Translation directories
|
||||||
|
LOCALE_PATHS = [
|
||||||
|
os.path.join(BASE_DIR, 'conf/locale'),
|
||||||
|
os.path.join(BASE_DIR, 'collection/locale')
|
||||||
|
]
|
0
collection/collection/environments/__init__.py
Normal file
0
collection/collection/environments/__init__.py
Normal file
18
collection/collection/environments/development.py
Normal file
18
collection/collection/environments/development.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = 'tqma23#v!#ecse_gz_u(1oa6+x%1uyi718an9%nefqhi$0q_eg'
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = ['*']
|
||||||
|
|
||||||
|
# CORS: allow all site to make cross site requests
|
||||||
|
CORS_ORIGIN_ALLOW_ALL = True
|
||||||
|
|
||||||
|
MIDDLEWARE += ('debug_toolbar.middleware.DebugToolbarMiddleware', )
|
||||||
|
|
||||||
|
# Add Django debug toolbar
|
||||||
|
INSTALLED_APPS += ('debug_toolbar', )
|
||||||
|
INTERNAL_IPS = ['127.0.0.1']
|
45
collection/collection/environments/production.py
Normal file
45
collection/collection/environments/production.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
"""
|
||||||
|
Django production settings
|
||||||
|
"""
|
||||||
|
|
||||||
|
DEBUG = os.getenv('DEBUG', False)
|
||||||
|
|
||||||
|
SECRET_KEY = os.getenv('SECRET_KEY')
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = [os.getenv('ALLOWED_HOSTS', '*')]
|
||||||
|
|
||||||
|
# Domain configuration:
|
||||||
|
# - if you have a specific domain to secure, change ALLOWED_HOSTS like this:
|
||||||
|
# ALLOWED_HOSTS = ['domain.tld', 'domain.tld.']
|
||||||
|
# - if you use a proxy like Nginx, you need to add this line into config:
|
||||||
|
# `proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`
|
||||||
|
USE_X_FORWARDED_HOST = True
|
||||||
|
|
||||||
|
# CORS: allow all site to make cross site requests
|
||||||
|
CORS_ORIGIN_ALLOW_ALL = True
|
||||||
|
|
||||||
|
# CORS: limit to API only
|
||||||
|
CORS_URLS_REGEX = r'^/api/.*$'
|
||||||
|
|
||||||
|
# WARNING: you need to install psycopg2 with pip
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||||
|
'NAME': os.getenv('POSTGRES_DB', 'postgres'),
|
||||||
|
'USER': os.getenv('POSTGRES_USER', 'postgres'),
|
||||||
|
'PASSWORD': os.getenv('POSTGRES_PASS', 'postgres'),
|
||||||
|
'HOST': os.getenv('POSTGRES_HOST', '127.0.0.1'),
|
||||||
|
'PORT': os.getenv('POSTGRES_PORT', '5432'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Security
|
||||||
|
SECURE_HSTS_SECONDS = 3600
|
||||||
|
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||||
|
SECURE_HSTS_PRELOAD = True
|
||||||
|
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||||
|
SECURE_BROWSER_XSS_FILTER = True
|
||||||
|
SECURE_SSL_REDIRECT = True
|
||||||
|
SESSION_COOKIE_SECURE = True
|
||||||
|
CSRF_COOKIE_SECURE = True
|
||||||
|
X_FRAME_OPTIONS = 'DENY'
|
26
collection/collection/locale/fr/LC_MESSAGES/django.po
Normal file
26
collection/collection/locale/fr/LC_MESSAGES/django.po
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# OpenBackloggery.
|
||||||
|
# Copyright (C) 2017
|
||||||
|
# This file is distributed under the same license as the openbackloggery package.
|
||||||
|
# Olivier DOSSMANN <git@dossmann.net>, 2017.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: 0.1\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2018-01-15 20:10+0000\n"
|
||||||
|
"PO-Revision-Date: 2017-09-16 17:16+0200\n"
|
||||||
|
"Last-Translator: Olivier DOSSMANN <git@dossmann.net>\n"
|
||||||
|
"Language-Team: \n"
|
||||||
|
"Language: fr\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"X-Generator: Poedit 2.0.3\n"
|
||||||
|
|
||||||
|
#: collection/components/i18n.py:8
|
||||||
|
msgid "French"
|
||||||
|
msgstr "Français"
|
||||||
|
|
||||||
|
#: collection/components/i18n.py:9
|
||||||
|
msgid "English"
|
||||||
|
msgstr "Anglais"
|
@ -1,18 +0,0 @@
|
|||||||
"""
|
|
||||||
Django production settings
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
from collection.settings import * # NOQA
|
|
||||||
|
|
||||||
DEBUG = os.getenv('DEBUG', False)
|
|
||||||
|
|
||||||
SECRET_KEY = os.getenv('SECRET_KEY')
|
|
||||||
|
|
||||||
ALLOWED_HOSTS = [os.getenv('ALLOWED_HOSTS', '*')]
|
|
||||||
|
|
||||||
# Domain configuration:
|
|
||||||
# - if you have a specific domain to secure, change ALLOWED_HOSTS like this:
|
|
||||||
# ALLOWED_HOSTS = ['domain.tld', 'domain.tld.']
|
|
||||||
# - if you use a proxy like Nginx, you need to add this line into config:
|
|
||||||
# `proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`
|
|
||||||
USE_X_FORWARDED_HOST = True
|
|
@ -1,125 +1,30 @@
|
|||||||
"""
|
"""
|
||||||
Django settings for collection project.
|
This is a django-split-settings main file.
|
||||||
|
For more information read this:
|
||||||
|
https://github.com/sobolevn/django-split-settings
|
||||||
|
|
||||||
Generated by 'django-admin startproject' using Django 1.11.
|
Default environment is `developement`.
|
||||||
|
|
||||||
For more information on this file, see
|
To change settings file:
|
||||||
https://docs.djangoproject.com/en/1.11/topics/settings/
|
`DJANGO_ENV=production python manage.py runserver`
|
||||||
|
|
||||||
For the full list of settings and their values, see
|
|
||||||
https://docs.djangoproject.com/en/1.11/ref/settings/
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
from split_settings.tools import optional, include
|
||||||
|
from os import environ
|
||||||
|
|
||||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
ENV = environ.get('DJANGO_ENV') or 'development'
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
|
|
||||||
|
base_settings = [
|
||||||
|
'components/common.py', # standard django settings
|
||||||
|
'components/database.py', # SQLite 3
|
||||||
|
'components/i18n.py', # Internationalisation and localization
|
||||||
|
'components/api.py', # API (django rest framework) + CORS
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# Select the right env:
|
||||||
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
|
'environments/%s.py' % ENV,
|
||||||
|
# Optionally override some settings:
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
optional('environments/local_settings.py'),
|
||||||
SECRET_KEY = 'tqma23#v!#ecse_gz_u(1oa6+x%1uyi718an9%nefqhi$0q_eg'
|
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
|
||||||
DEBUG = True
|
|
||||||
|
|
||||||
ALLOWED_HOSTS = []
|
|
||||||
|
|
||||||
|
|
||||||
# Application definition
|
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
|
||||||
'django.contrib.admin',
|
|
||||||
'django.contrib.auth',
|
|
||||||
'django.contrib.contenttypes',
|
|
||||||
'django.contrib.sessions',
|
|
||||||
'django.contrib.messages',
|
|
||||||
'django.contrib.staticfiles',
|
|
||||||
'core',
|
|
||||||
'games.apps.GamesConfig',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
# Include settings:
|
||||||
'django.middleware.security.SecurityMiddleware',
|
include(*base_settings)
|
||||||
'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 = 'collection.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 = 'collection.wsgi.application'
|
|
||||||
|
|
||||||
|
|
||||||
# Database
|
|
||||||
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
|
|
||||||
|
|
||||||
DATABASES = {
|
|
||||||
'default': {
|
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
|
||||||
'NAME': os.path.join(BASE_DIR, 'db', 'db.sqlite3'),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Password validation
|
|
||||||
# https://docs.djangoproject.com/en/1.11/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.11/topics/i18n/
|
|
||||||
|
|
||||||
LANGUAGE_CODE = 'en-us'
|
|
||||||
|
|
||||||
TIME_ZONE = 'UTC'
|
|
||||||
|
|
||||||
USE_I18N = True
|
|
||||||
|
|
||||||
USE_L10N = False
|
|
||||||
|
|
||||||
USE_TZ = False
|
|
||||||
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
|
||||||
# https://docs.djangoproject.com/en/1.11/howto/static-files/
|
|
||||||
|
|
||||||
STATIC_URL = '/static/'
|
|
||||||
STATIC_ROOT = os.path.join(os.path.abspath(os.path.curdir), 'static')
|
|
||||||
if os.getenv('STATIC_ROOT', None):
|
|
||||||
STATIC_ROOT = os.path.abspath(os.getenv('STATIC_ROOT'))
|
|
||||||
|
@ -13,15 +13,49 @@ Including another URLconf
|
|||||||
1. Import the include() function: from django.conf.urls import url, include
|
1. Import the include() function: from django.conf.urls import url, include
|
||||||
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
|
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
|
||||||
"""
|
"""
|
||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls import include
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from collection import __version__ as app_version
|
from django.views.generic import TemplateView
|
||||||
|
from figurines.views import FigurineViewSet
|
||||||
|
from figurines.views import SetViewSet
|
||||||
|
from games.views import GameTimelineViewSet
|
||||||
|
from games.views import GameViewSet
|
||||||
|
from games.views import PlatformViewSet
|
||||||
|
from rest_framework import routers
|
||||||
|
from rest_framework.documentation import include_docs_urls
|
||||||
|
|
||||||
|
from collection import __version__ as app_version
|
||||||
|
|
||||||
# Admin config
|
# Admin config
|
||||||
admin.site.site_title = 'OpenBackloggery'
|
admin.site.site_title = 'OpenBackloggery'
|
||||||
admin.site.site_header = '%s %s' % (admin.site.site_title, app_version)
|
admin.site.site_header = '%s %s' % (admin.site.site_title, app_version)
|
||||||
|
|
||||||
|
# Django Rest Framework router
|
||||||
|
router = routers.DefaultRouter()
|
||||||
|
router.register(r'games', GameViewSet)
|
||||||
|
router.register(r'figurines', FigurineViewSet)
|
||||||
|
router.register(r'platforms', PlatformViewSet)
|
||||||
|
router.register(r'sets', SetViewSet)
|
||||||
|
router.register(
|
||||||
|
r'game_timelines', GameTimelineViewSet, base_name='game_timeline')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
url(r'^$',
|
||||||
|
TemplateView.as_view(template_name="core/homepage.html"),
|
||||||
|
name='homepage'),
|
||||||
|
url(r'^games/', include('games.urls', namespace='games'), name='games'),
|
||||||
url(r'^admin/', admin.site.urls),
|
url(r'^admin/', admin.site.urls),
|
||||||
|
url(r'^api/v1/', include(router.urls), name='api'),
|
||||||
|
url(r'^api/v1/docs/',
|
||||||
|
include_docs_urls(title=' '.join([admin.site.site_title, 'API']))),
|
||||||
|
url(r'^api-auth/',
|
||||||
|
include('rest_framework.urls', namespace='rest_framework'))
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if settings.DEBUG:
|
||||||
|
import debug_toolbar
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^__debug__/', include(debug_toolbar.urls)),
|
||||||
|
] + urlpatterns
|
||||||
|
272
collection/conf/locale/fr/LC_MESSAGES/django.po
Normal file
272
collection/conf/locale/fr/LC_MESSAGES/django.po
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
# OpenBackloggery.
|
||||||
|
# Copyright (C) 2017
|
||||||
|
# This file is distributed under the same license as the openbackloggery package.
|
||||||
|
# Olivier DOSSMANN <git@dossmann.net>, 2017.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: 0.1\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2018-02-19 21:17+0000\n"
|
||||||
|
"PO-Revision-Date: 2018-02-19 22:18+0100\n"
|
||||||
|
"Last-Translator: \n"
|
||||||
|
"Language-Team: \n"
|
||||||
|
"Language: fr\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"X-Generator: Poedit 2.0.6\n"
|
||||||
|
|
||||||
|
#: core/models.py:9 core/models.py:29
|
||||||
|
msgid "name"
|
||||||
|
msgstr "nom"
|
||||||
|
|
||||||
|
#: core/models.py:10
|
||||||
|
msgid "shortname"
|
||||||
|
msgstr "nom court"
|
||||||
|
|
||||||
|
#: core/models.py:26
|
||||||
|
msgid "New"
|
||||||
|
msgstr "Nouveau"
|
||||||
|
|
||||||
|
#: core/models.py:30
|
||||||
|
msgid "achievement"
|
||||||
|
msgstr "progression"
|
||||||
|
|
||||||
|
#: core/models.py:31
|
||||||
|
msgid "out of"
|
||||||
|
msgstr "sur"
|
||||||
|
|
||||||
|
#: core/models.py:51 core/models.py:84
|
||||||
|
msgid "status"
|
||||||
|
msgstr "état"
|
||||||
|
|
||||||
|
#: core/models.py:70
|
||||||
|
msgid "date"
|
||||||
|
msgstr "date"
|
||||||
|
|
||||||
|
#: figurines/admin.py:9
|
||||||
|
msgid "Information"
|
||||||
|
msgstr "Information"
|
||||||
|
|
||||||
|
#: figurines/admin.py:11 games/admin.py:65
|
||||||
|
msgid "Progress"
|
||||||
|
msgstr "Progression"
|
||||||
|
|
||||||
|
#: figurines/models.py:17 figurines/models.py:32
|
||||||
|
msgid "set"
|
||||||
|
msgstr "collection"
|
||||||
|
|
||||||
|
#: figurines/models.py:18
|
||||||
|
msgid "sets"
|
||||||
|
msgstr "collections"
|
||||||
|
|
||||||
|
#: figurines/models.py:22
|
||||||
|
msgid "Usual name of figurines set."
|
||||||
|
msgstr "Nom habituel de la collection de figurines."
|
||||||
|
|
||||||
|
#: figurines/models.py:47
|
||||||
|
msgid "Character"
|
||||||
|
msgstr "Personnage"
|
||||||
|
|
||||||
|
#: figurines/models.py:48
|
||||||
|
msgid "Vehicle"
|
||||||
|
msgstr "Véhicule"
|
||||||
|
|
||||||
|
#: figurines/models.py:49
|
||||||
|
msgid "Level pack"
|
||||||
|
msgstr "Niveau supplémentaire"
|
||||||
|
|
||||||
|
#: figurines/models.py:50
|
||||||
|
msgid "Gadget"
|
||||||
|
msgstr "Gadget"
|
||||||
|
|
||||||
|
#: figurines/models.py:51
|
||||||
|
msgid "Battle pack"
|
||||||
|
msgstr "Arène supplémentaire"
|
||||||
|
|
||||||
|
#: figurines/models.py:52
|
||||||
|
msgid "Expansion pack"
|
||||||
|
msgstr "Extension"
|
||||||
|
|
||||||
|
#: figurines/models.py:53
|
||||||
|
msgid "Trophy"
|
||||||
|
msgstr "Trophé"
|
||||||
|
|
||||||
|
#: figurines/models.py:54
|
||||||
|
msgid "Trap"
|
||||||
|
msgstr "Piège"
|
||||||
|
|
||||||
|
#: figurines/models.py:59
|
||||||
|
msgid "kind"
|
||||||
|
msgstr "sorte"
|
||||||
|
|
||||||
|
#: figurines/models.py:63 games/models.py:67
|
||||||
|
msgid "wish?"
|
||||||
|
msgstr "envie de l'avoir ?"
|
||||||
|
|
||||||
|
#: figurines/models.py:64
|
||||||
|
msgid "You need this figurine."
|
||||||
|
msgstr "Vous avez besoin de cette figurine."
|
||||||
|
|
||||||
|
#: figurines/models.py:68
|
||||||
|
msgid "coins"
|
||||||
|
msgstr "monnaie"
|
||||||
|
|
||||||
|
#: figurines/models.py:75
|
||||||
|
msgid "Figurine denomination"
|
||||||
|
msgstr "Dénomination de la figurine"
|
||||||
|
|
||||||
|
#: figurines/models.py:76
|
||||||
|
msgid "Becoming set"
|
||||||
|
msgstr "Collection d'où cela provient"
|
||||||
|
|
||||||
|
#: figurines/models.py:77
|
||||||
|
msgid "Figurine progression"
|
||||||
|
msgstr "Progression de la figurine"
|
||||||
|
|
||||||
|
#: games/admin.py:17
|
||||||
|
msgid "state"
|
||||||
|
msgstr "état"
|
||||||
|
|
||||||
|
#: games/admin.py:40 games/admin.py:87 games/models.py:15 games/models.py:29
|
||||||
|
msgid "platform"
|
||||||
|
msgstr "plateforme"
|
||||||
|
|
||||||
|
#: games/admin.py:63
|
||||||
|
msgid "Game Information"
|
||||||
|
msgstr "Information du jeu"
|
||||||
|
|
||||||
|
#: games/admin.py:79 games/admin.py:117
|
||||||
|
msgid "Platform"
|
||||||
|
msgstr "Plateforme"
|
||||||
|
|
||||||
|
#: games/models.py:16
|
||||||
|
msgid "platforms"
|
||||||
|
msgstr "plateformes"
|
||||||
|
|
||||||
|
#: games/models.py:20
|
||||||
|
msgid "Most used platform name."
|
||||||
|
msgstr "Nom de plateforme le plus communément utilisé."
|
||||||
|
|
||||||
|
#: games/models.py:40
|
||||||
|
msgid "Beaten"
|
||||||
|
msgstr "Terminé (quête principale)"
|
||||||
|
|
||||||
|
#: games/models.py:41
|
||||||
|
msgid "Completed"
|
||||||
|
msgstr "Terminé complètement"
|
||||||
|
|
||||||
|
#: games/models.py:42
|
||||||
|
msgid "Excluded"
|
||||||
|
msgstr "Exclu"
|
||||||
|
|
||||||
|
#: games/models.py:43
|
||||||
|
msgid "Mastered"
|
||||||
|
msgstr "Usé / Épuisé"
|
||||||
|
|
||||||
|
#: games/models.py:44
|
||||||
|
msgid "Unfinished"
|
||||||
|
msgstr "Inachevé"
|
||||||
|
|
||||||
|
#: games/models.py:53
|
||||||
|
msgid "Progress note"
|
||||||
|
msgstr "Note de progression"
|
||||||
|
|
||||||
|
#: games/models.py:54
|
||||||
|
msgid "Short note displayed to your followers."
|
||||||
|
msgstr "Courte note affichée à ceux qui vous suive."
|
||||||
|
|
||||||
|
#: games/models.py:59
|
||||||
|
msgid "playing?"
|
||||||
|
msgstr "en train d'y jouer ?"
|
||||||
|
|
||||||
|
#: games/models.py:60
|
||||||
|
msgid "You're currently playing this game."
|
||||||
|
msgstr "Vous jouez actuellement à ce jeu."
|
||||||
|
|
||||||
|
#: games/models.py:63
|
||||||
|
msgid "unplayed?"
|
||||||
|
msgstr "jamais joué ?"
|
||||||
|
|
||||||
|
#: games/models.py:64
|
||||||
|
msgid "You never played this game."
|
||||||
|
msgstr "Vous n'avez jamais joué à ce jeu."
|
||||||
|
|
||||||
|
#: games/models.py:68
|
||||||
|
msgid "You're waiting X-mas father offers you this game."
|
||||||
|
msgstr "Vous patientez que le père Noël vous offre ce jeu."
|
||||||
|
|
||||||
|
#: games/models.py:72
|
||||||
|
msgid "game"
|
||||||
|
msgstr "jeu"
|
||||||
|
|
||||||
|
#: games/models.py:73
|
||||||
|
msgid "games"
|
||||||
|
msgstr "jeux"
|
||||||
|
|
||||||
|
#: games/models.py:77
|
||||||
|
msgid "Game title"
|
||||||
|
msgstr "Titre du jeu"
|
||||||
|
|
||||||
|
#: games/models.py:78
|
||||||
|
msgid "Game running platform"
|
||||||
|
msgstr "Plateforme sur laquelle se lance le jeu"
|
||||||
|
|
||||||
|
#: games/models.py:79
|
||||||
|
msgid "Game progression"
|
||||||
|
msgstr "Progression du jeu"
|
||||||
|
|
||||||
|
#: games/models.py:90
|
||||||
|
msgid "Timeline"
|
||||||
|
msgstr "Chronologie"
|
||||||
|
|
||||||
|
#: games/models.py:91
|
||||||
|
msgid "Timelines"
|
||||||
|
msgstr "Chronologies"
|
||||||
|
|
||||||
|
#: games/models.py:95
|
||||||
|
msgid "Status change date"
|
||||||
|
msgstr "Date de changement de statut"
|
||||||
|
|
||||||
|
#: games/models.py:96
|
||||||
|
msgid "Which game?"
|
||||||
|
msgstr "Quel jeu ?"
|
||||||
|
|
||||||
|
#: games/models.py:98
|
||||||
|
msgid "New status for this game at the given date"
|
||||||
|
msgstr "Nouveau statut pour ce jeu à la date donnée"
|
||||||
|
|
||||||
|
#: games/templates/games/index.html:5
|
||||||
|
msgid "Games"
|
||||||
|
msgstr "Jeux"
|
||||||
|
|
||||||
|
#: games/templates/games/index.html:13
|
||||||
|
msgid "No game found."
|
||||||
|
msgstr "Aucun jeu."
|
||||||
|
|
||||||
|
#: games/templates/games/summary.html:5
|
||||||
|
msgid "My summary"
|
||||||
|
msgstr "Mon résumé"
|
||||||
|
|
||||||
|
#: games/templates/games/summary.html:6
|
||||||
|
msgid "Now playing"
|
||||||
|
msgstr "En train d'y jouer"
|
||||||
|
|
||||||
|
#: games/templates/games/summary.html:15
|
||||||
|
msgid "No playing game found."
|
||||||
|
msgstr "Aucun jeu en cours."
|
||||||
|
|
||||||
|
#: games/templates/games/summary.html:17
|
||||||
|
msgid "Memory Card"
|
||||||
|
msgstr "Carte mémoire"
|
||||||
|
|
||||||
|
#: games/templates/games/summary.html:33
|
||||||
|
msgid "Empty memory."
|
||||||
|
msgstr "Mémoire vide."
|
||||||
|
|
||||||
|
#~ msgid "Complete list"
|
||||||
|
#~ msgstr "Liste complète"
|
||||||
|
|
||||||
|
#~ msgid "created"
|
||||||
|
#~ msgstr "créé"
|
@ -1,10 +1,13 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
|
||||||
class Collection(models.Model):
|
class Collection(models.Model):
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(
|
||||||
|
max_length=255, verbose_name=_('name'), unique=True)
|
||||||
|
shortname = models.CharField(max_length=30, verbose_name=_('shortname'))
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '%s' % self.name
|
return '%s' % self.name
|
||||||
@ -20,12 +23,12 @@ class Item(models.Model):
|
|||||||
|
|
||||||
# status choices
|
# status choices
|
||||||
CREATED = 'created'
|
CREATED = 'created'
|
||||||
STATUS_CHOICES = (
|
STATUS_CHOICES = ((CREATED, _('New')), )
|
||||||
(CREATED, _('New')),
|
|
||||||
)
|
|
||||||
DEFAULT_CHOICE = CREATED
|
DEFAULT_CHOICE = CREATED
|
||||||
|
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255, verbose_name=_('name'))
|
||||||
|
achievement = models.PositiveIntegerField(blank=True, null=True, verbose_name=_('achievement'))
|
||||||
|
achievement_max = models.PositiveIntegerField(blank=True, null=True, verbose_name=_('out of'))
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '%s' % self.name
|
return '%s' % self.name
|
||||||
@ -39,13 +42,16 @@ class Item(models.Model):
|
|||||||
target_field = models.ForeignKey(
|
target_field = models.ForeignKey(
|
||||||
cls.TARGET_MODEL,
|
cls.TARGET_MODEL,
|
||||||
related_name=cls.RELATED_TARGET_NAME,
|
related_name=cls.RELATED_TARGET_NAME,
|
||||||
verbose_name=cls.TARGET_VERBOSE_NAME)
|
verbose_name=_(cls.TARGET_VERBOSE_NAME))
|
||||||
target_field.contribute_to_class(cls, 'collection')
|
target_field.contribute_to_class(cls, 'collection')
|
||||||
status_field = models.CharField(
|
status_field = models.CharField(
|
||||||
max_length=30,
|
max_length=30,
|
||||||
choices=Item.STATUS_CHOICES + cls.STATUS_CHOICES,
|
choices=Item.STATUS_CHOICES + cls.STATUS_CHOICES,
|
||||||
default=cls.DEFAULT_CHOICE)
|
default=cls.DEFAULT_CHOICE,
|
||||||
|
verbose_name=_('status'))
|
||||||
status_field.contribute_to_class(cls, 'status')
|
status_field.contribute_to_class(cls, 'status')
|
||||||
|
# check that no other same item name on same collection exists
|
||||||
|
cls._meta.unique_together = (('collection', 'name'), )
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
@ -58,29 +64,30 @@ class Timeline(models.Model):
|
|||||||
For an example, a game cretion date. Or when you completed a game.
|
For an example, a game cretion date. Or when you completed a game.
|
||||||
"""
|
"""
|
||||||
TARGET_MODEL = None
|
TARGET_MODEL = None
|
||||||
|
TARGET_VERBOSE_NAME = None
|
||||||
STATUS_CHOICES = Item.STATUS_CHOICES
|
STATUS_CHOICES = Item.STATUS_CHOICES
|
||||||
DEFAULT_CHOICE = Item.CREATED
|
DEFAULT_CHOICE = Item.CREATED
|
||||||
date = models.DateTimeField(default=datetime.now)
|
date = models.DateField(default=datetime.now, verbose_name=_('date'))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def on_class_prepared(cls):
|
def on_class_prepared(cls):
|
||||||
"""
|
"""
|
||||||
Add new field 'item' which is a link to TARGET_MODEL
|
Add new field 'item' which is a link to TARGET_MODEL
|
||||||
"""
|
"""
|
||||||
target_field = models.ForeignKey(cls.TARGET_MODEL)
|
target_field = models.ForeignKey(
|
||||||
|
cls.TARGET_MODEL, verbose_name=_(cls.TARGET_VERBOSE_NAME))
|
||||||
target_field.contribute_to_class(cls, 'item')
|
target_field.contribute_to_class(cls, 'item')
|
||||||
status_field = models.CharField(
|
status_field = models.CharField(
|
||||||
max_length=30,
|
max_length=30,
|
||||||
choices=Item.STATUS_CHOICES + cls.STATUS_CHOICES,
|
choices=Item.STATUS_CHOICES + cls.STATUS_CHOICES,
|
||||||
default=cls.DEFAULT_CHOICE)
|
default=cls.DEFAULT_CHOICE,
|
||||||
|
verbose_name=_('status'))
|
||||||
status_field.contribute_to_class(cls, 'status')
|
status_field.contribute_to_class(cls, 'status')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '%s: %s - %s' % (
|
return '%s: %s - %s' % (self.date.strftime('%Y-%m-%d'), self.status,
|
||||||
self.date.strftime('%Y-%m-%d'),
|
self.item)
|
||||||
self.status,
|
|
||||||
self.item)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
ordering = ('-date',)
|
ordering = ('-date', )
|
||||||
|
4
collection/core/static/css/font-awesome.min.css
vendored
Normal file
4
collection/core/static/css/font-awesome.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
27
collection/core/static/css/ie8.css
Normal file
27
collection/core/static/css/ie8.css
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
Eventually by HTML5 UP
|
||||||
|
html5up.net | @ajlkn
|
||||||
|
Free for personal and commercial use under the CCA 3.0 license (html5up.net/license)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* BG */
|
||||||
|
|
||||||
|
#bg {
|
||||||
|
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=25)";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Type */
|
||||||
|
|
||||||
|
body, input, select, textarea {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form */
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
input[type="password"],
|
||||||
|
input[type="email"],
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
border: solid 2px #fff;
|
||||||
|
}
|
12
collection/core/static/css/ie9.css
Normal file
12
collection/core/static/css/ie9.css
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
Eventually by HTML5 UP
|
||||||
|
html5up.net | @ajlkn
|
||||||
|
Free for personal and commercial use under the CCA 3.0 license (html5up.net/license)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Signup Form */
|
||||||
|
|
||||||
|
#signup-form > * {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
1187
collection/core/static/css/main.css
Normal file
1187
collection/core/static/css/main.css
Normal file
File diff suppressed because it is too large
Load Diff
BIN
collection/core/static/fonts/FontAwesome.otf
Normal file
BIN
collection/core/static/fonts/FontAwesome.otf
Normal file
Binary file not shown.
BIN
collection/core/static/fonts/fontawesome-webfont.eot
Normal file
BIN
collection/core/static/fonts/fontawesome-webfont.eot
Normal file
Binary file not shown.
685
collection/core/static/fonts/fontawesome-webfont.svg
Normal file
685
collection/core/static/fonts/fontawesome-webfont.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 382 KiB |
BIN
collection/core/static/fonts/fontawesome-webfont.ttf
Normal file
BIN
collection/core/static/fonts/fontawesome-webfont.ttf
Normal file
Binary file not shown.
BIN
collection/core/static/fonts/fontawesome-webfont.woff
Normal file
BIN
collection/core/static/fonts/fontawesome-webfont.woff
Normal file
Binary file not shown.
BIN
collection/core/static/fonts/fontawesome-webfont.woff2
Normal file
BIN
collection/core/static/fonts/fontawesome-webfont.woff2
Normal file
Binary file not shown.
BIN
collection/core/static/images/bg01.jpg
Normal file
BIN
collection/core/static/images/bg01.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 870 KiB |
BIN
collection/core/static/images/bg02.jpg
Normal file
BIN
collection/core/static/images/bg02.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 MiB |
8
collection/core/static/js/ie/html5shiv.js
vendored
Normal file
8
collection/core/static/js/ie/html5shiv.js
vendored
Normal file
File diff suppressed because one or more lines are too long
6
collection/core/static/js/ie/respond.min.js
vendored
Normal file
6
collection/core/static/js/ie/respond.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
173
collection/core/static/js/main.js
Normal file
173
collection/core/static/js/main.js
Normal file
File diff suppressed because one or more lines are too long
11
collection/core/templates/base.html
Normal file
11
collection/core/templates/base.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
{% load i18n %}{% get_current_language as LANGUAGE_CODE %}<html lang="{{ LANGUAGE_CODE }}">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
|
||||||
|
<title>OpenBackloggery</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
43
collection/core/templates/core/homepage.html
Normal file
43
collection/core/templates/core/homepage.html
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{% load static %}
|
||||||
|
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<!--
|
||||||
|
Eventually by HTML5 UP
|
||||||
|
html5up.net | @ajlkn
|
||||||
|
Free for personal and commercial use under the CCA 3.0 license (html5up.net/license)
|
||||||
|
-->
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>OpenBackloggery</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
|
||||||
|
<!--[if lte IE 8]><script src="{% static 'js/ie/html5shiv.js' %}"></script><![endif]-->
|
||||||
|
<link rel="stylesheet" href="{% static 'css/main.css' %}" />
|
||||||
|
<!--[if lte IE 8]><link rel="stylesheet" href="{% static 'css/ie8.css' %}" /><![endif]-->
|
||||||
|
<!--[if lte IE 9]><link rel="stylesheet" href="{% static 'css/ie9.css' %}" /><![endif]-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<header id="header">
|
||||||
|
<h1>OpenBackloggery</h1>
|
||||||
|
<p>Just track games and figurine collection<br />
|
||||||
|
through a website.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer id="footer">
|
||||||
|
<ul class="icons">
|
||||||
|
<li><a href="https://forge.o9.re/olivier/openbackloggery" class="icon fa-git-square"><span class="label">GitHub</span></a></li>
|
||||||
|
</ul>
|
||||||
|
<ul class="copyright">
|
||||||
|
<li>© OpenBackloggery.</li><li>Credits: <a href="http://unsplash.com/">Unsplash</a> + <a href="http://html5up.net">HTML5 UP</a></li>
|
||||||
|
</ul>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
<!--[if lte IE 8]><script src="{% static 'js/ie/respond.min.js' %}"></script><![endif]-->
|
||||||
|
<script src="{% static 'js/main.js' %}"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
0
collection/figurines/__init__.py
Normal file
0
collection/figurines/__init__.py
Normal file
25
collection/figurines/admin.py
Normal file
25
collection/figurines/admin.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
from figurines.models import Figurine
|
||||||
|
from figurines.models import Set
|
||||||
|
|
||||||
|
|
||||||
|
class FigurineAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'kind', 'achievement', 'coins', 'collection')
|
||||||
|
fieldsets = [(_('Information'), {
|
||||||
|
'fields': [('name', 'kind', 'collection')]
|
||||||
|
}), (_('Progress'), {
|
||||||
|
'fields': [('achievement', 'achievement_max', 'coins')]
|
||||||
|
}), ('', {
|
||||||
|
'fields': [('wish')]
|
||||||
|
})]
|
||||||
|
list_filter = ('collection', 'kind')
|
||||||
|
search_fields = ('name', )
|
||||||
|
|
||||||
|
|
||||||
|
class SetAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('shortname', 'name')
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(Set, SetAdmin)
|
||||||
|
admin.site.register(Figurine, FigurineAdmin)
|
5
collection/figurines/apps.py
Normal file
5
collection/figurines/apps.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class FigurinesConfig(AppConfig):
|
||||||
|
name = 'figurines'
|
20
collection/figurines/fixtures/initial.yaml
Normal file
20
collection/figurines/fixtures/initial.yaml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
- model: figurines.set
|
||||||
|
pk: 1
|
||||||
|
fields:
|
||||||
|
name: Amiibo
|
||||||
|
shortname: Amiibo
|
||||||
|
- model: figurines.set
|
||||||
|
pk: 2
|
||||||
|
fields:
|
||||||
|
name: Disney Infinity
|
||||||
|
shortname: Infinity
|
||||||
|
- model: figurines.set
|
||||||
|
pk: 3
|
||||||
|
fields:
|
||||||
|
name: Lego Dimensions
|
||||||
|
shortname: LD
|
||||||
|
- model: figurines.set
|
||||||
|
pk: 4
|
||||||
|
fields:
|
||||||
|
name: "Skylanders Serie 1: Spyro's adventure"
|
||||||
|
shortname: Skylanders
|
43
collection/figurines/migrations/0001_initial.py
Normal file
43
collection/figurines/migrations/0001_initial.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.5 on 2017-09-18 19:11
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Figurine',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(help_text='Figurine denomination', max_length=255, verbose_name='nom')),
|
||||||
|
('wish', models.BooleanField(default=False, help_text='You need this figurine.', verbose_name='wish?')),
|
||||||
|
('status', models.CharField(choices=[('created', 'New'), ('created', 'New')], default='created', help_text='Figurine progression', max_length=30, verbose_name='status')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Set',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(help_text='Usual name of figurines set.', max_length=255, verbose_name='nom')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'set',
|
||||||
|
'verbose_name_plural': 'sets',
|
||||||
|
'ordering': ('name',),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='figurine',
|
||||||
|
name='collection',
|
||||||
|
field=models.ForeignKey(help_text='Becoming set', on_delete=django.db.models.deletion.CASCADE, related_name='figurines', to='figurines.Set', verbose_name='set'),
|
||||||
|
),
|
||||||
|
]
|
23
collection/figurines/migrations/0002_auto_20180115_1730.py
Normal file
23
collection/figurines/migrations/0002_auto_20180115_1730.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.9 on 2018-01-15 17:30
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('figurines', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='set',
|
||||||
|
name='shortname',
|
||||||
|
field=models.CharField(
|
||||||
|
default='TODO: Add shortname',
|
||||||
|
max_length=30,
|
||||||
|
verbose_name='shortname'), ),
|
||||||
|
]
|
31
collection/figurines/migrations/0003_auto_20180116_1756.py
Normal file
31
collection/figurines/migrations/0003_auto_20180116_1756.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.9 on 2018-01-16 17:56
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('figurines', '0002_auto_20180115_1730'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='figurine',
|
||||||
|
options={'ordering': ('name', )}, ),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='set',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(
|
||||||
|
help_text='Usual name of figurines set.',
|
||||||
|
max_length=255,
|
||||||
|
unique=True,
|
||||||
|
verbose_name='name'), ),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='set',
|
||||||
|
name='shortname',
|
||||||
|
field=models.CharField(max_length=30, verbose_name='shortname'), ),
|
||||||
|
]
|
21
collection/figurines/migrations/0004_add_figurines_kind.py
Normal file
21
collection/figurines/migrations/0004_add_figurines_kind.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.9 on 2018-01-20 20:54
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('figurines', '0003_auto_20180116_1756'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='figurine',
|
||||||
|
name='kind',
|
||||||
|
field=models.CharField(choices=[('character', 'Character'), ('vehicle', 'Vehicle'), ('world', 'World'), ('gadget', 'Gadget')], default='character', max_length=30, verbose_name='kind'),
|
||||||
|
),
|
||||||
|
]
|
28
collection/figurines/migrations/0005_add_achievements.py
Normal file
28
collection/figurines/migrations/0005_add_achievements.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.9 on 2018-01-21 11:06
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('figurines', '0004_add_figurines_kind'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='figurine',
|
||||||
|
name='achievement_max',
|
||||||
|
field=models.PositiveIntegerField(blank=True, null=True,
|
||||||
|
verbose_name='out of'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='figurine',
|
||||||
|
name='achievement',
|
||||||
|
field=models.PositiveIntegerField(blank=True, null=True,
|
||||||
|
verbose_name='achievement'),
|
||||||
|
),
|
||||||
|
]
|
22
collection/figurines/migrations/0006_add_coins.py
Normal file
22
collection/figurines/migrations/0006_add_coins.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.9 on 2018-01-21 14:40
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('figurines', '0005_add_achievements'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='figurine',
|
||||||
|
name='coins',
|
||||||
|
field=models.PositiveIntegerField(default=0, null=True, blank=True,
|
||||||
|
verbose_name='coins'),
|
||||||
|
),
|
||||||
|
]
|
35
collection/figurines/migrations/0007_new_kinds.py
Normal file
35
collection/figurines/migrations/0007_new_kinds.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.9 on 2018-02-04 19:34
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('figurines', '0006_add_coins'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='figurine',
|
||||||
|
name='kind',
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[('character', 'Character'), ('vehicle', 'Vehicle'),
|
||||||
|
('world', 'Level pack'), ('gadget', 'Gadget'),
|
||||||
|
('battle', 'Battle pack'), ('expansion',
|
||||||
|
'Expansion pack'),
|
||||||
|
('trophy', 'Trophy'), ('trap', 'Trap')],
|
||||||
|
default='character',
|
||||||
|
max_length=30,
|
||||||
|
verbose_name='kind'), ),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='figurine',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(
|
||||||
|
help_text='Figurine denomination',
|
||||||
|
max_length=255,
|
||||||
|
verbose_name='name'), ),
|
||||||
|
]
|
0
collection/figurines/migrations/__init__.py
Normal file
0
collection/figurines/migrations/__init__.py
Normal file
77
collection/figurines/models.py
Normal file
77
collection/figurines/models.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
from core.models import Collection
|
||||||
|
from core.models import Item
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
|
||||||
|
class Set(Collection):
|
||||||
|
"""
|
||||||
|
Common name used to describe a set of characters, objects or vehicles
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '%s' % self.name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('name', )
|
||||||
|
verbose_name = _('set')
|
||||||
|
verbose_name_plural = _('sets')
|
||||||
|
|
||||||
|
|
||||||
|
# Redefine help_text (for documentation, API, etc.)
|
||||||
|
Set._meta.get_field('name').help_text = _('Usual name of figurines set.')
|
||||||
|
|
||||||
|
|
||||||
|
class Figurine(Item):
|
||||||
|
"""
|
||||||
|
A character, a gadget, a vehicle or a world used on a Game to use specific
|
||||||
|
levels, abilities, etc.
|
||||||
|
"""
|
||||||
|
# class config
|
||||||
|
TARGET_MODEL = 'figurines.Set'
|
||||||
|
TARGET_VERBOSE_NAME = _('set')
|
||||||
|
RELATED_TARGET_NAME = 'figurines'
|
||||||
|
|
||||||
|
# No more status choices than "CREATED"
|
||||||
|
|
||||||
|
# Figurines can be Character, vehicle, world or gadget (weapon)
|
||||||
|
CHARACTER = 'character'
|
||||||
|
VEHICLE = 'vehicle'
|
||||||
|
WORLD = 'world'
|
||||||
|
GADGET = 'gadget'
|
||||||
|
BATTLE = 'battle'
|
||||||
|
EXPANSION = 'expansion'
|
||||||
|
TROPHY = 'trophy'
|
||||||
|
TRAP = 'trap'
|
||||||
|
KIND_CHOICES = (
|
||||||
|
(CHARACTER, _('Character')),
|
||||||
|
(VEHICLE, _('Vehicle')),
|
||||||
|
(WORLD, _('Level pack')),
|
||||||
|
(GADGET, _('Gadget')),
|
||||||
|
(BATTLE, _('Battle pack')),
|
||||||
|
(EXPANSION, _('Expansion pack')),
|
||||||
|
(TROPHY, _('Trophy')),
|
||||||
|
(TRAP, _('Trap')), )
|
||||||
|
kind = models.CharField(
|
||||||
|
max_length=30,
|
||||||
|
choices=KIND_CHOICES,
|
||||||
|
default=CHARACTER,
|
||||||
|
verbose_name=_('kind'))
|
||||||
|
|
||||||
|
wish = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name=_('wish?'),
|
||||||
|
help_text=_('You need this figurine.'))
|
||||||
|
|
||||||
|
# How money does your figurine have?
|
||||||
|
coins = models.PositiveIntegerField(
|
||||||
|
default=0, null=True, blank=True, verbose_name=_('coins'))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('name', )
|
||||||
|
|
||||||
|
|
||||||
|
# Redefine help_text (for documentation, API, etc.)
|
||||||
|
Figurine._meta.get_field('name').help_text = _('Figurine denomination')
|
||||||
|
Figurine._meta.get_field('collection').help_text = _('Becoming set')
|
||||||
|
Figurine._meta.get_field('status').help_text = _('Figurine progression')
|
22
collection/figurines/serializers.py
Normal file
22
collection/figurines/serializers.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
from figurines.models import Figurine
|
||||||
|
from figurines.models import Set
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class SetSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Set
|
||||||
|
fields = ('name', 'shortname')
|
||||||
|
|
||||||
|
|
||||||
|
class FigurineSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Figurine
|
||||||
|
fields = (
|
||||||
|
'collection',
|
||||||
|
'name',
|
||||||
|
'kind',
|
||||||
|
'achievement',
|
||||||
|
'achievement_max',
|
||||||
|
'coins',
|
||||||
|
'wish', )
|
0
collection/figurines/tests/__init__.py
Normal file
0
collection/figurines/tests/__init__.py
Normal file
86
collection/figurines/tests/test_api.py
Normal file
86
collection/figurines/tests/test_api.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.urls import reverse
|
||||||
|
from figurines.models import Figurine
|
||||||
|
from figurines.models import Set
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
|
||||||
|
class SetTest(APITestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
cls.superuser = User.objects.create_superuser(
|
||||||
|
'admin', 'admin@localhost', 'admin')
|
||||||
|
|
||||||
|
def test_create_set(self):
|
||||||
|
"""
|
||||||
|
Check we can create a set object.
|
||||||
|
"""
|
||||||
|
url = reverse('set-list')
|
||||||
|
data = {'name': 'Skypanzers', 'shortname': 'SP'}
|
||||||
|
self.client.force_authenticate(user=self.superuser)
|
||||||
|
response = self.client.post(url, data, format='json')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
self.assertEqual(Set.objects.count(), 1)
|
||||||
|
self.assertEqual(Set.objects.get().name, 'Skypanzers')
|
||||||
|
|
||||||
|
def test_sorted_set(self):
|
||||||
|
"""
|
||||||
|
Check that set list is sorted.
|
||||||
|
"""
|
||||||
|
Set.objects.create(name='Skypanzers')
|
||||||
|
Set.objects.create(name='Liibo')
|
||||||
|
Set.objects.create(name='Skypanzers2')
|
||||||
|
Set.objects.create(name='Amimoche')
|
||||||
|
url = reverse('set-list')
|
||||||
|
self.client.force_authenticate(user=self.superuser)
|
||||||
|
response = self.client.get(url, format='json')
|
||||||
|
sorted_sets = list(Set.objects.all().order_by('name')
|
||||||
|
.values_list('name', flat=True))
|
||||||
|
sets = [x.get('name') for x in json.loads(response.content)]
|
||||||
|
self.assertEqual(sets, sorted_sets)
|
||||||
|
|
||||||
|
|
||||||
|
class FigurineTest(APITestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
cls.superuser = User.objects.create_superuser(
|
||||||
|
'admin', 'admin@localhost', 'admin')
|
||||||
|
|
||||||
|
def test_create_figurine(self):
|
||||||
|
"""
|
||||||
|
Check we can create a figurine object.
|
||||||
|
"""
|
||||||
|
# Figurine comes from a specific Set
|
||||||
|
collection = Set.objects.create(name='Skypanzers')
|
||||||
|
collection_url = reverse('set-detail', kwargs={'pk': collection.id})
|
||||||
|
|
||||||
|
url = reverse('figurine-list')
|
||||||
|
data = {
|
||||||
|
'name': 'Posséïdon',
|
||||||
|
'collection': collection_url,
|
||||||
|
}
|
||||||
|
self.client.force_authenticate(user=self.superuser)
|
||||||
|
response = self.client.post(url, data, format='json')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
self.assertEqual(Figurine.objects.count(), 1)
|
||||||
|
self.assertEqual(Figurine.objects.get().name, 'Posséïdon')
|
||||||
|
self.assertEqual(Figurine.objects.get().collection_id, collection.id)
|
||||||
|
|
||||||
|
def test_sorted_figurines(self):
|
||||||
|
"""
|
||||||
|
Check that figurine list is sorted.
|
||||||
|
"""
|
||||||
|
collection = Set.objects.create(name='Amimoche')
|
||||||
|
Figurine.objects.create(name='Posséïdon', collection=collection)
|
||||||
|
Figurine.objects.create(name='Veronica', collection=collection)
|
||||||
|
Figurine.objects.create(name='Quintrépide', collection=collection)
|
||||||
|
url = reverse('figurine-list')
|
||||||
|
self.client.force_authenticate(user=self.superuser)
|
||||||
|
response = self.client.get(url, format='json')
|
||||||
|
sorted_figurines = list(Figurine.objects.all().order_by('name')
|
||||||
|
.values_list('name', flat=True))
|
||||||
|
figurines = [x.get('name') for x in json.loads(response.content)]
|
||||||
|
self.assertEqual(figurines, sorted_figurines)
|
17
collection/figurines/tests/test_set.py
Normal file
17
collection/figurines/tests/test_set.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
from figurines.models import Set
|
||||||
|
|
||||||
|
|
||||||
|
class SetTest(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
Set.objects.create(name='DimDim')
|
||||||
|
Set.objects.create(name='Rainbow')
|
||||||
|
Set.objects.create(name='Mystery')
|
||||||
|
|
||||||
|
|
||||||
|
def test_sorted_sets(self):
|
||||||
|
sets = list(Set.objects.all().values_list('name', flat=True))
|
||||||
|
sorted_sets = list(
|
||||||
|
Set.objects.all().order_by('name').values_list('name', flat=True))
|
||||||
|
self.assertEqual(sets, sorted_sets)
|
40
collection/figurines/views.py
Normal file
40
collection/figurines/views.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
from figurines.models import Figurine
|
||||||
|
from figurines.models import Set
|
||||||
|
from rest_framework import viewsets
|
||||||
|
|
||||||
|
from .serializers import FigurineSerializer
|
||||||
|
from .serializers import SetSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class FigurineViewSet(viewsets.ModelViewSet):
|
||||||
|
"""
|
||||||
|
API endpoints that allows figurines to be edited or viewed.
|
||||||
|
|
||||||
|
retrieve:
|
||||||
|
Return the given figurine
|
||||||
|
|
||||||
|
list:
|
||||||
|
Return a list of all existing figurines ordered by name.
|
||||||
|
|
||||||
|
create:
|
||||||
|
Create a new figurine instance.
|
||||||
|
"""
|
||||||
|
queryset = Figurine.objects.all().order_by('name')
|
||||||
|
serializer_class = FigurineSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class SetViewSet(viewsets.ModelViewSet):
|
||||||
|
"""
|
||||||
|
API endpoints that allows to edit or view sets.
|
||||||
|
|
||||||
|
retrieve:
|
||||||
|
Return the given set
|
||||||
|
|
||||||
|
list:
|
||||||
|
Return a list of all existing sets ordered by name.
|
||||||
|
|
||||||
|
create:
|
||||||
|
Create a new set instance.
|
||||||
|
"""
|
||||||
|
queryset = Set.objects.all().order_by('name')
|
||||||
|
serializer_class = SetSerializer
|
@ -1,22 +1,123 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from games.models import Console, Game, Timeline
|
from django.utils.translation import ugettext as _
|
||||||
|
from games.forms import GameForm
|
||||||
|
from games.models import Game
|
||||||
|
from games.models import Platform
|
||||||
|
from games.models import Timeline
|
||||||
|
|
||||||
|
|
||||||
|
class PlatformAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('shortname', 'name')
|
||||||
|
|
||||||
|
|
||||||
|
class StatusFilter(admin.SimpleListFilter):
|
||||||
|
"""
|
||||||
|
Remove CREATED status.
|
||||||
|
"""
|
||||||
|
title = _('state')
|
||||||
|
parameter_name = 'status'
|
||||||
|
|
||||||
|
def lookups(self, request, model_admin):
|
||||||
|
"""
|
||||||
|
Return only games choices (not CREATED from Item abstract class).
|
||||||
|
"""
|
||||||
|
return Game.STATUS_CHOICES
|
||||||
|
|
||||||
|
def queryset(self, request, queryset):
|
||||||
|
"""
|
||||||
|
Filter on 'status' field.
|
||||||
|
"""
|
||||||
|
if self.value():
|
||||||
|
return queryset.filter(status=self.value())
|
||||||
|
else:
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class PlatformFilter(admin.SimpleListFilter):
|
||||||
|
"""
|
||||||
|
Display Platform shortname.
|
||||||
|
"""
|
||||||
|
title = _('platform')
|
||||||
|
parameter_name = 'collection'
|
||||||
|
|
||||||
|
def lookups(self, request, model_admin):
|
||||||
|
"""
|
||||||
|
Get all platforms
|
||||||
|
"""
|
||||||
|
return [(x.id, x.shortname) for x in Platform.objects.all()]
|
||||||
|
|
||||||
|
def queryset(self, request, queryset):
|
||||||
|
"""
|
||||||
|
Filter on 'shortname' field.
|
||||||
|
"""
|
||||||
|
if self.value():
|
||||||
|
return queryset.filter(collection__id=self.value())
|
||||||
|
else:
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class GameAdmin(admin.ModelAdmin):
|
class GameAdmin(admin.ModelAdmin):
|
||||||
list_display = (
|
list_display = ('name', 'get_platform', 'playing', 'status', 'wish')
|
||||||
'name', 'playing', 'status', 'wish')
|
list_filter = [StatusFilter, PlatformFilter, 'playing', 'wish']
|
||||||
list_filter = [
|
search_fields = ('name', )
|
||||||
'status',
|
fieldsets = [(_('Game Information'), {
|
||||||
'playing',
|
'fields': [('name', 'collection')]
|
||||||
'wish']
|
}), (_('Progress'), {
|
||||||
search_fields = ('name',)
|
'fields': [('status'), ('achievement', 'achievement_max'), ('note')]
|
||||||
|
}), ('', {
|
||||||
|
'fields': [('playing'), ('unplayed'), ('wish')]
|
||||||
|
})]
|
||||||
|
|
||||||
|
form = GameForm
|
||||||
|
|
||||||
|
def get_platform(self, obj):
|
||||||
|
"""
|
||||||
|
Display platform shortname
|
||||||
|
"""
|
||||||
|
return obj.collection.shortname
|
||||||
|
|
||||||
|
get_platform.short_description = _('Platform')
|
||||||
|
get_platform.admin_order_field = 'collection__shortname'
|
||||||
|
|
||||||
|
|
||||||
|
class TimelinePlatformFilter(admin.SimpleListFilter):
|
||||||
|
"""
|
||||||
|
Display Platform shortname.
|
||||||
|
"""
|
||||||
|
title = _('platform')
|
||||||
|
parameter_name = 'collection'
|
||||||
|
|
||||||
|
def lookups(self, request, model_admin):
|
||||||
|
"""
|
||||||
|
Get all platforms
|
||||||
|
"""
|
||||||
|
return [(x.id, x.shortname) for x in Platform.objects.all()]
|
||||||
|
|
||||||
|
def queryset(self, request, queryset):
|
||||||
|
"""
|
||||||
|
Filter on 'shortname' field.
|
||||||
|
"""
|
||||||
|
if self.value():
|
||||||
|
return queryset.filter(item__collection__id=self.value())
|
||||||
|
else:
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class TimelineAdmin(admin.ModelAdmin):
|
class TimelineAdmin(admin.ModelAdmin):
|
||||||
list_display = (
|
list_display = ('date', 'status', 'item', 'get_platform')
|
||||||
'date', 'status', 'item')
|
search_fields = ('item__name', )
|
||||||
|
list_filter = (TimelinePlatformFilter, )
|
||||||
|
|
||||||
|
def get_platform(self, obj):
|
||||||
|
"""
|
||||||
|
Display platform shortname
|
||||||
|
"""
|
||||||
|
return obj.item.collection.shortname
|
||||||
|
|
||||||
|
get_platform.short_description = _('Platform')
|
||||||
|
get_platform.admin_order_field = ('item__collection__shortname')
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Console)
|
admin.site.register(Platform, PlatformAdmin)
|
||||||
admin.site.register(Game, GameAdmin)
|
admin.site.register(Game, GameAdmin)
|
||||||
admin.site.register(Timeline, TimelineAdmin)
|
admin.site.register(Timeline, TimelineAdmin)
|
||||||
|
@ -1,44 +1,55 @@
|
|||||||
- model: games.console
|
- model: games.platform
|
||||||
pk: 1
|
pk: 1
|
||||||
fields:
|
fields:
|
||||||
name: Steam
|
name: Steam
|
||||||
- model: games.console
|
shortname: Steam
|
||||||
|
- model: games.platform
|
||||||
pk: 2
|
pk: 2
|
||||||
fields:
|
fields:
|
||||||
name: Game Boy
|
name: Game Boy
|
||||||
- model: games.console
|
shortname: Game Boy
|
||||||
|
- model: games.platform
|
||||||
pk: 3
|
pk: 3
|
||||||
fields:
|
fields:
|
||||||
name: Nintendo 3DS
|
name: Nintendo 3DS
|
||||||
- model: games.console
|
shortname: 3DS
|
||||||
|
- model: games.platform
|
||||||
pk: 4
|
pk: 4
|
||||||
fields:
|
fields:
|
||||||
name: Nintendo Switch
|
name: Nintendo Switch
|
||||||
- model: games.console
|
shortname: Switch
|
||||||
|
- model: games.platform
|
||||||
pk: 5
|
pk: 5
|
||||||
fields:
|
fields:
|
||||||
name: PC
|
name: PC
|
||||||
- model: games.console
|
shortname: PC
|
||||||
|
- model: games.platform
|
||||||
pk: 6
|
pk: 6
|
||||||
fields:
|
fields:
|
||||||
name: Genesis / Mega Drive
|
name: Genesis / Mega Drive
|
||||||
- model: games.console
|
shortname: GEN
|
||||||
|
- model: games.platform
|
||||||
pk: 7
|
pk: 7
|
||||||
fields:
|
fields:
|
||||||
name: Nintendo Entertainment System
|
name: Nintendo Entertainment System
|
||||||
- model: games.console
|
shortname: NES
|
||||||
|
- model: games.platform
|
||||||
pk: 8
|
pk: 8
|
||||||
fields:
|
fields:
|
||||||
name: PlayStation
|
name: PlayStation
|
||||||
- model: games.console
|
shortname: PS
|
||||||
|
- model: games.platform
|
||||||
pk: 9
|
pk: 9
|
||||||
fields:
|
fields:
|
||||||
name: PlayStation 3
|
name: PlayStation 3
|
||||||
- model: games.console
|
shortname: PS3
|
||||||
|
- model: games.platform
|
||||||
pk: 10
|
pk: 10
|
||||||
fields:
|
fields:
|
||||||
name: PlayStation 4
|
name: PlayStation 4
|
||||||
- model: games.console
|
shortname: PS4
|
||||||
|
- model: games.platform
|
||||||
pk: 11
|
pk: 11
|
||||||
fields:
|
fields:
|
||||||
name: Wii
|
name: Wii
|
||||||
|
shortname: Wii
|
||||||
|
28
collection/games/fixtures/test.yaml
Normal file
28
collection/games/fixtures/test.yaml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
- model: games.game
|
||||||
|
pk: 1
|
||||||
|
fields: {name: Crash bandicoot 3, note: null, playing: false, unplayed: false, wish: false,
|
||||||
|
collection: 8, status: unfinished}
|
||||||
|
- model: games.game
|
||||||
|
pk: 2
|
||||||
|
fields: {name: Crash Bandicoot 2, note: null, playing: false, unplayed: false, wish: false,
|
||||||
|
collection: 8, status: excluded}
|
||||||
|
- model: games.game
|
||||||
|
pk: 3
|
||||||
|
fields: {name: 'The Legend of Zelda: Breath of the Wild', note: null, playing: true,
|
||||||
|
unplayed: false, wish: false, collection: 4, status: unfinished}
|
||||||
|
- model: games.game
|
||||||
|
pk: 4
|
||||||
|
fields: {name: Gravity Rush - Remasterised, note: null, playing: true, unplayed: false,
|
||||||
|
wish: false, collection: 10, status: unfinished}
|
||||||
|
- model: games.game
|
||||||
|
pk: 5
|
||||||
|
fields: {name: Deponia, note: null, playing: false, unplayed: true, wish: false,
|
||||||
|
collection: 10, status: excluded}
|
||||||
|
- model: games.game
|
||||||
|
pk: 6
|
||||||
|
fields: {name: Les chevaliers de Baphomet 5, note: "Tous les troph\xE9s obtenus",
|
||||||
|
playing: false, unplayed: false, wish: false, collection: 10, status: completed}
|
||||||
|
- model: games.game
|
||||||
|
pk: 7
|
||||||
|
fields: {name: Sherlock Holmes, note: null, playing: false, unplayed: false, wish: true,
|
||||||
|
collection: 10, status: unfinished}
|
12
collection/games/forms.py
Normal file
12
collection/games/forms.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from django import forms
|
||||||
|
from games.models import Game
|
||||||
|
|
||||||
|
|
||||||
|
class GameForm(forms.ModelForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Do not display CREATED status.
|
||||||
|
"""
|
||||||
|
super(GameForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.fields['status'].choices = Game.STATUS_CHOICES
|
30
collection/games/migrations/0002_auto_20170824_1843.py
Normal file
30
collection/games/migrations/0002_auto_20170824_1843.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11 on 2017-08-24 18:43
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('games', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='game',
|
||||||
|
name='playing',
|
||||||
|
field=models.BooleanField(default=False, help_text="You're currently playing this game."),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='game',
|
||||||
|
name='unplayed',
|
||||||
|
field=models.BooleanField(default=False, help_text='You never played this game.'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='game',
|
||||||
|
name='wish',
|
||||||
|
field=models.BooleanField(default=False, help_text="You're waiting X-mas father offers you this game."),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,74 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11 on 2017-08-24 20:22
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('games', '0002_auto_20170824_1843'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='console',
|
||||||
|
options={'verbose_name': 'console', 'verbose_name_plural': 'consoles'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='game',
|
||||||
|
options={'ordering': ('-playing', 'name'), 'verbose_name': 'game', 'verbose_name_plural': 'games'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='timeline',
|
||||||
|
options={'verbose_name': 'Timeline', 'verbose_name_plural': 'Timelines'},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='console',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=255, verbose_name='nom'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='game',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=255, verbose_name='nom'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='game',
|
||||||
|
name='playing',
|
||||||
|
field=models.BooleanField(default=False, help_text="You're currently playing this game.", verbose_name='playing?'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='game',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('created', 'New'), ('beaten', 'Beaten'), ('completed', 'Completed'), ('excluded', 'Excluded'), ('mastered', 'Mastered'), ('unfinished', 'Unfinished')], default='unfinished', max_length=30, verbose_name='status'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='game',
|
||||||
|
name='unplayed',
|
||||||
|
field=models.BooleanField(default=False, help_text='You never played this game.', verbose_name='unplayed?'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='game',
|
||||||
|
name='wish',
|
||||||
|
field=models.BooleanField(default=False, help_text="You're waiting X-mas father offers you this game.", verbose_name='wish?'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='timeline',
|
||||||
|
name='date',
|
||||||
|
field=models.DateTimeField(default=datetime.datetime.now, verbose_name='date'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='timeline',
|
||||||
|
name='item',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='games.Game', verbose_name='game'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='timeline',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('created', 'New'), ('beaten', 'Beaten'), ('completed', 'Completed'), ('excluded', 'Excluded'), ('mastered', 'Mastered'), ('unfinished', 'Unfinished')], default='created', max_length=30, verbose_name='status'),
|
||||||
|
),
|
||||||
|
]
|
33
collection/games/migrations/0004_add_game_note_field.py
Normal file
33
collection/games/migrations/0004_add_game_note_field.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.4 on 2017-08-25 22:11
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('games', '0003_add_verbose_translatable_names'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='console',
|
||||||
|
options={'ordering': ('name',), 'verbose_name': 'console', 'verbose_name_plural': 'consoles'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='game',
|
||||||
|
options={'ordering': ('-playing', 'name'), 'verbose_name': 'jeu', 'verbose_name_plural': 'jeux'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='timeline',
|
||||||
|
options={'ordering': ('-date',), 'verbose_name': 'Chronologie', 'verbose_name_plural': 'Chronologies'},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='game',
|
||||||
|
name='note',
|
||||||
|
field=models.CharField(help_text='Short note displayed to your followers.', max_length=150, null=True, blank=True, verbose_name='Progress note'),
|
||||||
|
),
|
||||||
|
]
|
51
collection/games/migrations/0005_auto_20170831_1211.py
Normal file
51
collection/games/migrations/0005_auto_20170831_1211.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.4 on 2017-08-31 12:11
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('games', '0004_add_game_note_field'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='game',
|
||||||
|
name='note',
|
||||||
|
field=models.CharField(blank=True, help_text='Courte note affichée à ceux qui vous suive', max_length=150, null=True, verbose_name='Note de progression'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='game',
|
||||||
|
name='playing',
|
||||||
|
field=models.BooleanField(default=False, help_text='Vous jouez actuellement à ce jeu.', verbose_name="en train d'y jouer ?"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='game',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('created', 'Nouveau'), ('beaten', 'Terminé (quête principale)'), ('completed', 'Terminé complètement'), ('excluded', 'Exclu'), ('mastered', 'Usé / Épuisé'), ('unfinished', 'Inachevé')], default='unfinished', max_length=30, verbose_name='état'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='game',
|
||||||
|
name='unplayed',
|
||||||
|
field=models.BooleanField(default=False, help_text="Vous n'avez jamais joué à ce jeu.", verbose_name='jamais joué ?'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='game',
|
||||||
|
name='wish',
|
||||||
|
field=models.BooleanField(default=False, help_text='Vous patientez que le père Noël vous offre ce jeu.', verbose_name="envie de l'avoir ?"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='timeline',
|
||||||
|
name='item',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='games.Game', verbose_name='jeu'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='timeline',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('created', 'Nouveau'), ('beaten', 'Terminé (quête principale)'), ('completed', 'Terminé complètement'), ('excluded', 'Exclu'), ('mastered', 'Usé / Épuisé'), ('unfinished', 'Inachevé')], default='created', max_length=30, verbose_name='état'),
|
||||||
|
),
|
||||||
|
]
|
21
collection/games/migrations/0006_auto_20170901_2041.py
Normal file
21
collection/games/migrations/0006_auto_20170901_2041.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.4 on 2017-09-01 20:41
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('games', '0005_auto_20170831_1211'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='timeline',
|
||||||
|
name='date',
|
||||||
|
field=models.DateField(default=datetime.datetime.now, verbose_name='date'),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,20 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.4 on 2017-09-04 21:46
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('games', '0006_auto_20170901_2041'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='console',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(help_text='Nom de console le plus utilisé.', max_length=255, verbose_name='nom'),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,38 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.5 on 2017-09-16 15:05
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('games', '0007_help_text_on_console_name'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameModel(
|
||||||
|
old_name='Console',
|
||||||
|
new_name='Platform',
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='game',
|
||||||
|
name='collection',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='games', to='games.Platform', verbose_name='platform'),
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='Platform',
|
||||||
|
options={
|
||||||
|
'verbose_name': 'platform',
|
||||||
|
'verbose_name_plural': 'platforms',
|
||||||
|
'ordering': ('name',),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='platform',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(help_text='Most used platform name.', max_length=255, verbose_name='nom'),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,61 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.5 on 2017-09-16 21:55
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('games', '0008_rename_console_to_platform'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='platform',
|
||||||
|
options={'ordering': ('name',), 'verbose_name': 'plateforme', 'verbose_name_plural': 'plateformes'},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='game',
|
||||||
|
name='collection',
|
||||||
|
field=models.ForeignKey(help_text='Game running platform', on_delete=django.db.models.deletion.CASCADE, related_name='games', to='games.Platform', verbose_name='plateforme'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='game',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(help_text='Game title', max_length=255, verbose_name='nom'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='game',
|
||||||
|
name='note',
|
||||||
|
field=models.CharField(blank=True, help_text='Courte note affichée à ceux qui vous suive.', max_length=150, null=True, verbose_name='Note de progression'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='game',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('created', 'Nouveau'), ('beaten', 'Terminé (quête principale)'), ('completed', 'Terminé complètement'), ('excluded', 'Exclu'), ('mastered', 'Usé / Épuisé'), ('unfinished', 'Inachevé')], default='unfinished', help_text='Game progression', max_length=30, verbose_name='état'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='platform',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(help_text='Nom de plateforme le plus utilisé.', max_length=255, verbose_name='nom'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='timeline',
|
||||||
|
name='date',
|
||||||
|
field=models.DateField(default=datetime.datetime.now, help_text='Status change date', verbose_name='date'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='timeline',
|
||||||
|
name='item',
|
||||||
|
field=models.ForeignKey(help_text='Which game?', on_delete=django.db.models.deletion.CASCADE, to='games.Game', verbose_name='jeu'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='timeline',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('created', 'Nouveau'), ('beaten', 'Terminé (quête principale)'), ('completed', 'Terminé complètement'), ('excluded', 'Exclu'), ('mastered', 'Usé / Épuisé'), ('unfinished', 'Inachevé')], default='created', help_text='New status for this game at the given date', max_length=30, verbose_name='état'),
|
||||||
|
),
|
||||||
|
]
|
23
collection/games/migrations/0010_auto_20180115_1730.py
Normal file
23
collection/games/migrations/0010_auto_20180115_1730.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.9 on 2018-01-15 17:30
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('games', '0009_add_help_text_for_documentation'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='platform',
|
||||||
|
name='shortname',
|
||||||
|
field=models.CharField(
|
||||||
|
default='TODO: Add shortname',
|
||||||
|
max_length=30,
|
||||||
|
verbose_name='shortname'), ),
|
||||||
|
]
|
24
collection/games/migrations/0011_auto_20180116_1756.py
Normal file
24
collection/games/migrations/0011_auto_20180116_1756.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.9 on 2018-01-16 17:56
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('games', '0010_auto_20180115_1730'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='platform',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(
|
||||||
|
help_text='Most used platform name.',
|
||||||
|
max_length=255,
|
||||||
|
unique=True,
|
||||||
|
verbose_name='name'), ),
|
||||||
|
]
|
29
collection/games/migrations/0012_add_achievements.py
Normal file
29
collection/games/migrations/0012_add_achievements.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.9 on 2018-01-21 11:06
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('games', '0011_auto_20180116_1756'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='game',
|
||||||
|
name='achievement_max',
|
||||||
|
field=models.PositiveIntegerField(blank=True, null=True,
|
||||||
|
verbose_name='out of'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='game',
|
||||||
|
name='achievement',
|
||||||
|
field=models.PositiveIntegerField(blank=True, null=True,
|
||||||
|
verbose_name='achievement'),
|
||||||
|
),
|
||||||
|
]
|
119
collection/games/migrations/0013_auto_20180204_1934.py
Normal file
119
collection/games/migrations/0013_auto_20180204_1934.py
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.9 on 2018-02-04 19:34
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('games', '0012_add_achievements'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='game',
|
||||||
|
options={
|
||||||
|
'ordering': ('-playing', 'name'),
|
||||||
|
'verbose_name': 'game',
|
||||||
|
'verbose_name_plural': 'games'
|
||||||
|
}, ),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='platform',
|
||||||
|
options={
|
||||||
|
'ordering': ('name', ),
|
||||||
|
'verbose_name': 'platform',
|
||||||
|
'verbose_name_plural': 'platforms'
|
||||||
|
}, ),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='timeline',
|
||||||
|
options={
|
||||||
|
'ordering': ('-date', ),
|
||||||
|
'verbose_name': 'Timeline',
|
||||||
|
'verbose_name_plural': 'Timelines'
|
||||||
|
}, ),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='game',
|
||||||
|
name='collection',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
help_text='Game running platform',
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name='games',
|
||||||
|
to='games.Platform',
|
||||||
|
verbose_name='platform'), ),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='game',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(
|
||||||
|
help_text='Game title', max_length=255, verbose_name='name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='game',
|
||||||
|
name='note',
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
help_text='Short note displayed to your followers.',
|
||||||
|
max_length=150,
|
||||||
|
null=True,
|
||||||
|
verbose_name='Progress note'), ),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='game',
|
||||||
|
name='playing',
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="You're currently playing this game.",
|
||||||
|
verbose_name='playing?'), ),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='game',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[('created', 'New'), ('beaten', 'Beaten'),
|
||||||
|
('completed', 'Completed'), ('excluded', 'Excluded'),
|
||||||
|
('mastered', 'Mastered'), ('unfinished',
|
||||||
|
'Unfinished')],
|
||||||
|
default='unfinished',
|
||||||
|
help_text='Game progression',
|
||||||
|
max_length=30,
|
||||||
|
verbose_name='status'), ),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='game',
|
||||||
|
name='unplayed',
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text='You never played this game.',
|
||||||
|
verbose_name='unplayed?'), ),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='game',
|
||||||
|
name='wish',
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="You're waiting X-mas father offers you this game.",
|
||||||
|
verbose_name='wish?'), ),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='platform',
|
||||||
|
name='shortname',
|
||||||
|
field=models.CharField(max_length=30, verbose_name='shortname'), ),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='timeline',
|
||||||
|
name='item',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
help_text='Which game?',
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to='games.Game',
|
||||||
|
verbose_name='game'), ),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='timeline',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[('created', 'New'), ('beaten', 'Beaten'),
|
||||||
|
('completed', 'Completed'), ('excluded', 'Excluded'),
|
||||||
|
('mastered', 'Mastered'), ('unfinished',
|
||||||
|
'Unfinished')],
|
||||||
|
default='created',
|
||||||
|
help_text='New status for this game at the given date',
|
||||||
|
max_length=30,
|
||||||
|
verbose_name='status'), ),
|
||||||
|
]
|
@ -3,21 +3,30 @@ from django.db import models
|
|||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
|
||||||
class Console(Collection):
|
class Platform(Collection):
|
||||||
"""
|
"""
|
||||||
All console, system or box that can be used to play video games.
|
All platform, system or box that can be used to play video games.
|
||||||
"""
|
"""
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '%s' % self.name
|
return '%s' % self.name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('name',)
|
||||||
|
verbose_name = _('platform')
|
||||||
|
verbose_name_plural = _('platforms')
|
||||||
|
|
||||||
|
|
||||||
|
# Redefine help_text (for documentation, API, etc.)
|
||||||
|
Platform._meta.get_field('name').help_text = _('Most used platform name.')
|
||||||
|
|
||||||
|
|
||||||
class Game(Item):
|
class Game(Item):
|
||||||
"""
|
"""
|
||||||
A video game you will use on a specific Console.
|
A video game you will use on a specific Platform.
|
||||||
"""
|
"""
|
||||||
# class config
|
# class config
|
||||||
TARGET_MODEL = 'games.Console'
|
TARGET_MODEL = 'games.Platform'
|
||||||
TARGET_VERBOSE_NAME = _('console')
|
TARGET_VERBOSE_NAME = _('platform')
|
||||||
RELATED_TARGET_NAME = 'games'
|
RELATED_TARGET_NAME = 'games'
|
||||||
|
|
||||||
# Status choices
|
# Status choices
|
||||||
@ -36,16 +45,54 @@ class Game(Item):
|
|||||||
)
|
)
|
||||||
DEFAULT_CHOICE = UNFINISHED
|
DEFAULT_CHOICE = UNFINISHED
|
||||||
|
|
||||||
|
# progression
|
||||||
|
note = models.CharField(
|
||||||
|
max_length=150,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_('Progress note'),
|
||||||
|
help_text=_('Short note displayed to your followers.'))
|
||||||
|
|
||||||
# others
|
# others
|
||||||
playing = models.BooleanField(default=False)
|
playing = models.BooleanField(
|
||||||
unplayed = models.BooleanField(default=False)
|
default=False,
|
||||||
wish = models.BooleanField(default=False)
|
verbose_name=_('playing?'),
|
||||||
|
help_text=_('You\'re currently playing this game.'))
|
||||||
|
unplayed = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name=_('unplayed?'),
|
||||||
|
help_text=_('You never played this game.'))
|
||||||
|
wish = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name=_('wish?'),
|
||||||
|
help_text=_('You\'re waiting X-mas father offers you this game.'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('-playing', 'name')
|
ordering = ('-playing', 'name')
|
||||||
|
verbose_name = _('game')
|
||||||
|
verbose_name_plural = _('games')
|
||||||
|
|
||||||
|
|
||||||
|
# Redefine help_text (for documentation, API, etc.)
|
||||||
|
Game._meta.get_field('name').help_text = _('Game title')
|
||||||
|
Game._meta.get_field('collection').help_text = _('Game running platform')
|
||||||
|
Game._meta.get_field('status').help_text = _('Game progression')
|
||||||
|
|
||||||
|
|
||||||
class Timeline(BaseTimeline):
|
class Timeline(BaseTimeline):
|
||||||
TARGET_MODEL = 'games.Game'
|
TARGET_MODEL = 'games.Game'
|
||||||
|
TARGET_VERBOSE_NAME = 'game'
|
||||||
STATUS_CHOICES = Game.STATUS_CHOICES
|
STATUS_CHOICES = Game.STATUS_CHOICES
|
||||||
DEFAULT_CHOICE = Item.DEFAULT_CHOICE
|
DEFAULT_CHOICE = Item.DEFAULT_CHOICE
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('-date',)
|
||||||
|
verbose_name = _('Timeline')
|
||||||
|
verbose_name_plural = _('Timelines')
|
||||||
|
|
||||||
|
|
||||||
|
# Redefine help_text (for documentation, API, etc.)
|
||||||
|
Timeline._meta.get_field('date').help_text = _('Status change date')
|
||||||
|
Timeline._meta.get_field('item').help_text = _('Which game?')
|
||||||
|
Timeline._meta.get_field('status').help_text = _(
|
||||||
|
'New status for this game at the given date')
|
||||||
|
32
collection/games/serializers.py
Normal file
32
collection/games/serializers.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
from games.models import Game
|
||||||
|
from games.models import Platform
|
||||||
|
from games.models import Timeline
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class GameSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Game
|
||||||
|
fields = (
|
||||||
|
'collection',
|
||||||
|
'name',
|
||||||
|
'note',
|
||||||
|
'playing',
|
||||||
|
'status',
|
||||||
|
'unplayed',
|
||||||
|
'wish', )
|
||||||
|
|
||||||
|
|
||||||
|
class GameTimelineSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Timeline
|
||||||
|
fields = (
|
||||||
|
'date',
|
||||||
|
'item',
|
||||||
|
'status', )
|
||||||
|
|
||||||
|
|
||||||
|
class PlatformSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Platform
|
||||||
|
fields = ('name', 'shortname')
|
@ -18,5 +18,6 @@ def game_saved(sender, instance, created, raw, using, update_fields,
|
|||||||
new_entry = dict(entry)
|
new_entry = dict(entry)
|
||||||
new_entry.update({'status': instance.CREATED})
|
new_entry.update({'status': instance.CREATED})
|
||||||
Timeline.objects.create(**new_entry)
|
Timeline.objects.create(**new_entry)
|
||||||
# Add new timeline entry
|
# Add new timeline entry ONLY if status is difference from CREATED one
|
||||||
Timeline.objects.create(**entry)
|
if instance.status != instance.CREATED:
|
||||||
|
Timeline.objects.create(**entry)
|
||||||
|
15
collection/games/templates/games/index.html
Normal file
15
collection/games/templates/games/index.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{% trans "Games" %}</h1>
|
||||||
|
{% if non_excluded_games %}
|
||||||
|
<ul>
|
||||||
|
{% for game in non_excluded_games %}
|
||||||
|
<li><strong>{{ game.collection.shortname }}</strong> - {{ game.name }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p>{% trans "No game found." %}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
35
collection/games/templates/games/summary.html
Normal file
35
collection/games/templates/games/summary.html
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{% trans "My summary" %}</h1>
|
||||||
|
<h2>{% trans "Now playing" %}</h2>
|
||||||
|
{% if playing_games %}
|
||||||
|
<ul>
|
||||||
|
{% for playing in playing_games %}
|
||||||
|
<li><strong>{{ playing.collection.shortname }}</strong> - {{ playing.name }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p>{% trans "No playing game found." %}</p>
|
||||||
|
{% endif %}
|
||||||
|
<h2>{% trans "Memory Card" %}</h2>
|
||||||
|
{% if last_timelines %}
|
||||||
|
{% regroup last_timelines by date as dates %}
|
||||||
|
{% for date in dates %}
|
||||||
|
<p><u>{{ date.grouper }} : </u>
|
||||||
|
<ul>
|
||||||
|
{% for timeline in date.list|dictsortreversed:"id" %}
|
||||||
|
<li>
|
||||||
|
<strong>{{ timeline.get_status_display }} :</strong> {{ timeline.item.name }}
|
||||||
|
(<strong>{{ timeline.item.collection.shortname }}</strong>)
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<p>{% trans "Empty memory." %}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
165
collection/games/tests/test_api.py
Normal file
165
collection/games/tests/test_api.py
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import json
|
||||||
|
from datetime import date
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.urls import reverse
|
||||||
|
from games.models import Game
|
||||||
|
from games.models import Platform
|
||||||
|
from games.models import Timeline
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
|
||||||
|
class PlatformTest(APITestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
cls.superuser = User.objects.create_superuser(
|
||||||
|
'admin', 'admin@localhost', 'admin')
|
||||||
|
|
||||||
|
def test_create_platform(self):
|
||||||
|
"""
|
||||||
|
Check we can create a platform object.
|
||||||
|
"""
|
||||||
|
url = reverse('platform-list')
|
||||||
|
data = {'name': 'GP2X', 'shortname': 'GP2X'}
|
||||||
|
self.client.force_authenticate(user=self.superuser)
|
||||||
|
response = self.client.post(url, data, format='json')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
self.assertEqual(Platform.objects.count(), 1)
|
||||||
|
self.assertEqual(Platform.objects.get().name, 'GP2X')
|
||||||
|
|
||||||
|
def test_sorted_platform(self):
|
||||||
|
"""
|
||||||
|
Check that platform list is sorted.
|
||||||
|
"""
|
||||||
|
Platform.objects.create(name='GP2X')
|
||||||
|
Platform.objects.create(name='3DS')
|
||||||
|
Platform.objects.create(name='Game Boy')
|
||||||
|
Platform.objects.create(name='Amiga')
|
||||||
|
url = reverse('platform-list')
|
||||||
|
self.client.force_authenticate(user=self.superuser)
|
||||||
|
response = self.client.get(url, format='json')
|
||||||
|
sorted_platforms = list(Platform.objects.all().order_by('name')
|
||||||
|
.values_list('name', flat=True))
|
||||||
|
platforms = [x.get('name') for x in json.loads(response.content)]
|
||||||
|
self.assertEqual(platforms, sorted_platforms)
|
||||||
|
|
||||||
|
|
||||||
|
class GameTest(APITestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
cls.superuser = User.objects.create_superuser(
|
||||||
|
'admin', 'admin@localhost', 'admin')
|
||||||
|
|
||||||
|
def test_create_game(self):
|
||||||
|
"""
|
||||||
|
Check we can create a game object.
|
||||||
|
"""
|
||||||
|
# Game works on specific platform
|
||||||
|
console = Platform.objects.create(name='Game Boy')
|
||||||
|
console_url = reverse('platform-detail', kwargs={'pk': console.id})
|
||||||
|
|
||||||
|
url = reverse('game-list')
|
||||||
|
data = {
|
||||||
|
'name': 'Tetris',
|
||||||
|
'collection': console_url,
|
||||||
|
}
|
||||||
|
self.client.force_authenticate(user=self.superuser)
|
||||||
|
response = self.client.post(url, data, format='json')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
self.assertEqual(Game.objects.count(), 1)
|
||||||
|
self.assertEqual(Game.objects.get().name, 'Tetris')
|
||||||
|
self.assertEqual(Game.objects.get().collection_id, console.id)
|
||||||
|
|
||||||
|
def test_sorted_games(self):
|
||||||
|
"""
|
||||||
|
Check that game list is sorted.
|
||||||
|
"""
|
||||||
|
console = Platform.objects.create(name='BestPlatform4Ever')
|
||||||
|
Game.objects.create(name='Pomperman', collection=console)
|
||||||
|
Game.objects.create(name='Vektoria', collection=console)
|
||||||
|
Game.objects.create(name='Qrackovitchya', collection=console)
|
||||||
|
url = reverse('game-list')
|
||||||
|
self.client.force_authenticate(user=self.superuser)
|
||||||
|
response = self.client.get(url, format='json')
|
||||||
|
sorted_games = list(
|
||||||
|
Game.objects.all().order_by('name').values_list('name', flat=True))
|
||||||
|
games = [x.get('name') for x in json.loads(response.content)]
|
||||||
|
self.assertEqual(games, sorted_games)
|
||||||
|
|
||||||
|
def test_not_allowed_status(self):
|
||||||
|
"""
|
||||||
|
Check that we cannot insert a not allowed status in a Game
|
||||||
|
"""
|
||||||
|
Platform.objects.create(name='BestPlatform4Ever')
|
||||||
|
url = reverse('game-list')
|
||||||
|
data = {
|
||||||
|
'name': 'Vilebrequin',
|
||||||
|
'status': 'wrecked',
|
||||||
|
}
|
||||||
|
self.client.force_authenticate(user=self.superuser)
|
||||||
|
response = self.client.post(url, data, format='json')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertEqual(Game.objects.count(), 0)
|
||||||
|
|
||||||
|
|
||||||
|
class TimelineTest(APITestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
cls.superuser = User.objects.create_superuser(
|
||||||
|
'admin', 'admin@localhost', 'admin')
|
||||||
|
|
||||||
|
def test_create_timeline(self):
|
||||||
|
"""
|
||||||
|
Check we can create a timeline object.
|
||||||
|
"""
|
||||||
|
console = Platform.objects.create(name='BestPlatform4Ever')
|
||||||
|
game = Game.objects.create(
|
||||||
|
name='Cherubin', collection=console, status='created')
|
||||||
|
|
||||||
|
# By default a timeline is generated with current datetime.
|
||||||
|
# We need to change this date.
|
||||||
|
timeline = Timeline.objects.first()
|
||||||
|
timeline.date = date(2017, 9, 15)
|
||||||
|
timeline.save()
|
||||||
|
|
||||||
|
url = reverse('game_timeline-list')
|
||||||
|
data = {
|
||||||
|
'item': reverse('game-detail', kwargs={'pk': game.id}),
|
||||||
|
'date': date(2017, 9, 16),
|
||||||
|
'status': 'beaten',
|
||||||
|
}
|
||||||
|
self.client.force_authenticate(user=self.superuser)
|
||||||
|
response = self.client.post(url, data, format='json')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED,
|
||||||
|
response.content)
|
||||||
|
self.assertEqual(Timeline.objects.count(), 2)
|
||||||
|
|
||||||
|
def test_sorted_game_timeline(self):
|
||||||
|
"""
|
||||||
|
Check that timelines are sorted by date in descending order.
|
||||||
|
"""
|
||||||
|
# Prepare timeline with some different dates
|
||||||
|
console = Platform.objects.create(name='BestPlatform4Ever')
|
||||||
|
game = Game.objects.create(
|
||||||
|
name='Symptomatic', collection=console, status='created')
|
||||||
|
game.status = 'beaten'
|
||||||
|
game.save()
|
||||||
|
game.status = 'completed'
|
||||||
|
game.save()
|
||||||
|
# Change date from Timelines
|
||||||
|
Timeline.objects.filter(status='created').update(date='2017-03-01')
|
||||||
|
Timeline.objects.filter(status='completed').update(date='2017-03-05')
|
||||||
|
Timeline.objects.filter(status='beaten').update(date='2017-03-07')
|
||||||
|
# Check dates
|
||||||
|
url = reverse('game_timeline-list')
|
||||||
|
self.client.force_authenticate(user=self.superuser)
|
||||||
|
response = self.client.get(url, format='json')
|
||||||
|
# sorted_timelines are datetime.date
|
||||||
|
sorted_timelines = list(Timeline.objects.all().order_by('-date')
|
||||||
|
.values_list('date', flat=True))
|
||||||
|
# dates are string date, format %Y-%m-%d
|
||||||
|
dates = [x.get('date') for x in json.loads(response.content)]
|
||||||
|
da = [datetime.date(datetime.strptime(x, '%Y-%m-%d')) for x in dates]
|
||||||
|
self.assertEqual(da, sorted_timelines)
|
@ -1,20 +1,20 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from games.models import Console
|
from games.models import Platform
|
||||||
|
|
||||||
|
|
||||||
class ConsoleTest(TestCase):
|
class PlatformTest(TestCase):
|
||||||
"""
|
"""
|
||||||
Console Model
|
Platform Model
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
Console.objects.create(name='GP2X')
|
Platform.objects.create(name='GP2X')
|
||||||
Console.objects.create(name='3DS')
|
Platform.objects.create(name='3DS')
|
||||||
|
|
||||||
|
|
||||||
def test_console_are_sorted_by_name(self):
|
def test_platform_are_sorted_by_name(self):
|
||||||
consoles = list(Console.objects.all().values_list('name', flat=True))
|
platforms = list(Platform.objects.all().values_list('name', flat=True))
|
||||||
sorted_consoles = list(
|
sorted_platforms = list(
|
||||||
Console.objects.all().order_by('name').values_list(
|
Platform.objects.all().order_by('name').values_list(
|
||||||
'name', flat=True))
|
'name', flat=True))
|
||||||
self.assertEqual(consoles, sorted_consoles)
|
self.assertEqual(platforms, sorted_platforms)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from games.models import Console, Game
|
from games.models import Game, Platform
|
||||||
|
|
||||||
|
|
||||||
class GameTest(TestCase):
|
class GameTest(TestCase):
|
||||||
@ -8,13 +8,22 @@ class GameTest(TestCase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.console = Console.objects.create(name='BestConsole4Ever')
|
self.platform = Platform.objects.create(name='BestPlatform4Ever')
|
||||||
Game.objects.create(
|
Game.objects.create(
|
||||||
name='Deponia', playing=False, collection=self.console)
|
name='Deponia',
|
||||||
|
playing=True,
|
||||||
|
collection=self.platform,
|
||||||
|
status=Game.EXCLUDED)
|
||||||
Game.objects.create(
|
Game.objects.create(
|
||||||
name='Aladdin', playing=True, collection=self.console)
|
name='Aladdin', playing=True, collection=self.platform)
|
||||||
Game.objects.create(
|
Game.objects.create(
|
||||||
name='Persona 5', playing=True, collection=self.console)
|
name='Persona 5', playing=True, collection=self.platform)
|
||||||
|
Game.objects.create(
|
||||||
|
name='The Witcher III',
|
||||||
|
playing=False,
|
||||||
|
collection=self.platform,
|
||||||
|
wish=True)
|
||||||
|
self.index_url = '/games/'
|
||||||
|
|
||||||
|
|
||||||
def test_game_are_sorted_by_playing_and_name(self):
|
def test_game_are_sorted_by_playing_and_name(self):
|
||||||
@ -26,3 +35,48 @@ class GameTest(TestCase):
|
|||||||
sorted_games = list(Game.objects.all().order_by(
|
sorted_games = list(Game.objects.all().order_by(
|
||||||
'-playing', 'name').values_list('name', flat=True))
|
'-playing', 'name').values_list('name', flat=True))
|
||||||
self.assertEqual(games, sorted_games)
|
self.assertEqual(games, sorted_games)
|
||||||
|
|
||||||
|
def test_index(self):
|
||||||
|
"""
|
||||||
|
Context gives 'playing_games'.
|
||||||
|
Context gives 'last_timelines'.
|
||||||
|
"""
|
||||||
|
res = self.client.get(self.index_url)
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
self.assertTrue('playing_games' in res.context)
|
||||||
|
self.assertTrue('last_timelines' in res.context)
|
||||||
|
self.assertTrue('object_list' in res.context)
|
||||||
|
|
||||||
|
def test_index_queryset(self):
|
||||||
|
"""
|
||||||
|
Queryset excludes wishlist games.
|
||||||
|
Queryset excludes games that have status EXCLUDED.
|
||||||
|
Queryset is sorted by name.
|
||||||
|
"""
|
||||||
|
res = self.client.get(self.index_url)
|
||||||
|
games = res.context.get('object_list')
|
||||||
|
for game in games:
|
||||||
|
self.assertFalse(game.wish)
|
||||||
|
self.assertTrue(game.status != Game.EXCLUDED)
|
||||||
|
sorted_games = list(games.order_by('name'))
|
||||||
|
self.assertEqual([x.name for x in games], [s.name for s in sorted_games])
|
||||||
|
|
||||||
|
def test_index_playing_games(self):
|
||||||
|
"""
|
||||||
|
'playing_games' contains games that have playing=True.
|
||||||
|
"""
|
||||||
|
res = self.client.get(self.index_url)
|
||||||
|
playing_games = res.context.get('playing_games')
|
||||||
|
for game in playing_games:
|
||||||
|
self.assertTrue(game.playing)
|
||||||
|
|
||||||
|
def test_index_last_timelines(self):
|
||||||
|
"""
|
||||||
|
'last_timelines' only have 5 items.
|
||||||
|
'last_timelines' have NO games with status EXCLUDED.
|
||||||
|
"""
|
||||||
|
res = self.client.get(self.index_url)
|
||||||
|
games = res.context.get('last_timelines')
|
||||||
|
self.assertEqual(len(games), 5)
|
||||||
|
for game in games:
|
||||||
|
self.assertTrue(game.status != Game.EXCLUDED)
|
||||||
|
34
collection/games/tests/test_timeline.py
Normal file
34
collection/games/tests/test_timeline.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
from games.models import Game, Platform, Timeline
|
||||||
|
|
||||||
|
|
||||||
|
class TimelineTest(TestCase):
|
||||||
|
"""
|
||||||
|
Timeline Model
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
cls.console = Platform.objects.create(name='GP2X')
|
||||||
|
|
||||||
|
|
||||||
|
def test_game_with_status_created_gives_one_timeline(self):
|
||||||
|
"""
|
||||||
|
Game with status "created" should only generate ONE status. Not more.
|
||||||
|
"""
|
||||||
|
# Games creation should generate timelines.
|
||||||
|
game1 = Game.objects.create(
|
||||||
|
name='Vektronizor',
|
||||||
|
collection=self.console,
|
||||||
|
status='beaten')
|
||||||
|
game2 = Game.objects.create(
|
||||||
|
name='Pomperman',
|
||||||
|
collection=self.console,
|
||||||
|
status='created')
|
||||||
|
|
||||||
|
game1_timeline = Timeline.objects.filter(
|
||||||
|
item=game1)
|
||||||
|
game2_timeline = Timeline.objects.filter(
|
||||||
|
item=game2)
|
||||||
|
self.assertEqual(game1_timeline.count(), 2)
|
||||||
|
self.assertEqual(game2_timeline.count(), 1)
|
8
collection/games/urls.py
Normal file
8
collection/games/urls.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from django.conf.urls import url
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'summary/$', views.SummaryList.as_view()),
|
||||||
|
url(r'$', views.GameList.as_view()),
|
||||||
|
]
|
@ -1,3 +1,91 @@
|
|||||||
from django.shortcuts import render
|
from django.db.models import Q
|
||||||
|
from django.views.generic import ListView
|
||||||
|
from rest_framework import viewsets
|
||||||
|
|
||||||
# Create your views here.
|
from .models import Game
|
||||||
|
from .models import Platform
|
||||||
|
from .models import Timeline
|
||||||
|
from .serializers import GameSerializer
|
||||||
|
from .serializers import GameTimelineSerializer
|
||||||
|
from .serializers import PlatformSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class PlatformViewSet(viewsets.ModelViewSet):
|
||||||
|
"""
|
||||||
|
API endpoints that allows platforms to be edited or viewed.
|
||||||
|
|
||||||
|
retrieve:
|
||||||
|
Return the given platform
|
||||||
|
|
||||||
|
list:
|
||||||
|
Return a list of all existing platforms ordered by name.
|
||||||
|
|
||||||
|
create:
|
||||||
|
Create a new platform instance.
|
||||||
|
"""
|
||||||
|
queryset = Platform.objects.all().order_by('name')
|
||||||
|
serializer_class = PlatformSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class GameTimelineViewSet(viewsets.ModelViewSet):
|
||||||
|
"""
|
||||||
|
API endpoints that allows to keep games life.
|
||||||
|
BE CAREFUL. You shouldn't change these entries because they're generated
|
||||||
|
by Game status change. It could be only useful to change the acquisition
|
||||||
|
date. Not more.
|
||||||
|
|
||||||
|
retrieve:
|
||||||
|
Return the given timeline entry.
|
||||||
|
|
||||||
|
list:
|
||||||
|
Return a list of timeline entries sorted by date in descending order.
|
||||||
|
|
||||||
|
create:
|
||||||
|
Create a new timeline instance.
|
||||||
|
"""
|
||||||
|
queryset = Timeline.objects.all().order_by('-date')
|
||||||
|
serializer_class = GameTimelineSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class GameViewSet(viewsets.ModelViewSet):
|
||||||
|
"""
|
||||||
|
API endpoints that allows games to be edited or viewed.
|
||||||
|
|
||||||
|
retrieve:
|
||||||
|
Return the given game.
|
||||||
|
|
||||||
|
list:
|
||||||
|
Return a list of all existing games ordered by name.
|
||||||
|
|
||||||
|
create:
|
||||||
|
Create a new game instance.
|
||||||
|
"""
|
||||||
|
queryset = Game.objects.all().order_by('name')
|
||||||
|
serializer_class = GameSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class GameList(ListView):
|
||||||
|
model = Game
|
||||||
|
context_object_name = 'non_excluded_games'
|
||||||
|
template_name = 'games/index.html'
|
||||||
|
queryset = Game.objects.filter(~Q(status=Game.EXCLUDED) & Q(
|
||||||
|
wish=False)).order_by('name').prefetch_related('collection')
|
||||||
|
|
||||||
|
|
||||||
|
class SummaryList(ListView):
|
||||||
|
model = Game
|
||||||
|
context_object_name = 'playing_games'
|
||||||
|
template_name = 'games/summary.html'
|
||||||
|
queryset = Game.objects.filter(
|
||||||
|
playing=True).order_by('name').prefetch_related('collection')
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Add playing games list.
|
||||||
|
Add 5 last current activities from Timeline
|
||||||
|
"""
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['last_timelines'] = Timeline.objects.filter(
|
||||||
|
~Q(item__status=Game.EXCLUDED)).order_by(
|
||||||
|
'-date')[:5].prefetch_related('item__collection')
|
||||||
|
return context
|
||||||
|
6
dev-requirements.txt
Normal file
6
dev-requirements.txt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
-r requirements.txt
|
||||||
|
django-debug-toolbar
|
||||||
|
flake8
|
||||||
|
pre-commit
|
||||||
|
pylint
|
||||||
|
pylint_django
|
@ -5,12 +5,14 @@ set -e
|
|||||||
chown -R guest "$DB_DIR"
|
chown -R guest "$DB_DIR"
|
||||||
|
|
||||||
if [ "$1" = 'dev' ]; then
|
if [ "$1" = 'dev' ]; then
|
||||||
export DJANGO_SETTINGS_MODULE=collection.settings
|
export DJANGO_ENV='development'
|
||||||
exec python3 manage.py runserver 0.0.0.0:8000
|
exec python3 manage.py runserver 0.0.0.0:8000
|
||||||
elif [ "$1" = 'prod' ]; then
|
elif [ "$1" = 'prod' ]; then
|
||||||
export DJANGO_SETTINGS_MODULE=collection.production
|
export DJANGO_ENV='production'
|
||||||
# Collect static files
|
# Collect static files
|
||||||
python3 manage.py collectstatic --noinput --clear -v 0
|
python3 manage.py collectstatic --noinput --clear -v 0
|
||||||
|
# translations generation
|
||||||
|
python3 manage.py compilemessages
|
||||||
exec uwsgi --ini uwsgi.ini --pythonpath "$APPS_DIR" --static-map=/static/="$STATIC_ROOT"
|
exec uwsgi --ini uwsgi.ini --pythonpath "$APPS_DIR" --static-map=/static/="$STATIC_ROOT"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
Django==1.11
|
coreapi==2.3.1
|
||||||
PyYAML==3.12
|
Django==1.11.15
|
||||||
|
django-cors-headers==2.1.0
|
||||||
|
django-split-settings==0.2.5
|
||||||
|
djangorestframework==3.6.4
|
||||||
|
psycopg2==2.7.3.2
|
||||||
|
PyYAML==3.12 ; python_version < 3.7
|
||||||
|
PyYAML==3.13 ; python_version >= 3.7
|
||||||
uWSGI==2.0.15
|
uWSGI==2.0.15
|
||||||
|
Reference in New Issue
Block a user