public ActionResult GetSchedules() { var schedules = PaulRepository.GetSchedules(); var json = Json(schedules); return(json); }
// Configure is called after ConfigureServices is called. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { #if DEBUG // Configure the HTTP request pipeline. app.UseDeveloperExceptionPage(); #endif loggerFactory.AddConsole(); loggerFactory.AddDebug(LogLevel.Debug); // Add the following to the request pipeline only in development environment. if (env.IsDevelopment()) { app.UseBrowserLink(); } else { // Add Error handling middleware which catches all application specific errors and // send the request to the following path or controller action. //app.UseExceptionHandler("/Home/Error"); } app.UseSignalR(); app.Use(next => async context => { context.Response.Headers.Add(new KeyValuePair <string, StringValues>("Access-Control-Allow-Origin", AllowedOrigins)); await next.Invoke(context); // call the next guy // do some more stuff here as the call is unwinding }); // Add the platform handler to the request pipeline. //app.UseIISPlatformHandler(); // Add static files to the request pipeline. app.UseStaticFiles(); // Add MVC to the request pipeline. app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); // Uncomment the following line to add a route for porting Web API 2 controllers. // routes.MapWebApiRoute("DefaultApi", "api/{controller}/{id?}"); }); // Add SignalR to the request pipeline. var db = new DatabaseContext(PaulRepository.Filename, env.ContentRootPath); db.Database.Migrate(); db.Database.EnsureCreated(); //Uncomment to see SQL queries //db.LogToConsole(); PaulRepository.BasePath = env.ContentRootPath; PaulRepository.Initialize(); }
/// <summary> /// This method is used to update the courses of the course catalog in small steps (to not overwelm the server) /// </summary> /// <param name="db">Database context</param> /// <param name="courseList">List of already existing courses (from database)</param> /// <param name="c">Course catalog</param> /// <returns></returns> private async Task UpdateCoursesInDatabase(DatabaseContext db, List <Course> courseList, CourseCatalog c) { int counter = 0; int stepCount = 80; var stepCourses = courseList.Take(stepCount); var modifiableList = new List <Course>(courseList); while (stepCourses.Any()) { counter += stepCourses.Count(); //Get details for all courses await Task.WhenAll(stepCourses.Select(course => GetCourseDetailAsync(course, db, modifiableList, c))); await Task.WhenAll(stepCourses.Select(course => GetTutorialDetailAsync(course, db))); //Get details for connected courses var connectedCourses = stepCourses.SelectMany(s => s.ParsedConnectedCourses).Distinct(); await Task.WhenAll(connectedCourses.Select(course => GetCourseDetailAsync(course, db, modifiableList, c, true))); await Task.WhenAll(connectedCourses.Select(course => GetTutorialDetailAsync(course, db))); await db.SaveChangesAsync(); PaulRepository.AddLog($"Completed parsing of {counter}/{courseList.Count} courses", FatalityLevel.Normal, ""); stepCourses = courseList.Skip(counter).Take(stepCount); } }
/// <summary> /// Upadtes the category filters for a given course catalog /// </summary> /// <param name="cat">Course catalog</param> /// <param name="allCourses">List of existing courses</param> /// <param name="db">Database context</param> /// <returns></returns> private async Task UpdateCategoryFiltersForCatalog(CourseCatalog cat, List <Course> allCourses, DatabaseContext db) { var doc = await SendGetRequest(_categoryUrl); var navi = doc.GetElementbyId("pageTopNavi"); var links = navi.Descendants().Where((d) => d.Name == "a" && d.Attributes.Any(a => a.Name == "class" && a.Value.Contains("depth_2"))); var modifiedCatalogText = cat.ShortTitle.Replace("WS", "Winter").Replace("SS", "Sommer"); if (links.Any(l => l.InnerText == modifiedCatalogText)) { var url = links.First(l => l.InnerText == modifiedCatalogText).Attributes["href"].Value; doc = await SendGetRequest(BaseUrl + WebUtility.HtmlDecode(url)); var nodes = GetNodesForCategories(doc); var parentCategories = await UpdateCategoriesInDatabase(db, null, nodes, doc, true, cat, allCourses); do { foreach (var category in parentCategories.Select(e => e.Value)) { PaulRepository.AddLog($"Currently at filter {category.Title}", FatalityLevel.Verbose, ""); } var tasks = parentCategories.Keys.Select(node => UpdateCategoryForHtmlNode(db, node, parentCategories[node], cat, allCourses)).ToList(); parentCategories = (await Task.WhenAll(tasks)).SelectMany(r => r).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); await db.SaveChangesAsync(); } while (parentCategories.Keys.Any()); } }
/// <summary> /// This method is used to parse the database from a given website /// </summary> /// <param name="doc">HtmlDocument to parse</param> /// <param name="db">Database context</param> /// <returns></returns> static List <Date> GetDates(HtmlDocument doc, DatabaseContext db) { var list = new List <Date>(); var tables = doc.DocumentNode.GetDescendantsByClass("tb list rw-table rw-all"); var table = tables.FirstOrDefault(t => t.ChildNodes.Any(n => n.InnerText == "Termine")); if (table == null) { return(list); } var trs = table.ChildNodes.Where(n => n.Name == "tr").Skip(1); if (!table.InnerHtml.Contains("Es liegen keine Termine vor")) { foreach (var tr in trs) { if (!tr.GetDescendantsByName("appointmentDate").First().InnerText.Contains('*')) { //Umlaute werden falsch geparst, deshalb werden Umlaute ersetzt var date = DateTimeOffset.Parse(tr.GetDescendantsByName("appointmentDate").First().InnerText.Replace("Mär", "Mar"), new CultureInfo("de-DE")); if (_timezone != null) { var tzOffset = _timezone.GetUtcOffset(date.DateTime); date = new DateTimeOffset(date.DateTime, tzOffset); } else { PaulRepository.AddLog("Timezone not present", FatalityLevel.Critical, ""); } var fromNode = tr.GetDescendantsByName("appointmentTimeFrom").First(); var toNode = tr.GetDescendantsByName("appointmentDateTo").First(); var from = date.Add(TimeSpan.Parse(fromNode.InnerText)); DateTimeOffset to; if (toNode.InnerText.Trim() != "24:00") { to = date.Add(TimeSpan.Parse(toNode.InnerText)); } else { to = date.Add(new TimeSpan(23, 59, 59)); } var room = tr.GetDescendantsByName("appointmentRooms").FirstOrDefault()?.InnerText; if (room == null) { room = tr.Descendants("td").Skip(4).FirstOrDefault()?.InnerText.TrimWhitespace(); } var instructor = tr.GetDescendantsByName("appointmentInstructors").First().InnerText.TrimWhitespace(); list.Add(new Date { From = from, To = to, Room = room, Instructor = instructor }); } } } return(list); }
/// <summary> /// Updates all courses in the database for a given course catalog /// </summary> /// <param name="catalog">Catalog for which the courses should be updated</param> /// <param name="allCourses">List of courses that have already been parsed (from database)</param> /// <param name="db">Database context</param> public async Task UpdateCoursesInCourseCatalog(CourseCatalog catalog, List <Course> allCourses, DatabaseContext db) { var counter = 1; try { PaulRepository.AddLog($"Update for {catalog.ShortTitle} started!", FatalityLevel.Normal, ""); var courseList = allCourses.Where(co => co.Catalogue.InternalID == catalog.InternalID && !co.IsTutorial).ToList(); //ensure that every course has the right instance of the course catalog so that we don't get a tracking exception courseList.ForEach(course => course.Catalogue = catalog); var messages = await Task.WhenAll(new[] { "1", "2" }.Select(useLogo => SendPostRequest(catalog.InternalID, "", useLogo))); foreach (var message in messages) { var document = new HtmlDocument(); document.Load(await message.Content.ReadAsStreamAsync()); var pageResult = GetPageSearchResult(document, counter); if (pageResult.HasCourses) { await GetCourseList(db, document, catalog, courseList, updateUrls : true); } while (pageResult.LinksToNextPages.Count > 0) { var docs = await Task.WhenAll(pageResult.LinksToNextPages.Select(s => SendGetRequest(BaseUrl + s))); //Getting course list for at most 3 pages var courses = await Task.WhenAll(docs.Select(d => GetCourseList(db, d, catalog, courseList, updateUrls: true))); counter += pageResult.LinksToNextPages.Count; pageResult = GetPageSearchResult(docs.Last(), counter); } } await UpdateCoursesInDatabase(db, courseList, catalog); PaulRepository.AddLog($"Update for {catalog.ShortTitle} completed!", FatalityLevel.Normal, ""); } catch (DbUpdateConcurrencyException e) { //db.ChangeTracker.Entries().First(entry => entry.Equals(e)).State == EntityState.Detached; var str = new StringBuilder(); foreach (var entry in e.Entries) { str.AppendLine("Entry involved: " + entry.Entity + " Type: " + entry.Entity.GetType().Name); } PaulRepository.AddLog($"DbUpdateConcurrency failure: {e} in {catalog.Title} at round {counter}", FatalityLevel.Critical, "Nightly Update"); PaulRepository.AddLog($"DbUpdateConcurrency failure: {str} in {catalog.Title}", FatalityLevel.Critical, "Nightly Update"); } catch (Exception e) { PaulRepository.AddLog("Update failure: " + e + " in " + catalog.Title, FatalityLevel.Critical, "Nightly Update"); } }
public async Task <ActionResult> ExportShortCatalogueTitles(string token) { if (PaulRepository.IsUpdating) { return(StatusCode(503)); // Service unavailable } var catalogues = await PaulRepository.GetCourseCataloguesAsync(); return(Json(catalogues.Select(c => c.ShortTitle))); }
/// <summary> /// RPC-method for optaining metadata, such as the course catalog, /// from schedules specified by their IDs. /// </summary> /// <param name="scheduleIds">Schedule IDs</param> public object[] GetScheduleMetadata(string[] scheduleIds) { var metadata = PaulRepository.GetSchedules(scheduleIds) .Select(schedule => new { Id = schedule.Id, Title = schedule.Name, Users = string.Join(", ", schedule.Users.Select(user => user.Name)) }); return(metadata.ToArray()); }
public ActionResult ExportSchedule(string id, string username) { var name = username.FromBase64String(); var schedule = PaulRepository.GetSchedule(id); if (schedule != null && schedule.Users.Any(u => u.Name == name)) { return(File(Encoding.UTF8.GetBytes(ScheduleExporter.ExportSchedule(schedule, name)), "text/calendar", $"schedule{schedule.Id}_{name}.ics")); } else { return(BadRequest()); } }
public async Task <ActionResult> UpdateAllCourses() { try { await PaulRepository.UpdateAllCoursesAsync(); } catch (Exception e) { try { PaulRepository.AddLog(e.ToString(), FatalityLevel.Critical, "Manual Update"); } catch { } } return(Ok()); }
private async Task <ActionResult> TestDatabaseStoring() { //var courses = PaulRepository.GetLocalCourses("Stochastik"); //Schedule s = new Schedule(); //var user = new User() { Name = "Test" }; //s.User.Add(user); //await PaulRepository.StoreInDatabaseAsync(s, Microsoft.Data.Entity.GraphBehavior.IncludeDependents); //courses.ForEach((async c => await PaulRepository.AddCourseToSchedule(s, c.Id, new List<int>() { user.Id }))); var s = PaulRepository.GetSchedules().Last(); await PaulRepository.RemoveScheduleAsync(s); //var selectedCourse = s.SelectedCourses.First(); //var user = selectedCourse.Users.First(); //await PaulRepository.RemoveUserFromSelectedCourseAsync(selectedCourse, user); //await PaulRepository.AddUserToSelectedCourseAsync(selectedCourse, user.User); return(Ok()); }
/// <summary> /// This method checks PAUL for the course catalogs that are available. It parses PAUL's search website and considers the entries in the dropdown that contain the name "Vorlesungsverzeichnis" /// </summary> /// <returns>List of course catalogs</returns> public async Task <IEnumerable <CourseCatalog> > GetAvailableCourseCatalogs() { var doc = new HtmlDocument(); doc.Load(await _client.GetStreamAsync(_searchUrl), Encoding.UTF8); var catalogue = doc.GetElementbyId("course_catalogue"); if (catalogue == null) { PaulRepository.AddLog("No course catalogs could be found! Maybe the search url has changed?", FatalityLevel.Critical, ""); throw new ArgumentException("No course catalogs could be found in PAUL!", nameof(_searchUrl)); } var options = catalogue.Descendants().Where(c => c.Name == "option" && c.Attributes.Any(a => a.Name == "title" && a.Value.Contains("Vorlesungsverzeichnis"))); return(options.Select(n => new CourseCatalog { InternalID = n.Attributes["value"].Value, Title = n.Attributes["title"].Value })); }
public void UpdateSearchResults(ErrorReporter errorReporter) { if (SearchQuery == null || SearchQuery.Count() < 3) { SearchResults.Clear(); errorReporter.Throw( new InvalidOperationException("Search query is null or too short"), UserErrorsViewModel.SearchQueryTooShortMessage); } var results = PaulRepository.SearchCourses(SearchQuery, _catalog).Take(searchResultCount); SearchResults.Clear(); SearchResults.AddRange(results.Select(course => { var added = _schedule.SelectedCourses.Any(s => s.CourseId == course.Id); return(new SearchResultViewModel(course, added)); })); }
/// <summary> /// Adds the specified user to the list of current users. /// The user name can either be a known user name (i.e. a user /// that has already joined the schedule sometime before) or /// a new user name (in this case the user's info is added to the DB). /// </summary> /// <param name="userVM"></param> public async Task AddUserAsync(UserViewModel userVM, ErrorReporter errorReporter) { if (userVM == null) { errorReporter.Throw( new ArgumentNullException(nameof(userVM), UserErrorsViewModel.GenericErrorMessage)); } if (string.IsNullOrWhiteSpace(userVM.Name)) { errorReporter.Throw( new ArgumentException("The name of the specified user is invalid", nameof(userVM)), UserErrorsViewModel.UserNameInvalidMessage); } if (Users.Any(o => o.Name == userVM.Name)) { errorReporter.Throw( new ArgumentException($"The user name '{userVM.Name}' is already in use", nameof(userVM.Name)), UserErrorsViewModel.UserNameAlreadyInUseMessage); } if (Schedule.Users.Any(o => o.Name == userVM.Name)) { // This is a known user name Users.Add(userVM); AvailableUserNames.Remove(userVM.Name); } else { // The client is a new user Users.Add(userVM); // Create new known user in DB var dbUser = new User { Name = userVM.Name }; await PaulRepository.AddUserToScheduleAsync(Schedule, dbUser); } }
public async Task <ActionResult> TestParsing() { var searchString = "L.104.12270"; var course = PaulRepository.Courses.FirstOrDefault(c => c.Id.Contains(searchString)); var parser = new PaulParser(); var courseCatalog = (await PaulRepository.GetCourseCataloguesAsync()).First(); var httpClient = new HttpClient(); var response = await httpClient.GetAsync("https://paul.uni-paderborn.de/scripts/mgrqispi.dll?APPNAME=CampusNet&PRGNAME=COURSEDETAILS&ARGUMENTS=-N000000000000001,-N000443,-N0,-N360765878897321,-N360765878845322,-N0,-N0,-N3,-A4150504E414D453D43616D7075734E6574265052474E414D453D414354494F4E26415247554D454E54533D2D4179675978627166464D546570353271395952533363394A33415A7A346450656F347A72514F7661686C327A34706559594179354333386A6C636975396B71334456666E492D4B6E6872493545326F45672E74437349727130616D55426B4B37627573455048356D4351544F42326B4759696B507333596C316E7555742E6E3D3D"); var doc = new HtmlDocument(); using (var db = new DatabaseContext(PaulRepository.Filename, "")) { db.Attach(courseCatalog); doc.LoadHtml(await response.Content.ReadAsStringAsync()); await parser.UpdateExamDates(doc, db, course); await db.SaveChangesAsync(); } return(Ok()); }
/// <summary> /// Used to update all category filters for every course catalog /// </summary> /// <param name="allCourses">List of existing courses</param> /// <param name="context">Database context</param> /// <returns></returns> public async Task UpdateCategoryFilters(List <Course> allCourses, DatabaseContext context) { PaulRepository.AddLog("Update for category filters has started!", FatalityLevel.Normal, "Update category filters"); var catalogues = (await PaulRepository.GetCourseCataloguesAsync(context)); foreach (var cat in catalogues) { try { PaulRepository.AddLog($"Update for course catalog {cat.ShortTitle} has started!", FatalityLevel.Normal, ""); await UpdateCategoryFiltersForCatalog(cat, allCourses, context); PaulRepository.AddLog($"Update for course catalog {cat.ShortTitle} completed!", FatalityLevel.Normal, ""); } catch (Exception e) { PaulRepository.AddLog($"Updating Categories failed: {e}", FatalityLevel.Critical, "Nightly Update"); } } PaulRepository.AddLog("Update for category filters completed!", FatalityLevel.Normal, "Update category filters"); }
/// <summary> /// Creates a new empty schedule in the database. /// </summary> /// <returns>A ViewModel that represents the new schedule</returns> public async Task <SharedScheduleViewModel> CreateScheduleAsync(string catalogId, ErrorReporter errorReporter) { var catalogs = await PaulRepository.GetCourseCataloguesAsync(); var selectedCatalog = catalogs.FirstOrDefault(o => o.InternalID == catalogId); if (selectedCatalog == null) { errorReporter.Throw( new ArgumentException($"A CourseCatalog with the specified ID '{catalogId}' does not exist"), UserErrorsViewModel.GenericErrorMessage); } // Create a new schedule in DB var schedule = await PaulRepository.CreateNewScheduleAsync(selectedCatalog); var vm = new SharedScheduleViewModel(schedule); _loadedSchedules.Add(schedule.Id, vm); return(vm); }
/// <summary> /// Loads the schedule with the specified ID either from /// cache or from the database. /// </summary> /// <param name="scheduleId">Schedule ID</param> /// <returns> /// A ViewModel representing the schedule or null if no /// schedule with the specified ID exists /// </returns> public SharedScheduleViewModel GetOrLoadSchedule(string scheduleId) { SharedScheduleViewModel scheduleVm; if (_loadedSchedules.TryGetValue(scheduleId, out scheduleVm)) { return(scheduleVm); } else { var schedule = PaulRepository.GetSchedule(scheduleId); if (schedule == null) { // Schedule with that ID does not exist return(null); } var vm = new SharedScheduleViewModel(schedule); _loadedSchedules.Add(scheduleId, vm); return(vm); } }
public async Task RefreshAvailableSemestersAsync() { AvailableSemesters = (await PaulRepository.GetCourseCataloguesAsync()). Where(c => PaulRepository.Courses.Any(course => course.Catalogue.Equals(c))) .OrderByDescending(catalog => GetCourseCatalogOrder(catalog)); }
/// <summary> /// RPC-method for removing a course from the schedule /// </summary> /// <param name="courseId"></param> /// <returns></returns> public async Task RemoveCourse(string courseId) { using (var errorReporter = new ErrorReporter(s => CallingClient.Errors.ScheduleMessage = s)) { var course = PaulRepository.Courses.FirstOrDefault(c => c.Id == courseId); if (course == null) { errorReporter.Throw( new ArgumentException("Course not found", nameof(courseId)), UserErrorsViewModel.GenericErrorMessage); } var schedule = CallingClient.SharedScheduleVM.Schedule; await CallingClient.SharedScheduleVM.TimetableHubSemaphore.WaitAsync(); try { var selectedCourse = schedule.SelectedCourses.FirstOrDefault(c => c.CourseId == courseId); if (selectedCourse != null) { //Find selected Tutorials var selectedTutorials = schedule.SelectedCourses .Where(sel => selectedCourse.Course.Tutorials.Any(it => it.Id == sel.CourseId)) .Select(s => s.Course) .ToList(); var courses = selectedCourse.Course.ConnectedCourses .Concat(selectedTutorials) .Concat(new[] { selectedCourse.Course }); foreach (var course1 in courses) { try { if (course1.Tutorials.Any()) { // Remove all pending tutorials from all TailoredSchedules foreach (var user in CallingClient.SharedScheduleVM.Users) { user.TailoredScheduleVM.RemovePendingTutorials(course1.Tutorials.FirstOrDefault(), errorReporter); } } await PaulRepository.RemoveCourseFromScheduleAsync(schedule, course1.Id); UpdateAddedStateInSearchResultsAndCourseList(course1, isAdded: false); } catch (NullReferenceException e) { // This is just for purposes of compatibility // with development versions. Can be safely removed // after product launch PaulRepository.AddLog(e.Message, FatalityLevel.Normal, typeof(TimetableHub).Name); } } UpdateTailoredViewModels(); } else if (course.IsTutorial) { // The user has decided to remove a tutorial before joining one CallingClient.TailoredScheduleVM.RemovePendingTutorials(course, errorReporter); UpdateTailoredViewModels(); } else { errorReporter.Throw( new ArgumentException("Course not found in the schedule!"), UserErrorsViewModel.GenericErrorMessage); } } finally { CallingClient.SharedScheduleVM.TimetableHubSemaphore.Release(); } } }
/// <summary> /// RPC-method for adding a course to the schedule and for /// adding the calling user to a course that someone else /// has already added. /// </summary> /// <param name="courseId">Course ID</param> /// <returns></returns> public async Task AddCourse(string courseId) { using (var errorReporter = new ErrorReporter(s => CallingClient.Errors.ScheduleMessage = s)) { var course = PaulRepository.Courses.FirstOrDefault(c => c.Id == courseId); if (course == null) { errorReporter.Throw( new ArgumentException("Course not found", nameof(courseId)), UserErrorsViewModel.GenericErrorMessage); } var schedule = CallingClient.SharedScheduleVM.Schedule; await CallingClient.SharedScheduleVM.TimetableHubSemaphore.WaitAsync(); try { var selectedCourse = schedule.SelectedCourses.FirstOrDefault(c => c.CourseId == courseId); if (course.IsTutorial) { // The user has decided to select a pending tutorial or one // that was already selected by another user, so remove all pending // tutorials of this course CallingClient.TailoredScheduleVM.RemovePendingTutorials(course, errorReporter); // Remove user from other possibly selected tutorial var parentCourse = schedule.SelectedCourses.First(sel => sel.Course.AllTutorials.SelectMany(it => it).Contains(course)); var otherSelectedTutorial = parentCourse.Course.AllTutorials .FirstOrDefault(group => group.Contains(course)) ?.FirstOrDefault(tut => schedule.SelectedCourses.Any(sel => Equals(tut, sel.Course) && sel.Users.Select(it => it.User).Contains(CallingClient.User))); if (otherSelectedTutorial != null) { await RemoveUserFromCourse(otherSelectedTutorial.Id, acquireSemaphore : false); } //If the user hasn't selected the parent course of the tutorial, it will be added here if (parentCourse.Users.All(u => u.User.Name != CallingClient.Name)) { var connectedCourses = parentCourse.Course.ConnectedCourses.Concat(new[] { parentCourse.Course }); foreach (var c in connectedCourses) { await PaulRepository.AddUserToSelectedCourseAsync(schedule.SelectedCourses.First(s => s.CourseId == c.Id), CallingClient.User); } } } if (selectedCourse == null) { var connectedCourses = course.ConnectedCourses.Concat(new[] { course }) .Select(it => PaulRepository.CreateSelectedCourse(schedule, CallingClient.User, it)) .ToList(); await PaulRepository.AddCourseToScheduleAsync(schedule, connectedCourses); AddTutorialsForCourse(courseId); } else if (selectedCourse.Users.All(u => u.User != CallingClient.User)) { // The course has already been added to the schedule by someone else. // Add the calling user to the selected course (if not yet done). await PaulRepository.AddUserToSelectedCourseAsync(selectedCourse, CallingClient.User); var connectedCourseIds = selectedCourse.Course.ConnectedCourses.Select(it => it.Id).ToList(); var selectedConnectedCourses = schedule.SelectedCourses .Where(selCo => connectedCourseIds.Contains(selCo.CourseId)); foreach (var connectedCourse in selectedConnectedCourses) { await PaulRepository.AddUserToSelectedCourseAsync(connectedCourse, CallingClient.User); } } UpdateAddedStateInSearchResultsAndCourseList(course, isAdded: true); } finally { CallingClient.SharedScheduleVM.TimetableHubSemaphore.Release(); } UpdateTailoredViewModels(); } }
public async Task ChangeScheduleName(string name) { await PaulRepository.ChangeScheduleName(Schedule, name); RaisePropertyChanged(nameof(Name)); }
/// <summary> /// Removes the calling user from the specified selected course. /// If after the removal no other user has selected the course, /// the course is removed from the schedule. /// </summary> /// <remarks> /// If the user has not selected the course with the specified ID, /// nothing happens. /// </remarks> /// <param name="courseId">Course ID</param> /// <returns></returns> public async Task RemoveUserFromCourse(string courseId, bool acquireSemaphore = true) { using (var errorReporter = new ErrorReporter(s => CallingClient.Errors.ScheduleMessage = s)) { if (PaulRepository.Courses.All(c => c.Id != courseId)) { errorReporter.Throw( new ArgumentException("Course not found", nameof(courseId)), UserErrorsViewModel.GenericErrorMessage); } var schedule = CallingClient.SharedScheduleVM.Schedule; var selectedCourse = schedule.SelectedCourses .FirstOrDefault(c => c.CourseId == courseId); if (selectedCourse == null) { errorReporter.Throw( new ArgumentException("Course not found in the schedule!"), UserErrorsViewModel.GenericErrorMessage); } var selectedConnectedCourses = schedule.SelectedCourses .Where(sel => selectedCourse.Course.ConnectedCourses.Any(it => it.Id == sel.CourseId)) .ToList(); if (acquireSemaphore) { await CallingClient.SharedScheduleVM.TimetableHubSemaphore.WaitAsync(); } try { var selectedCourseUser = selectedCourse.Users.FirstOrDefault(o => o.User == CallingClient.User); //Find selected Tutorials var selectedTutorials = schedule.SelectedCourses .Where(sel => selectedCourse.Course.Tutorials .Concat(selectedConnectedCourses.SelectMany(s => s.Course.Tutorials)) .Any(it => it.Id == sel.CourseId)) .ToList(); if (selectedCourseUser != null) { // Remove user from selected courses foreach (var sel in selectedConnectedCourses.Concat(selectedTutorials).Concat(new[] { selectedCourse })) { await PaulRepository.RemoveUserFromSelectedCourseAsync(sel, selectedCourseUser); } } if (!selectedCourse.Users.Any()) { var firstTutorials = selectedCourse.Course.Tutorials.Take(1) .Concat(selectedConnectedCourses.SelectMany(s => s.Course.Tutorials.Take(1))); // Remove all Pending Tutorials from all TailoredSchedules foreach (var user in CallingClient.SharedScheduleVM.Users) { foreach (var t in firstTutorials) { user.TailoredScheduleVM.RemovePendingTutorials(t, errorReporter); } } // The course is no longer selected by anyone // -> Remove the whole course from schedule foreach (var sel in selectedConnectedCourses.Concat(selectedTutorials).Concat(new[] { selectedCourse })) { await PaulRepository.RemoveCourseFromScheduleAsync(schedule, sel.CourseId); } } UpdateAddedStateInSearchResultsAndCourseList(selectedCourse.Course, isAdded: false); UpdateTailoredViewModels(); } finally { if (acquireSemaphore) { CallingClient.SharedScheduleVM.TimetableHubSemaphore.Release(); } } } }
/// <summary> /// This method updates the (more detailed) properties of a given course such as dates, connected courses, description etc. /// </summary> /// <param name="course">Course for which the information should be updated</param> /// <param name="db">Database context</param> /// <param name="list">List of existing courses</param> /// <param name="catalog">Course catalog</param> /// <param name="isConnectedCourse">Determines if the current parsing happens for a connected course (is used to prevent cirular parsing)</param> /// <returns></returns> public async Task GetCourseDetailAsync(Course course, DatabaseContext db, List <Course> list, CourseCatalog catalog, bool isConnectedCourse = false) { HtmlDocument doc = await GetHtmlDocumentForCourse(course, db); if (doc == null) { return; } var changed = false; //case of isConnectedCourse is set to false (on PAUL website) is not handled if (isConnectedCourse) { if (course.ParsedConnectedCourses.All(c => c.Name.Length > course.Name.Length)) { var valueBefore = course.IsConnectedCourse; course.IsConnectedCourse = false; if (course.IsConnectedCourse != valueBefore) { changed = true; } } else if (course.IsConnectedCourse != isConnectedCourse) { course.IsConnectedCourse = isConnectedCourse; changed = true; } } //Update InternalID if not set before (migration code) if (course.InternalCourseID == null) { course.InternalCourseID = course.Id.Split(',')[1]; changed = true; } //Get Shortname var descr = doc.DocumentNode.GetDescendantsByName("shortdescription").FirstOrDefault(); if (descr != null && course.ShortName != descr.Attributes["value"].Value) { course.ShortName = descr.Attributes["value"].Value; changed = true; } try { //Termine parsen var dates = GetDates(doc, db).ToList(); await UpdateDatesInDatabase(course, dates, db); await UpdateExamDates(doc, db, course); } catch { //if the updating of dates fails, not the whole update should crash PaulRepository.AddLog($"Date parsing failed for course {course.CourseId}", FatalityLevel.Error, "Date parsing"); } //Verbundene Veranstaltungen parsen var divs = doc.DocumentNode.GetDescendantsByClass("dl-ul-listview"); var courses = divs.FirstOrDefault(l => l.InnerHtml.Contains("Veranstaltung anzeigen"))?.ChildNodes.Where(l => l.Name == "li" && l.InnerHtml.Contains("Veranstaltung anzeigen")); if (courses != null) { foreach (var c in courses) { var text = c.Descendants().First(n => n.Name == "strong")?.InnerText; var name = text.Split(new[] { ' ' }, 2)[1]; var id = text.Split(new[] { ' ' }, 2)[0]; var url = c.Descendants().First(n => n.Name == "a")?.Attributes["href"].Value; var docent = c.Descendants().Where(n => n.Name == "p").Skip(2).First().InnerText; await _writeLock.WaitAsync(); Course c2 = list.FirstOrDefault(co => co.Id == $"{course.Catalogue.InternalID},{id}"); if (c2 == null) { c2 = new Course { Name = name, TrimmedUrl = url, Catalogue = course.Catalogue, Id = $"{course.Catalogue.InternalID},{id}" }; //db.Courses.Add(c2); db.Entry(c2).State = EntityState.Added; list.Add(c2); } //prevent that two separate threads add the connected courses if (course.Id != c2.Id && !course.ParsedConnectedCourses.Any(co => co.Id == c2.Id) && !c2.ParsedConnectedCourses.Any(co => co.Id == course.Id)) { var con1 = new ConnectedCourse { CourseId = course.Id, CourseId2 = c2.Id }; course.ParsedConnectedCourses.Add(c2); db.ConnectedCourses.Add(con1); var con2 = new ConnectedCourse { CourseId = c2.Id, CourseId2 = course.Id }; c2.ParsedConnectedCourses.Add(course); db.ConnectedCourses.Add(con2); } _writeLock.Release(); } } //Gruppen parsen var groups = divs.FirstOrDefault(l => l.InnerHtml.Contains("Kleingruppe anzeigen"))?.ChildNodes.Where(l => l.Name == "li"); if (groups != null) { var parsedTutorials = groups.Select(group => { var name = group.Descendants().First(n => n.Name == "strong")?.InnerText; var url = group.Descendants().First(n => n.Name == "a")?.Attributes["href"].Value; return(new Course { Id = course.Id + $",{name}", Name = name, TrimmedUrl = url, CourseId = course.Id, IsTutorial = true, Catalogue = catalog }); }); foreach (var parsedTutorial in parsedTutorials) { var tutorial = course.ParsedTutorials.FirstOrDefault(t => t == parsedTutorial); if (tutorial != null) { tutorial.NewUrl = parsedTutorial.TrimmedUrl; } } var newTutorials = parsedTutorials.Except(course.ParsedTutorials).ToList(); if (newTutorials.Any()) { await _writeLock.WaitAsync(); //db.Courses.AddRange(newTutorials); foreach (var t in newTutorials) { var entry = db.Entry(t); if (entry.State != EntityState.Added) { entry.State = EntityState.Added; } } course.ParsedTutorials.AddRange(newTutorials); _writeLock.Release(); } var oldTutorials = course.ParsedTutorials.Except(parsedTutorials).ToList(); if (oldTutorials.Any() && parsedTutorials.Any()) { await _writeLock.WaitAsync(); await db.Database.ExecuteSqlCommandAsync($"DELETE FROM Date Where CourseId IN ({string.Join(",", oldTutorials.Select(o => "'" + o.Id + "'"))})"); var selectedCourses = db.SelectedCourses.Where(p => oldTutorials.Any(o => o.Id == p.CourseId)).Include(s => s.Users).ThenInclude(u => u.User).ToList(); foreach (var selectedCourseUser in selectedCourses.SelectMany(s => s.Users)) { await db.Database.ExecuteSqlCommandAsync($"DELETE FROM SelectedCourseUser Where UserId IN ({selectedCourseUser.User.Id}) And SelectedCourseId IN ({string.Join(",", selectedCourses.Select(s => "'" + s.Id + "'"))}) "); } await db.Database.ExecuteSqlCommandAsync($"DELETE FROM SelectedCourse Where CourseId IN ({string.Join(",", oldTutorials.Select(o => "'" + o.Id + "'"))})"); await db.Database.ExecuteSqlCommandAsync($"DELETE FROM Course Where Id IN ({string.Join(",", oldTutorials.Select(o => "'" + o.Id + "'"))})"); foreach (var old in oldTutorials) { course.ParsedTutorials.Remove(old); } _writeLock.Release(); } } //mark course as modified if (changed) { await _writeLock.WaitAsync(); db.ChangeTracker.TrackObject(course); _writeLock.Release(); } }
/// <summary> /// Update the exam dates in the database for a given course /// </summary> /// <param name="doc">HtmlDocment used for parsing</param> /// <param name="db">Database context</param> /// <param name="course">Relevant course</param> /// <returns></returns> public async Task UpdateExamDates(HtmlDocument doc, DatabaseContext db, Course course) { try { var list = new List <ExamDate>(); var node = doc.GetElementbyId("contentlayoutleft"); var tables = node.ChildNodes.Where(n => n.Name == "table"); if (tables.Count() >= 5) { var table = tables.ElementAt(4); var trs = table.ChildNodes.Where(n => n.Name == "tr").Skip(1); foreach (var tr in trs) { var dateString = tr.GetDescendantsByName("examDateTime").FirstOrDefault(); if (dateString != null) { var name = tr.GetDescendantsByName("examName").First().InnerText.TrimWhitespace(); var lastIndex = dateString.InnerText.LastIndexOf(' '); var date = DateTimeOffset.Parse(dateString.InnerText.Substring(0, lastIndex).Replace("Mär", "Mar"), new CultureInfo("de-DE")); if (_timezone != null) { var tzOffset = _timezone.GetUtcOffset(date.DateTime); date = new DateTimeOffset(date.DateTime, tzOffset); } else { PaulRepository.AddLog("Timezone not present", FatalityLevel.Critical, ""); } var time = dateString.InnerText.Substring(lastIndex, dateString.InnerText.Length - lastIndex); var from = date.Add(TimeSpan.Parse(time.Split('-')[0])); var toString = time.Split('-')[1]; DateTimeOffset to; if (toString.Trim() != "24:00") { to = date.Add(TimeSpan.Parse(toString)); } else { to = date.Add(new TimeSpan(23, 59, 59)); } var instructor = tr.GetDescendantsByClass("tbdata")[3].InnerText.TrimWhitespace(); list.Add(new ExamDate { From = from, To = to, Description = name, Instructor = instructor }); } } await _writeLock.WaitAsync(); var difference = list.Except(course.ExamDates).ToList(); var old = course.ExamDates.Except(list).ToList(); if (difference.Any() && list.Any()) { difference.ForEach(d => d.CourseId = course.Id); foreach (var d in difference) { db.Entry(d).State = EntityState.Added; } } if (old.Any() && list.Any()) { await db.Database.ExecuteSqlCommandAsync($"Delete from ExamDate Where Id IN ({string.Join(",", old.Select(d => d.Id))})"); } _writeLock.Release(); } } catch (Exception) { } }
// GET: api/values public async Task <ActionResult> GetCourseCatalogues() { return(Json(await PaulRepository.GetCourseCataloguesAsync())); }
public ActionResult ClearLogs() { PaulRepository.ClearLogs(); return(Ok()); }
public IActionResult GetLogs() => View("~/Views/Home/Logs.cshtml", PaulRepository.GetLogs());