mirror of
https://gitlab.com/pamhyr/pamhyr2.git
synced 2026-10-30 03:20:41 +01:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a0c00d877 | ||
|
|
8d2c18b6af | ||
|
|
2d2f83436c | ||
|
|
beece68498 | ||
|
|
d8e9d786d9 | ||
|
|
7ef305e724 |
No files matched your search
@@ -531,7 +531,7 @@ class BoundaryConditionAdisTS(SQLSubModel):
|
||||
new_0 = self._types[0](data[0].replace(",", "."))
|
||||
new_1 = self._types[1](data[1].replace(",", "."))
|
||||
|
||||
return (new_0, new_1)
|
||||
return Data(new_0, new_1, types=self._types, status=self._status)
|
||||
|
||||
def add(self, index: int):
|
||||
value = Data(
|
||||
@@ -543,6 +543,9 @@ class BoundaryConditionAdisTS(SQLSubModel):
|
||||
return value
|
||||
|
||||
def insert(self, index: int, value):
|
||||
if value in self._data:
|
||||
value.set_as_not_deleted()
|
||||
else:
|
||||
self._data.insert(index, value)
|
||||
self.modified()
|
||||
|
||||
|
||||
@@ -419,7 +419,7 @@ class BoundaryConditionTemperature(SQLSubModel):
|
||||
new_0 = self._types[0](data[0].replace(",", "."))
|
||||
new_1 = self._types[1](data[1].replace(",", "."))
|
||||
|
||||
return (new_0, new_1)
|
||||
return Data(new_0, new_1, types=self._types, status=self._status)
|
||||
|
||||
def add(self, index: int):
|
||||
value = Data(
|
||||
@@ -431,6 +431,9 @@ class BoundaryConditionTemperature(SQLSubModel):
|
||||
return value
|
||||
|
||||
def insert(self, index: int, value):
|
||||
if value in self._data:
|
||||
value.set_as_not_deleted()
|
||||
else:
|
||||
self._data.insert(index, value)
|
||||
self.modified()
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ from datetime import date, time, datetime, timedelta
|
||||
from tools import (
|
||||
trace, timer,
|
||||
timestamp_to_old_pamhyr_date,
|
||||
old_pamhyr_date_to_timestamp, format_datetime
|
||||
old_pamhyr_date_to_timestamp, format_datetime, parse_datetime
|
||||
)
|
||||
|
||||
from View.Tools.PamhyrTable import PamhyrTableModel
|
||||
@@ -98,7 +98,7 @@ class TableModel(PamhyrTableModel):
|
||||
value = timestamp_to_old_pamhyr_date(int(v))
|
||||
else:
|
||||
value = format_datetime(
|
||||
self._start_date + timedelta(seconds=float(v))
|
||||
datetime(1970, 1, 1) + timedelta(seconds=float(v))
|
||||
)
|
||||
else:
|
||||
value = f"{v}"
|
||||
@@ -118,7 +118,7 @@ class TableModel(PamhyrTableModel):
|
||||
try:
|
||||
if (self._data.header[column] == "time"
|
||||
and self._opt_data == "date"):
|
||||
value = self.date_to_elapsed_seconds(value)
|
||||
value = self.date_to_timestamp(value)
|
||||
|
||||
row = self.get_true_data_row(row)
|
||||
|
||||
@@ -238,17 +238,18 @@ class TableModel(PamhyrTableModel):
|
||||
if len(data) == 0:
|
||||
return
|
||||
|
||||
try:
|
||||
rows = [self._data.new_from_data(header, d) for d in data]
|
||||
except (ValueError, TypeError, IndexError) as error:
|
||||
logger.warning(error)
|
||||
return False
|
||||
|
||||
self.layoutAboutToBeChanged.emit()
|
||||
|
||||
self._undo.push(
|
||||
PasteCommand(
|
||||
self._data, row,
|
||||
list(
|
||||
map(
|
||||
lambda d: self._data.new_from_data(header, d),
|
||||
data
|
||||
)
|
||||
)
|
||||
rows
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ class TableModel(PamhyrTableModel):
|
||||
value = timestamp_to_old_pamhyr_date(int(v))
|
||||
else:
|
||||
value = format_datetime(
|
||||
self._start_date + timedelta(seconds=float(v))
|
||||
datetime(1970, 1, 1) + timedelta(seconds=float(v))
|
||||
)
|
||||
else:
|
||||
value = f"{v}"
|
||||
@@ -108,7 +108,7 @@ class TableModel(PamhyrTableModel):
|
||||
try:
|
||||
if (self._data.header[column] == "time"
|
||||
and self._opt_data == "date"):
|
||||
value = self.date_to_elapsed_seconds(value)
|
||||
value = self.date_to_timestamp(value)
|
||||
|
||||
row = self.get_true_data_row(row)
|
||||
|
||||
@@ -169,17 +169,18 @@ class TableModel(PamhyrTableModel):
|
||||
if len(data) == 0:
|
||||
return
|
||||
|
||||
try:
|
||||
rows = [self._data.new_from_data(header, d) for d in data]
|
||||
except (ValueError, TypeError, IndexError) as error:
|
||||
logger.warning(error)
|
||||
return False
|
||||
|
||||
self.layoutAboutToBeChanged.emit()
|
||||
|
||||
self._undo.push(
|
||||
PasteCommand(
|
||||
self._data, row,
|
||||
list(
|
||||
map(
|
||||
lambda d: self._data.new_from_data(header, d),
|
||||
data
|
||||
)
|
||||
)
|
||||
rows
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ class TableModel(PamhyrTableModel):
|
||||
value = timestamp_to_old_pamhyr_date(int(v))
|
||||
else:
|
||||
value = format_datetime(
|
||||
self._start_date + timedelta(seconds=float(v))
|
||||
datetime(1970, 1, 1) + timedelta(seconds=float(v))
|
||||
)
|
||||
else:
|
||||
value = f"{v}"
|
||||
@@ -108,7 +108,7 @@ class TableModel(PamhyrTableModel):
|
||||
try:
|
||||
if (self._data.header[column] == "time"
|
||||
and self._opt_data == "date"):
|
||||
value = self.date_to_elapsed_seconds(value)
|
||||
value = self.date_to_timestamp(value)
|
||||
|
||||
row = self.get_true_data_row(row)
|
||||
|
||||
@@ -169,17 +169,18 @@ class TableModel(PamhyrTableModel):
|
||||
if len(data) == 0:
|
||||
return
|
||||
|
||||
try:
|
||||
rows = [self._data.new_from_data(header, d) for d in data]
|
||||
except (ValueError, TypeError, IndexError) as error:
|
||||
logger.warning(error)
|
||||
return False
|
||||
|
||||
self.layoutAboutToBeChanged.emit()
|
||||
|
||||
self._undo.push(
|
||||
PasteCommand(
|
||||
self._data, row,
|
||||
list(
|
||||
map(
|
||||
lambda d: self._data.new_from_data(header, d),
|
||||
data
|
||||
)
|
||||
)
|
||||
rows
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -192,7 +192,7 @@ class PlotXY(PamhyrPlot):
|
||||
for xy in zip(self.data.get_x(), self.data.get_y()):
|
||||
self.line_xy.append(np.column_stack(xy))
|
||||
|
||||
self._colors, self._style = self.color_hightlight()
|
||||
self._colors, self._style = self.color_highlight()
|
||||
self.line_xy_collection = collections.LineCollection(
|
||||
self.line_xy,
|
||||
colors=self._colors,
|
||||
@@ -201,7 +201,7 @@ class PlotXY(PamhyrPlot):
|
||||
)
|
||||
self.canvas.axes.add_collection(self.line_xy_collection)
|
||||
|
||||
def color_hightlight(self):
|
||||
def color_highlight(self):
|
||||
rows = sorted(list(
|
||||
set(
|
||||
(i.row() for i in self.parent.tableView.selectedIndexes())
|
||||
@@ -320,7 +320,7 @@ class PlotXY(PamhyrPlot):
|
||||
|
||||
def update_current(self):
|
||||
if self._current_data_update:
|
||||
self._colors, self._style = self.color_hightlight()
|
||||
self._colors, self._style = self.color_highlight()
|
||||
self.line_xy_collection.set_colors(self._colors)
|
||||
self.line_xy_collection.set_linestyle(self._style)
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ class TableModel(PamhyrTableModel):
|
||||
value = timestamp_to_old_pamhyr_date(int(v))
|
||||
else:
|
||||
value = format_datetime(
|
||||
self._start_date + timedelta(seconds=float(v))
|
||||
datetime(1970, 1, 1) + timedelta(seconds=float(v))
|
||||
)
|
||||
else:
|
||||
value = f"{v}"
|
||||
@@ -97,7 +97,7 @@ class TableModel(PamhyrTableModel):
|
||||
try:
|
||||
if (self._data.header[column] == "time"
|
||||
and self._opt_data == "date"):
|
||||
value = self.date_to_elapsed_seconds(value)
|
||||
value = self.date_to_timestamp(value)
|
||||
|
||||
self._undo.push(
|
||||
SetDataCommand(
|
||||
@@ -184,17 +184,18 @@ class TableModel(PamhyrTableModel):
|
||||
if len(data) == 0:
|
||||
return
|
||||
|
||||
try:
|
||||
rows = [self._data.new_from_data(header, d) for d in data]
|
||||
except (ValueError, TypeError, IndexError) as error:
|
||||
logger.warning(error)
|
||||
return False
|
||||
|
||||
self.layoutAboutToBeChanged.emit()
|
||||
|
||||
self._undo.push(
|
||||
PasteCommand(
|
||||
self._data, row,
|
||||
list(
|
||||
map(
|
||||
lambda d: self._data.new_from_data(header, d),
|
||||
data
|
||||
)
|
||||
)
|
||||
rows
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ class PlotXY(PamhyrPlot):
|
||||
for xy in zip(self.data.get_x(), self.data.get_y()):
|
||||
self.line_xy.append(np.column_stack(xy))
|
||||
|
||||
self._colors, self._style = self.color_hightlight()
|
||||
self._colors, self._style = self.color_highlight()
|
||||
self.line_xy_collection = collections.LineCollection(
|
||||
self.line_xy,
|
||||
colors=self._colors,
|
||||
@@ -136,7 +136,7 @@ class PlotXY(PamhyrPlot):
|
||||
)
|
||||
self.canvas.axes.add_collection(self.line_xy_collection)
|
||||
|
||||
def color_hightlight(self):
|
||||
def color_highlight(self):
|
||||
rk_min, rk_max = (-1, -1)
|
||||
if self._highlight_data is not None:
|
||||
rk_min, rk_max = self._highlight_data
|
||||
@@ -250,7 +250,7 @@ class PlotXY(PamhyrPlot):
|
||||
|
||||
def update_current(self):
|
||||
if self._current_data_update:
|
||||
self._colors, self._style = self.color_hightlight()
|
||||
self._colors, self._style = self.color_highlight()
|
||||
self.line_xy_collection.set_colors(self._colors)
|
||||
self.line_xy_collection.set_linestyle(self._style)
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ class TableModel(PamhyrTableModel):
|
||||
value = timestamp_to_old_pamhyr_date_adists(int(v))
|
||||
else:
|
||||
value = format_datetime(
|
||||
self._start_date + timedelta(seconds=float(v))
|
||||
datetime(1970, 1, 1) + timedelta(seconds=float(v))
|
||||
)
|
||||
else:
|
||||
value = f"{v}"
|
||||
@@ -111,7 +111,7 @@ class TableModel(PamhyrTableModel):
|
||||
try:
|
||||
if (self._data.header[column] == "time"
|
||||
and self._opt_data == "date"):
|
||||
value = self.date_to_elapsed_seconds(value)
|
||||
value = self.date_to_timestamp(value)
|
||||
|
||||
row = self.get_true_data_row(row)
|
||||
|
||||
|
||||
@@ -57,6 +57,15 @@ class EditLateralContributionAdisTSWindow(PamhyrWindow):
|
||||
name = trad[self._pamhyr_name]
|
||||
|
||||
if self._data is not None:
|
||||
pollutant_name = next(
|
||||
(
|
||||
pollutant.name
|
||||
for pollutant in study.river.Pollutants.Pollutants_List
|
||||
if pollutant.id == self._data.pollutant
|
||||
),
|
||||
None
|
||||
)
|
||||
|
||||
if self._data.reach is not None:
|
||||
reach_name = next(filter(
|
||||
lambda reach: reach.id == self._data.reach,
|
||||
@@ -75,12 +84,10 @@ class EditLateralContributionAdisTSWindow(PamhyrWindow):
|
||||
else:
|
||||
end_rk = trad['not_associated']
|
||||
|
||||
name += (
|
||||
f" - {study.name} - " +
|
||||
f"{reach_name} - " +
|
||||
f"({begin_rk} - " +
|
||||
f"{end_rk})"
|
||||
)
|
||||
name += f" - {study.name}"
|
||||
if pollutant_name is not None:
|
||||
name += f" - {pollutant_name}"
|
||||
name += f" - {reach_name} - ({begin_rk} - {end_rk})"
|
||||
|
||||
super(EditLateralContributionAdisTSWindow, self).__init__(
|
||||
title=name,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# PlotXY.py -- Pamhyr
|
||||
# Copyright (C) 2023-2026 INRAE
|
||||
#
|
||||
# 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 3 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, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from View.Geometry.PlotXY import PlotXY as GeometryPlotXY
|
||||
|
||||
|
||||
class PlotXY(GeometryPlotXY):
|
||||
"""Geometry plot highlighting the lateral-contribution extent."""
|
||||
|
||||
def color_highlight(self):
|
||||
rk_min, rk_max = (-1, -1)
|
||||
if self.highlight is not None:
|
||||
rk_min, rk_max = self.highlight
|
||||
|
||||
colors = [
|
||||
self.color_plot_river_bottom for _ in range(len(self.data))
|
||||
]
|
||||
for index, rk in enumerate(self.data.get_rk_complete_profiles()):
|
||||
if rk_min <= rk <= rk_max:
|
||||
colors[index] = self.color_plot_current
|
||||
|
||||
styles = ["-" for _ in range(len(self.data))]
|
||||
return colors, styles
|
||||
@@ -45,7 +45,7 @@ from View.LateralContributionsAdisTS.Table import (
|
||||
)
|
||||
|
||||
from View.Tools.Plot.PamhyrCanvas import MplCanvas
|
||||
from View.Geometry.PlotXY import PlotXY
|
||||
from View.LateralContributionsAdisTS.PlotXY import PlotXY
|
||||
from View.LateralContributionsAdisTS.translate \
|
||||
import LCTranslate
|
||||
from View.LateralContributionsAdisTS.Edit.Window \
|
||||
|
||||
@@ -76,10 +76,13 @@ class PlotRKC(PamhyrPlot):
|
||||
@staticmethod
|
||||
def reach_geometry(reach):
|
||||
"""Return geometry values for profiles present in the results."""
|
||||
# The study's enabled profiles can change after a calculation. Keep
|
||||
# coordinates in the same order as the result profiles and columns.
|
||||
profiles = reach.profiles
|
||||
return (
|
||||
reach.geometry.get_rk_enabled_profiles(),
|
||||
reach.geometry.get_z_min_enabled_profiles(),
|
||||
reach.geometry.get_z_max_enabled_profiles()
|
||||
[profile.rk for profile in profiles],
|
||||
[profile.geometry.z_min() for profile in profiles],
|
||||
[profile.geometry.z_max() for profile in profiles],
|
||||
)
|
||||
|
||||
@timer
|
||||
|
||||
@@ -45,6 +45,36 @@ logger = logging.getLogger()
|
||||
_translate = QCoreApplication.translate
|
||||
|
||||
|
||||
class ValueDelegate(QItemDelegate):
|
||||
"""Use a combo box for solver parameters backed by an index."""
|
||||
|
||||
def createEditor(self, parent, option, index):
|
||||
choices = index.model().value_choices(index)
|
||||
if choices is None:
|
||||
return QLineEdit(parent)
|
||||
|
||||
editor = QComboBox(parent)
|
||||
for value, label in choices.items():
|
||||
editor.addItem(label, value)
|
||||
return editor
|
||||
|
||||
def setEditorData(self, editor, index):
|
||||
if isinstance(editor, QComboBox):
|
||||
current = index.model().raw_value(index)
|
||||
editor.setCurrentIndex(editor.findData(current))
|
||||
else:
|
||||
editor.setText(str(index.model().raw_value(index)))
|
||||
|
||||
def setModelData(self, editor, model, index):
|
||||
if isinstance(editor, QComboBox):
|
||||
model.setData(index, editor.currentData())
|
||||
else:
|
||||
model.setData(index, editor.text())
|
||||
|
||||
def updateEditorGeometry(self, editor, option, index):
|
||||
editor.setGeometry(option.rect)
|
||||
|
||||
|
||||
class TableModel(PamhyrTableModel):
|
||||
def _setup_lst(self):
|
||||
self._tab = self._opt_data
|
||||
@@ -68,6 +98,10 @@ class TableModel(PamhyrTableModel):
|
||||
elif cname == "value":
|
||||
value = self._lst.get(row)[cname]
|
||||
|
||||
choices = self.value_choices(index)
|
||||
if choices is not None:
|
||||
return choices.get(value, value)
|
||||
|
||||
if value in self._trad.get_dict("yes_no"):
|
||||
value = self._trad.get_dict("yes_no")[value]
|
||||
|
||||
@@ -79,7 +113,14 @@ class TableModel(PamhyrTableModel):
|
||||
if not index.isValid() or role != Qt.EditRole:
|
||||
return False
|
||||
|
||||
if self.is_same_data(index, value):
|
||||
choices = self.value_choices(index)
|
||||
if choices is not None:
|
||||
reverse_choices = {label: key for key, label in choices.items()}
|
||||
value = reverse_choices.get(value, value)
|
||||
if str(self.raw_value(index)) == str(value):
|
||||
return False
|
||||
|
||||
elif self.is_same_data(index, value):
|
||||
return False
|
||||
|
||||
row = index.row()
|
||||
@@ -103,6 +144,15 @@ class TableModel(PamhyrTableModel):
|
||||
self.dataChanged.emit(index, index)
|
||||
return True
|
||||
|
||||
def value_choices(self, index):
|
||||
if not index.isValid():
|
||||
return None
|
||||
name = self._lst.get(index.row())["name"]
|
||||
return self._trad.get_dict("indexed_values").get(name)
|
||||
|
||||
def raw_value(self, index):
|
||||
return self._lst.get(index.row())["value"]
|
||||
|
||||
def undo(self):
|
||||
self._undo.undo()
|
||||
self.layoutChanged.emit()
|
||||
|
||||
@@ -41,7 +41,7 @@ from PyQt5.QtWidgets import (
|
||||
)
|
||||
|
||||
from View.SolverParameters.UndoCommand import *
|
||||
from View.SolverParameters.Table import TableModel
|
||||
from View.SolverParameters.Table import TableModel, ValueDelegate
|
||||
from View.SolverParameters.translate import ParamTranslate
|
||||
|
||||
from Solver.Solvers import solver_long_name, solver_type_list
|
||||
@@ -103,6 +103,7 @@ class SolverParametersWindow(PamhyrWindow):
|
||||
opt_data=st,
|
||||
table_headers=self._trad.get_dict("table_headers"),
|
||||
editable_headers=editable_headers,
|
||||
delegates={"value": ValueDelegate(table)},
|
||||
trad=self._trad,
|
||||
undo=self._undo_stack[st],
|
||||
)
|
||||
|
||||
@@ -50,6 +50,30 @@ class ParamTranslate(MainTranslate):
|
||||
_translate("SolverParameters", "N"): "n",
|
||||
}
|
||||
|
||||
self._sub_dict["indexed_values"] = {
|
||||
"mage_shields_critique": {
|
||||
"1": _translate("SolverParameters", "Constant (0.047)"),
|
||||
"2": _translate("SolverParameters", "Soulsby (1997)"),
|
||||
"3": _translate("SolverParameters", "Camenen (2012)"),
|
||||
},
|
||||
"mage_shields_correction": {
|
||||
"0": _translate("SolverParameters", "No correction"),
|
||||
"1": _translate("SolverParameters", "Effective Shields"),
|
||||
},
|
||||
"mage_capacite_solide": {
|
||||
"1": _translate(
|
||||
"SolverParameters", "Meyer-Peter & Muller (1948)"
|
||||
),
|
||||
"2": _translate(
|
||||
"SolverParameters", "Camenen & Larson (2005)"
|
||||
),
|
||||
"3": _translate(
|
||||
"SolverParameters", "Engelund & Hansen (1967)"
|
||||
),
|
||||
"4": _translate("SolverParameters", "Recking (2013)"),
|
||||
},
|
||||
}
|
||||
|
||||
self._sub_dict["names"] = {
|
||||
"all_init_time":
|
||||
_translate("SolverParameters",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
import logging
|
||||
import traceback
|
||||
from datetime import timezone
|
||||
|
||||
from tools import trace, timer, parse_datetime
|
||||
|
||||
@@ -62,8 +63,8 @@ class PamhyrTextDelegate(QStyledItemDelegate):
|
||||
|
||||
|
||||
class PamhyrTableModel(QAbstractTableModel):
|
||||
def date_to_elapsed_seconds(self, value):
|
||||
"""Convert an entered date to seconds since the study start date."""
|
||||
def date_to_timestamp(self, value):
|
||||
"""Convert an accepted calendar date to its raw UTC timestamp."""
|
||||
try:
|
||||
entered_date = parse_datetime(value)
|
||||
except ValueError as error:
|
||||
@@ -81,7 +82,7 @@ class PamhyrTableModel(QAbstractTableModel):
|
||||
)
|
||||
raise error
|
||||
|
||||
return (entered_date - self._start_date).total_seconds()
|
||||
return entered_date.replace(tzinfo=timezone.utc).timestamp()
|
||||
|
||||
def _setup_delegates(self):
|
||||
if self._table_view is None:
|
||||
|
||||
@@ -78,7 +78,7 @@ class TableModel(PamhyrTableModel):
|
||||
value = timestamp_to_old_pamhyr_date(int(v))
|
||||
else:
|
||||
value = format_datetime(
|
||||
self._start_date + timedelta(seconds=float(v))
|
||||
datetime(1970, 1, 1) + timedelta(seconds=float(v))
|
||||
)
|
||||
else:
|
||||
value = f"{v}"
|
||||
@@ -98,7 +98,7 @@ class TableModel(PamhyrTableModel):
|
||||
try:
|
||||
if (self._data.header[column] == "time"
|
||||
and self._opt_data == "date"):
|
||||
value = self.date_to_elapsed_seconds(value)
|
||||
value = self.date_to_timestamp(value)
|
||||
|
||||
self._undo.push(
|
||||
SetDataCommand(
|
||||
@@ -175,17 +175,18 @@ class TableModel(PamhyrTableModel):
|
||||
if len(data) == 0:
|
||||
return
|
||||
|
||||
try:
|
||||
rows = [self._data.new_from_data(header, d) for d in data]
|
||||
except (ValueError, TypeError, IndexError) as error:
|
||||
logger.warning(error)
|
||||
return False
|
||||
|
||||
self.layoutAboutToBeChanged.emit()
|
||||
|
||||
self._undo.push(
|
||||
PasteCommand(
|
||||
self._data, row,
|
||||
list(
|
||||
map(
|
||||
lambda d: self._data.new_from_data(header, d),
|
||||
data
|
||||
)
|
||||
)
|
||||
rows
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
Reference in new issue
Block a user