Поправил ошибку. Чутка поменял статистику.
This commit is contained in:
@@ -737,25 +737,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="modalErrorMessage" class="alert alert-danger" style="display: none;"></div>
|
<div id="modalErrorMessage" class="alert alert-danger" style="display: none;"></div>
|
||||||
<div id="modalContent" style="display: none;">
|
<div id="modalContent" style="display: none;">
|
||||||
<!-- Marks Section -->
|
|
||||||
<div id="marksSection" class="mb-3" style="display: none;">
|
|
||||||
<h6 class="mb-2">Наличие сигнала объекта (<span id="marksCount">0</span>):</h6>
|
|
||||||
<div class="table-responsive" style="max-height: 200px; overflow-y: auto;">
|
|
||||||
<table class="table table-sm table-bordered">
|
|
||||||
<thead class="table-light">
|
|
||||||
<tr>
|
|
||||||
<th style="width: 20%;">Наличие сигнала</th>
|
|
||||||
<th style="width: 40%;">Дата и время</th>
|
|
||||||
<th style="width: 40%;">Пользователь</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="marksTableBody">
|
|
||||||
<!-- Marks will be loaded here -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
<h6 class="mb-0">Связанные точки (<span id="objitemCount">0</span>):</h6>
|
<h6 class="mb-0">Связанные точки (<span id="objitemCount">0</span>):</h6>
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
@@ -1764,33 +1745,6 @@ function showSourceDetails(sourceId) {
|
|||||||
// Hide loading spinner
|
// Hide loading spinner
|
||||||
document.getElementById('modalLoadingSpinner').style.display = 'none';
|
document.getElementById('modalLoadingSpinner').style.display = 'none';
|
||||||
|
|
||||||
// Show marks if available
|
|
||||||
if (data.marks && data.marks.length > 0) {
|
|
||||||
document.getElementById('marksSection').style.display = 'block';
|
|
||||||
document.getElementById('marksCount').textContent = data.marks.length;
|
|
||||||
|
|
||||||
const marksTableBody = document.getElementById('marksTableBody');
|
|
||||||
marksTableBody.innerHTML = '';
|
|
||||||
|
|
||||||
data.marks.forEach(mark => {
|
|
||||||
const row = document.createElement('tr');
|
|
||||||
|
|
||||||
let markBadge = '<span class="badge bg-secondary">-</span>';
|
|
||||||
if (mark.mark === true) {
|
|
||||||
markBadge = '<span class="badge bg-success">Есть</span>';
|
|
||||||
} else if (mark.mark === false) {
|
|
||||||
markBadge = '<span class="badge bg-danger">Нет</span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
row.innerHTML = '<td class="text-center">' + markBadge + '</td>' +
|
|
||||||
'<td>' + mark.timestamp + '</td>' +
|
|
||||||
'<td>' + mark.created_by + '</td>';
|
|
||||||
marksTableBody.appendChild(row);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
document.getElementById('marksSection').style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.objitems && data.objitems.length > 0) {
|
if (data.objitems && data.objitems.length > 0) {
|
||||||
// Show content
|
// Show content
|
||||||
document.getElementById('modalContent').style.display = 'block';
|
document.getElementById('modalContent').style.display = 'block';
|
||||||
|
|||||||
@@ -80,6 +80,39 @@
|
|||||||
.stats-block.hidden {
|
.stats-block.hidden {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Satellite selection styles */
|
||||||
|
.satellite-selection-container {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.satellite-selection-container .form-check {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.satellite-selection-container .form-check:hover {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.satellite-selection-container .form-check-label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chart zoom hint */
|
||||||
|
.card-body canvas {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body canvas:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -331,7 +364,7 @@
|
|||||||
<div class="col-md-6 stats-block" id="bar-chart-block">
|
<div class="col-md-6 stats-block" id="bar-chart-block">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<i class="bi bi-bar-chart"></i> Топ-10 спутников по количеству точек
|
<i class="bi bi-bar-chart"></i> Выбранные спутники по количеству точек
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<canvas id="satelliteBarChart"></canvas>
|
<canvas id="satelliteBarChart"></canvas>
|
||||||
@@ -441,6 +474,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Satellite Selection for Charts -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h6 class="text-warning mb-2">
|
||||||
|
<i class="bi bi-broadcast"></i> Спутники для диаграмм
|
||||||
|
</h6>
|
||||||
|
<div class="ps-3">
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="btn-group w-100 mb-2" role="group">
|
||||||
|
<button type="button" class="btn btn-outline-success btn-sm" id="select-all-satellites">
|
||||||
|
<i class="bi bi-check-all"></i> Все спутники
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-warning btn-sm" id="select-top10-satellites">
|
||||||
|
<i class="bi bi-trophy"></i> Топ-10
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-danger btn-sm" id="clear-satellites">
|
||||||
|
<i class="bi bi-x-circle"></i> Очистить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="satellite-selection-container" style="max-height: 200px; overflow-y: auto; border: 1px solid #dee2e6; border-radius: 0.375rem; padding: 1.4rem;">
|
||||||
|
<div id="satellite-checkboxes">
|
||||||
|
<!-- Будет заполнено JavaScript -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Quick Actions -->
|
<!-- Quick Actions -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<h6 class="text-secondary mb-2">
|
<h6 class="text-secondary mb-2">
|
||||||
@@ -520,10 +580,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
presetBtns.forEach(b => b.classList.remove('active'));
|
presetBtns.forEach(b => b.classList.remove('active'));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register datalabels plugin
|
// Register plugins
|
||||||
Chart.register(ChartDataLabels);
|
Chart.register(ChartDataLabels);
|
||||||
|
|
||||||
// Daily Chart
|
// Daily Chart with zoom
|
||||||
const dailyData = {{ daily_data|safe }};
|
const dailyData = {{ daily_data|safe }};
|
||||||
const dailyLabels = dailyData.map(d => {
|
const dailyLabels = dailyData.map(d => {
|
||||||
if (d.date) {
|
if (d.date) {
|
||||||
@@ -535,8 +595,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const dailyPoints = dailyData.map(d => d.points);
|
const dailyPoints = dailyData.map(d => d.points);
|
||||||
const dailySources = dailyData.map(d => d.sources);
|
const dailySources = dailyData.map(d => d.sources);
|
||||||
|
|
||||||
|
let dailyChart = null;
|
||||||
if (dailyData.length > 0) {
|
if (dailyData.length > 0) {
|
||||||
new Chart(document.getElementById('dailyChart'), {
|
dailyChart = new Chart(document.getElementById('dailyChart'), {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: {
|
data: {
|
||||||
labels: dailyLabels,
|
labels: dailyLabels,
|
||||||
@@ -559,6 +620,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
|
interaction: {
|
||||||
|
intersect: false,
|
||||||
|
mode: 'index'
|
||||||
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
position: 'top',
|
position: 'top',
|
||||||
@@ -576,34 +641,127 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Satellite Statistics
|
// Satellite Statistics with selection
|
||||||
const satelliteStats = {{ satellite_stats_json|safe }};
|
const satelliteStats = {{ satellite_stats_json|safe }};
|
||||||
|
const satelliteSettingsKey = 'statistics_satellite_selection';
|
||||||
// Pie Chart (top 10)
|
|
||||||
const top10Stats = satelliteStats.slice(0, 10);
|
|
||||||
const otherPoints = satelliteStats.slice(10).reduce((sum, s) => sum + s.points_count, 0);
|
|
||||||
|
|
||||||
const pieLabels = top10Stats.map(s => s.parameter_obj__id_satellite__name);
|
|
||||||
const pieData = top10Stats.map(s => s.points_count);
|
|
||||||
|
|
||||||
if (otherPoints > 0) {
|
|
||||||
pieLabels.push('Другие');
|
|
||||||
pieData.push(otherPoints);
|
|
||||||
}
|
|
||||||
|
|
||||||
const colors = [
|
const colors = [
|
||||||
'#0d6efd', '#198754', '#dc3545', '#ffc107', '#0dcaf0',
|
'#0d6efd', '#198754', '#dc3545', '#ffc107', '#0dcaf0',
|
||||||
'#6f42c1', '#fd7e14', '#20c997', '#6c757d', '#d63384', '#adb5bd'
|
'#6f42c1', '#fd7e14', '#20c997', '#6c757d', '#d63384', '#adb5bd'
|
||||||
];
|
];
|
||||||
|
|
||||||
if (pieData.length > 0) {
|
let pieChart = null;
|
||||||
new Chart(document.getElementById('satellitePieChart'), {
|
let barChart = null;
|
||||||
|
|
||||||
|
// Load satellite selection settings
|
||||||
|
function loadSatelliteSettings() {
|
||||||
|
const saved = localStorage.getItem(satelliteSettingsKey);
|
||||||
|
if (saved) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(saved);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse satellite settings:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Default: all satellites selected
|
||||||
|
return satelliteStats.reduce((acc, stat) => {
|
||||||
|
acc[stat.parameter_obj__id_satellite__name] = true;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save satellite selection settings
|
||||||
|
function saveSatelliteSettings(settings) {
|
||||||
|
localStorage.setItem(satelliteSettingsKey, JSON.stringify(settings));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize satellite checkboxes
|
||||||
|
function initSatelliteCheckboxes() {
|
||||||
|
const container = document.getElementById('satellite-checkboxes');
|
||||||
|
const settings = loadSatelliteSettings();
|
||||||
|
|
||||||
|
container.innerHTML = '';
|
||||||
|
satelliteStats.forEach((stat, index) => {
|
||||||
|
const satelliteName = stat.parameter_obj__id_satellite__name;
|
||||||
|
const isChecked = settings[satelliteName] !== false;
|
||||||
|
|
||||||
|
const checkboxHtml = `
|
||||||
|
<div class="form-check mb-1">
|
||||||
|
<input class="form-check-input satellite-checkbox" type="checkbox"
|
||||||
|
id="sat-${index}" value="${satelliteName}" ${isChecked ? 'checked' : ''}>
|
||||||
|
<label class="form-check-label" for="sat-${index}">
|
||||||
|
<span class="badge" style="background-color: ${colors[index % colors.length]}; width: 12px; height: 12px; display: inline-block; margin-right: 5px;"></span>
|
||||||
|
${satelliteName} (${stat.points_count})
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
container.insertAdjacentHTML('beforeend', checkboxHtml);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add event listeners
|
||||||
|
container.querySelectorAll('.satellite-checkbox').forEach(checkbox => {
|
||||||
|
checkbox.addEventListener('change', function() {
|
||||||
|
const settings = loadSatelliteSettings();
|
||||||
|
settings[this.value] = this.checked;
|
||||||
|
saveSatelliteSettings(settings);
|
||||||
|
updateCharts();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get filtered satellite data based on selection
|
||||||
|
function getFilteredSatelliteData() {
|
||||||
|
const settings = loadSatelliteSettings();
|
||||||
|
return satelliteStats.filter(stat => settings[stat.parameter_obj__id_satellite__name] !== false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update charts based on satellite selection
|
||||||
|
function updateCharts() {
|
||||||
|
const filteredStats = getFilteredSatelliteData();
|
||||||
|
|
||||||
|
// Update pie chart
|
||||||
|
if (pieChart) {
|
||||||
|
const pieLabels = filteredStats.map(s => s.parameter_obj__id_satellite__name);
|
||||||
|
const pieData = filteredStats.map(s => s.points_count);
|
||||||
|
const pieColors = filteredStats.map((s, i) => colors[satelliteStats.indexOf(s) % colors.length]);
|
||||||
|
|
||||||
|
pieChart.data.labels = pieLabels;
|
||||||
|
pieChart.data.datasets[0].data = pieData;
|
||||||
|
pieChart.data.datasets[0].backgroundColor = pieColors;
|
||||||
|
pieChart.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update bar chart (limit to top 15 for readability)
|
||||||
|
if (barChart) {
|
||||||
|
const topStats = filteredStats.slice(0, 15);
|
||||||
|
const barLabels = topStats.map(s => s.parameter_obj__id_satellite__name);
|
||||||
|
const barData = topStats.map(s => s.points_count);
|
||||||
|
const barColors = topStats.map((s, i) => colors[satelliteStats.indexOf(s) % colors.length]);
|
||||||
|
|
||||||
|
barChart.data.labels = barLabels;
|
||||||
|
barChart.data.datasets[0].data = barData;
|
||||||
|
barChart.data.datasets[0].backgroundColor = barColors;
|
||||||
|
barChart.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize charts
|
||||||
|
function initCharts() {
|
||||||
|
const filteredStats = getFilteredSatelliteData();
|
||||||
|
|
||||||
|
// Pie Chart
|
||||||
|
if (filteredStats.length > 0) {
|
||||||
|
const pieLabels = filteredStats.map(s => s.parameter_obj__id_satellite__name);
|
||||||
|
const pieData = filteredStats.map(s => s.points_count);
|
||||||
|
const pieColors = filteredStats.map((s, i) => colors[satelliteStats.indexOf(s) % colors.length]);
|
||||||
|
|
||||||
|
pieChart = new Chart(document.getElementById('satellitePieChart'), {
|
||||||
type: 'doughnut',
|
type: 'doughnut',
|
||||||
data: {
|
data: {
|
||||||
labels: pieLabels,
|
labels: pieLabels,
|
||||||
datasets: [{
|
datasets: [{
|
||||||
data: pieData,
|
data: pieData,
|
||||||
backgroundColor: colors.slice(0, pieData.length)
|
backgroundColor: pieColors
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
@@ -611,17 +769,23 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
position: 'right',
|
position: 'right',
|
||||||
|
labels: {
|
||||||
|
boxWidth: 12,
|
||||||
|
font: {
|
||||||
|
size: 11
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
datalabels: {
|
datalabels: {
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
font: {
|
font: {
|
||||||
weight: 'bold',
|
weight: 'bold',
|
||||||
size: 11
|
size: 10
|
||||||
},
|
},
|
||||||
formatter: function(value, context) {
|
formatter: function(value, context) {
|
||||||
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
||||||
const percentage = ((value / total) * 100).toFixed(1);
|
const percentage = ((value / total) * 100).toFixed(1);
|
||||||
if (percentage < 5) return '';
|
if (percentage < 3) return '';
|
||||||
return value + '\n(' + percentage + '%)';
|
return value + '\n(' + percentage + '%)';
|
||||||
},
|
},
|
||||||
textAlign: 'center'
|
textAlign: 'center'
|
||||||
@@ -631,21 +795,30 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bar Chart (top 10) with data labels
|
// Bar Chart (top 15)
|
||||||
if (top10Stats.length > 0) {
|
if (filteredStats.length > 0) {
|
||||||
new Chart(document.getElementById('satelliteBarChart'), {
|
const topStats = filteredStats.slice(0, 15);
|
||||||
|
const barLabels = topStats.map(s => s.parameter_obj__id_satellite__name);
|
||||||
|
const barData = topStats.map(s => s.points_count);
|
||||||
|
const barColors = topStats.map((s, i) => colors[satelliteStats.indexOf(s) % colors.length]);
|
||||||
|
|
||||||
|
barChart = new Chart(document.getElementById('satelliteBarChart'), {
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: {
|
data: {
|
||||||
labels: top10Stats.map(s => s.parameter_obj__id_satellite__name),
|
labels: barLabels,
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: 'Количество точек',
|
label: 'Количество точек',
|
||||||
data: top10Stats.map(s => s.points_count),
|
data: barData,
|
||||||
backgroundColor: colors.slice(0, top10Stats.length)
|
backgroundColor: barColors
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
indexAxis: 'y',
|
indexAxis: 'y',
|
||||||
|
interaction: {
|
||||||
|
intersect: false,
|
||||||
|
mode: 'index'
|
||||||
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
display: false
|
display: false
|
||||||
@@ -672,6 +845,58 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Satellite selection buttons
|
||||||
|
function initSatelliteButtons() {
|
||||||
|
const selectAllSatBtn = document.getElementById('select-all-satellites');
|
||||||
|
const selectTop10Btn = document.getElementById('select-top10-satellites');
|
||||||
|
const clearSatBtn = document.getElementById('clear-satellites');
|
||||||
|
|
||||||
|
if (selectAllSatBtn) {
|
||||||
|
selectAllSatBtn.addEventListener('click', function() {
|
||||||
|
const settings = {};
|
||||||
|
satelliteStats.forEach(stat => {
|
||||||
|
settings[stat.parameter_obj__id_satellite__name] = true;
|
||||||
|
});
|
||||||
|
saveSatelliteSettings(settings);
|
||||||
|
initSatelliteCheckboxes();
|
||||||
|
updateCharts();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectTop10Btn) {
|
||||||
|
selectTop10Btn.addEventListener('click', function() {
|
||||||
|
const settings = {};
|
||||||
|
satelliteStats.forEach((stat, index) => {
|
||||||
|
settings[stat.parameter_obj__id_satellite__name] = index < 10;
|
||||||
|
});
|
||||||
|
saveSatelliteSettings(settings);
|
||||||
|
initSatelliteCheckboxes();
|
||||||
|
updateCharts();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clearSatBtn) {
|
||||||
|
clearSatBtn.addEventListener('click', function() {
|
||||||
|
const settings = {};
|
||||||
|
satelliteStats.forEach(stat => {
|
||||||
|
settings[stat.parameter_obj__id_satellite__name] = false;
|
||||||
|
});
|
||||||
|
saveSatelliteSettings(settings);
|
||||||
|
initSatelliteCheckboxes();
|
||||||
|
updateCharts();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize satellite functionality
|
||||||
|
initSatelliteCheckboxes();
|
||||||
|
initSatelliteButtons();
|
||||||
|
initCharts();
|
||||||
|
|
||||||
|
// Note: Zoom functionality temporarily disabled due to plugin loading issues
|
||||||
|
// Can be re-enabled when zoom plugin is properly configured
|
||||||
|
|
||||||
// Settings functionality
|
// Settings functionality
|
||||||
const settingsKey = 'statistics_display_settings_detailed';
|
const settingsKey = 'statistics_display_settings_detailed';
|
||||||
|
|||||||
@@ -199,8 +199,8 @@ class SourceObjItemsAPIView(LoginRequiredMixin, View):
|
|||||||
'source_objitems__transponder',
|
'source_objitems__transponder',
|
||||||
'source_objitems__created_by__user',
|
'source_objitems__created_by__user',
|
||||||
'source_objitems__updated_by__user',
|
'source_objitems__updated_by__user',
|
||||||
'marks',
|
# 'marks',
|
||||||
'marks__created_by__user'
|
# 'marks__created_by__user'
|
||||||
).get(id=source_id)
|
).get(id=source_id)
|
||||||
|
|
||||||
# Get all related ObjItems, sorted by created_at
|
# Get all related ObjItems, sorted by created_at
|
||||||
|
|||||||
Reference in New Issue
Block a user