feat(class): improve class/id
This commit is contained in:
@@ -35,8 +35,12 @@ from schemas.class_group import (
|
||||
HistogramBin,
|
||||
DomainStats,
|
||||
CompetenceStats,
|
||||
AssessmentScore,
|
||||
DomainStudentStats,
|
||||
CompetenceStudentStats,
|
||||
)
|
||||
from domain.services.grading_calculator import GradingCalculator
|
||||
from domain.services.class_statistics_service import ClassStatisticsService
|
||||
from schemas.student import StudentWithClass, StudentList
|
||||
from schemas.csv_import import (
|
||||
CSVImportResponse,
|
||||
@@ -221,10 +225,10 @@ async def get_class_stats(
|
||||
Récupère les statistiques complètes d'une classe pour un trimestre.
|
||||
|
||||
Inclut:
|
||||
- Moyennes par élève
|
||||
- Moyennes par élève avec détail par évaluation
|
||||
- Statistiques globales (moyenne, médiane, écart-type)
|
||||
- Histogramme des moyennes
|
||||
- Analyse par domaines et compétences
|
||||
- Analyse par domaines et compétences (nombre d'évaluations + points)
|
||||
"""
|
||||
# Vérifier que la classe existe
|
||||
class_query = select(ClassGroup).where(ClassGroup.id == class_id)
|
||||
@@ -247,26 +251,34 @@ async def get_class_stats(
|
||||
students_result = await session.execute(students_query)
|
||||
students = students_result.scalars().all()
|
||||
|
||||
# Récupérer les évaluations du trimestre
|
||||
assessments_query = select(Assessment).where(
|
||||
Assessment.class_group_id == class_id,
|
||||
Assessment.trimester == trimester
|
||||
# Récupérer les évaluations du trimestre avec leurs relations
|
||||
assessments_query = (
|
||||
select(Assessment)
|
||||
.options(
|
||||
selectinload(Assessment.exercises).selectinload(Exercise.grading_elements)
|
||||
)
|
||||
.where(
|
||||
Assessment.class_group_id == class_id,
|
||||
Assessment.trimester == trimester
|
||||
)
|
||||
.order_by(Assessment.date)
|
||||
)
|
||||
assessments_result = await session.execute(assessments_query)
|
||||
assessments = assessments_result.scalars().all()
|
||||
|
||||
# Calculer les moyennes de chaque élève
|
||||
calculator = GradingCalculator()
|
||||
student_averages = []
|
||||
all_averages = []
|
||||
# Récupérer les domaines et compétences
|
||||
domains_query = select(Domain).order_by(Domain.name)
|
||||
domains_result = await session.execute(domains_query)
|
||||
domains = domains_result.scalars().all()
|
||||
|
||||
competences_query = select(Competence).order_by(Competence.order_index)
|
||||
competences_result = await session.execute(competences_query)
|
||||
competences = competences_result.scalars().all()
|
||||
|
||||
# Récupérer toutes les notes en une seule requête pour optimiser
|
||||
grades_by_student_assessment = {}
|
||||
for student in students:
|
||||
weighted_sum = 0.0
|
||||
total_coefficient = 0.0
|
||||
assessment_count = 0
|
||||
|
||||
for assessment in assessments:
|
||||
# Récupérer les notes de l'élève pour cette évaluation
|
||||
grades_query = (
|
||||
select(Grade, GradingElement)
|
||||
.join(GradingElement, Grade.grading_element_id == GradingElement.id)
|
||||
@@ -277,45 +289,30 @@ async def get_class_stats(
|
||||
)
|
||||
)
|
||||
grades_result = await session.execute(grades_query)
|
||||
grades_data = grades_result.all()
|
||||
grades_by_student_assessment[(student.id, assessment.id)] = grades_result.all()
|
||||
|
||||
if grades_data:
|
||||
total_score = 0.0
|
||||
total_max_points = 0.0
|
||||
# Utiliser le service pour calculer les statistiques
|
||||
stats_service = ClassStatisticsService()
|
||||
student_averages = await stats_service.calculate_student_statistics(
|
||||
students=students,
|
||||
assessments=assessments,
|
||||
grades_by_student_assessment=grades_by_student_assessment,
|
||||
domains=domains,
|
||||
competences=competences,
|
||||
)
|
||||
|
||||
for grade, element in grades_data:
|
||||
if grade.value:
|
||||
score = calculator.calculate_score(
|
||||
grade.value, element.grading_type, element.max_points
|
||||
)
|
||||
if score is not None and calculator.is_counted_in_total(grade.value):
|
||||
total_score += score
|
||||
total_max_points += element.max_points
|
||||
|
||||
if total_max_points > 0:
|
||||
# Ramener sur 20
|
||||
score_on_20 = total_score / total_max_points * 20
|
||||
weighted_sum += score_on_20 * assessment.coefficient
|
||||
total_coefficient += assessment.coefficient
|
||||
assessment_count += 1
|
||||
|
||||
# Calculer la moyenne pondérée
|
||||
average = None
|
||||
if total_coefficient > 0:
|
||||
average = round(weighted_sum / total_coefficient, 2)
|
||||
all_averages.append(average)
|
||||
|
||||
student_averages.append(StudentAverage(
|
||||
student_id=student.id,
|
||||
first_name=student.first_name,
|
||||
last_name=student.last_name,
|
||||
full_name=f"{student.first_name} {student.last_name}",
|
||||
average=average,
|
||||
assessment_count=assessment_count
|
||||
))
|
||||
# Calculer les statistiques domaines/compétences depuis les éléments de notation
|
||||
# Perspective enseignant : ce qui a été évalué, pas les résultats des élèves
|
||||
domains_stats, competences_stats = stats_service.calculate_domain_competence_from_elements(
|
||||
assessments=assessments,
|
||||
domains=domains,
|
||||
competences=competences,
|
||||
)
|
||||
|
||||
# Calculer les statistiques globales
|
||||
all_averages = [s.average for s in student_averages if s.average is not None]
|
||||
mean = median = std_dev = min_score = max_score = None
|
||||
|
||||
if all_averages:
|
||||
mean = round(sum(all_averages) / len(all_averages), 2)
|
||||
sorted_averages = sorted(all_averages)
|
||||
@@ -345,9 +342,10 @@ async def get_class_stats(
|
||||
count=count
|
||||
))
|
||||
# Ajouter le dernier bin pour 20
|
||||
count_20 = sum(1 for avg in all_averages if avg == 20)
|
||||
if count_20 > 0:
|
||||
histogram[-1].count += count_20
|
||||
if histogram:
|
||||
count_20 = sum(1 for avg in all_averages if avg == 20)
|
||||
if count_20 > 0:
|
||||
histogram[-1].count += count_20
|
||||
|
||||
# Compter les évaluations par statut
|
||||
assessments_completed = 0
|
||||
@@ -379,36 +377,6 @@ async def get_class_stats(
|
||||
elif grades_count > 0:
|
||||
assessments_in_progress += 1
|
||||
|
||||
# Statistiques par domaine et compétence (simplifié)
|
||||
domains_stats = []
|
||||
competences_stats = []
|
||||
|
||||
# Récupérer les domaines
|
||||
domains_query = select(Domain).order_by(Domain.name)
|
||||
domains_result = await session.execute(domains_query)
|
||||
domains = domains_result.scalars().all()
|
||||
for domain in domains:
|
||||
domains_stats.append(DomainStats(
|
||||
id=domain.id,
|
||||
name=domain.name,
|
||||
color=domain.color,
|
||||
mean=None,
|
||||
elements_count=0
|
||||
))
|
||||
|
||||
# Récupérer les compétences
|
||||
competences_query = select(Competence).order_by(Competence.order_index)
|
||||
competences_result = await session.execute(competences_query)
|
||||
competences = competences_result.scalars().all()
|
||||
for competence in competences:
|
||||
competences_stats.append(CompetenceStats(
|
||||
id=competence.id,
|
||||
name=competence.name,
|
||||
color=competence.color,
|
||||
mean=None,
|
||||
elements_count=0
|
||||
))
|
||||
|
||||
return ClassDashboardStats(
|
||||
class_id=class_id,
|
||||
class_name=cls.name,
|
||||
|
||||
@@ -29,6 +29,7 @@ from .student_report_service import (
|
||||
StudentReportData,
|
||||
generate_report_html,
|
||||
)
|
||||
from .class_statistics_service import ClassStatisticsService
|
||||
|
||||
__all__ = [
|
||||
# Grading Calculator
|
||||
@@ -39,6 +40,7 @@ __all__ = [
|
||||
"ScoreStrategy",
|
||||
# Statistics
|
||||
"StatisticsService",
|
||||
"ClassStatisticsService",
|
||||
# Score Calculator
|
||||
"StudentScoreCalculator",
|
||||
"ProgressCalculator",
|
||||
|
||||
331
backend/domain/services/class_statistics_service.py
Normal file
331
backend/domain/services/class_statistics_service.py
Normal file
@@ -0,0 +1,331 @@
|
||||
"""
|
||||
Service de calcul des statistiques de classe.
|
||||
|
||||
Calcule les statistiques complètes pour le dashboard de classe:
|
||||
- Moyennes par élève
|
||||
- Scores par évaluation pour chaque élève
|
||||
- Statistiques par domaine et compétence
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Optional, Tuple
|
||||
from collections import defaultdict
|
||||
|
||||
from infrastructure.database.models import (
|
||||
Student,
|
||||
Assessment,
|
||||
Exercise,
|
||||
GradingElement,
|
||||
Grade,
|
||||
Domain,
|
||||
Competence,
|
||||
)
|
||||
from domain.services.grading_calculator import GradingCalculator
|
||||
from schemas.class_group import (
|
||||
StudentAverage,
|
||||
AssessmentScore,
|
||||
DomainStudentStats,
|
||||
CompetenceStudentStats,
|
||||
DomainStats,
|
||||
CompetenceStats,
|
||||
)
|
||||
|
||||
|
||||
class ClassStatisticsService:
|
||||
"""Service de calcul des statistiques de classe."""
|
||||
|
||||
def __init__(self):
|
||||
self.calculator = GradingCalculator()
|
||||
|
||||
async def calculate_student_statistics(
|
||||
self,
|
||||
students: List[Student],
|
||||
assessments: List[Assessment],
|
||||
grades_by_student_assessment: Dict[Tuple[int, int], List[Tuple[Grade, GradingElement]]],
|
||||
domains: List[Domain],
|
||||
competences: List[Competence],
|
||||
) -> List[StudentAverage]:
|
||||
"""
|
||||
Calcule les statistiques complètes pour chaque élève.
|
||||
|
||||
Args:
|
||||
students: Liste des élèves
|
||||
assessments: Liste des évaluations du trimestre
|
||||
grades_by_student_assessment: Dict[(student_id, assessment_id)] -> [(grade, element)]
|
||||
domains: Liste des domaines
|
||||
competences: Liste des compétences
|
||||
|
||||
Returns:
|
||||
Liste des StudentAverage avec toutes les statistiques
|
||||
"""
|
||||
student_averages = []
|
||||
|
||||
for student in students:
|
||||
# Initialiser les statistiques par domaine/compétence
|
||||
domain_stats: Dict[int, DomainStudentStats] = {
|
||||
domain.id: DomainStudentStats(
|
||||
domain_id=domain.id,
|
||||
evaluation_count=0,
|
||||
total_points_obtained=0.0,
|
||||
total_points_possible=0.0,
|
||||
)
|
||||
for domain in domains
|
||||
}
|
||||
|
||||
competence_stats: Dict[int, CompetenceStudentStats] = {}
|
||||
|
||||
# Calculer les scores par évaluation
|
||||
assessment_scores: Dict[int, AssessmentScore] = {}
|
||||
weighted_sum = 0.0
|
||||
total_coefficient = 0.0
|
||||
assessment_count = 0
|
||||
|
||||
for assessment in assessments:
|
||||
grades_data = grades_by_student_assessment.get((student.id, assessment.id), [])
|
||||
|
||||
if not grades_data:
|
||||
continue
|
||||
|
||||
# Calculer le score total pour cette évaluation
|
||||
total_score = 0.0
|
||||
total_max_points = 0.0
|
||||
|
||||
for grade, element in grades_data:
|
||||
if grade.value:
|
||||
score = self.calculator.calculate_score(
|
||||
grade.value, element.grading_type, element.max_points
|
||||
)
|
||||
|
||||
if score is not None and self.calculator.is_counted_in_total(grade.value):
|
||||
total_score += score
|
||||
total_max_points += element.max_points
|
||||
|
||||
# Statistiques par domaine
|
||||
if element.domain_id and element.domain_id in domain_stats:
|
||||
domain_stats[element.domain_id].evaluation_count += 1
|
||||
domain_stats[element.domain_id].total_points_obtained += score
|
||||
domain_stats[element.domain_id].total_points_possible += element.max_points
|
||||
|
||||
# Statistiques par compétence (skill)
|
||||
# Note: On utilise element.skill pour identifier la compétence
|
||||
if element.skill:
|
||||
# Trouver la compétence correspondante
|
||||
matching_competence = next(
|
||||
(c for c in competences if c.name == element.skill),
|
||||
None
|
||||
)
|
||||
if matching_competence:
|
||||
if matching_competence.id not in competence_stats:
|
||||
competence_stats[matching_competence.id] = CompetenceStudentStats(
|
||||
competence_id=matching_competence.id,
|
||||
evaluation_count=0,
|
||||
total_points_obtained=0.0,
|
||||
total_points_possible=0.0,
|
||||
)
|
||||
|
||||
competence_stats[matching_competence.id].evaluation_count += 1
|
||||
competence_stats[matching_competence.id].total_points_obtained += score
|
||||
competence_stats[matching_competence.id].total_points_possible += element.max_points
|
||||
|
||||
# Calculer le score sur 20
|
||||
score_on_20 = None
|
||||
if total_max_points > 0:
|
||||
score_on_20 = round(total_score / total_max_points * 20, 2)
|
||||
weighted_sum += score_on_20 * assessment.coefficient
|
||||
total_coefficient += assessment.coefficient
|
||||
assessment_count += 1
|
||||
|
||||
# Sauvegarder le score de cette évaluation
|
||||
assessment_scores[assessment.id] = AssessmentScore(
|
||||
assessment_id=assessment.id,
|
||||
assessment_title=assessment.title,
|
||||
score=round(total_score, 2) if total_score > 0 else None,
|
||||
max_points=round(total_max_points, 2),
|
||||
score_on_20=score_on_20,
|
||||
)
|
||||
|
||||
# Calculer la moyenne pondérée
|
||||
average = None
|
||||
if total_coefficient > 0:
|
||||
average = round(weighted_sum / total_coefficient, 2)
|
||||
|
||||
student_averages.append(StudentAverage(
|
||||
student_id=student.id,
|
||||
first_name=student.first_name,
|
||||
last_name=student.last_name,
|
||||
full_name=f"{student.first_name} {student.last_name}",
|
||||
average=average,
|
||||
assessment_count=assessment_count,
|
||||
assessment_scores=assessment_scores,
|
||||
domain_stats=domain_stats,
|
||||
competence_stats=competence_stats,
|
||||
))
|
||||
|
||||
return student_averages
|
||||
|
||||
def aggregate_domain_competence_stats(
|
||||
self,
|
||||
student_averages: List[StudentAverage],
|
||||
domains: List[Domain],
|
||||
competences: List[Competence],
|
||||
) -> Tuple[List[DomainStats], List[CompetenceStats]]:
|
||||
"""
|
||||
Agrège les statistiques par domaine et compétence pour tous les élèves.
|
||||
|
||||
Args:
|
||||
student_averages: Liste des statistiques par élève
|
||||
domains: Liste des domaines
|
||||
competences: Liste des compétences
|
||||
|
||||
Returns:
|
||||
Tuple (domains_stats, competences_stats)
|
||||
"""
|
||||
# Agréger par domaine
|
||||
domain_aggregates: Dict[int, Dict] = defaultdict(
|
||||
lambda: {
|
||||
"evaluation_count": 0,
|
||||
"total_points_obtained": 0.0,
|
||||
"total_points_possible": 0.0,
|
||||
}
|
||||
)
|
||||
|
||||
for student in student_averages:
|
||||
for domain_id, stats in student.domain_stats.items():
|
||||
domain_aggregates[domain_id]["evaluation_count"] += stats.evaluation_count
|
||||
domain_aggregates[domain_id]["total_points_obtained"] += stats.total_points_obtained
|
||||
domain_aggregates[domain_id]["total_points_possible"] += stats.total_points_possible
|
||||
|
||||
domains_stats = []
|
||||
for domain in domains:
|
||||
agg = domain_aggregates.get(domain.id, {
|
||||
"evaluation_count": 0,
|
||||
"total_points_obtained": 0.0,
|
||||
"total_points_possible": 0.0,
|
||||
})
|
||||
domains_stats.append(DomainStats(
|
||||
id=domain.id,
|
||||
name=domain.name,
|
||||
color=domain.color,
|
||||
evaluation_count=agg["evaluation_count"],
|
||||
total_points_obtained=round(agg["total_points_obtained"], 2),
|
||||
total_points_possible=round(agg["total_points_possible"], 2),
|
||||
))
|
||||
|
||||
# Agréger par compétence
|
||||
competence_aggregates: Dict[int, Dict] = defaultdict(
|
||||
lambda: {
|
||||
"evaluation_count": 0,
|
||||
"total_points_obtained": 0.0,
|
||||
"total_points_possible": 0.0,
|
||||
}
|
||||
)
|
||||
|
||||
for student in student_averages:
|
||||
for competence_id, stats in student.competence_stats.items():
|
||||
competence_aggregates[competence_id]["evaluation_count"] += stats.evaluation_count
|
||||
competence_aggregates[competence_id]["total_points_obtained"] += stats.total_points_obtained
|
||||
competence_aggregates[competence_id]["total_points_possible"] += stats.total_points_possible
|
||||
|
||||
competences_stats = []
|
||||
for competence in competences:
|
||||
agg = competence_aggregates.get(competence.id, {
|
||||
"evaluation_count": 0,
|
||||
"total_points_obtained": 0.0,
|
||||
"total_points_possible": 0.0,
|
||||
})
|
||||
competences_stats.append(CompetenceStats(
|
||||
id=competence.id,
|
||||
name=competence.name,
|
||||
color=competence.color,
|
||||
evaluation_count=agg["evaluation_count"],
|
||||
total_points_obtained=round(agg["total_points_obtained"], 2),
|
||||
total_points_possible=round(agg["total_points_possible"], 2),
|
||||
))
|
||||
|
||||
return domains_stats, competences_stats
|
||||
|
||||
def calculate_domain_competence_from_elements(
|
||||
self,
|
||||
assessments: List[Assessment],
|
||||
domains: List[Domain],
|
||||
competences: List[Competence],
|
||||
) -> Tuple[List[DomainStats], List[CompetenceStats]]:
|
||||
"""
|
||||
Calcule les statistiques domaines/compétences depuis les GradingElements.
|
||||
|
||||
Perspective enseignant : ce qui a été évalué, pas les résultats des élèves.
|
||||
|
||||
Args:
|
||||
assessments: Liste des évaluations (avec exercises et grading_elements chargés)
|
||||
domains: Liste des domaines
|
||||
competences: Liste des compétences
|
||||
|
||||
Returns:
|
||||
Tuple (domains_stats, competences_stats)
|
||||
"""
|
||||
# Compter les GradingElements par domaine
|
||||
domain_aggregates: Dict[int, Dict] = defaultdict(
|
||||
lambda: {
|
||||
"evaluation_count": 0,
|
||||
"total_points_possible": 0.0,
|
||||
}
|
||||
)
|
||||
|
||||
competence_aggregates: Dict[int, Dict] = defaultdict(
|
||||
lambda: {
|
||||
"evaluation_count": 0,
|
||||
"total_points_possible": 0.0,
|
||||
}
|
||||
)
|
||||
|
||||
# Parcourir tous les éléments de notation
|
||||
for assessment in assessments:
|
||||
for exercise in assessment.exercises:
|
||||
for element in exercise.grading_elements:
|
||||
# Compter par domaine
|
||||
if element.domain_id:
|
||||
domain_aggregates[element.domain_id]["evaluation_count"] += 1
|
||||
domain_aggregates[element.domain_id]["total_points_possible"] += element.max_points
|
||||
|
||||
# Compter par compétence (via skill)
|
||||
if element.skill:
|
||||
matching_competence = next(
|
||||
(c for c in competences if c.name == element.skill),
|
||||
None
|
||||
)
|
||||
if matching_competence:
|
||||
competence_aggregates[matching_competence.id]["evaluation_count"] += 1
|
||||
competence_aggregates[matching_competence.id]["total_points_possible"] += element.max_points
|
||||
|
||||
# Créer les stats par domaine
|
||||
domains_stats = []
|
||||
for domain in domains:
|
||||
agg = domain_aggregates.get(domain.id, {
|
||||
"evaluation_count": 0,
|
||||
"total_points_possible": 0.0,
|
||||
})
|
||||
domains_stats.append(DomainStats(
|
||||
id=domain.id,
|
||||
name=domain.name,
|
||||
color=domain.color,
|
||||
evaluation_count=agg["evaluation_count"],
|
||||
total_points_obtained=0.0, # Non utilisé dans cette perspective
|
||||
total_points_possible=round(agg["total_points_possible"], 2),
|
||||
))
|
||||
|
||||
# Créer les stats par compétence
|
||||
competences_stats = []
|
||||
for competence in competences:
|
||||
agg = competence_aggregates.get(competence.id, {
|
||||
"evaluation_count": 0,
|
||||
"total_points_possible": 0.0,
|
||||
})
|
||||
competences_stats.append(CompetenceStats(
|
||||
id=competence.id,
|
||||
name=competence.name,
|
||||
color=competence.color,
|
||||
evaluation_count=agg["evaluation_count"],
|
||||
total_points_obtained=0.0, # Non utilisé
|
||||
total_points_possible=round(agg["total_points_possible"], 2),
|
||||
))
|
||||
|
||||
return domains_stats, competences_stats
|
||||
@@ -3,7 +3,7 @@ Schemas Pydantic pour ClassGroup.
|
||||
"""
|
||||
|
||||
from datetime import date
|
||||
from typing import Optional, List
|
||||
from typing import Optional, List, Dict
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
@@ -117,6 +117,9 @@ class StudentAverage(BaseSchema):
|
||||
full_name: str
|
||||
average: Optional[float] = None
|
||||
assessment_count: int = 0
|
||||
assessment_scores: Dict[int, "AssessmentScore"] = {}
|
||||
domain_stats: Dict[int, "DomainStudentStats"] = {}
|
||||
competence_stats: Dict[int, "CompetenceStudentStats"] = {}
|
||||
|
||||
|
||||
class HistogramBin(BaseSchema):
|
||||
@@ -128,14 +131,43 @@ class HistogramBin(BaseSchema):
|
||||
count: int
|
||||
|
||||
|
||||
class AssessmentScore(BaseSchema):
|
||||
"""Score d'un élève pour une évaluation."""
|
||||
|
||||
assessment_id: int
|
||||
assessment_title: str
|
||||
score: Optional[float] = None
|
||||
max_points: float = 0.0
|
||||
score_on_20: Optional[float] = None
|
||||
|
||||
|
||||
class DomainStudentStats(BaseSchema):
|
||||
"""Statistiques d'un élève pour un domaine."""
|
||||
|
||||
domain_id: int
|
||||
evaluation_count: int = 0
|
||||
total_points_obtained: float = 0.0
|
||||
total_points_possible: float = 0.0
|
||||
|
||||
|
||||
class CompetenceStudentStats(BaseSchema):
|
||||
"""Statistiques d'un élève pour une compétence."""
|
||||
|
||||
competence_id: int
|
||||
evaluation_count: int = 0
|
||||
total_points_obtained: float = 0.0
|
||||
total_points_possible: float = 0.0
|
||||
|
||||
|
||||
class DomainStats(BaseSchema):
|
||||
"""Statistiques par domaine."""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
color: str
|
||||
mean: Optional[float] = None
|
||||
elements_count: int = 0
|
||||
evaluation_count: int = 0
|
||||
total_points_obtained: float = 0.0
|
||||
total_points_possible: float = 0.0
|
||||
|
||||
|
||||
class CompetenceStats(BaseSchema):
|
||||
@@ -144,8 +176,9 @@ class CompetenceStats(BaseSchema):
|
||||
id: int
|
||||
name: str
|
||||
color: str
|
||||
mean: Optional[float] = None
|
||||
elements_count: int = 0
|
||||
evaluation_count: int = 0
|
||||
total_points_obtained: float = 0.0
|
||||
total_points_possible: float = 0.0
|
||||
|
||||
|
||||
class ClassDashboardStats(BaseSchema):
|
||||
|
||||
Reference in New Issue
Block a user