diff --git a/README.md b/README.md index 6dd8318..12a4b6a 100644 --- a/README.md +++ b/README.md @@ -35,28 +35,42 @@ todo list, watch list, ranking... ## Changelog +## 3.5.0 + +* Platform is now visible in the history screen. +* Added a trading feature: you can list the games you bought or sell. +* Added a position (rank) in the "to play" and "to watch" sections. +* Restructuration of the menu + ### 3.4.0 + * Added watched and played badges in the history section. ### 3.3.0 + * Added a new feature: "Best Game Forever". Different from the hall of fames. BGF games are still valuable today, while some entries in the HOF were valuable only at time. ### 3.2.0 + * Added a blacktheme (the white one is no longer applied). * Added a "Has the box" badge when we have the original box (not a re-edition). * Fixed the "To do with help" badge that was not displayed in the header of a game details page. * Resized the textarea for the comment section when editing a game. ### 3.1.0 + * Added a "todo with help" icon in the game list if the game is to be completed with some help. * The game id is now visible in the game lists. * Added a History menu to log the games we finish (watched or played). ### 3.0.1 + * Fixed a bug when adding a new game while edition was still working. ## Todo + Yet the project is fully working, there are some features or elements to improve: + * Internationalization. * Add a logical check if the user already exists when creating an account. diff --git a/V4_FEATURES.md b/V4_FEATURES.md new file mode 100644 index 0000000..9f887d2 --- /dev/null +++ b/V4_FEATURES.md @@ -0,0 +1,8 @@ +# Estimated list of changes in the V4 + +* Front app is no longer included. +* To watch serious and to watch background are merged. +* True REST APIs: + * endpoint are reflecting resources and no longer screens ; + * using HTTP verbs instead of different endpoints for each action. +* Games are no longer duplicated (one entry per platform). We link them to the platforms instead. diff --git a/app.py b/app.py index 7f83a23..345a5c0 100644 --- a/app.py +++ b/app.py @@ -7,6 +7,7 @@ from src.controller.games import GameController from src.controller.user import UserController from src.controller.history import HistoryController +from src.controller.trading import TradeController from src.repository.user_repository import UserRepository from src.connection.mysql_factory import MySQLFactory @@ -182,9 +183,30 @@ def add_history(): controller = HistoryController return controller.add(MySQLFactory.get()) -@app.route('/history/delete/', methods=['DELETE']) +@app.route('/history/delete/', methods=['DELETE']) @login_required -def delete_history(game_id): +def delete_history(entity_id): """History deletion""" controller = HistoryController - return controller.delete(MySQLFactory.get(), game_id) + return controller.delete(MySQLFactory.get(), entity_id) + +# Trading management +@app.route('/trading/history', methods=['GET']) +def trading_history(): + """Trading history""" + controller = TradeController() + return controller.get_list(MySQLFactory.get()) + +@app.route('/trading/add', methods=['GET', 'POST']) +@login_required +def add_trade(): + """Adding a new trading entry""" + controller = TradeController + return controller.add(MySQLFactory.get()) + +@app.route('/trading/delete/', methods=['DELETE']) +@login_required +def delete_trade(entity_id): + """Trading deletion""" + controller = TradeController + return controller.delete(MySQLFactory.get(), entity_id) diff --git a/games_empty.sql b/games_empty.sql index fad443f..55f0ea1 100644 --- a/games_empty.sql +++ b/games_empty.sql @@ -7,7 +7,7 @@ # # Hôte: 0.0.0.0 (MySQL 5.7.29) # Base de données: games -# Temps de génération: 2021-08-05 12:36:24 +0000 +# Temps de génération: 2021-09-12 14:43:35 +0000 # ************************************************************ @@ -66,6 +66,8 @@ CREATE TABLE `games_meta` ( `todo_with_help` tinyint(3) unsigned NOT NULL DEFAULT '0', `has_box` tinyint(3) unsigned NOT NULL DEFAULT '0', `bgf` tinyint(3) unsigned NOT NULL DEFAULT '0', + `to_watch_position` tinyint(3) unsigned NOT NULL DEFAULT '0', + `to_do_position` tinyint(3) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`game_id`), CONSTRAINT `games_meta_ibfk_1` FOREIGN KEY (`game_id`) REFERENCES `games` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; @@ -82,6 +84,8 @@ CREATE TABLE `history` ( `game_id` smallint(11) unsigned NOT NULL, `year` smallint(6) unsigned NOT NULL, `position` smallint(6) unsigned NOT NULL, + `watched` tinyint(3) unsigned NOT NULL DEFAULT '0', + `played` tinyint(3) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `game_id` (`game_id`), CONSTRAINT `history_ibfk_1` FOREIGN KEY (`game_id`) REFERENCES `games` (`id`) @@ -103,6 +107,25 @@ CREATE TABLE `platforms` ( +# Affichage de la table trades +# ------------------------------------------------------------ + +DROP TABLE IF EXISTS `trades`; + +CREATE TABLE `trades` ( + `id` int(4) unsigned NOT NULL AUTO_INCREMENT, + `game_id` smallint(5) unsigned NOT NULL, + `year` smallint(1) unsigned NOT NULL, + `month` smallint(2) unsigned NOT NULL, + `day` smallint(2) unsigned NOT NULL, + `type` tinyint(1) unsigned NOT NULL, + PRIMARY KEY (`id`), + KEY `game_id` (`game_id`), + CONSTRAINT `trades_ibfk_1` FOREIGN KEY (`game_id`) REFERENCES `games` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + + + # Affichage de la table users # ------------------------------------------------------------ diff --git a/src/controller/trading.py b/src/controller/trading.py new file mode 100644 index 0000000..a98ae2f --- /dev/null +++ b/src/controller/trading.py @@ -0,0 +1,53 @@ +""" Trading controller for the GMG project """ +from flask import jsonify, request, render_template, session +from src.repository.trade_repository import TradeRepository + +class TradeController: + """ Trading controller for the GMG project """ + @classmethod + def get_list(cls, mysql): + """Return the whole trading history.""" + trade_repo = TradeRepository(mysql) + games_list = trade_repo.get_all() + return jsonify( + games=[game.serialize() for game in games_list] + ) + + @classmethod + def add(cls, mysql): + """Add a new trade.""" + if request.method == 'GET': + form = render_template( + 'general/trading-history-form.html', + token=session['csrfToken'] + ) + + return jsonify(form=form, title="Ajouter une transaction") + + if request.form['_token'] != session['csrfToken']: + return jsonify(), 400 + + game_id = request.form['game_id'] + year = request.form['year'] + month = request.form['month'] + day = request.form['day'] + operation = request.form['type'] + + if game_id == '' or year == '' or month == '' or day == '' or operation == '': + return "Form is incomplete" + + trade_repo = TradeRepository(mysql) + trade_repo.insert(game_id, year, month, day, operation) + + return jsonify(), 200 + + @classmethod + def delete(cls, mysql, entry_id): + """Delete a trading entry.""" + if request.form['_token'] != session['csrfToken']: + return jsonify(), 400 + + history_repo = TradeRepository(mysql) + history_repo.delete(entry_id) + + return jsonify(), 204 diff --git a/src/entity/history.py b/src/entity/history.py index b238b15..055c597 100644 --- a/src/entity/history.py +++ b/src/entity/history.py @@ -8,6 +8,7 @@ def __init__( entity_id, game_id, title, + platform, year, position, watched, @@ -16,6 +17,7 @@ def __init__( self.entity_id = entity_id self.game_id = game_id self.title = title + self.platform = platform self.year = year self.position = position self.watched = watched @@ -36,6 +38,11 @@ def get_title(self): return self.title + def get_platform(self): + """Platform?""" + + return self.platform + def get_year(self): """Return the year.""" @@ -66,6 +73,7 @@ def serialize(self): 'id': self.entity_id, 'game_id': self.game_id, 'title': self.title, + 'platform': self.platform, 'year': self.year, 'position': self.position, 'watched': self.watched, diff --git a/src/entity/trade.py b/src/entity/trade.py new file mode 100644 index 0000000..5bce5a7 --- /dev/null +++ b/src/entity/trade.py @@ -0,0 +1,81 @@ +""" Trading entity for the GMG project """ +import json + +class Trade: + """ This class represent a trading entry, for instance I sell or bought a game """ + def __init__( + self, + entity_id, + game_id, + title, + platform, + year, + month, + day, + operation + ): + self.entity_id = entity_id + self.game_id = game_id + self.title = title + self.platform = platform + self.year = year + self.month = month + self.day = day + self.type = operation + + def get_id(self): + """Return the id entry.""" + + return self.entity_id + + def get_game_id(self): + """Return the id of the game, for instance "125".""" + + return self.game_id + + def get_title(self): + """Return the title of the game, for instance "Fifa 98".""" + + return self.title + + def get_platform(self): + """Platform?""" + + return self.platform + + def get_year(self): + """Return the year""" + + return self.year + + def get_month(self): + """Return the month.""" + + return self.month + + def get_day(self): + """Return the day.""" + + return self.day + + def get_type(self): + """0: sold, 1: bought""" + + return self.type + + def to_json(self): + """Jsonify the object""" + return json.dumps(self, default=lambda o: o.__dict__) + + def serialize(self): + """serialize the object""" + return { + 'id': self.entity_id, + 'game_id': self.game_id, + 'title': self.title, + 'platform': self.platform, + 'year': self.year, + 'month': self.month, + 'day': self.day, + 'type': self.type + } diff --git a/src/repository/game_repository.py b/src/repository/game_repository.py index 69bab0d..877891e 100644 --- a/src/repository/game_repository.py +++ b/src/repository/game_repository.py @@ -21,7 +21,9 @@ class GameRepository(AbstractRepository): 'to_buy', 'original', 'ongoing', - 'bgf' + 'bgf', + 'to_watch_position', + 'to_do_position' ] random_cases = [ @@ -153,9 +155,9 @@ def insert(self, title, platform, form_content): request += "many, top_game," request += "hall_of_fame, hall_of_fame_year," request += "hall_of_fame_position, played_it_often, ongoing, comments, todo_with_help" - request += ", has_box, bgf) " + request += ", has_box, bgf, to_watch_position, to_do_position) " request += "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s, %s, %s, %s" - request += ", %s)" + request += ", %s, %s, %s)" self.write(request, meta) @@ -189,7 +191,9 @@ def update(self, game_id, title, platform, form_content): request += "comments=%s, " request += "todo_with_help=%s, " request += "has_box=%s, " - request += "bgf=%s " + request += "bgf=%s, " + request += "to_watch_position=%s, " + request += "to_do_position=%s " request += "WHERE game_id = %s" self.write(request, meta) @@ -222,6 +226,8 @@ def get_meta_form_request(cls, game_id, form_content, operation): form_content['todo_with_help'], form_content['has_box'], form_content['bgf'], + form_content['to_watch_position'], + form_content['to_do_position'], ) if operation == 'INSERT': diff --git a/src/repository/history_repository.py b/src/repository/history_repository.py index 566199d..6a25945 100644 --- a/src/repository/history_repository.py +++ b/src/repository/history_repository.py @@ -9,8 +9,9 @@ def get_all(self): """Gets all the history.""" request = "SELECT history.id as id, history.game_id as game_id, history.year as year" request += ", history.position as position, games.title as title, history.watched" - request += ", history.played FROM history, games" - request += " WHERE history.game_id = games.id ORDER BY year, position" + request += ", platforms.name as platform, history.played FROM history, games, platforms" + request += " WHERE history.game_id = games.id AND games.platform = platforms.id" + request += " ORDER BY year, position" return self.fetch_multiple(request, ()) @@ -33,6 +34,7 @@ def hydrate(cls, row): row['id'], row['game_id'], row['title'], + row['platform'], row['year'], row['position'], row['watched'], diff --git a/src/repository/trade_repository.py b/src/repository/trade_repository.py new file mode 100644 index 0000000..d616e32 --- /dev/null +++ b/src/repository/trade_repository.py @@ -0,0 +1,46 @@ +""" Repository to handle trading """ +from src.repository.abstract_repository import AbstractRepository +from src.entity.trade import Trade + +class TradeRepository(AbstractRepository): + """ Another useless comment """ + + def get_all(self): + """Gets all the trading entries.""" + request = "SELECT trades.id as id, trades.game_id as game_id, trades.year as year," + request += " trades.month as month, trades.day as day, trades.type as type," + request += " games.title as title, platforms.name as platform" + request += " FROM trades,games, platforms" + request += " WHERE trades.game_id = games.id AND games.platform = platforms.id" + request += " ORDER BY year, month, day" + + return self.fetch_multiple(request, ()) + + def insert(self, game_id, year, month, day, operation): + """Insert a new trading entry""" + request = "INSERT INTO trades (game_id, year, month, day, type) " + request += "VALUES (%s,%s,%s,%s,%s)" + return self.write(request, (game_id, year, month, day, operation,)) + + def delete(self, entity_id): + """Delete a trade entry""" + + request = "DELETE FROM trades WHERE id=%s" + self.write(request, (entity_id,)) + + @classmethod + def hydrate(cls, row): + """Hydrate an object from a row.""" + print(row, flush=True) + trade = Trade( + row['id'], + row['game_id'], + row['title'], + row['platform'], + row['year'], + row['month'], + row['day'], + row['type'] + ) + + return trade diff --git a/standard.rc b/standard.rc index 04dab9a..bfc2821 100644 --- a/standard.rc +++ b/standard.rc @@ -595,4 +595,4 @@ overgeneral-exceptions=BaseException, Exception # disable=R0913, R0914, R0902, R0904 -disable=R0913, W1514 +disable=R0913, W1514, R0801, R0902 diff --git a/static/images/in.png b/static/images/in.png new file mode 100755 index 0000000..204ccbe Binary files /dev/null and b/static/images/in.png differ diff --git a/static/images/out.png b/static/images/out.png new file mode 100755 index 0000000..172aa8f Binary files /dev/null and b/static/images/out.png differ diff --git a/static/js/app/main.js b/static/js/app/main.js index 4a5a985..08f7ef9 100644 --- a/static/js/app/main.js +++ b/static/js/app/main.js @@ -17,7 +17,9 @@ requirejs.config({ "platformEditor": "forms/platform", "gameEditor": "forms/game", "historyEditor": "forms/history", - "history": "pages/history" + "history": "pages/history", + "tradingEditor": "forms/trading", + "trading": "pages/trading" }, // Define dependencies between modules and libraries, and the order of loading shim: { diff --git a/static/js/forms/trading.js b/static/js/forms/trading.js new file mode 100644 index 0000000..2be3bbe --- /dev/null +++ b/static/js/forms/trading.js @@ -0,0 +1,27 @@ +/** + * @author Eric COURTIAL + */ + define( + ["jquery"], + function ($) { + "use strict"; + + return { + /** + * Add or edit a trading history entry + */ + diplayData: function (data, context) { + $('#contentTitle').html(this.getTitle(context)); + $('#content').empty().html(data.form); + }, + + getTitle: function(context) { + if (context === 'add') { + return "Ajouter une entrée dans l'historique commercial"; + } else { + return '???'; + } + } + }; + } +); diff --git a/static/js/pages/content.js b/static/js/pages/content.js index dfe7169..f71bb1d 100644 --- a/static/js/pages/content.js +++ b/static/js/pages/content.js @@ -2,8 +2,8 @@ * @author Eric COURTIAL */ define( - ["jquery", "platforms", "games", "game", "home", "platformEditor", "gameEditor", "historyEditor", "history"], - function ($, platforms, games, game, home, platformEditor, gameEditor, historyEditor, history) { + ["jquery", "platforms", "games", "game", "home", "platformEditor", "gameEditor", "historyEditor", "history", "tradingEditor", "trading"], + function ($, platforms, games, game, home, platformEditor, gameEditor, historyEditor, history, tradingEditor, trading) { "use strict"; /** @@ -110,6 +110,19 @@ define( dataManager.request(deleteHistoryUrl + id, null, null, false, {'_token': $('#tokenCSRF').html()}, 'DELETE', callback); } + function deleteTradingHistory(id) { + if (confirm("Etes-vous sûr de vouloir supprimer cette entrée ?") === false) { + return false; + } + + var callback = function() { + $("#trading_history").trigger("click"); + }; + + dataManager.showTempMsg(true); + dataManager.request(deleteTradeHistoryUrl + id, null, null, false, {'_token': $('#tokenCSRF').html()}, 'DELETE', callback); + } + /** * Event listeners */ @@ -185,6 +198,14 @@ define( return false; }); + // Click on the trading menu + $('#trading_history').click(function() { + dataManager.showTempMsg(true); + dataManager.request(getTradingHistoryUrl, trading, null); + + return false; + }); + // Search form $('#searchForm').submit(function() { var url = gamesSpecialListUrl + 'search?query=' + $('#gameSearch').val(); @@ -217,6 +238,14 @@ define( return false; }); + + // Add trading history entry form + $('#addTradingHistory').click(function() { + dataManager.showTempMsg(true); + dataManager.request(addTradingHistoryUrl, tradingEditor, 'add'); + + return false; + }); /** Content listeners */ @@ -244,9 +273,11 @@ define( return false; } else if(linkType === 'historyDelete') { deleteHistory(id); - return false; - } else { + } else if(linkType === 'tradingHistoryDelete') { + deleteTradingHistory(id); + return false; + }else { return false; } diff --git a/static/js/pages/games.js b/static/js/pages/games.js index 0814dba..229a3b0 100644 --- a/static/js/pages/games.js +++ b/static/js/pages/games.js @@ -16,6 +16,8 @@ define( var content = this.getSubtitle(context); content += '
    '; + data.games = that.order(data.games, context); + $.each(data.games, function (index, value) { var gameEntry = tools.filterContent(value.title); gameEntry = that.getStartIcon(value) + gameEntry; @@ -129,6 +131,42 @@ define( } return gameEntry; + }, + + order: function(entries, context) { + var filter = null; + + if (context == "to_do") { + filter = "to_do_position"; + } else if(context == "to_watch_background" || context == "to_watch_serious") { + filter = "to_watch_position"; + } + + if (filter !== null ) { + entries.sort(function(x, y) { + x = x.meta[filter]; + y = y.meta[filter]; + + if (x == 0) { + x = 9999; + } + + if (y == 0) { + y = 9999; + } + + if (x < y) { + return -1; + } + if (x > y) { + return 1; + } + + return 0; + }); + } + + return entries; } }; } diff --git a/static/js/pages/history.js b/static/js/pages/history.js index 0e2cc6f..ef29a64 100644 --- a/static/js/pages/history.js +++ b/static/js/pages/history.js @@ -30,7 +30,7 @@ define( } } - var gameEntry = value.position + "- " + tools.filterContent(value.title); + var gameEntry = value.position + "- " + tools.filterContent(value.title) + " (" + tools.filterContent(value.platform) + ")"; gameEntry = that.getBadges(gameEntry, value); gameEntry += ' - Détails'; diff --git a/static/js/pages/trading.js b/static/js/pages/trading.js new file mode 100644 index 0000000..c0a9082 --- /dev/null +++ b/static/js/pages/trading.js @@ -0,0 +1,64 @@ +/** + * @author Eric COURTIAL + */ + define( + ["jquery", "tools"], + function ($, tools) { + "use strict"; + + return { + /** + * Diplay the list of games trading history + */ + diplayData: function (data, context) { + $('#contentTitle').html("Historique commercial"); + var content = '

    Jeux vendus ou achetés

    '; + var currentYear = 0; + var that = this; + + $.each(data.games, function (index, value) { + var gameYear = parseInt(value.year); + var yearString = '' + gameYear + ''; + + if (currentYear === 0) { + content += yearString + '
      '; + currentYear = gameYear; + } else { + if (currentYear != gameYear) { + content += '
    ' + yearString + '
      '; + currentYear = gameYear; + } + } + + var gameEntry = that.getBadges(value) + tools.filterContent(value.game_id) + + "- " + tools.filterContent(value.title) + " (" + tools.filterContent(value.platform) + ")"; + gameEntry += ' - Détails'; + + if (logged) { + gameEntry += ' - Editer'; + gameEntry += ' - Supprimer'; + gameEntry += ' - Supprimer entrée historique'; + } + + content += '
    • ' + gameEntry + '
    • ' + }); + + content += '
'; + + $('#content').empty().html(content); + }, + + getBadges: function(value) { + if (value.type === 0) { + return ' ' + } + + if (value.type === 1) { + return ' ' + } + + return ""; + } + }; + } +); diff --git a/templates/general/game-form.html b/templates/general/game-form.html index 8ad28a1..b5a609c 100644 --- a/templates/general/game-form.html +++ b/templates/general/game-form.html @@ -165,6 +165,22 @@

+

+ + +

+

+ + +

diff --git a/templates/general/header.html b/templates/general/header.html index bafe2bf..38afefe 100755 --- a/templates/general/header.html +++ b/templates/general/header.html @@ -32,8 +32,6 @@
  • Multi
  • -
  • En cours
  • -
  • À faire
  • Multi
  • +
  • Originaux
  • -
  • Originaux
  • -
  • A acheter
  • -
  • Historique
  • {% if current_user.is_authenticated %}
  • Ajout plateforme
  • Ajout entrée dans l'historique
  • +
  • Ajout entrée dans l'historique commercial
  • Profil
  • Déconnexion
  • diff --git a/templates/general/trading-history-form.html b/templates/general/trading-history-form.html new file mode 100644 index 0000000..b6f7294 --- /dev/null +++ b/templates/general/trading-history-form.html @@ -0,0 +1,45 @@ +
    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    + + +
    + \ No newline at end of file diff --git a/templates/layout.html b/templates/layout.html index 618c773..8e00c02 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -38,6 +38,8 @@

    {{ content_title }}

    var diamondImageUrl = "{{ url_for('static', filename='images/diamond.png') }}"; var watchedImageUrl = "{{ url_for('static', filename='images/eye.png') }}"; var playedImageUrl = "{{ url_for('static', filename='images/controller.png') }}"; + var inImageUrl = "{{ url_for('static', filename='images/in.png') }}"; + var outImageUrl = "{{ url_for('static', filename='images/out.png') }}"; // API URLs var hallOfFameUrl = "{{ url_for('get_home_content') }}"; @@ -46,10 +48,15 @@

    {{ content_title }}

    var addGameUrl = "{{ url_for('add_game') }}"; var addHistoryUrl = "{{ url_for('add_history') }}"; var getHistoryUrl = "{{ url_for('game_history') }}"; + var addTradingHistoryUrl = "{{ url_for('add_trade') }}"; + var getTradingHistoryUrl = "{{ url_for('trading_history') }}"; - var deleteHistoryUrl = "{{ url_for('delete_history', game_id=1) }}"; + var deleteHistoryUrl = "{{ url_for('delete_history', entity_id=1) }}"; deleteHistoryUrl = deleteHistoryUrl.substring(0, deleteHistoryUrl.length -1); + var deleteTradeHistoryUrl = "{{ url_for('delete_trade', entity_id=1) }}"; + deleteTradeHistoryUrl = deleteTradeHistoryUrl.substring(0, deleteTradeHistoryUrl.length -1); + var editGameUrl = "{{ url_for('edit_game', game_id=1) }}"; editGameUrl = editGameUrl.substring(0, editGameUrl.length -1);