From 242b0edff02207c064a88f5e39ae7e5d72c6a2f6 Mon Sep 17 00:00:00 2001 From: Olivier DOSSMANN Date: Tue, 22 Aug 2017 21:02:43 +0200 Subject: [PATCH] Refonte de la base: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * utilisation de classes abstraites Django : Item et Collection * héritage de ces classes pour Game et Console * création d'un objet Timeline contenant le changement d'état des jeux * affichage de la Timeline sur l'interface Admin --- TODO | 5 +- collection/collection/settings.py | 3 +- collection/core/__init__.py | 0 collection/core/apps.py | 6 ++ collection/core/models.py | 86 +++++++++++++++++++++ collection/games/admin.py | 9 ++- collection/games/apps.py | 23 ++++++ collection/games/migrations/0001_initial.py | 33 ++++++-- collection/games/models.py | 44 +++++------ collection/games/signals.py | 22 ++++++ collection/games/tests/test_game.py | 6 +- 11 files changed, 198 insertions(+), 39 deletions(-) create mode 100644 collection/core/__init__.py create mode 100644 collection/core/apps.py create mode 100644 collection/core/models.py create mode 100644 collection/games/signals.py diff --git a/TODO b/TODO index c3b17a7..eebac9c 100644 --- a/TODO +++ b/TODO @@ -8,10 +8,7 @@ * API django rest framework * documentation API * travis.yml to launch test - * ajouter une date d'obtention du jeu vidéo (par défaut aujourd'hui) - * gérer l'historique de changement d'un jeu : faire une table qui contient jeu, date et état. Triée par date descendant (le plus récent au dessus). L'appeler memory ? - * que faire pour fusionner les dates d'obtention des jeux vidéos et l'historique des changements sur ces derniers afin d'obtenir une sorte de table contenant l'ensemble des activités sur l'application ? Ceci permettrait d'avoir un encart de "news" des derniers changements effectués sur le site - + * ajouter une date d'obtention du jeu vidéo (par défaut aujourd'hui) # Idée diff --git a/collection/collection/settings.py b/collection/collection/settings.py index 3742bb2..8ef4de8 100644 --- a/collection/collection/settings.py +++ b/collection/collection/settings.py @@ -37,7 +37,8 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'games', + 'core', + 'games.apps.GamesConfig', ] MIDDLEWARE = [ diff --git a/collection/core/__init__.py b/collection/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/collection/core/apps.py b/collection/core/apps.py new file mode 100644 index 0000000..c42474a --- /dev/null +++ b/collection/core/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + name = 'core' + diff --git a/collection/core/models.py b/collection/core/models.py new file mode 100644 index 0000000..e84d149 --- /dev/null +++ b/collection/core/models.py @@ -0,0 +1,86 @@ +from datetime import datetime +from django.db import models +from django.utils.translation import ugettext as _ + + +class Collection(models.Model): + name = models.CharField(max_length=255) + + def __str__(self): + return '%s' % self.name + + class Meta: + abstract = True + ordering = ['name'] + + +class Item(models.Model): + TARGET_MODEL = None + TARGET_VERBOSE_NAME = None + + # status choices + CREATED = 'created' + STATUS_CHOICES = ( + (CREATED, _('New')), + ) + DEFAULT_CHOICE = CREATED + + name = models.CharField(max_length=255) + + def __str__(self): + return '%s' % self.name + + @classmethod + def on_class_prepared(cls): + """ + Add new field 'collection' which is a link to TARGET_MODEL. + Add new field 'status' which is a list of choices. With CREATED. + """ + target_field = models.ForeignKey( + cls.TARGET_MODEL, + related_name=cls.RELATED_TARGET_NAME, + verbose_name=cls.TARGET_VERBOSE_NAME) + target_field.contribute_to_class(cls, 'collection') + status_field = models.CharField( + max_length=30, + choices=Item.STATUS_CHOICES + cls.STATUS_CHOICES, + default=cls.DEFAULT_CHOICE) + status_field.contribute_to_class(cls, 'status') + + class Meta: + abstract = True + ordering = ['name'] + + +class Timeline(models.Model): + """ + Keep changes on collection. + For an example, a game cretion date. Or when you completed a game. + """ + TARGET_MODEL = None + STATUS_CHOICES = Item.STATUS_CHOICES + DEFAULT_CHOICE = Item.CREATED + date = models.DateTimeField(default=datetime.now) + + @classmethod + def on_class_prepared(cls): + """ + Add new field 'item' which is a link to TARGET_MODEL + """ + target_field = models.ForeignKey(cls.TARGET_MODEL) + target_field.contribute_to_class(cls, 'item') + status_field = models.CharField( + max_length=30, + choices=Item.STATUS_CHOICES + cls.STATUS_CHOICES, + default=cls.DEFAULT_CHOICE) + status_field.contribute_to_class(cls, 'status') + + def __str__(self): + return '%s: %s - %s' % ( + self.date.strftime('%Y-%m-%d'), + self.status, + self.item) + + class Meta: + abstract = True + ordering = ('-date',) diff --git a/collection/games/admin.py b/collection/games/admin.py index 3588369..961c6a7 100644 --- a/collection/games/admin.py +++ b/collection/games/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from games.models import Console, Game +from games.models import Console, Game, Timeline class GameAdmin(admin.ModelAdmin): @@ -11,5 +11,12 @@ class GameAdmin(admin.ModelAdmin): 'wish'] search_fields = ('name',) + +class TimelineAdmin(admin.ModelAdmin): + list_display = ( + 'date', 'status', 'item') + + admin.site.register(Console) admin.site.register(Game, GameAdmin) +admin.site.register(Timeline, TimelineAdmin) diff --git a/collection/games/apps.py b/collection/games/apps.py index b74f62c..c9185f7 100644 --- a/collection/games/apps.py +++ b/collection/games/apps.py @@ -1,5 +1,28 @@ from django.apps import AppConfig +from django.db.models import signals + + +def call_on_class_prepared(sender, **kwargs): + """ + Calls the function only if it is defined in the class being prepared + """ + try: + sender.on_class_prepared() + except AttributeError: + pass class GamesConfig(AppConfig): name = 'games' + + def ready(self): + """ + Add signals to the application + """ + from .models import Game + from .signals import game_saved + signals.post_save.connect(game_saved, sender='games.Game') + + def __init__(self, app_name, app_module): + super(GamesConfig, self).__init__(app_name, app_module) + signals.class_prepared.connect(call_on_class_prepared) diff --git a/collection/games/migrations/0001_initial.py b/collection/games/migrations/0001_initial.py index 46c5adf..5e22007 100644 --- a/collection/games/migrations/0001_initial.py +++ b/collection/games/migrations/0001_initial.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2017-08-16 19:55 +# Generated by Django 1.11 on 2017-08-22 18:56 from __future__ import unicode_literals +import datetime from django.db import migrations, models import django.db.models.deletion @@ -18,21 +19,39 @@ class Migration(migrations.Migration): name='Console', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=254)), + ('name', models.CharField(max_length=255)), ], - options={'ordering': ('name',)}, + options={ + 'ordering': ['name'], + 'abstract': False, + }, ), migrations.CreateModel( name='Game', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=254)), - ('console', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='games.Console')), + ('name', models.CharField(max_length=255)), ('playing', models.BooleanField(default=False)), - ('status', models.IntegerField(choices=[(4, 'Unfinished'), (0, 'Beaten'), (1, 'Completed'), (2, 'Excluded'), (3, 'Mastered')], default=4)), ('unplayed', models.BooleanField(default=False)), ('wish', models.BooleanField(default=False)), + ('status', models.CharField(choices=[('created', 'New'), ('beaten', 'Beaten'), ('completed', 'Completed'), ('excluded', 'Excluded'), ('mastered', 'Mastered'), ('unfinished', 'Unfinished')], default='unfinished', max_length=30)), + ('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='games', to='games.Console', verbose_name='console')), ], - options={'ordering': ('-playing', 'name')}, + options={ + 'ordering': ('-playing', 'name'), + }, + ), + migrations.CreateModel( + name='Timeline', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateTimeField(default=datetime.datetime.now)), + ('status', models.CharField(choices=[('created', 'New'), ('beaten', 'Beaten'), ('completed', 'Completed'), ('excluded', 'Excluded'), ('mastered', 'Mastered'), ('unfinished', 'Unfinished')], default='created', max_length=30)), + ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='games.Game')), + ], + options={ + 'ordering': ('-date',), + 'abstract': False, + }, ), ] diff --git a/collection/games/models.py b/collection/games/models.py index 5431e5a..e06e134 100644 --- a/collection/games/models.py +++ b/collection/games/models.py @@ -1,53 +1,51 @@ +from core.models import Collection, Item, Timeline as BaseTimeline from django.db import models from django.utils.translation import ugettext as _ -class Console(models.Model): +class Console(Collection): """ All console, system or box that can be used to play video games. """ - name = models.CharField(max_length=254) - def __str__(self): return '%s' % self.name - class Meta: - ordering = ('name',) - -class Game(models.Model): +class Game(Item): """ A video game you will use on a specific Console. """ + # class config + TARGET_MODEL = 'games.Console' + TARGET_VERBOSE_NAME = _('console') + RELATED_TARGET_NAME = 'games' + # Status choices - BEATEN = 0 - COMPLETED = 1 - EXCLUDED = 2 - MASTERED = 3 - UNFINISHED = 4 + BEATEN = 'beaten' + COMPLETED = 'completed' + EXCLUDED = 'excluded' + MASTERED = 'mastered' + UNFINISHED = 'unfinished' STATUS_CHOICES = ( - (UNFINISHED, _('Unfinished')), (BEATEN, _('Beaten')), (COMPLETED, _('Completed')), (EXCLUDED, _('Excluded')), (MASTERED, _('Mastered')), + (UNFINISHED, _('Unfinished')), ) - - # required - name = models.CharField(max_length=254) - console = models.ForeignKey('games.Console') - status = models.IntegerField( - choices=STATUS_CHOICES, - default=UNFINISHED) + DEFAULT_CHOICE = UNFINISHED # others playing = models.BooleanField(default=False) unplayed = models.BooleanField(default=False) wish = models.BooleanField(default=False) - def __str__(self): - return '%s' % self.name - class Meta: ordering = ('-playing', 'name') + + +class Timeline(BaseTimeline): + TARGET_MODEL = 'games.Game' + STATUS_CHOICES = Game.STATUS_CHOICES + DEFAULT_CHOICE = Item.DEFAULT_CHOICE diff --git a/collection/games/signals.py b/collection/games/signals.py new file mode 100644 index 0000000..8cd7c9f --- /dev/null +++ b/collection/games/signals.py @@ -0,0 +1,22 @@ +from games.models import Timeline + + +def game_saved(sender, instance, created, raw, using, update_fields, + **kwargs): + """ + Add timeline entry. + If game is created, add 2 timlines: 1 with CREATED status. The other with + current object status. + """ + # FIXME: don't write a timeline if previous have same title and object_id + entry = { + 'item': instance, + 'status': instance.status, + } + # Add CREATED status if Game was created + if created is True: + new_entry = dict(entry) + new_entry.update({'status': instance.CREATED}) + Timeline.objects.create(**new_entry) + # Add new timeline entry + Timeline.objects.create(**entry) diff --git a/collection/games/tests/test_game.py b/collection/games/tests/test_game.py index a5565d3..7ff186b 100644 --- a/collection/games/tests/test_game.py +++ b/collection/games/tests/test_game.py @@ -10,11 +10,11 @@ class GameTest(TestCase): def setUp(self): self.console = Console.objects.create(name='BestConsole4Ever') Game.objects.create( - name='Deponia', playing=False, console=self.console) + name='Deponia', playing=False, collection=self.console) Game.objects.create( - name='Aladdin', playing=True, console=self.console) + name='Aladdin', playing=True, collection=self.console) Game.objects.create( - name='Persona 5', playing=True, console=self.console) + name='Persona 5', playing=True, collection=self.console) def test_game_are_sorted_by_playing_and_name(self):