Add cards to want lists from search view.
This commit is contained in:
@@ -13,6 +13,8 @@ except ImportError as ex:
|
|||||||
import os
|
import os
|
||||||
import copy
|
import copy
|
||||||
import re
|
import re
|
||||||
|
import mtgsdk
|
||||||
|
from typing import Type, Dict, List
|
||||||
|
|
||||||
from cardvault import handlers
|
from cardvault import handlers
|
||||||
from cardvault import util
|
from cardvault import util
|
||||||
@@ -21,6 +23,8 @@ from cardvault import lib_funct
|
|||||||
from cardvault import wants_funct
|
from cardvault import wants_funct
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Application:
|
class Application:
|
||||||
# ---------------------------------Initialize the Application----------------------------------------------
|
# ---------------------------------Initialize the Application----------------------------------------------
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -57,15 +61,15 @@ class Application:
|
|||||||
|
|
||||||
self.sets = util.load_sets(util.get_root_filename("sets"))
|
self.sets = util.load_sets(util.get_root_filename("sets"))
|
||||||
|
|
||||||
self.library = None
|
self.library = Dict[str, Type[mtgsdk.Card]]
|
||||||
self.tags = None
|
self.tags = Dict[str, str]
|
||||||
self.wants = None
|
self.wants = Dict[str, List[Type[mtgsdk.Card]]]
|
||||||
self.load_library()
|
self.load_library()
|
||||||
|
|
||||||
self.handlers = handlers.Handlers(self)
|
self.handlers = handlers.Handlers(self)
|
||||||
self.ui.connect_signals(self.handlers)
|
self.ui.connect_signals(self.handlers)
|
||||||
|
|
||||||
# Inizialize the views
|
# Initialize the views
|
||||||
|
|
||||||
search_funct.init_search_view(self)
|
search_funct.init_search_view(self)
|
||||||
|
|
||||||
@@ -207,6 +211,8 @@ class Application:
|
|||||||
util.save_file(util.get_root_filename("library"), self.library)
|
util.save_file(util.get_root_filename("library"), self.library)
|
||||||
# Save tags file
|
# Save tags file
|
||||||
util.save_file(util.get_root_filename("tags"), self.tags)
|
util.save_file(util.get_root_filename("tags"), self.tags)
|
||||||
|
# Save wants file
|
||||||
|
util.save_file(util.get_root_filename("wants"), self.wants)
|
||||||
self.unsaved_changes = False
|
self.unsaved_changes = False
|
||||||
self.push_status("Library saved")
|
self.push_status("Library saved")
|
||||||
|
|
||||||
@@ -283,12 +289,24 @@ class Application:
|
|||||||
util.log("Tag '" + old + "' renamed to '" + new + "'", util.LogLevel.Info)
|
util.log("Tag '" + old + "' renamed to '" + new + "'", util.LogLevel.Info)
|
||||||
self.unsaved_changes = True
|
self.unsaved_changes = True
|
||||||
|
|
||||||
|
def get_wanted_card_ids(self) -> List[str]:
|
||||||
|
all_ids = []
|
||||||
|
for cards in self.wants.values():
|
||||||
|
next_ids = [card.multiverse_id for card in cards]
|
||||||
|
all_ids = list(set(all_ids) | set(next_ids))
|
||||||
|
return all_ids
|
||||||
|
|
||||||
def add_want_list(self, name):
|
def add_want_list(self, name):
|
||||||
self.wants[name] = {}
|
self.wants[name] = []
|
||||||
util.log("Want list '" + name + "' created", util.LogLevel.Info)
|
util.log("Want list '" + name + "' created", util.LogLevel.Info)
|
||||||
self.push_status("Created want list '" + name + "'")
|
self.push_status("Created want list '" + name + "'")
|
||||||
self.unsaved_changes = True
|
self.unsaved_changes = True
|
||||||
|
|
||||||
|
def add_card_to_want_list(self, list_name, card):
|
||||||
|
self.wants[list_name].append(card)
|
||||||
|
util.log(card.name + " added to want list " + list_name, util.LogLevel.Info)
|
||||||
|
self.unsaved_changes = True
|
||||||
|
|
||||||
def add_card_to_lib(self, card, tag=None):
|
def add_card_to_lib(self, card, tag=None):
|
||||||
if tag is not None:
|
if tag is not None:
|
||||||
self.tag_card(card, tag)
|
self.tag_card(card, tag)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import gi
|
import gi
|
||||||
from cardvault import util
|
from cardvault import util
|
||||||
|
from cardvault import application
|
||||||
from gi.repository import Gtk, GdkPixbuf, Gdk
|
from gi.repository import Gtk, GdkPixbuf, Gdk
|
||||||
import time
|
import time
|
||||||
gi.require_version('Gtk', '3.0')
|
gi.require_version('Gtk', '3.0')
|
||||||
@@ -7,7 +8,7 @@ gi.require_version('Gdk', '3.0')
|
|||||||
|
|
||||||
|
|
||||||
class CardList(Gtk.ScrolledWindow):
|
class CardList(Gtk.ScrolledWindow):
|
||||||
def __init__(self, with_filter, app):
|
def __init__(self, with_filter, app : 'application.Application'):
|
||||||
Gtk.ScrolledWindow.__init__(self)
|
Gtk.ScrolledWindow.__init__(self)
|
||||||
self.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
self.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
||||||
self.set_hexpand(True)
|
self.set_hexpand(True)
|
||||||
@@ -134,11 +135,16 @@ class CardList(Gtk.ScrolledWindow):
|
|||||||
util.log("Updating tree view", util.LogLevel.Info)
|
util.log("Updating tree view", util.LogLevel.Info)
|
||||||
|
|
||||||
start = time.time()
|
start = time.time()
|
||||||
for card_id, card in library.items():
|
|
||||||
|
|
||||||
|
all_wants = self.app.get_wanted_card_ids()
|
||||||
|
|
||||||
|
for card_id, card in library.items():
|
||||||
if card.multiverse_id is not None:
|
if card.multiverse_id is not None:
|
||||||
|
|
||||||
if self.app.library.__contains__(card_id) and colorize:
|
if self.app.library.__contains__(card_id) and colorize:
|
||||||
color = util.card_view_colors["owned"]
|
color = util.card_view_colors["owned"]
|
||||||
|
elif all_wants.__contains__(card_id) and colorize:
|
||||||
|
color = util.card_view_colors["wanted"]
|
||||||
else:
|
else:
|
||||||
color = util.card_view_colors["unowned"]
|
color = util.card_view_colors["unowned"]
|
||||||
if card.type == "Land":
|
if card.type == "Land":
|
||||||
|
|||||||
@@ -100,7 +100,6 @@
|
|||||||
<property name="model">tagStore</property>
|
<property name="model">tagStore</property>
|
||||||
<property name="search_column">1</property>
|
<property name="search_column">1</property>
|
||||||
<signal name="button-press-event" handler="do_tag_tree_press_event" swapped="no"/>
|
<signal name="button-press-event" handler="do_tag_tree_press_event" swapped="no"/>
|
||||||
<signal name="drag-data-received" handler="on_drag_data_received" swapped="no"/>
|
|
||||||
<signal name="row-activated" handler="on_tag_selected" object="tagTreeSelection" swapped="no"/>
|
<signal name="row-activated" handler="on_tag_selected" object="tagTreeSelection" swapped="no"/>
|
||||||
<child internal-child="selection">
|
<child internal-child="selection">
|
||||||
<object class="GtkTreeSelection" id="tagTreeSelection"/>
|
<object class="GtkTreeSelection" id="tagTreeSelection"/>
|
||||||
|
|||||||
@@ -2,31 +2,24 @@
|
|||||||
<!-- Generated with glade 3.20.0 -->
|
<!-- Generated with glade 3.20.0 -->
|
||||||
<interface>
|
<interface>
|
||||||
<requires lib="gtk+" version="3.20"/>
|
<requires lib="gtk+" version="3.20"/>
|
||||||
<object class="GtkGrid" id="searchView">
|
<object class="GtkMenu" id="searchListPopup">
|
||||||
<property name="name">Search</property>
|
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="hexpand">True</property>
|
<signal name="show" handler="search_tree_popup_showed" swapped="no"/>
|
||||||
<property name="vexpand">True</property>
|
|
||||||
<property name="row_spacing">2</property>
|
|
||||||
<property name="column_spacing">2</property>
|
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkOverlay" id="searchResults">
|
<object class="GtkMenuItem" id="searchListPopupTags">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="hexpand">True</property>
|
<property name="label" translatable="yes">Tag Cards</property>
|
||||||
<property name="vexpand">True</property>
|
<property name="use_underline">True</property>
|
||||||
<child>
|
</object>
|
||||||
<placeholder/>
|
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<object class="GtkPaned" id="searchView">
|
||||||
<property name="left_attach">1</property>
|
<property name="visible">True</property>
|
||||||
<property name="top_attach">1</property>
|
<property name="can_focus">True</property>
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkBox" id="leftPane">
|
<object class="GtkBox" id="searchLeftPane">
|
||||||
<property name="name">leftPane</property>
|
<property name="name">leftPane</property>
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
@@ -275,14 +268,20 @@
|
|||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="left_attach">0</property>
|
<property name="resize">False</property>
|
||||||
<property name="top_attach">0</property>
|
<property name="shrink">True</property>
|
||||||
<property name="height">2</property>
|
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox" id="searchRightPane">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="spacing">2</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkBox" id="selectionToolsBox">
|
<object class="GtkBox" id="selectionToolsBox">
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
|
<property name="spacing">2</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkButton" id="addRemoveButton">
|
<object class="GtkButton" id="addRemoveButton">
|
||||||
<property name="label" translatable="yes">Add to Library</property>
|
<property name="label" translatable="yes">Add to Library</property>
|
||||||
@@ -306,8 +305,31 @@
|
|||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="left_attach">1</property>
|
<property name="expand">False</property>
|
||||||
<property name="top_attach">0</property>
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkOverlay" id="searchResults">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="vexpand">True</property>
|
||||||
|
<child>
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="resize">True</property>
|
||||||
|
<property name="shrink">True</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">False</property>
|
<property name="expand">True</property>
|
||||||
<property name="fill">True</property>
|
<property name="fill">True</property>
|
||||||
<property name="position">0</property>
|
<property name="position">0</property>
|
||||||
</packing>
|
</packing>
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ from cardvault import application
|
|||||||
|
|
||||||
|
|
||||||
class Handlers:
|
class Handlers:
|
||||||
def __init__(self, app):
|
def __init__(self, app: 'application.Application'):
|
||||||
self.app = Type[application.Application]
|
|
||||||
self.app = app
|
self.app = app
|
||||||
|
|
||||||
# ---------------------------------Main Window----------------------------------------------
|
# ---------------------------------Main Window----------------------------------------------
|
||||||
@@ -115,6 +114,29 @@ class Handlers:
|
|||||||
search_funct.reload_serach_view(self.app)
|
search_funct.reload_serach_view(self.app)
|
||||||
self.app.ui.get_object("searchEntry").grab_focus()
|
self.app.ui.get_object("searchEntry").grab_focus()
|
||||||
|
|
||||||
|
def search_tree_popup_showed(self, menu):
|
||||||
|
# Create wants Submenu
|
||||||
|
wants_item = self.app.ui.get_object("searchListPopupTags")
|
||||||
|
wants_sub = Gtk.Menu()
|
||||||
|
wants_item.set_submenu(wants_sub)
|
||||||
|
|
||||||
|
for list_name in self.app.wants.keys():
|
||||||
|
item = Gtk.MenuItem()
|
||||||
|
wants_sub.add(item)
|
||||||
|
item.set_label(list_name)
|
||||||
|
item.connect('activate', self.search_popup_add_wants)
|
||||||
|
|
||||||
|
wants_item.show_all()
|
||||||
|
|
||||||
|
def search_popup_add_wants(self, item):
|
||||||
|
# Get selected cards
|
||||||
|
card_list = self.app.ui.get_object("searchResults").get_child()
|
||||||
|
cards = card_list.get_selected_cards()
|
||||||
|
for card in cards.values():
|
||||||
|
self.app.add_card_to_want_list(item.get_label(), card)
|
||||||
|
search_funct.reload_serach_view(self.app)
|
||||||
|
self.app.push_status("Added " + str(len(cards)) + " card(s) to Want List '" + item.get_label() + "'")
|
||||||
|
|
||||||
# ---------------------------------Library----------------------------------------------
|
# ---------------------------------Library----------------------------------------------
|
||||||
|
|
||||||
def do_reload_library(self, view):
|
def do_reload_library(self, view):
|
||||||
@@ -149,9 +171,6 @@ class Handlers:
|
|||||||
lib_funct.reload_library(self.app, tag)
|
lib_funct.reload_library(self.app, tag)
|
||||||
entry.set_text("")
|
entry.set_text("")
|
||||||
|
|
||||||
def on_drag_data_received(self, widget, drag_context, x, y, data, info, time):
|
|
||||||
print("drag received")
|
|
||||||
|
|
||||||
def on_tag_selected(self, selection, path, column):
|
def on_tag_selected(self, selection, path, column):
|
||||||
(model, pathlist) = selection.get_selected_rows()
|
(model, pathlist) = selection.get_selected_rows()
|
||||||
for path in pathlist:
|
for path in pathlist:
|
||||||
@@ -262,6 +281,15 @@ class Handlers:
|
|||||||
else:
|
else:
|
||||||
add_remove_button.set_sensitive(False)
|
add_remove_button.set_sensitive(False)
|
||||||
|
|
||||||
|
def on_search_tree_press_event(self, treeview, event):
|
||||||
|
if event.button == 3: # right click
|
||||||
|
path = treeview.get_path_at_pos(int(event.x), int(event.y))
|
||||||
|
if path:
|
||||||
|
tree_iter = treeview.get_model().get_iter(path[0])
|
||||||
|
self.app.ui.get_object("searchListPopup").emit('show')
|
||||||
|
self.app.ui.get_object("searchListPopup").popup(None, None, None, None, 0, event.time)
|
||||||
|
return True
|
||||||
|
|
||||||
# ---------------------------------Library Tree----------------------------------------------
|
# ---------------------------------Library Tree----------------------------------------------
|
||||||
|
|
||||||
def on_library_card_selected(self, tree, row_no, column):
|
def on_library_card_selected(self, tree, row_no, column):
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ from gi.repository import Gtk, Gdk
|
|||||||
|
|
||||||
from cardvault import cardlist
|
from cardvault import cardlist
|
||||||
from cardvault import util
|
from cardvault import util
|
||||||
|
from cardvault import application
|
||||||
from mtgsdk import Card
|
from mtgsdk import Card
|
||||||
|
|
||||||
gi.require_version('Gtk', '3.0')
|
gi.require_version('Gtk', '3.0')
|
||||||
|
|
||||||
|
|
||||||
def init_search_view(app):
|
def init_search_view(app: 'application.Application'):
|
||||||
# set mana icons on filter buttons
|
# set mana icons on filter buttons
|
||||||
buttons = [x for x in app.ui.get_object("manaFilterGrid").get_children()
|
buttons = [x for x in app.ui.get_object("manaFilterGrid").get_children()
|
||||||
if isinstance(x, Gtk.ToggleButton)]
|
if isinstance(x, Gtk.ToggleButton)]
|
||||||
@@ -24,16 +25,14 @@ def init_search_view(app):
|
|||||||
# Create Model for search results
|
# Create Model for search results
|
||||||
_init_results_tree(app)
|
_init_results_tree(app)
|
||||||
|
|
||||||
app.ui.get_object("tagTree").drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
|
|
||||||
|
|
||||||
|
def reload_serach_view(app: 'application.Application'):
|
||||||
def reload_serach_view(app):
|
|
||||||
results_tree = app.ui.get_object("searchResults").get_child()
|
results_tree = app.ui.get_object("searchResults").get_child()
|
||||||
cards = results_tree.lib
|
cards = results_tree.lib
|
||||||
results_tree.update(cards, True)
|
results_tree.update(cards, True)
|
||||||
|
|
||||||
|
|
||||||
def get_filters(app):
|
def get_filters(app: 'application.Application') -> dict:
|
||||||
output = {}
|
output = {}
|
||||||
# Mana colors
|
# Mana colors
|
||||||
color_list = []
|
color_list = []
|
||||||
@@ -58,7 +57,7 @@ def get_filters(app):
|
|||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
def search_cards(term, filters):
|
def search_cards(term: str, filters: dict) -> dict:
|
||||||
util.log("Starting online search for '" + term + "'", util.LogLevel.Info)
|
util.log("Starting online search for '" + term + "'", util.LogLevel.Info)
|
||||||
util.log("Used Filters: " + str(filters), util.LogLevel.Info)
|
util.log("Used Filters: " + str(filters), util.LogLevel.Info)
|
||||||
|
|
||||||
@@ -72,12 +71,15 @@ def search_cards(term, filters):
|
|||||||
.where(pageSize=50) \
|
.where(pageSize=50) \
|
||||||
.where(page=1).all()
|
.where(page=1).all()
|
||||||
except (URLError, HTTPError) as err:
|
except (URLError, HTTPError) as err:
|
||||||
print("Error connecting to the internet")
|
util.log(err, util.LogLevel.Error)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Check if results were found
|
||||||
if len(cards) == 0:
|
if len(cards) == 0:
|
||||||
# TODO UI show no cards found
|
# TODO UI show no cards found
|
||||||
|
util.log("No Cards found", util.LogLevel.Info)
|
||||||
return
|
return
|
||||||
|
|
||||||
util.log("Found " + str(len(cards)) + " cards", util.LogLevel.Info)
|
util.log("Found " + str(len(cards)) + " cards", util.LogLevel.Info)
|
||||||
# Remove duplicate entries
|
# Remove duplicate entries
|
||||||
if util.SHOW_FROM_ALL_SETS is False:
|
if util.SHOW_FROM_ALL_SETS is False:
|
||||||
@@ -90,7 +92,7 @@ def search_cards(term, filters):
|
|||||||
return lib
|
return lib
|
||||||
|
|
||||||
|
|
||||||
def _init_results_tree(app):
|
def _init_results_tree(app: 'application.Application'):
|
||||||
overlay = app.ui.get_object("searchResults")
|
overlay = app.ui.get_object("searchResults")
|
||||||
card_list = cardlist.CardList(False, app)
|
card_list = cardlist.CardList(False, app)
|
||||||
card_list.set_name("resultsScroller")
|
card_list.set_name("resultsScroller")
|
||||||
@@ -100,8 +102,11 @@ def _init_results_tree(app):
|
|||||||
overlay.add_overlay(app.ui.get_object("searchOverlay"))
|
overlay.add_overlay(app.ui.get_object("searchOverlay"))
|
||||||
overlay.show_all()
|
overlay.show_all()
|
||||||
|
|
||||||
|
# Connect signal for context menu
|
||||||
|
card_list.list.connect("button-press-event", app.handlers.on_search_tree_press_event)
|
||||||
|
|
||||||
def _init_combo_box(combo, list):
|
|
||||||
|
def _init_combo_box(combo, list: list):
|
||||||
model = Gtk.ListStore(str)
|
model = Gtk.ListStore(str)
|
||||||
model.append(["All"])
|
model.append(["All"])
|
||||||
for entry in list:
|
for entry in list:
|
||||||
@@ -113,7 +118,7 @@ def _init_combo_box(combo, list):
|
|||||||
combo.set_active(0)
|
combo.set_active(0)
|
||||||
|
|
||||||
|
|
||||||
def _remove_duplicates(cards):
|
def _remove_duplicates(cards: list) -> list:
|
||||||
unique_cards = []
|
unique_cards = []
|
||||||
unique_names = []
|
unique_names = []
|
||||||
# Reverse cardlist so we get the version with the most modern art
|
# Reverse cardlist so we get the version with the most modern art
|
||||||
@@ -124,7 +129,7 @@ def _remove_duplicates(cards):
|
|||||||
return unique_cards
|
return unique_cards
|
||||||
|
|
||||||
|
|
||||||
def _get_combo_value(combo):
|
def _get_combo_value(combo) -> str:
|
||||||
tree_iter = combo.get_active_iter()
|
tree_iter = combo.get_active_iter()
|
||||||
value = combo.get_model().get_value(tree_iter, 0)
|
value = combo.get_model().get_value(tree_iter, 0)
|
||||||
return value.replace("All", "")
|
return value.replace("All", "")
|
||||||
@@ -136,7 +141,7 @@ def _init_mana_buttons(app, button_list):
|
|||||||
button.set_image(image)
|
button.set_image(image)
|
||||||
|
|
||||||
|
|
||||||
def _init_set_entry(app, entry):
|
def _init_set_entry(app: 'application.Application', entry):
|
||||||
set_store = Gtk.ListStore(str, str)
|
set_store = Gtk.ListStore(str, str)
|
||||||
for set in app.sets.values():
|
for set in app.sets.values():
|
||||||
set_store.append([set.name, set.code])
|
set_store.append([set.name, set.code])
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ class Card(object):
|
|||||||
self.foreign_names = response_dict.get('foreignNames')
|
self.foreign_names = response_dict.get('foreignNames')
|
||||||
self.owned = None
|
self.owned = None
|
||||||
self.tags = []
|
self.tags = []
|
||||||
|
self.wanted = None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find(id):
|
def find(id):
|
||||||
|
|||||||
Reference in New Issue
Block a user