= Финална имплементација на случаи на употреба = На следната табела се прикажани финалните кориснички сценарија: || '''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)]] {{{ public class EmailService : IIdentityMessageService { private readonly string _host; private readonly int _port; private readonly string _username; private readonly string _password; private readonly bool _enableSsl; private readonly string _from; private readonly string _fromDisplay; public EmailService() { _host = ConfigurationManager.AppSettings["Smtp:Host"] ?? Environment.GetEnvironmentVariable("SMTP_HOST"); _port = ParseInt(ConfigurationManager.AppSettings["Smtp:Port"]) ?? ParseInt(Environment.GetEnvironmentVariable("SMTP_PORT")) ?? 587; _username = ConfigurationManager.AppSettings["Smtp:Username"] ?? Environment.GetEnvironmentVariable("SMTP_USER"); _password = ConfigurationManager.AppSettings["Smtp:Password"] ?? Environment.GetEnvironmentVariable("SMTP_PASS"); _enableSsl = ParseBool(ConfigurationManager.AppSettings["Smtp:EnableSsl"]) ?? ParseBool(Environment.GetEnvironmentVariable("SMTP_ENABLESSL")) ?? true; _from = ConfigurationManager.AppSettings["Email:From"] ?? _username ?? Environment.GetEnvironmentVariable("EMAIL_FROM"); _fromDisplay = ConfigurationManager.AppSettings["Email:FromDisplayName"] ?? "Најди Ментор"; } private int? ParseInt(string s) => int.TryParse(s, out var v) ? (int?)v : null; private bool? ParseBool(string s) => bool.TryParse(s, out var v) ? (bool?)v : null; // Called by ASP.NET Identity when it needs to send e-mail (confirmations, password reset, etc.) public async Task SendAsync(IdentityMessage message) { if (message == null) throw new ArgumentNullException(nameof(message)); await SendEmailAsync(message.Destination, message.Subject, message.Body, null).ConfigureAwait(false); } public async Task SendEmailAsync(string toEmail, string subject, string htmlBody, string plainBody = null) { if (string.IsNullOrWhiteSpace(_host)) throw new InvalidOperationException("SMTP host is not configured (Smtp:Host)."); if (string.IsNullOrWhiteSpace(toEmail)) throw new ArgumentException("toEmail required", nameof(toEmail)); var mail = new MailMessage { From = new MailAddress(_from ?? _username, _fromDisplay), Subject = subject ?? string.Empty, BodyEncoding = Encoding.UTF8, SubjectEncoding = Encoding.UTF8, IsBodyHtml = true, Body = htmlBody ?? string.Empty }; if (!string.IsNullOrEmpty(plainBody)) mail.AlternateViews.Add(AlternateView.CreateAlternateViewFromString(plainBody, null, "text/plain")); mail.To.Add(toEmail); using (var client = new SmtpClient(_host, _port)) { client.EnableSsl = _enableSsl; if (!string.IsNullOrEmpty(_username)) client.Credentials = new NetworkCredential(_username, _password); await client.SendMailAsync(mail).ConfigureAwait(false); } } } }}} Пример на имплементација внатре во {{{ public async Task SendContact(MentorContact model) }}} , односно контролерот за испраќање на контакт кон менторот. {{{ // Email mentor try { var mentor = db.Users.OfType().FirstOrDefault(m => m.Id == model.MentorId); if (mentor != null && !string.IsNullOrEmpty(mentor.Email)) { string selectedTopicTitle = null; if (model.TopicSuggestionId.HasValue) { var ts = db.TopicSuggestions.Find(model.TopicSuggestionId.Value); if (ts != null) selectedTopicTitle = ts.Title; } var subject = "Ново барање за контакт на NajdiMentor"; var body = $@"

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

Добијавте ново барање за менторство од {HttpUtility.HtmlEncode(student.Name)} {HttpUtility.HtmlEncode(student.Surname)}.

Предмет: {HttpUtility.HtmlEncode(model.SubjectName ?? "—")}

Тип: {HttpUtility.HtmlEncode(model.Type ?? "—")}
Големина на тим: {(model.TeamSize.HasValue ? model.TeamSize.Value.ToString() : "—")}

" + (selectedTopicTitle != null ? $"

Предлог тема: {HttpUtility.HtmlEncode(selectedTopicTitle)}

" : $"

Предлог тема:

") + $@"

Порака:
{HttpUtility.HtmlEncode(model.Message)}

Прегледајте го барањето: Inbox


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

"; await _emailService.SendEmailAsync(mentor.Email, subject, body); } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine("Email send failed (SendContact): " + ex); } }}}