= Финална имплементација на случаи на употреба = На следната табела се прикажани финалните кориснички сценарија: || '''ID''' || '''Use Case Scenario''' || '''Actor''' || || 14 || Најава преку OAuth2 (Google, Microsoft, GitHub) || Корисник || || 15 || Поставување на предлог теми за проект || Ментор || || 16 || Доделување теми на студент || Ментор || || 17 || Контакт со ментор (променет) || Студент || И дополнително се имплементирани E-mail нотификации, поврзани со inbox на самиот сајт. ---- = Корисник = == Најава преку OAuth2 == Кога прв пат ќе ја отвори страната, корисникот е редиректиран кон страната за најава. Долу, има дополнителни три копчиња за најава со Google, Microsoft и GitHub. [[Image(најава.png)]] Да го земеме како пример Google. Корисникот ја одбира опцијата за најава со Google и е редиректиран кон најавата поставена од Google: [[Image(најава2.png)]] Откако ќе го одбере својот Google профил и успешно помине најавата, тогаш корисникот може да продолжи со регистрација. Тука се пополуваат полињата како име, презиме и мејл: [[Image(најава3.png)]] Сега што останува е корисникот да пополни биографија, предмети, интереси, доколку е студент тогаш индекс, година, семестар, итн. Но тие се опционални, и регистрацијата преку Google е завршена. Сега, секогаш кога корисникот сака да се најави на Најди Ментор, може преку својот Google профил. {{{ var googleId = System.Configuration.ConfigurationManager.AppSettings["GoogleClientId"]; var googleSecret = System.Configuration.ConfigurationManager.AppSettings["GoogleClientSecret"]; if (!string.IsNullOrWhiteSpace(googleId) && !string.IsNullOrWhiteSpace(googleSecret)) { app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions { ClientId = googleId, ClientSecret = googleSecret, CallbackPath = new PathString("/signin-google"), CookieManager = cookieManager }); } }}} {{{ // POST: /Account/ExternalLogin [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public ActionResult ExternalLogin(string provider, string returnUrl) { // Request a redirect to the external login provider var redirectUrl = Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl }); return new ChallengeResult(provider, redirectUrl); } // POST: /Account/ExternalLoginConfirmation [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task ExternalLoginConfirmation(WebApplication1.Models.ExternalLoginConfirmationViewModel model, string returnUrl) { if (User.Identity.IsAuthenticated) { return RedirectToAction("Index", "Manage"); } if (!ModelState.IsValid) { return View(model); } // Obtain external login info again var info = await AuthenticationManager.GetExternalLoginInfoAsync(); if (info == null) { ModelState.AddModelError("", "Не можам да ги вчитам информации за надворешната најава."); return View(model); } ApplicationUser user; if (model.UserType == "Student") { user = new Student { UserName = model.Email, Email = model.Email, Name = model.Name, Surname = model.Surname, Biography = model.Biography, Index = model.Index, Major = model.Major, Cycle = model.Cycle, Semester = model.Semester }; } else // Mentor { user = new Mentor { UserName = model.Email, Email = model.Email, Name = model.Name, Surname = model.Surname, Biography = model.Biography, Timeslots = model.Timeslots, TypesOfProject = model.TypesOfProject, Available = model.Available, ImageURL = null }; } // Create user var result = await UserManager.CreateAsync(user); if (!result.Succeeded) { AddErrors(result); return View(model); } // Add the external login result = await UserManager.AddLoginAsync(user.Id, info.Login); if (!result.Succeeded) { AddErrors(result); return View(model); } if (model.UserType == "Student") { var student = db.Users .OfType() .Include(s => s.Subjects) .Include(s => s.Topics) .SingleOrDefault(u => u.Id == user.Id); if (student != null) { // clear any placeholder (should be empty for a brand new user) and add incoming items student.Subjects.Clear(); student.Topics.Clear(); foreach (var subjName in model.Subjects ?? Enumerable.Empty()) student.Subjects.Add(new Subject { Name = subjName, UserId = student.Id }); foreach (var topicName in model.Topics ?? Enumerable.Empty()) student.Topics.Add(new Topic { Name = topicName, UserId = student.Id }); db.SaveChanges(); } } else // Mentor { var mentor = db.Users .OfType() .Include(m => m.Subjects) .Include(m => m.Topics) .SingleOrDefault(u => u.Id == user.Id); if (mentor != null) { mentor.Subjects.Clear(); mentor.Topics.Clear(); foreach (var subjName in model.Subjects ?? Enumerable.Empty()) mentor.Subjects.Add(new Subject { Name = subjName, UserId = mentor.Id }); foreach (var topicName in model.Topics ?? Enumerable.Empty()) mentor.Topics.Add(new Topic { Name = topicName, UserId = mentor.Id }); db.SaveChanges(); } } [HttpPost] [ValidateAntiForgeryToken] public ActionResult LinkLogin(string provider) { // Request redirect to external login provider to link to current user return new ChallengeResult(provider, Url.Action("LinkLoginCallback", "Account"), User.Identity.GetUserId()); } [AllowAnonymous] public async Task LinkLoginCallback() { var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync(XsrfKey, User.Identity.GetUserId()); if (loginInfo == null) { return RedirectToAction("Manage", new { Message = ManageMessageId.Error }); } var result = await UserManager.AddLoginAsync(User.Identity.GetUserId(), loginInfo.Login); if (result.Succeeded) { return RedirectToAction("Manage", new { Message = ManageMessageId.AddLoginSuccess }); } return RedirectToAction("Manage", new { Message = ManageMessageId.Error }); } }}} = Ментор = == Поставување на предлог теми за проект == Менторот може да постави предлог теми за проект на својот профил. Најпрво треба да го отвори прозорецот за уредување на својот профил и да го најде следното поле: [[Image(теми.png)]] Откако ќе додаде тема, ќе може да ја уреди, избрише или додели на некој студент, притоа студентот мора да стапил во контакт со менторот претходно на самата страна и да има договор помеѓу студентот и менторот за таа тема. Доколку темата е доделена, менторот може да ја одземе. {{{ // POST: /Account/AddTopic [HttpPost] [ValidateAntiForgeryToken] [Authorize] public ActionResult AddTopic(TopicSuggestionCreateModel model) { var currentUserId = User.Identity.GetUserId(); var mentor = db.Users.OfType().FirstOrDefault(m => m.Id == currentUserId); if (mentor == null) return new HttpUnauthorizedResult(); if (!ModelState.IsValid) { var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToArray(); return Json(new { success = false, errors }); } var topic = new TopicSuggestion { MentorId = currentUserId, Title = model.Title, Description = model.Description, CreatedAt = DateTime.UtcNow }; db.TopicSuggestions.Add(topic); db.SaveChanges(); return Json(new { success = true, topic = new { topic.Id, topic.Title, topic.Description, topic.IsAssigned, topic.AssignedStudentId } }); } // POST: /Account/EditTopic [HttpPost] [ValidateAntiForgeryToken] [Authorize] public ActionResult EditTopic(int id, TopicSuggestionCreateModel model) { var currentUserId = User.Identity.GetUserId(); var topic = db.TopicSuggestions.Find(id); if (topic == null) return HttpNotFound(); if (topic.MentorId != currentUserId) return new HttpUnauthorizedResult(); if (!ModelState.IsValid) { var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToArray(); return Json(new { success = false, errors }); } topic.Title = model.Title; topic.Description = model.Description; db.SaveChanges(); return Json(new { success = true }); } // POST: /Account/DeleteTopic [HttpPost] [ValidateAntiForgeryToken] [Authorize] public ActionResult DeleteTopic(int id) { var currentUserId = User.Identity.GetUserId(); var topic = db.TopicSuggestions.Find(id); if (topic == null) return HttpNotFound(); if (topic.MentorId != currentUserId) return new HttpUnauthorizedResult(); if (topic.IsAssigned) { return Json(new { success = false, message = "Cannot delete an assigned topic." }); } db.TopicSuggestions.Remove(topic); db.SaveChanges(); return Json(new { success = true }); } }}} == Доделување теми на студент == Менторот може да додели тема на студент преку dropdown кое е пополнето со сите студенти кои го контактирале менторот: [[Image(доделитема.png)]] {{{ // POST: /Account/AssignTopic [HttpPost] [ValidateAntiForgeryToken] [Authorize] public async Task AssignTopic(int topicId, string studentId) { var currentUserId = User.Identity.GetUserId(); var topic = db.TopicSuggestions.Include(t => t.Mentor).FirstOrDefault(t => t.Id == topicId); if (topic == null) return HttpNotFound(); if (topic.MentorId != currentUserId) return new HttpUnauthorizedResult(); if (topic.IsAssigned) return Json(new { success = false, message = "Topic already assigned." }); var student = db.Users.OfType().FirstOrDefault(s => s.Id == studentId); if (student == null) return Json(new { success = false, message = "Student not found." }); topic.IsAssigned = true; topic.AssignedStudentId = studentId; topic.AssignedAt = DateTime.UtcNow; db.SaveChanges(); // create an on-site message to student so it shows in their notifications/inbox var msg = new Message { FromUserId = currentUserId, ToUserId = studentId, Subject = $"Ви е доделенa тема: {topic.Title}", Body = $"Ви е доделена тема од менторот {topic.Mentor.Name} {topic.Mentor.Surname}: {topic.Title}\n\n{topic.Description}", Timestamp = DateTime.UtcNow, IsRead = false }; db.Messages.Add(msg); db.SaveChanges(); // Email the student (best-effort) try { if (!string.IsNullOrWhiteSpace(student.Email)) { var subject = $"Доделена ви е тема: {topic.Title}"; var body = $@"

Почитуван/а {HttpUtility.HtmlEncode(student.Name)} {HttpUtility.HtmlEncode(student.Surname)},

Менторот {HttpUtility.HtmlEncode(topic.Mentor.Name)} {HttpUtility.HtmlEncode(topic.Mentor.Surname)} ви додели тема:

{HttpUtility.HtmlEncode(topic.Title)}

{HttpUtility.HtmlEncode(topic.Description)}


Ова е автоматска порака.

"; await _emailService.SendEmailAsync(student.Email, subject, body); } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine("Email send failed (AssignTopic): " + ex); } return Json(new { success = true }); } // POST: /Account/UnassignTopic [HttpPost] [ValidateAntiForgeryToken] [Authorize] public async Task UnassignTopic(int topicId) { var currentUserId = User.Identity.GetUserId(); var topic = db.TopicSuggestions.Include(t => t.Mentor).Include(t => t.AssignedStudent).FirstOrDefault(t => t.Id == topicId); if (topic == null) return HttpNotFound(); if (topic.MentorId != currentUserId) return new HttpUnauthorizedResult(); if (!topic.IsAssigned) return Json(new { success = false, message = "Topic not assigned." }); var student = topic.AssignedStudent; topic.IsAssigned = false; topic.AssignedStudentId = null; topic.AssignedAt = null; db.SaveChanges(); // notify student with message if (student != null) { var msg = new Message { FromUserId = currentUserId, ToUserId = student.Id, Subject = $"Тема {topic.Title} е оневозможена", Body = $"Менторот {topic.Mentor.Name} {topic.Mentor.Surname} ја оневозможи или одзеде темата {topic.Title}.", Timestamp = DateTime.UtcNow, IsRead = false }; db.Messages.Add(msg); db.SaveChanges(); try { if (!string.IsNullOrWhiteSpace(student.Email)) { var subject = $"Тема {topic.Title} е одземена/оневозможена"; var body = $@"

Почитуван/а {HttpUtility.HtmlEncode(student.Name)} {HttpUtility.HtmlEncode(student.Surname)},

Менторот {HttpUtility.HtmlEncode(topic.Mentor.Name)} {HttpUtility.HtmlEncode(topic.Mentor.Surname)} ја одзеде темата {HttpUtility.HtmlEncode(topic.Title)}.


Ова е автоматска порака.

"; await _emailService.SendEmailAsync(student.Email, subject, body); } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine("Email send failed (UnassignTopic): " + ex); } } return Json(new { success = true }); } }}} Секој корисник може да ги види предлог темите на менторот на неговиот профил. Може да види наслов, опис, доколку се доделени или не. Ако е менторот, или пак студентот на кој е доделена темата, тогаш има посебно поле „Доделена на студент:“ [[Image(темипрофил.png)]] [[Image(темипрофил2.png)]] {{{ // GET: /Account/GetMentorTopics (returns JSON list of available topics for a mentor) [Authorize] public JsonResult GetMentorTopics(string mentorId) { var topics = db.TopicSuggestions .Where(t => t.MentorId == mentorId && !t.IsAssigned) .OrderByDescending(t => t.CreatedAt) .Select(t => new { t.Id, t.Title, t.Description }) .ToList(); return Json(new { success = true, topics }, JsonRequestBehavior.AllowGet); } [Authorize] public JsonResult GetTopicCandidates(int topicId) { var topic = db.TopicSuggestions.Find(topicId); if (topic == null) return Json(new { success = false, message = "Topic not found" }, JsonRequestBehavior.AllowGet); // candidates: students who contacted this mentor and optionally selected this topic, fallback to all students who contacted var candidates = db.MentorContacts .Where(c => c.MentorId == topic.MentorId) .Include(c => c.Student) .GroupBy(c => c.StudentId) .Select(g => new { id = g.Key, name = g.Select(x => x.Student.Name + " " + x.Student.Surname).FirstOrDefault() }) .Where(x => x.id != null) .ToList(); // ensure distinct and non-null candidates = candidates.GroupBy(x => x.id).Select(g => g.First()).ToList(); return Json(new { success = true, candidates }, JsonRequestBehavior.AllowGet); } }}} = Студент = == Контакт со ментор (променет) == Контактирање на ментор од страна на студент останува исто, само е додадено поле за избор на предлог тема, која студентот може да ја избере но е опционална. Студентот стиска на копчето за контакт кај профилот на менторот и се отвара следниот прозорец: [[Image(контакт.png)]] По испраќање на контакт барање на менторот, менторот добива нотификација како и претходно, само што сега е излистана и темата која ја одбрал студентот. Ова е со цел да знае професорот на кого да ја додели темата понатаму, но тоа мора да го направи преку својот профил, во случај да има повеќе заинтересирани студенти. [[Image(контактнотиф.png)]] = Нотификации преку E-mail = Воведени се нотификација преку E-mail. освен што стигаат во постоечкиот Inbox, сега сите нотификации стигаат и преку E-mail на корисникот. Пример: [[Image(нотиф.png)]] [[Image(нотиф2.png)]]