Skip to content

Commit fe1d2d1

Browse files
Использование Timetable API для получания программ и групп (#583)
* feat: use API instead of HTML parsing * feat: use async * fix: remove extra dependency * feat: use InterruptibleLazy * fixes --------- Co-authored-by: Alexey.Berezhnykh <alexey.berezhnykh@jetbrains.com>
1 parent 939559d commit fe1d2d1

5 files changed

Lines changed: 133 additions & 75 deletions

File tree

HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,9 @@ public async Task<IActionResult> GetGroups(string programName)
8989
[HttpGet("getProgramNames")]
9090
[Authorize(Roles = Roles.LecturerRole)]
9191
[ProducesResponseType(typeof(List<ProgramModel>), (int)HttpStatusCode.OK)]
92-
public IActionResult GetProgramNames()
92+
public async Task<IActionResult> GetProgramNames()
9393
{
94-
return Ok(_studentsInfo.GetProgramNames());
94+
return Ok(await _studentsInfo.GetProgramNames());
9595
}
9696

9797
[HttpPost("create")]

HwProj.StudentInfo/IStudentsInfo/IStudentsInfo.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,8 @@
44
<TargetFramework>netstandard2.0</TargetFramework>
55
</PropertyGroup>
66

7+
<ItemGroup>
8+
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
9+
</ItemGroup>
10+
711
</Project>

HwProj.StudentInfo/IStudentsInfo/IStudentsInformation.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,6 @@ public interface IStudentsInformationProvider
3535
List<StudentModel> GetStudentInformation(string groupName);
3636

3737
/// Возвращает список образовательных программ
38-
List<ProgramModel> GetProgramNames();
38+
Task<List<ProgramModel>> GetProgramNames();
3939
}
4040
}

HwProj.StudentInfo/StudentsInfo.Tests/StudentsInformationTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ public void SetUp()
2222
}
2323

2424
[Test]
25-
public void Constructor_ShouldPopulateProgramGroups()
25+
public async Task Constructor_ShouldPopulateProgramGroups()
2626
{
27-
var programNamesModels = _studentsInformation.GetProgramNames();
27+
var programNamesModels = await _studentsInformation.GetProgramNames();
2828
var programNames = programNamesModels.Select(model => model.ProgramName).ToList();
2929

3030
Assert.IsNotEmpty(programNames);
Lines changed: 124 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,73 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4-
using HtmlAgilityPack;
5-
using Novell.Directory.Ldap;
4+
using System.Net.Http;
65
using System.Threading.Tasks;
76
using IStudentsInfo;
7+
using Newtonsoft.Json;
8+
using Novell.Directory.Ldap;
9+
using System.Threading;
810

911
namespace StudentsInfo
1012
{
13+
public class InterruptibleLazy<T>
14+
{
15+
private Func<T> _valueFactory;
16+
private readonly object _lockObj = new object();
17+
private T _value;
18+
19+
public InterruptibleLazy(Func<T> valueFactory)
20+
{
21+
_valueFactory = valueFactory;
22+
}
23+
24+
public T Value
25+
{
26+
get
27+
{
28+
if (_valueFactory == null) return _value;
29+
30+
lock (_lockObj)
31+
{
32+
if (_valueFactory == null) return _value;
33+
34+
_value = _valueFactory();
35+
Interlocked.MemoryBarrier();
36+
_valueFactory = null;
37+
}
38+
39+
return _value;
40+
}
41+
}
42+
}
43+
1144
public class StudentsInformationProvider : IStudentsInformationProvider
1245
{
13-
private readonly Lazy<Dictionary<string, List<string>>> _lazyProgramsGroups;
46+
private readonly InterruptibleLazy<Task<Dictionary<string, List<string>>>> _programsGroups;
1447
private readonly string _ldapHost = "ad.pu.ru";
1548
private readonly int _ldapPort = 389;
1649
private readonly string _searchBase = "DC=ad,DC=pu,DC=ru";
50+
private readonly HttpClient _httpClient;
1751

1852
private string _username;
1953
private string _password;
20-
54+
2155
public async Task<List<GroupModel>> GetGroups(string programName)
2256
{
23-
return await Task.Run(() =>
24-
{
25-
return _lazyProgramsGroups.Value.ContainsKey(programName)
26-
? _lazyProgramsGroups.Value[programName]
27-
.Aggregate((current, next) => current + "," + next)
28-
.Split(',')
29-
.Select(group => new GroupModel { GroupName = group.Trim() })
30-
.ToList()
31-
: new List<GroupModel>();
32-
});
57+
var programsGroups = await _programsGroups.Value;
58+
if (!programsGroups.TryGetValue(programName, out var groups))
59+
return new List<GroupModel>();
60+
61+
return groups.Select(group => new GroupModel { GroupName = group }).ToList();
3362
}
34-
63+
3564
public List<StudentModel> GetStudentInformation(string groupName)
3665
{
37-
var searchFilter = $"(&(objectClass=person)(memberOf=CN=АкадемГруппа_{groupName},OU=АкадемГруппа,OU=Группы,DC=ad,DC=pu,DC=ru))";
66+
var searchFilter =
67+
$"(&(objectClass=person)(memberOf=CN=АкадемГруппа_{groupName},OU=АкадемГруппа,OU=Группы,DC=ad,DC=pu,DC=ru))";
3868
var studentsList = new List<StudentModel>();
3969
LdapConnection connection = null;
40-
70+
4171
try
4272
{
4373
connection = new LdapConnection();
@@ -48,7 +78,7 @@ public List<StudentModel> GetStudentInformation(string groupName)
4878
{
4979
return studentsList;
5080
}
51-
81+
5282
var results = connection.Search(
5383
_searchBase,
5484
LdapConnection.SCOPE_SUB,
@@ -62,7 +92,7 @@ public List<StudentModel> GetStudentInformation(string groupName)
6292
var entry = results.next();
6393
var cn = entry.getAttribute("cn")?.StringValue;
6494
var displayName = entry.getAttribute("displayName")?.StringValue;
65-
95+
6696
if (cn != null && displayName != null)
6797
{
6898
string[] splitNames = displayName.Split(' ');
@@ -81,11 +111,11 @@ public List<StudentModel> GetStudentInformation(string groupName)
81111
{
82112
return studentsList;
83113
}
84-
catch (LdapException ldapEx)
114+
catch (LdapException)
85115
{
86116
return studentsList;
87117
}
88-
catch (Exception ex)
118+
catch (Exception)
89119
{
90120
return studentsList;
91121
}
@@ -95,12 +125,11 @@ public List<StudentModel> GetStudentInformation(string groupName)
95125
{
96126
if (connection != null && connection.Connected)
97127
{
98-
SafeDisconnect(connection);
128+
SafeDisconnect(connection);
99129
}
100130
}
101-
catch (Exception ex)
131+
catch (Exception)
102132
{
103-
Console.WriteLine($"Error during disconnect: {ex.Message}");
104133
}
105134
}
106135

@@ -116,71 +145,69 @@ private void SafeDisconnect(LdapConnection connection)
116145
catch (PlatformNotSupportedException)
117146
{
118147
}
119-
catch (Exception ex)
148+
catch (Exception)
120149
{
121-
Console.WriteLine($"SafeDisconnect error: {ex.Message}");
122150
}
123151
}
124-
125-
public List<ProgramModel> GetProgramNames()
152+
153+
public async Task<List<ProgramModel>> GetProgramNames()
126154
{
127-
return _lazyProgramsGroups.Value.Keys
155+
var programGroups = await _programsGroups.Value;
156+
return programGroups.Keys
128157
.Select(key => new ProgramModel { ProgramName = key })
129158
.ToList();
130159
}
131160

132-
public StudentsInformationProvider(string username, string password, string ldapHost, int ldapPort, string searchBase)
161+
public StudentsInformationProvider(string username, string password, string ldapHost, int ldapPort,
162+
string searchBase)
133163
{
134-
this._username = username;
135-
this._password = password;
136-
this._ldapHost = ldapHost;
137-
this._ldapPort = ldapPort;
138-
this._searchBase = searchBase;
164+
_username = username;
165+
_password = password;
166+
_ldapHost = ldapHost;
167+
_ldapPort = ldapPort;
168+
_searchBase = searchBase;
169+
_httpClient = new HttpClient();
139170

140-
_lazyProgramsGroups = new Lazy<Dictionary<string, List<string>>>(() =>
171+
_programsGroups = new InterruptibleLazy<Task<Dictionary<string, List<string>>>>(async () =>
141172
{
142173
var programsGroups = new Dictionary<string, List<string>>();
143-
174+
144175
try
145176
{
146-
const string url = "https://timetable.spbu.ru/MATH?lang=ru";
147-
var web = new HtmlWeb();
148-
149-
web.PreRequest = request =>
177+
var programsResponse =
178+
await _httpClient.GetAsync(
179+
"https://timetable.spbu.ru/api/v1/study/divisions/MATH/programs/levels");
180+
if (programsResponse.IsSuccessStatusCode)
150181
{
151-
request.Headers.Add("Accept-Language", "ru");
152-
return true;
153-
};
182+
var content = await programsResponse.Content.ReadAsStringAsync();
183+
var studyLevels = JsonConvert.DeserializeObject<List<StudyLevel>>(content);
154184

155-
var doc = web.Load(url);
156-
var programNodes = doc.DocumentNode.SelectNodes("//li[contains(@class, 'common-list-item row')]");
157-
158-
foreach (var programNode in programNodes)
159-
{
160-
var programNameNode = programNode.SelectSingleNode(".//div[contains(@class, 'col-sm-5')]");
161-
var programName = programNameNode?.InnerText.Trim();
162-
163-
var titleNodes = programNode.SelectNodes(".//div[contains(@class, 'col-sm-1')]");
164-
165-
if (titleNodes != null && programName != null)
185+
foreach (var level in studyLevels)
166186
{
167-
var titles = new List<string>();
168-
foreach (var titleNode in titleNodes)
187+
foreach (var programCombination in level.StudyProgramCombinations)
169188
{
170-
var title = titleNode.SelectSingleNode(".//a")?.Attributes["title"]?.Value;
171-
if (title != null)
189+
foreach (var admissionYear in programCombination.AdmissionYears)
172190
{
173-
titles.Add(title);
174-
}
175-
}
191+
var groupsResponse = await _httpClient.GetAsync(
192+
$"https://timetable.spbu.ru/api/v1/programs/{admissionYear.StudyProgramId}/groups");
193+
if (groupsResponse.IsSuccessStatusCode)
194+
{
195+
var groupsContent = await groupsResponse.Content.ReadAsStringAsync();
196+
var programGroups = JsonConvert.DeserializeObject<ProgramGroups>(groupsContent);
176197

177-
if (programsGroups.ContainsKey(programName))
178-
{
179-
programsGroups[programName].AddRange(titles);
180-
}
181-
else
182-
{
183-
programsGroups[programName] = titles;
198+
var programName = programCombination.Name;
199+
var groups = programGroups.Groups.Select(g => g.StudentGroupName).ToList();
200+
201+
if (programsGroups.ContainsKey(programName))
202+
{
203+
programsGroups[programName].AddRange(groups);
204+
}
205+
else
206+
{
207+
programsGroups[programName] = groups;
208+
}
209+
}
210+
}
184211
}
185212
}
186213
}
@@ -193,5 +220,32 @@ public StudentsInformationProvider(string username, string password, string ldap
193220
return programsGroups;
194221
});
195222
}
223+
224+
private class StudyLevel
225+
{
226+
public string StudyLevelName { get; set; }
227+
public List<StudyProgramCombination> StudyProgramCombinations { get; set; }
228+
}
229+
230+
private class StudyProgramCombination
231+
{
232+
public string Name { get; set; }
233+
public List<AdmissionYear> AdmissionYears { get; set; }
234+
}
235+
236+
private class AdmissionYear
237+
{
238+
public int StudyProgramId { get; set; }
239+
}
240+
241+
private class ProgramGroups
242+
{
243+
public List<GroupInfo> Groups { get; set; }
244+
}
245+
246+
private class GroupInfo
247+
{
248+
public string StudentGroupName { get; set; }
249+
}
196250
}
197-
}
251+
}

0 commit comments

Comments
 (0)