mirror of
https://github.com/Gnucash/gnucash.git
synced 2025-02-25 18:55:30 -06:00
Merge c7f19c97c8
into 5ce3a9dd1d
This commit is contained in:
commit
92fb925b09
@ -156,6 +156,7 @@ set (gnome_utils_HEADERS
|
|||||||
gnc-gui-query.h
|
gnc-gui-query.h
|
||||||
gnc-icons.h
|
gnc-icons.h
|
||||||
gnc-keyring.h
|
gnc-keyring.h
|
||||||
|
gnc-list-model-container.hpp
|
||||||
gnc-main-window.h
|
gnc-main-window.h
|
||||||
gnc-menu-extensions.h
|
gnc-menu-extensions.h
|
||||||
gnc-plugin-file-history.h
|
gnc-plugin-file-history.h
|
||||||
|
187
gnucash/gnome-utils/gnc-list-model-container.hpp
Normal file
187
gnucash/gnome-utils/gnc-list-model-container.hpp
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
/********************************************************************\
|
||||||
|
* gnc-tree-container.hpp
|
||||||
|
* *
|
||||||
|
* This program is free software; you can redistribute it and/or *
|
||||||
|
* modify it under the terms of the GNU General Public License as *
|
||||||
|
* published by the Free Software Foundation; either version 2 of *
|
||||||
|
* the License, or (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, *
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||||
|
* GNU General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License*
|
||||||
|
* along with this program; if not, contact: *
|
||||||
|
* *
|
||||||
|
* Free Software Foundation Voice: +1-617-542-5942 *
|
||||||
|
* 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
|
||||||
|
* Boston, MA 02110-1301, USA gnu@gnu.org *
|
||||||
|
\********************************************************************/
|
||||||
|
|
||||||
|
#ifndef GNC_LIST_MODEL_CONTAINER_HPP
|
||||||
|
#define GNC_LIST_MODEL_CONTAINER_HPP
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <optional>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
class GncListModelData
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GncListModelData (GtkTreeModel* model, const GtkTreeIter& iter) : m_model{model}, m_iter{iter} {};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
T get_column (int column)
|
||||||
|
{
|
||||||
|
gpointer rv;
|
||||||
|
gtk_tree_model_get(m_model, &m_iter, column, &rv, -1);
|
||||||
|
return static_cast<T>(rv);
|
||||||
|
}
|
||||||
|
|
||||||
|
GtkTreeIter& get_iter () { return m_iter; };
|
||||||
|
|
||||||
|
int get_column_int (int column)
|
||||||
|
{
|
||||||
|
int rv;
|
||||||
|
gtk_tree_model_get(m_model, &m_iter, column, &rv, -1);
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string get_column_string (int column)
|
||||||
|
{
|
||||||
|
auto str = get_column<char*>(column);
|
||||||
|
std::string rv{str};
|
||||||
|
g_free (str);
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_columns (int unused, ...)
|
||||||
|
{
|
||||||
|
va_list var_args;
|
||||||
|
va_start (var_args, unused);
|
||||||
|
gtk_list_store_set_valist (GTK_LIST_STORE(m_model), &get_iter(), var_args);
|
||||||
|
va_end (var_args);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
void set_column (int column, T data) { gtk_list_store_set(GTK_LIST_STORE(m_model), &get_iter(), column, data, -1); }
|
||||||
|
|
||||||
|
// overloads the template when data is a std::string
|
||||||
|
void set_column (int column, const std::string& str) { set_column (column, str.c_str()); }
|
||||||
|
|
||||||
|
bool operator==(const GncListModelData& other) const
|
||||||
|
{
|
||||||
|
return (m_model == other.m_model) &&
|
||||||
|
(m_iter.stamp == other.m_iter.stamp) &&
|
||||||
|
(m_iter.user_data == other.m_iter.user_data) &&
|
||||||
|
(m_iter.user_data2 == other.m_iter.user_data2) &&
|
||||||
|
(m_iter.user_data3 == other.m_iter.user_data3);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
GtkTreeModel* m_model;
|
||||||
|
GtkTreeIter m_iter;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Custom container class
|
||||||
|
template <typename ModelType = GncListModelData>
|
||||||
|
class GncListModelContainer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
|
||||||
|
// Custom iterator class
|
||||||
|
class GncListModelIter
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
/* Set iterator traits queried by STL algorithms. These are
|
||||||
|
required for std::find_if etc to iterate through the
|
||||||
|
container. */
|
||||||
|
using iterator_category = std::forward_iterator_tag;
|
||||||
|
using value_type = ModelType;
|
||||||
|
using difference_type = std::ptrdiff_t;
|
||||||
|
using pointer = ModelType*;
|
||||||
|
using reference = ModelType&;
|
||||||
|
|
||||||
|
GncListModelIter(GtkTreeModel* model, std::optional<GtkTreeIter> iter) : m_model(model), m_iter(iter) {}
|
||||||
|
|
||||||
|
GncListModelIter(GtkTreeModel* model) : m_model (model)
|
||||||
|
{
|
||||||
|
GtkTreeIter iter;
|
||||||
|
m_iter = gtk_tree_model_get_iter_first(m_model, &iter) ? std::make_optional(iter) : std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
GncListModelIter& operator++()
|
||||||
|
{
|
||||||
|
if (!m_iter.has_value())
|
||||||
|
throw "no value, cannot increment";
|
||||||
|
if (!gtk_tree_model_iter_next(m_model, &m_iter.value()))
|
||||||
|
m_iter = std::nullopt;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ModelType operator*() const
|
||||||
|
{
|
||||||
|
if (!m_iter.has_value())
|
||||||
|
throw "no value, cannot dereference";
|
||||||
|
return ModelType (m_model, *m_iter);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<ModelType> operator->()
|
||||||
|
{
|
||||||
|
if (!m_iter.has_value())
|
||||||
|
throw "no value, cannot dereference";
|
||||||
|
return std::make_unique<ModelType> (m_model, *m_iter);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool has_value() const { return m_iter.has_value(); };
|
||||||
|
|
||||||
|
bool operator==(const GncListModelIter& other) const
|
||||||
|
{
|
||||||
|
if (m_model != other.m_model)
|
||||||
|
return false;
|
||||||
|
if (!m_iter.has_value() && !other.m_iter.has_value())
|
||||||
|
return true;
|
||||||
|
if (!m_iter.has_value() || !other.m_iter.has_value())
|
||||||
|
return false;
|
||||||
|
return (ModelType (m_model, *m_iter) == ModelType (m_model, *other.m_iter));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator!=(const GncListModelIter& other) const { return !(*this == other); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
GtkTreeModel* m_model;
|
||||||
|
std::optional<GtkTreeIter> m_iter;
|
||||||
|
};
|
||||||
|
|
||||||
|
GncListModelContainer(GtkTreeModel* model) : m_model(model)
|
||||||
|
{
|
||||||
|
g_return_if_fail (GTK_IS_TREE_MODEL (m_model));
|
||||||
|
g_object_ref (m_model);
|
||||||
|
}
|
||||||
|
|
||||||
|
~GncListModelContainer () { g_object_unref (m_model); }
|
||||||
|
|
||||||
|
GncListModelIter begin() const { return GncListModelIter(m_model); };
|
||||||
|
|
||||||
|
GncListModelIter end() const { return GncListModelIter(m_model, std::nullopt); };
|
||||||
|
|
||||||
|
GncListModelIter append()
|
||||||
|
{
|
||||||
|
GtkTreeIter iter;
|
||||||
|
gtk_list_store_append (GTK_LIST_STORE(m_model), &iter);
|
||||||
|
return GncListModelIter(m_model, iter);
|
||||||
|
};
|
||||||
|
|
||||||
|
size_t size() const { return std::distance (begin(), end()); }
|
||||||
|
|
||||||
|
bool empty() const { return begin() == end(); };
|
||||||
|
|
||||||
|
void clear() { gtk_list_store_clear (GTK_LIST_STORE (m_model)); };
|
||||||
|
|
||||||
|
private:
|
||||||
|
GtkTreeModel* m_model;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
@ -39,13 +39,31 @@ set(test_autoclear_LIBS
|
|||||||
gtest
|
gtest
|
||||||
)
|
)
|
||||||
|
|
||||||
|
set(test_list_model_container_SOURCES
|
||||||
|
test-list-model-container.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
set(test_list_model_container_INCLUDE_DIRS
|
||||||
|
)
|
||||||
|
|
||||||
|
set(test_list_model_container_LIBS
|
||||||
|
gnc-gnome-utils
|
||||||
|
gtest
|
||||||
|
)
|
||||||
|
|
||||||
gnc_add_test(test-autoclear "${test_autoclear_SOURCES}"
|
gnc_add_test(test-autoclear "${test_autoclear_SOURCES}"
|
||||||
test_autoclear_INCLUDE_DIRS
|
test_autoclear_INCLUDE_DIRS
|
||||||
test_autoclear_LIBS
|
test_autoclear_LIBS
|
||||||
)
|
)
|
||||||
|
|
||||||
|
gnc_add_test(test-list-model-container "${test_list_model_container_SOURCES}"
|
||||||
|
test_list_model_container_INCLUDE_DIRS
|
||||||
|
test_list_model_container_LIBS
|
||||||
|
)
|
||||||
|
|
||||||
gnc_add_scheme_tests(test-load-gnome-utils-module.scm)
|
gnc_add_scheme_tests(test-load-gnome-utils-module.scm)
|
||||||
|
|
||||||
|
|
||||||
set_dist_list(test_gnome_utils_DIST CMakeLists.txt test-load-gnome-utils-module.scm
|
set_dist_list(test_gnome_utils_DIST CMakeLists.txt test-load-gnome-utils-module.scm
|
||||||
|
${test_list_model_container_SOURCES}
|
||||||
${test_autoclear_SOURCES})
|
${test_autoclear_SOURCES})
|
||||||
|
124
gnucash/gnome-utils/test/test-list-model-container.cpp
Normal file
124
gnucash/gnome-utils/test/test-list-model-container.cpp
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
/********************************************************************
|
||||||
|
* test-tree-container.cpp: *
|
||||||
|
* *
|
||||||
|
* This program is free software; you can redistribute it and/or *
|
||||||
|
* modify it under the terms of the GNU General Public License as *
|
||||||
|
* published by the Free Software Foundation; either version 2 of *
|
||||||
|
* the License, or (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, *
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||||
|
* GNU General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License*
|
||||||
|
* along with this program; if not, you can retrieve it from *
|
||||||
|
* https://www.gnu.org/licenses/old-licenses/gpl-2.0.html *
|
||||||
|
* or contact: *
|
||||||
|
* *
|
||||||
|
* Free Software Foundation Voice: +1-617-542-5942 *
|
||||||
|
* 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
|
||||||
|
* Boston, MA 02110-1301, USA gnu@gnu.org *
|
||||||
|
********************************************************************/
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include <glib.h>
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
#include "../gnc-list-model-container.hpp"
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
enum {
|
||||||
|
COLUMN_STRING,
|
||||||
|
COLUMN_INT,
|
||||||
|
COLUMN_BOOLEAN,
|
||||||
|
N_COLUMNS
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
TEST(GncListModelContainer, Equality)
|
||||||
|
{
|
||||||
|
auto store1 = gtk_list_store_new (N_COLUMNS, G_TYPE_STRING, G_TYPE_INT, G_TYPE_BOOLEAN);
|
||||||
|
auto store2 = gtk_list_store_new (N_COLUMNS, G_TYPE_STRING, G_TYPE_INT, G_TYPE_BOOLEAN);
|
||||||
|
|
||||||
|
GncListModelContainer container1{GTK_TREE_MODEL(store1)};
|
||||||
|
GncListModelContainer container2{GTK_TREE_MODEL(store2)};
|
||||||
|
|
||||||
|
// these are null tests
|
||||||
|
EXPECT_TRUE (container1.begin() == container1.begin());
|
||||||
|
EXPECT_TRUE (container1.end() == container1.end());
|
||||||
|
|
||||||
|
EXPECT_TRUE (container1.begin() == container1.end());
|
||||||
|
EXPECT_TRUE (container1.size() == 0);
|
||||||
|
EXPECT_TRUE (container2.size() == 0);
|
||||||
|
EXPECT_TRUE (container1.empty());
|
||||||
|
EXPECT_TRUE (container1.empty());
|
||||||
|
|
||||||
|
EXPECT_FALSE (container1.begin() == container2.begin());
|
||||||
|
EXPECT_FALSE (container1.end() == container2.end());
|
||||||
|
|
||||||
|
// both containers have identical contents
|
||||||
|
container1.append()->set_column (COLUMN_STRING, "1");
|
||||||
|
container2.append()->set_column (COLUMN_STRING, "1");
|
||||||
|
|
||||||
|
// the containers are now no longer empty
|
||||||
|
EXPECT_FALSE (container1.begin() == container1.end());
|
||||||
|
EXPECT_FALSE (container2.begin() == container2.end());
|
||||||
|
EXPECT_TRUE (container1.size() == 1);
|
||||||
|
EXPECT_TRUE (container2.size() == 1);
|
||||||
|
|
||||||
|
// however the iterators behave as expected -- iterators from
|
||||||
|
// store1 must differ from iterators from store2
|
||||||
|
EXPECT_FALSE (container1.begin() == container2.begin());
|
||||||
|
EXPECT_FALSE (container1.end() == container2.end());
|
||||||
|
|
||||||
|
g_object_unref (store1);
|
||||||
|
g_object_unref (store2);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(GncListModelContainer, Basic)
|
||||||
|
{
|
||||||
|
auto store = gtk_list_store_new (N_COLUMNS, G_TYPE_STRING, G_TYPE_INT, G_TYPE_BOOLEAN);
|
||||||
|
GncListModelContainer container{GTK_TREE_MODEL(store)};
|
||||||
|
|
||||||
|
// test empty
|
||||||
|
EXPECT_TRUE (container.empty());
|
||||||
|
|
||||||
|
for (size_t i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
auto str = std::string("string ") + std::to_string(i);
|
||||||
|
auto iter = container.append ();
|
||||||
|
iter->set_columns (0,
|
||||||
|
COLUMN_STRING, str.c_str(),
|
||||||
|
COLUMN_INT, i,
|
||||||
|
COLUMN_BOOLEAN, (i % 2) == 0,
|
||||||
|
-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// test non-empty
|
||||||
|
EXPECT_FALSE (container.empty());
|
||||||
|
|
||||||
|
// test size
|
||||||
|
EXPECT_TRUE (10 == container.size());
|
||||||
|
|
||||||
|
auto int_is_five = [](auto it){ return it.get_column_int(COLUMN_INT) == 5; };
|
||||||
|
auto iter_found = std::find_if (container.begin(), container.end(), int_is_five);
|
||||||
|
EXPECT_TRUE (iter_found.has_value());
|
||||||
|
EXPECT_EQ ("string 5", iter_found->get_column_string (COLUMN_STRING));
|
||||||
|
|
||||||
|
g_object_unref (store);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv)
|
||||||
|
{
|
||||||
|
if (gtk_init_check (nullptr, nullptr))
|
||||||
|
std::cout << "gtk init completed!" << std::endl;
|
||||||
|
else
|
||||||
|
std::cout << "no display present!" << std::endl;
|
||||||
|
|
||||||
|
// Initialize the Google Test framework
|
||||||
|
::testing::InitGoogleTest(&argc, argv);
|
||||||
|
|
||||||
|
// Run tests
|
||||||
|
return RUN_ALL_TESTS();
|
||||||
|
}
|
@ -31,12 +31,15 @@
|
|||||||
#include <gtk/gtk.h>
|
#include <gtk/gtk.h>
|
||||||
#include <glib/gi18n.h>
|
#include <glib/gi18n.h>
|
||||||
|
|
||||||
|
#include "gnc-list-model-container.hpp"
|
||||||
#include "import-match-picker.h"
|
#include "import-match-picker.h"
|
||||||
#include "qof.h"
|
#include "qof.h"
|
||||||
#include "gnc-ui-util.h"
|
#include "gnc-ui-util.h"
|
||||||
#include "dialog-utils.h"
|
#include "dialog-utils.h"
|
||||||
#include "gnc-prefs.h"
|
#include "gnc-prefs.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
/********************************************************************\
|
/********************************************************************\
|
||||||
* Constants *
|
* Constants *
|
||||||
\********************************************************************/
|
\********************************************************************/
|
||||||
@ -98,32 +101,22 @@ downloaded_transaction_append(GNCImportMatchPicker * matcher,
|
|||||||
g_return_if_fail (matcher);
|
g_return_if_fail (matcher);
|
||||||
g_return_if_fail (transaction_info);
|
g_return_if_fail (transaction_info);
|
||||||
|
|
||||||
auto found = false;
|
|
||||||
auto store = GTK_LIST_STORE(gtk_tree_view_get_model(matcher->downloaded_view));
|
auto store = GTK_LIST_STORE(gtk_tree_view_get_model(matcher->downloaded_view));
|
||||||
auto split = gnc_import_TransInfo_get_fsplit(transaction_info);
|
auto split = gnc_import_TransInfo_get_fsplit(transaction_info);
|
||||||
auto trans = gnc_import_TransInfo_get_trans(transaction_info);
|
auto trans = gnc_import_TransInfo_get_trans(transaction_info);
|
||||||
|
GncListModelContainer container{GTK_TREE_MODEL(store)};
|
||||||
|
|
||||||
/* Has the transaction already been added? */
|
auto it_matches = [&transaction_info](auto it)
|
||||||
GtkTreeIter iter;
|
{ return it.template get_column<GNCImportTransInfo*>(DOWNLOADED_COL_INFO_PTR) == transaction_info; };
|
||||||
if (gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter))
|
|
||||||
{
|
|
||||||
do
|
|
||||||
{
|
|
||||||
GNCImportTransInfo *local_info;
|
|
||||||
gtk_tree_model_get(GTK_TREE_MODEL(store), &iter,
|
|
||||||
DOWNLOADED_COL_INFO_PTR, &local_info,
|
|
||||||
-1);
|
|
||||||
if (local_info == transaction_info)
|
|
||||||
{
|
|
||||||
found = TRUE;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
while (gtk_tree_model_iter_next(GTK_TREE_MODEL(store), &iter));
|
|
||||||
}
|
|
||||||
if (!found)
|
|
||||||
gtk_list_store_append(store, &iter);
|
|
||||||
|
|
||||||
|
// find the GncTreeIter whose DOWNLOADED_COL_INFO_PTR matches transaction_info
|
||||||
|
auto iter = std::find_if (container.begin(), container.end(), it_matches);
|
||||||
|
|
||||||
|
// not found. append a new GtkTreeIter in container.
|
||||||
|
if (iter == container.end())
|
||||||
|
iter = container.append();
|
||||||
|
|
||||||
|
// now iter is a GncTreeIter; iter->get_iter() is the GtkTreeIter
|
||||||
auto account = xaccAccountGetName(xaccSplitGetAccount(split));
|
auto account = xaccAccountGetName(xaccSplitGetAccount(split));
|
||||||
auto date = qof_print_date(xaccTransGetDate(trans));
|
auto date = qof_print_date(xaccTransGetDate(trans));
|
||||||
auto amount = g_strdup (xaccPrintAmount(xaccSplitGetAmount(split), gnc_split_amount_print_info(split, TRUE)));
|
auto amount = g_strdup (xaccPrintAmount(xaccSplitGetAmount(split), gnc_split_amount_print_info(split, TRUE)));
|
||||||
@ -137,17 +130,17 @@ downloaded_transaction_append(GNCImportMatchPicker * matcher,
|
|||||||
auto imbalance = g_strdup (xaccPrintAmount (xaccTransGetImbalanceValue(trans),
|
auto imbalance = g_strdup (xaccPrintAmount (xaccTransGetImbalanceValue(trans),
|
||||||
gnc_commodity_print_info (xaccTransGetCurrency (trans), TRUE)));
|
gnc_commodity_print_info (xaccTransGetCurrency (trans), TRUE)));
|
||||||
|
|
||||||
gtk_list_store_set (store, &iter,
|
iter->set_columns (0,
|
||||||
DOWNLOADED_COL_ACCOUNT, account,
|
DOWNLOADED_COL_ACCOUNT, account,
|
||||||
DOWNLOADED_COL_DATE, date,
|
DOWNLOADED_COL_DATE, date,
|
||||||
DOWNLOADED_COL_AMOUNT, amount,
|
DOWNLOADED_COL_AMOUNT, amount,
|
||||||
DOWNLOADED_COL_DESCRIPTION, desc,
|
DOWNLOADED_COL_DESCRIPTION, desc,
|
||||||
DOWNLOADED_COL_MEMO, memo,
|
DOWNLOADED_COL_MEMO, memo,
|
||||||
DOWNLOADED_COL_BALANCED, imbalance,
|
DOWNLOADED_COL_BALANCED, imbalance,
|
||||||
DOWNLOADED_COL_INFO_PTR, transaction_info,
|
DOWNLOADED_COL_INFO_PTR, transaction_info,
|
||||||
-1);
|
-1);
|
||||||
|
|
||||||
gtk_tree_selection_select_iter (gtk_tree_view_get_selection(matcher->downloaded_view), &iter);
|
gtk_tree_selection_select_iter (gtk_tree_view_get_selection(matcher->downloaded_view), &iter->get_iter());
|
||||||
|
|
||||||
g_free (date);
|
g_free (date);
|
||||||
g_free (amount);
|
g_free (amount);
|
||||||
|
Loading…
Reference in New Issue
Block a user