source: iknow-api/Services/Implementations/AdminService.cs

Last change on this file was ea40556, checked in by Stefan-Saveski <stefansaveski19@…>, 8 days ago
  • ready for presentation
  • Property mode set to 100644
File size: 19.3 KB
Line 
1using iknow_api.Data;
2using iknow_api.DTOs;
3using iknow_api.Models;
4using Microsoft.EntityFrameworkCore;
5
6namespace iknow_api.Services
7{
8 /// <summary>
9 /// Backs UC012 (subjects), UC013 (active semesters) and UC014 (professor
10 /// assignments). Every method reports constraint problems as a sentence
11 /// rather than letting a database exception reach the controller.
12 /// </summary>
13 public class AdminService : IAdminService
14 {
15 private readonly AppDbContext _context;
16
17 public AdminService(AppDbContext context)
18 {
19 _context = context;
20 }
21
22 private static string SemesterName(ActiveSemesters s) =>
23 $"{(s.Type == sType.winter ? "Зимски" : "Летен")}({s.Year}/{s.Year + 1})";
24
25 // ============================================================
26 // UC012 - subjects
27 // ============================================================
28
29 public async Task<List<AdminSubjectDto>> GetSubjectsAsync()
30 {
31 var subjects = await _context.Subjects.AsNoTracking().ToListAsync();
32
33 var majorLinks = await _context.MajorSubjects.AsNoTracking()
34 .Include(ms => ms.Majors).ToListAsync();
35
36 var prerequisites = await _context.DependencySubjects.AsNoTracking().ToListAsync();
37
38 // A subject that students already took cannot be deleted.
39 var enrolledCounts = await _context.SemesterSubjects.AsNoTracking()
40 .GroupBy(ss => ss.SubjectId)
41 .Select(g => new { SubjectId = g.Key, Count = g.Count() })
42 .ToDictionaryAsync(x => x.SubjectId, x => x.Count);
43
44 var byId = subjects.ToDictionary(s => s.Id);
45
46 return subjects
47 .OrderBy(s => s.Code)
48 .Select(s => new AdminSubjectDto
49 {
50 Id = s.Id,
51 Name = s.Name,
52 Code = s.Code,
53 AwardedCredits = s.AwardedCredits ?? 0,
54 DependencyCredit = s.DependencyCredit,
55 EnrolledCount = enrolledCounts.TryGetValue(s.Id, out var c) ? c : 0,
56 Majors = majorLinks
57 .Where(ms => ms.SubjectId == s.Id)
58 .Select(ms => new SubjectMajorDto
59 {
60 MajorId = ms.MajorId,
61 MajorName = ms.Majors?.Name,
62 MandatorySemester = ms.MandatorySemester
63 })
64 .OrderBy(m => m.MajorName)
65 .ToList(),
66 Prerequisites = prerequisites
67 .Where(d => d.SubjectId == s.Id && byId.ContainsKey(d.DependencyId))
68 .Select(d => new SubjectRefDto
69 {
70 Id = d.DependencyId,
71 Name = byId[d.DependencyId].Name,
72 Code = byId[d.DependencyId].Code
73 })
74 .OrderBy(p => p.Code)
75 .ToList()
76 })
77 .ToList();
78 }
79
80 public async Task<AdminResultDto> CreateSubjectAsync(SubjectWriteDto request)
81 {
82 if (string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.Code))
83 {
84 return Fail("Name and code are required.");
85 }
86 if (request.AwardedCredits <= 0)
87 {
88 return Fail("Awarded credits must be greater than zero.");
89 }
90 // subjects.name and subjects.code are both UNIQUE.
91 if (await _context.Subjects.AnyAsync(s => s.Name == request.Name))
92 {
93 return Fail($"A subject named '{request.Name}' already exists.");
94 }
95 if (await _context.Subjects.AnyAsync(s => s.Code == request.Code))
96 {
97 return Fail($"Subject code '{request.Code}' is already taken.");
98 }
99
100 var subject = new Subject
101 {
102 Name = request.Name,
103 Code = request.Code,
104 AwardedCredits = request.AwardedCredits,
105 DependencyCredit = request.DependencyCredit
106 };
107 _context.Subjects.Add(subject);
108 await _context.SaveChangesAsync();
109
110 return Ok($"Subject '{subject.Name}' created.", subject.Id);
111 }
112
113 public async Task<AdminResultDto> UpdateSubjectAsync(int subjectId, SubjectWriteDto request)
114 {
115 var subject = await _context.Subjects.FirstOrDefaultAsync(s => s.Id == subjectId);
116 if (subject is null) return Fail("Subject not found.");
117
118 if (string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.Code))
119 {
120 return Fail("Name and code are required.");
121 }
122 if (request.AwardedCredits <= 0)
123 {
124 return Fail("Awarded credits must be greater than zero.");
125 }
126 if (await _context.Subjects.AnyAsync(s => s.Name == request.Name && s.Id != subjectId))
127 {
128 return Fail($"Another subject is already named '{request.Name}'.");
129 }
130 if (await _context.Subjects.AnyAsync(s => s.Code == request.Code && s.Id != subjectId))
131 {
132 return Fail($"Subject code '{request.Code}' is already taken.");
133 }
134
135 subject.Name = request.Name;
136 subject.Code = request.Code;
137 subject.AwardedCredits = request.AwardedCredits;
138 subject.DependencyCredit = request.DependencyCredit;
139 await _context.SaveChangesAsync();
140
141 return Ok($"Subject '{subject.Name}' updated.", subject.Id);
142 }
143
144 public async Task<AdminResultDto> DeleteSubjectAsync(int subjectId)
145 {
146 var subject = await _context.Subjects.FirstOrDefaultAsync(s => s.Id == subjectId);
147 if (subject is null) return Fail("Subject not found.");
148
149 // semesters_subjects references subjects, so a taken subject cannot go.
150 var taken = await _context.SemesterSubjects.CountAsync(ss => ss.SubjectId == subjectId);
151 if (taken > 0)
152 {
153 return Fail($"Cannot delete: {taken} enrolment(s) already contain this subject.");
154 }
155
156 await using var transaction = await _context.Database.BeginTransactionAsync();
157 try
158 {
159 // The mapping rows must go first; their foreign keys would block the delete.
160 var prereqs = await _context.DependencySubjects
161 .Where(d => d.SubjectId == subjectId || d.DependencyId == subjectId)
162 .ToListAsync();
163 _context.DependencySubjects.RemoveRange(prereqs);
164
165 var majors = await _context.MajorSubjects
166 .Where(ms => ms.SubjectId == subjectId).ToListAsync();
167 _context.MajorSubjects.RemoveRange(majors);
168
169 var teaching = await _context.ProfessorSubjects
170 .Where(ps => ps.SubjectId == subjectId).ToListAsync();
171 _context.ProfessorSubjects.RemoveRange(teaching);
172
173 _context.Subjects.Remove(subject);
174 await _context.SaveChangesAsync();
175 await transaction.CommitAsync();
176
177 return Ok($"Subject '{subject.Name}' deleted.");
178 }
179 catch
180 {
181 await transaction.RollbackAsync();
182 throw;
183 }
184 }
185
186 public async Task<AdminResultDto> LinkMajorAsync(int subjectId, MajorLinkDto request)
187 {
188 if (!await _context.Subjects.AnyAsync(s => s.Id == subjectId))
189 return Fail("Subject not found.");
190 if (!await _context.Majors.AnyAsync(m => m.Id == request.MajorId))
191 return Fail("Study programme not found.");
192 if (request.MandatorySemester < 1)
193 return Fail("Semester must be 1 or greater.");
194
195 var existing = await _context.MajorSubjects
196 .FirstOrDefaultAsync(ms => ms.SubjectId == subjectId && ms.MajorId == request.MajorId);
197
198 if (existing is not null)
199 {
200 existing.MandatorySemester = request.MandatorySemester;
201 await _context.SaveChangesAsync();
202 return Ok("Study programme mapping updated.");
203 }
204
205 _context.MajorSubjects.Add(new MajorSubjects
206 {
207 SubjectId = subjectId,
208 MajorId = request.MajorId,
209 MandatorySemester = request.MandatorySemester
210 });
211 await _context.SaveChangesAsync();
212 return Ok("Subject added to the study programme.");
213 }
214
215 public async Task<AdminResultDto> UnlinkMajorAsync(int subjectId, int majorId)
216 {
217 var row = await _context.MajorSubjects
218 .FirstOrDefaultAsync(ms => ms.SubjectId == subjectId && ms.MajorId == majorId);
219 if (row is null) return Fail("That subject is not in that study programme.");
220
221 _context.MajorSubjects.Remove(row);
222 await _context.SaveChangesAsync();
223 return Ok("Subject removed from the study programme.");
224 }
225
226 public async Task<AdminResultDto> AddPrerequisiteAsync(int subjectId, PrerequisiteDto request)
227 {
228 if (subjectId == request.DependencyId)
229 {
230 // Mirrors CHECK (subject_id <> dependency_id).
231 return Fail("A subject cannot be its own prerequisite.");
232 }
233 if (!await _context.Subjects.AnyAsync(s => s.Id == subjectId))
234 return Fail("Subject not found.");
235 if (!await _context.Subjects.AnyAsync(s => s.Id == request.DependencyId))
236 return Fail("Prerequisite subject not found.");
237 if (await _context.DependencySubjects.AnyAsync(
238 d => d.SubjectId == subjectId && d.DependencyId == request.DependencyId))
239 return Fail("That prerequisite is already set.");
240
241 // Refuse a direct cycle: A requires B while B already requires A.
242 if (await _context.DependencySubjects.AnyAsync(
243 d => d.SubjectId == request.DependencyId && d.DependencyId == subjectId))
244 return Fail("That would create a circular prerequisite.");
245
246 _context.DependencySubjects.Add(new DependencySubject
247 {
248 SubjectId = subjectId,
249 DependencyId = request.DependencyId
250 });
251 await _context.SaveChangesAsync();
252 return Ok("Prerequisite added.");
253 }
254
255 public async Task<AdminResultDto> RemovePrerequisiteAsync(int subjectId, int dependencyId)
256 {
257 var row = await _context.DependencySubjects
258 .FirstOrDefaultAsync(d => d.SubjectId == subjectId && d.DependencyId == dependencyId);
259 if (row is null) return Fail("That prerequisite is not set.");
260
261 _context.DependencySubjects.Remove(row);
262 await _context.SaveChangesAsync();
263 return Ok("Prerequisite removed.");
264 }
265
266 // ============================================================
267 // UC013 - active semesters
268 // ============================================================
269
270 public async Task<List<AdminSemesterDto>> GetSemestersAsync()
271 {
272 var semesters = await _context.ActiveSemesters.AsNoTracking().ToListAsync();
273 var subjects = await _context.Subjects.AsNoTracking().ToListAsync();
274 var teaching = await _context.ProfessorSubjects.AsNoTracking().ToListAsync();
275
276 var enrolments = await _context.EnrolledSemesters.AsNoTracking()
277 .GroupBy(es => es.SemesterId)
278 .Select(g => new { SemesterId = g.Key, Count = g.Count() })
279 .ToDictionaryAsync(x => x.SemesterId, x => x.Count);
280
281 return semesters
282 .OrderByDescending(s => s.Year)
283 .ThenByDescending(s => s.Type == sType.summer)
284 .Select(s =>
285 {
286 var covered = teaching
287 .Where(t => t.SemesterId == s.Id)
288 .Select(t => t.SubjectId)
289 .ToHashSet();
290
291 return new AdminSemesterDto
292 {
293 Id = s.Id,
294 Year = s.Year,
295 Type = s.Type.ToString(),
296 Name = SemesterName(s),
297 EnrolmentCount = enrolments.TryGetValue(s.Id, out var c) ? c : 0,
298 UncoveredSubjects = subjects
299 .Where(sub => !covered.Contains(sub.Id))
300 .OrderBy(sub => sub.Code)
301 .Select(sub => new SubjectRefDto { Id = sub.Id, Name = sub.Name, Code = sub.Code })
302 .ToList()
303 };
304 })
305 .ToList();
306 }
307
308 public async Task<AdminResultDto> CreateSemesterAsync(SemesterWriteDto request)
309 {
310 if (!Enum.TryParse<sType>(request.Type, ignoreCase: true, out var type))
311 {
312 return Fail("Semester type must be 'winter' or 'summer'.");
313 }
314 if (request.Year < 2000 || request.Year > 2100)
315 {
316 return Fail("Year looks wrong; expected something between 2000 and 2100.");
317 }
318 // active_semesters has UNIQUE (year, type).
319 if (await _context.ActiveSemesters.AnyAsync(a => a.Year == request.Year && a.Type == type))
320 {
321 return Fail("That semester is already open.");
322 }
323
324 var semester = new ActiveSemesters { Year = request.Year, Type = type };
325 _context.ActiveSemesters.Add(semester);
326 await _context.SaveChangesAsync();
327
328 return Ok($"Semester {SemesterName(semester)} opened.", semester.Id);
329 }
330
331 // ============================================================
332 // UC014 - professor assignments
333 // ============================================================
334
335 public async Task<ScheduleDto?> GetScheduleAsync(int semesterId)
336 {
337 var semester = await _context.ActiveSemesters.AsNoTracking()
338 .FirstOrDefaultAsync(a => a.Id == semesterId);
339 if (semester is null) return null;
340
341 var subjects = await _context.Subjects.AsNoTracking().ToListAsync();
342 var professors = await _context.User.AsNoTracking()
343 .Where(u => u.Role == Models.UserRole.Professor).ToListAsync();
344 var assignments = await _context.ProfessorSubjects.AsNoTracking()
345 .Where(ps => ps.SemesterId == semesterId).ToListAsync();
346
347 var subjectById = subjects.ToDictionary(s => s.Id);
348 var profById = professors.ToDictionary(p => p.Id);
349 var covered = assignments.Select(a => a.SubjectId).ToHashSet();
350
351 return new ScheduleDto
352 {
353 SemesterId = semester.Id,
354 SemesterName = SemesterName(semester),
355 Assignments = assignments
356 .Where(a => subjectById.ContainsKey(a.SubjectId) && profById.ContainsKey(a.ProfessorId))
357 .Select(a => new ScheduleRowDto
358 {
359 SubjectId = a.SubjectId,
360 SubjectName = subjectById[a.SubjectId].Name,
361 SubjectCode = subjectById[a.SubjectId].Code,
362 ProfessorId = a.ProfessorId,
363 ProfessorName = $"{profById[a.ProfessorId].Name} {profById[a.ProfessorId].Surname}".Trim()
364 })
365 .OrderBy(r => r.SubjectCode)
366 .ToList(),
367 Load = professors
368 .Select(p => new ProfessorLoadDto
369 {
370 ProfessorId = p.Id,
371 ProfessorName = $"{p.Name} {p.Surname}".Trim(),
372 Subjects = assignments.Count(a => a.ProfessorId == p.Id)
373 })
374 .OrderByDescending(l => l.Subjects).ThenBy(l => l.ProfessorName)
375 .ToList(),
376 UncoveredSubjects = subjects
377 .Where(s => !covered.Contains(s.Id))
378 .OrderBy(s => s.Code)
379 .Select(s => new SubjectRefDto { Id = s.Id, Name = s.Name, Code = s.Code })
380 .ToList(),
381 AllSubjects = subjects
382 .OrderBy(s => s.Code)
383 .Select(s => new SubjectRefDto { Id = s.Id, Name = s.Name, Code = s.Code })
384 .ToList(),
385 Professors = professors
386 .OrderBy(p => p.Surname)
387 .Select(p => new ProfessorRefDto { Id = p.Id, Name = $"{p.Name} {p.Surname}".Trim() })
388 .ToList()
389 };
390 }
391
392 public async Task<AdminResultDto> AssignAsync(ScheduleWriteDto request)
393 {
394 var professor = await _context.User
395 .FirstOrDefaultAsync(u => u.Id == request.ProfessorId);
396 if (professor is null) return Fail("Professor not found.");
397 if (professor.Role != Models.UserRole.Professor)
398 return Fail("That user is not a professor.");
399 if (!await _context.ActiveSemesters.AnyAsync(a => a.Id == request.SemesterId))
400 return Fail("Semester not found.");
401 if (!await _context.Subjects.AnyAsync(s => s.Id == request.SubjectId))
402 return Fail("Subject not found.");
403
404 var exists = await _context.ProfessorSubjects.AnyAsync(
405 ps => ps.ProfessorId == request.ProfessorId
406 && ps.SemesterId == request.SemesterId
407 && ps.SubjectId == request.SubjectId);
408 if (exists) return Ok("That assignment already exists.");
409
410 _context.ProfessorSubjects.Add(new ProfessorSubjects
411 {
412 ProfessorId = request.ProfessorId,
413 SemesterId = request.SemesterId,
414 SubjectId = request.SubjectId
415 });
416 await _context.SaveChangesAsync();
417 return Ok("Professor assigned to the subject.");
418 }
419
420 public async Task<AdminResultDto> UnassignAsync(ScheduleWriteDto request)
421 {
422 var row = await _context.ProfessorSubjects.FirstOrDefaultAsync(
423 ps => ps.ProfessorId == request.ProfessorId
424 && ps.SemesterId == request.SemesterId
425 && ps.SubjectId == request.SubjectId);
426 if (row is null) return Fail("That assignment does not exist.");
427
428 _context.ProfessorSubjects.Remove(row);
429 await _context.SaveChangesAsync();
430 return Ok("Assignment removed.");
431 }
432
433 public async Task<List<SubjectRefDto>> GetMajorsAsync() =>
434 await _context.Majors.AsNoTracking()
435 .OrderBy(m => m.Name)
436 .Select(m => new SubjectRefDto { Id = m.Id, Name = m.Name, Code = null })
437 .ToListAsync();
438
439 private static AdminResultDto Ok(string message, int? id = null) =>
440 new() { Ok = true, Message = message, Id = id };
441
442 private static AdminResultDto Fail(string message) =>
443 new() { Ok = false, Message = message };
444 }
445}
Note: See TracBrowser for help on using the repository browser.