Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Docs/sql/homework-hide-ratings-local.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-- Только локальная база EnsureCreated без истории EF-миграций.
USE [CoursesServiceDB];
GO
SET XACT_ABORT ON;
BEGIN TRANSACTION;
IF OBJECT_ID(N'dbo.__EFMigrationsHistory', N'U') IS NOT NULL
THROW 50000, 'Use EF migrations for databases with migration history.', 1;
IF OBJECT_ID(N'dbo.Homeworks', N'U') IS NULL
THROW 50000, 'Homeworks table not found.', 1;
IF COL_LENGTH(N'dbo.Homeworks', N'HideRatings') IS NULL
ALTER TABLE dbo.Homeworks ADD HideRatings bit NOT NULL
CONSTRAINT DF_Homeworks_HideRatings DEFAULT (0) WITH VALUES;
COMMIT TRANSACTION;
22 changes: 22 additions & 0 deletions Docs/sql/notification-digest-local.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-- Локальная база EnsureCreated: изменения схемы из upstream NotificationDigest.
-- Для баз с историей миграций следует применять штатную EF-миграцию.
USE [NotificationsServiceDB];
GO
SET XACT_ABORT ON;
BEGIN TRANSACTION;
IF OBJECT_ID(N'dbo.__EFMigrationsHistory', N'U') IS NOT NULL
THROW 50000, 'Use EF migrations for databases with migration history.', 1;
IF OBJECT_ID(N'dbo.Notifications', N'U') IS NULL
THROW 50000, 'Notifications table not found.', 1;
IF COL_LENGTH(N'dbo.Notifications', N'DigestData') IS NULL
ALTER TABLE dbo.Notifications ADD DigestData nvarchar(max) NULL;
IF COL_LENGTH(N'dbo.Notifications', N'DigestKind') IS NULL
ALTER TABLE dbo.Notifications ADD DigestKind int NULL;
IF COL_LENGTH(N'dbo.Notifications', N'IsDigestOnly') IS NULL
ALTER TABLE dbo.Notifications ADD IsDigestOnly bit NOT NULL
CONSTRAINT DF_Notifications_IsDigestOnly DEFAULT (0) WITH VALUES;
IF NOT EXISTS (SELECT 1 FROM sys.indexes
WHERE object_id = OBJECT_ID(N'dbo.Notifications') AND name = N'IX_Notifications_Date')
EXEC(N'CREATE INDEX IX_Notifications_Date ON dbo.Notifications ([Date]) INCLUDE ([Owner])
WHERE DigestKind IS NOT NULL');
COMMIT TRANSACTION;
22 changes: 22 additions & 0 deletions Docs/sql/solution-requires-revision-local.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-- Только для локальной SolutionsServiceDB, созданной через EnsureCreated
-- без таблицы __EFMigrationsHistory. Для баз с историей применяйте EF-миграцию.
USE [SolutionsServiceDB];
GO

SET XACT_ABORT ON;
BEGIN TRANSACTION;

IF OBJECT_ID(N'dbo.Solutions', N'U') IS NULL
THROW 50000, 'Solutions table not found. Check the target database.', 1;

IF OBJECT_ID(N'dbo.__EFMigrationsHistory', N'U') IS NOT NULL
THROW 50000, 'Use EF migrations for databases with migration history.', 1;

IF COL_LENGTH(N'dbo.Solutions', N'RequiresRevision') IS NULL
BEGIN
ALTER TABLE [dbo].[Solutions]
ADD [RequiresRevision] bit NOT NULL
CONSTRAINT [DF_Solutions_RequiresRevision] DEFAULT (0) WITH VALUES;
END;

COMMIT TRANSACTION;
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Threading.Tasks;
using HwProj.APIGateway.API.Models;
using HwProj.APIGateway.API.Models.Tasks;
using HwProj.APIGateway.API.Services;
using HwProj.AuthService.Client;
using HwProj.CoursesService.Client;
using HwProj.Models.AuthService.DTO;
Expand Down Expand Up @@ -69,6 +70,21 @@ public async Task<IActionResult> GetUserData()
var taskDeadlines = await coursesClient.GetTaskDeadlines();
var taskIds = taskDeadlines.Select(t => t.TaskId).ToArray();
var solutions = await solutionsServiceClient.GetLastTaskSolutions(taskIds, UserId);
// Группируем по курсу, чтобы не запрашивать его заново для каждой задачи.
var processedTasks = new System.Collections.Generic.HashSet<long>();
foreach (var solution in solutions.Where(s => s != null))
{
if (processedTasks.Contains(solution.TaskId)) continue;
var course = await coursesClient.GetCourseByTask(solution.TaskId);
if (course == null) return Forbid();
var hiddenTasks = GradeVisibility.HiddenTaskIds(course, UserId);
foreach (var item in solutions.Where(s => s != null &&
course.Homeworks.Any(h => h.Tasks.Any(t => t.Id == s.TaskId))))
{
GradeVisibility.Apply(item, hiddenTasks);
processedTasks.Add(item.TaskId);
}
}
var taskDeadlinesInfo = taskDeadlines
.Zip(solutions, (deadline, solution) => (deadline, solution))
.Where(t => currentTime <= t.deadline.DeadlineDate || t.solution == null)
Expand All @@ -78,6 +94,8 @@ public async Task<IActionResult> GetUserData()
SolutionState = t.solution?.State,
MaxRating = t.deadline.MaxRating,
Rating = t.solution?.Rating,
IsRatingHidden = t.solution?.IsRatingHidden ?? false,
RequiresRevision = t.solution?.RequiresRevision ?? false,
DeadlinePast = currentTime > t.deadline.DeadlineDate
}).ToArray();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
using System;
using System.Net;
using System.Threading.Tasks;
using System.Linq;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using HwProj.CoursesService.Client;
using HwProj.APIGateway.API.Services;
using HwProj.Models.NotificationsService;
using HwProj.Models.Result;
using HwProj.Models.Roles;
Expand All @@ -16,10 +21,12 @@ namespace HwProj.APIGateway.API.Controllers;
public class NotificationsController : AggregationController
{
private readonly INotificationsServiceClient _notificationsClient;
private readonly ICoursesServiceClient _coursesClient;

public NotificationsController(INotificationsServiceClient notificationsClient) : base(null)
public NotificationsController(INotificationsServiceClient notificationsClient, ICoursesServiceClient coursesClient) : base(null)
{
_notificationsClient = notificationsClient;
_coursesClient = coursesClient;
}

[HttpGet("getNewNotificationsCount")]
Expand All @@ -34,6 +41,30 @@ public async Task<int> GetNewNotificationsCount()
public async Task<IActionResult> Get()
{
var result = await _notificationsClient.Get(UserId);
var hiddenTasks = new Dictionary<long, bool>();
foreach (var notification in result.SelectMany(c => c.SeenNotifications.Concat(c.NotSeenNotifications)))
{
var body = notification.Body ?? "";
// Старые уведомления могли сохранить оценку до включения скрытия.
if (!body.Contains("оценена на") &&
!(notification.Sender == "SolutionService" &&
(body.Contains("Решение проверено") || body.Contains("Оценка обновлена")))) continue;
var match = Regex.Match(body, @"taskId=(\d+)");
if (!match.Success || !long.TryParse(match.Groups[1].Value, out var taskId)) continue;
if (!hiddenTasks.TryGetValue(taskId, out var hidden))
{
var course = await _coursesClient.GetCourseByTask(taskId);
hidden = course == null || GradeVisibility.HiddenTaskIds(course, UserId).Contains(taskId);
hiddenTasks[taskId] = hidden;
}
if (hidden)
{
var endOfLink = body.IndexOf("</a>", StringComparison.Ordinal);
notification.Body = endOfLink >= 0
? body.Substring(0, endOfLink + 4) + " проверена. Результат доступен на странице решения."
: "Решение проверено. Результат доступен на странице решения.";
}
}
return Ok(result);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Threading.Tasks;
using HwProj.APIGateway.API.ExceptionFilters;
using HwProj.APIGateway.API.Models.Solutions;
using HwProj.APIGateway.API.Services;
using HwProj.AuthService.Client;
using HwProj.CoursesService.Client;
using HwProj.Models.CoursesService;
Expand Down Expand Up @@ -37,13 +38,21 @@ public SolutionsController(ISolutionsServiceClient solutionsClient, IAuthService
}

[HttpGet("{solutionId}")]
[Authorize]
[ProducesResponseType(typeof(SolutionDto), (int)HttpStatusCode.OK)]
public async Task<IActionResult> GetSolutionById(long solutionId)
{
var result = await _solutionsClient.GetSolutionById(solutionId);
return result == null
? NotFound()
: Ok(result);
if (result == null) return NotFound();
var course = await _coursesServiceClient.GetCourseByTask(result.TaskId);
if (course == null) return NotFound();
var mentor = course.MentorIds.Contains(UserId);
var groupMember = result.GroupId is { } groupId && course.Groups.Any(g =>
g.Id == groupId && g.StudentsIds.Contains(UserId));
if (!mentor && (!course.AcceptedStudents.Any(s => s.StudentId == UserId) ||
(result.StudentId != UserId && !groupMember))) return Forbid();
GradeVisibility.Apply(result, GradeVisibility.HiddenTaskIds(course, UserId));
return Ok(result);
}

[HttpGet("taskSolution/{taskId}/{studentId}")]
Expand All @@ -56,9 +65,11 @@ public async Task<IActionResult> GetStudentSolution(long taskId, string studentI

var courseMate = course.AcceptedStudents.FirstOrDefault(t => t.StudentId == studentId);
if (courseMate == null) return NotFound();
if (!course.MentorIds.Contains(UserId) && studentId != UserId) return Forbid();

var studentSolutions = (await _solutionsClient.GetCourseStatistics(course.Id, UserId))
.Single().Homeworks
var statistics = await _solutionsClient.GetCourseStatistics(course.Id, UserId);
GradeVisibility.Apply(statistics, GradeVisibility.HiddenTaskIds(course, UserId));
var studentSolutions = statistics.Single(s => s.StudentId == studentId).Homeworks
.ToDictionary(x => x.Id);

var homeworks = course.Homeworks
Expand Down Expand Up @@ -516,6 +527,8 @@ public async Task<IActionResult> GetSolutionAchievement(long taskId, long soluti
course.AcceptedStudents.FirstOrDefault(t => t.StudentId == UserId) == null)
return BadRequest($"Студента или преподавателя с id {UserId} не существует");

if (GradeVisibility.HiddenTaskIds(course, UserId).Contains(taskId)) return Ok(null);

var solutions = await _solutionsClient.GetTaskSolutionStatistics(course.Id, taskId);
var lastRatedSolutions = solutions
.Select(t => t.Solutions.LastOrDefault(x => x.State != SolutionState.Posted))
Expand All @@ -529,11 +542,11 @@ public async Task<IActionResult> GetSolutionAchievement(long taskId, long soluti
if (lastRatedSolutions.Any(x => x!.GroupId != null))
lastRatedSolutions = lastRatedSolutions.DistinctBy(t => t!.Id).ToArray();

var score = solution.Rating;
var score = solution.Rating.GetValueOrDefault();
var maxScore = course.Homeworks.SelectMany(x => x.Tasks).First(x => x.Id == taskId).MaxRating;
var otherScores = lastRatedSolutions
.Where(x => x!.Id != solutionId)
.Select(x => x!.Rating)
.Select(x => x!.Rating.GetValueOrDefault())
.ToArray();

var absolute = (double)score / maxScore;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Net;
using System.Threading.Tasks;
using HwProj.APIGateway.API.Models.Statistics;
using HwProj.APIGateway.API.Services;
using HwProj.AuthService.Client;
using HwProj.CoursesService.Client;
using HwProj.Models.AuthService.DTO;
Expand Down Expand Up @@ -53,11 +54,16 @@ public async Task<IActionResult> GetLecturersStatistics(long courseId)
}

[HttpGet("{courseId}")]
[Authorize]
[ProducesResponseType(typeof(StatisticsCourseMatesModel[]), (int)HttpStatusCode.OK)]
public async Task<IActionResult> GetCourseStatistics(long courseId)
{
var course = await _coursesClient.GetCourseById(courseId);
if (course == null) return Forbid();
if (!course.MentorIds.Contains(UserId) && !course.AcceptedStudents.Any(s => s.StudentId == UserId)) return Forbid();
var statistics = await _solutionClient.GetCourseStatistics(courseId, UserId);
if (statistics == null) return Forbid();
GradeVisibility.Apply(statistics, GradeVisibility.HiddenTaskIds(course, UserId));

var studentIds = statistics.Select(t => t.StudentId).ToArray();
var getStudentsTask = AuthServiceClient.GetAccountsData(studentIds);
Expand Down Expand Up @@ -85,14 +91,18 @@ public async Task<IActionResult> GetCourseStatistics(long courseId)
}

[HttpGet("{courseId}/charts")]
[Authorize]
[ProducesResponseType(typeof(AdvancedCourseStatisticsViewModel), (int)HttpStatusCode.OK)]
public async Task<IActionResult> GetChartStatistics(long courseId)
{
var course = await _coursesClient.GetCourseById(courseId);
if (course == null)
return Forbid();
if (!course.MentorIds.Contains(UserId) && !course.AcceptedStudents.Any(s => s.StudentId == UserId)) return Forbid();

var statistics = await _solutionClient.GetCourseStatistics(courseId, UserId);
var hiddenTasks = GradeVisibility.HiddenTaskIds(course, UserId);
GradeVisibility.Apply(statistics, hiddenTasks);
var studentIds = statistics.Select(t => t.StudentId).ToArray();
var studentsData = await AuthServiceClient.GetAccountsData(studentIds);

Expand All @@ -109,6 +119,7 @@ public async Task<IActionResult> GetChartStatistics(long courseId)

var result = new AdvancedCourseStatisticsViewModel
{
HiddenTaskIds = hiddenTasks.ToArray(),
Course = new CoursePreview
{
Id = course.Id,
Expand All @@ -117,8 +128,8 @@ public async Task<IActionResult> GetChartStatistics(long courseId)
},
StudentStatistics = students.ToArray(),
Homeworks = course.Homeworks,
AverageStudentSolutions = statisticsMeasure.AverageStudentSolutions,
BestStudentSolutions = statisticsMeasure.BestStudentSolutions
AverageStudentSolutions = statisticsMeasure.AverageStudentSolutions.Where(s => !hiddenTasks.Contains(s.TaskId)).ToArray(),
BestStudentSolutions = statisticsMeasure.BestStudentSolutions.Where(s => !hiddenTasks.Contains(s.TaskId)).ToArray()
};

return Ok(result);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace HwProj.APIGateway.API.Models.Statistics;

public class AdvancedCourseStatisticsViewModel
{
public long[] HiddenTaskIds { get; set; } = System.Array.Empty<long>();
public CoursePreview Course { get; set; }
public HomeworkViewModel[] Homeworks { get; set; }
public StatisticsCourseMatesModel[] StudentStatistics { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ public class TaskDeadlineView
public TaskDeadlineDto Deadline { get; set; }
public SolutionState? SolutionState { get; set; }
public long? Rating { get; set; }
public bool IsRatingHidden { get; set; }
public bool RequiresRevision { get; set; }
public long MaxRating { get; set; }
public bool DeadlinePast { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.Collections.Generic;
using System.Linq;
using HwProj.Models.CoursesService.ViewModels;
using HwProj.Models.SolutionsService;

namespace HwProj.APIGateway.API.Services;

/// <summary>Применяется только к ответам Gateway: внутренние сервисы сохраняют точные баллы.</summary>
public static class GradeVisibility
{
public static HashSet<long> HiddenTaskIds(CourseDTO course, string? viewerId) =>
course.MentorIds.Contains(viewerId)
? new HashSet<long>()
: course.Homeworks.Where(h => h.HideRatings).SelectMany(h => h.Tasks).Select(t => t.Id).ToHashSet();

public static void Apply(SolutionDto solution, ISet<long> hiddenTaskIds)
{
if (!hiddenTaskIds.Contains(solution.TaskId)) return;
solution.Rating = null;
solution.IsRatingHidden = true;
// Final автоматически выдаётся при максимальном балле и мог бы раскрыть его.
if (solution.State == SolutionState.Final) solution.State = SolutionState.Rated;
solution.LecturerComment = RemoveCriteriaScores(solution.LecturerComment);
}

public static void Apply(IEnumerable<StudentSolutionsTableDto> statistics, ISet<long> hiddenTaskIds)
{
foreach (var student in statistics)
foreach (var homework in student.Homeworks)
foreach (var task in homework.Tasks)
foreach (var solution in task.Solutions)
Apply(solution, hiddenTaskIds);
}

public static string RemoveCriteriaScores(string comment)
{
if (string.IsNullOrEmpty(comment)) return comment;
return SolutionCommentUtils.WithoutCriterionScores(comment);
}
}
Loading