| Version 10 (modified by , 3 days ago) ( diff ) |
|---|
Финална имплементација на случаи на употреба
На следната табела се прикажани финалните кориснички сценарија:
| ID | Use Case Scenario | Actor |
| 14 | Најава преку OAuth2 (Google, Microsoft, GitHub) | Корисник |
| 15 | Поставување на предлог теми за проект | Ментор |
| 16 | Доделување теми на студент | Ментор |
| 17 | Контакт со ментор (променет) | Студент |
И дополнително се имплементирани E-mail нотификации, поврзани со inbox на самиот сајт.
Корисник
Најава преку OAuth2
Кога прв пат ќе ја отвори страната, корисникот е редиректиран кон страната за најава. Долу, има дополнителни три копчиња за најава со Google, Microsoft и GitHub.
Да го земеме како пример Google. Корисникот ја одбира опцијата за најава со Google и е редиректиран кон најавата поставена од Google:
Откако ќе го одбере својот Google профил и успешно помине најавата, тогаш корисникот може да продолжи со регистрација. Тука се пополуваат полињата како име, презиме и мејл:
Сега што останува е корисникот да пополни биографија, предмети, интереси, доколку е студент тогаш индекс, година, семестар, итн. Но тие се опционални, и регистрацијата преку 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<ActionResult> 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<Student>()
.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<string>())
student.Subjects.Add(new Subject { Name = subjName, UserId = student.Id });
foreach (var topicName in model.Topics ?? Enumerable.Empty<string>())
student.Topics.Add(new Topic { Name = topicName, UserId = student.Id });
db.SaveChanges();
}
}
else // Mentor
{
var mentor = db.Users
.OfType<Mentor>()
.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<string>())
mentor.Subjects.Add(new Subject { Name = subjName, UserId = mentor.Id });
foreach (var topicName in model.Topics ?? Enumerable.Empty<string>())
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<ActionResult> 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 });
}
Ментор
Поставување на предлог теми за проект
Менторот може да постави предлог теми за проект на својот профил. Најпрво треба да го отвори прозорецот за уредување на својот профил и да го најде следното поле:
Откако ќе додаде тема, ќе може да ја уреди, избрише или додели на некој студент, притоа студентот мора да стапил во контакт со менторот претходно на самата страна и да има договор помеѓу студентот и менторот за таа тема. Доколку темата е доделена, менторот може да ја одземе.
// POST: /Account/AddTopic
[HttpPost]
[ValidateAntiForgeryToken]
[Authorize]
public ActionResult AddTopic(TopicSuggestionCreateModel model)
{
var currentUserId = User.Identity.GetUserId();
var mentor = db.Users.OfType<Mentor>().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 кое е пополнето со сите студенти кои го контактирале менторот:
// POST: /Account/AssignTopic
[HttpPost]
[ValidateAntiForgeryToken]
[Authorize]
public async Task<ActionResult> 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<Student>().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 = $@"
<p>Почитуван/а {HttpUtility.HtmlEncode(student.Name)} {HttpUtility.HtmlEncode(student.Surname)},</p>
<p>Менторот <strong>{HttpUtility.HtmlEncode(topic.Mentor.Name)} {HttpUtility.HtmlEncode(topic.Mentor.Surname)}</strong> ви додели тема:</p>
<h4>{HttpUtility.HtmlEncode(topic.Title)}</h4>
<p>{HttpUtility.HtmlEncode(topic.Description)}</p>
<hr/><p>Ова е автоматска порака.</p>";
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<ActionResult> 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 = $@"
<p>Почитуван/а {HttpUtility.HtmlEncode(student.Name)} {HttpUtility.HtmlEncode(student.Surname)},</p>
<p>Менторот <strong>{HttpUtility.HtmlEncode(topic.Mentor.Name)} {HttpUtility.HtmlEncode(topic.Mentor.Surname)}</strong> ја одзеде темата <strong>{HttpUtility.HtmlEncode(topic.Title)}</strong>.</p>
<hr/><p>Ова е автоматска порака.</p>";
await _emailService.SendEmailAsync(student.Email, subject, body);
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("Email send failed (UnassignTopic): " + ex);
}
}
return Json(new { success = true });
}
Секој корисник може да ги види предлог темите на менторот на неговиот профил. Може да види наслов, опис, доколку се доделени или не. Ако е менторот, или пак студентот на кој е доделена темата, тогаш има посебно поле „Доделена на студент:“
// 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);
}
Студент
Контакт со ментор (променет)
Контактирање на ментор од страна на студент останува исто, само е додадено поле за избор на предлог тема, која студентот може да ја избере но е опционална.
Студентот стиска на копчето за контакт кај профилот на менторот и се отвара следниот прозорец:
По испраќање на контакт барање на менторот, менторот добива нотификација како и претходно, само што сега е излистана и темата која ја одбрал студентот. Ова е со цел да знае професорот на кого да ја додели темата понатаму, но тоа мора да го направи преку својот профил, во случај да има повеќе заинтересирани студенти.
Нотификации преку E-mail
Воведени се нотификација преку E-mail. освен што стигаат во постоечкиот Inbox, сега сите нотификации стигаат и преку E-mail на корисникот.
Пример:
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<ActionResult> SendContact(MentorContact model) , односно контролерот за испраќање на контакт кон менторот.
// Email mentor
try
{
var mentor = db.Users.OfType<Mentor>().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 = $@"
<p>Почитуван/а {HttpUtility.HtmlEncode(mentor.Name)} {HttpUtility.HtmlEncode(mentor.Surname)},</p>
<p>Добијавте ново барање за менторство од <strong>{HttpUtility.HtmlEncode(student.Name)} {HttpUtility.HtmlEncode(student.Surname)}</strong>.</p>
<p><strong>Предмет:</strong> {HttpUtility.HtmlEncode(model.SubjectName ?? "—")}</p>
<p><strong>Тип:</strong> {HttpUtility.HtmlEncode(model.Type ?? "—")}<br/>
<strong>Големина на тим:</strong> {(model.TeamSize.HasValue ? model.TeamSize.Value.ToString() : "—")}</p>
" + (selectedTopicTitle != null ? $"<p><strong>Предлог тема:</strong> {HttpUtility.HtmlEncode(selectedTopicTitle)}</p>" : $"<p><strong>Предлог тема:</strong> —</p>") +
$@"
<p>Порака:<br/>{HttpUtility.HtmlEncode(model.Message)}</p>
<p>Прегледајте го барањето: <a href='{Url.Action("Inbox", "Account", null, Request?.Url?.Scheme)}'>Inbox</a></p>
<hr/><p>Ова е автоматска порака.</p>";
await _emailService.SendEmailAsync(mentor.Email, subject, body);
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("Email send failed (SendContact): " + ex);
}
Attachments (11)
- најава.png (31.4 KB ) - added by 3 days ago.
- најава2.png (26.6 KB ) - added by 3 days ago.
- најава3.png (30.8 KB ) - added by 3 days ago.
- теми.png (53.5 KB ) - added by 3 days ago.
- доделитема.png (15.2 KB ) - added by 3 days ago.
- темипрофил.png (34.3 KB ) - added by 3 days ago.
- темипрофил2.png (46.1 KB ) - added by 3 days ago.
- нотиф.png (52.5 KB ) - added by 3 days ago.
- нотиф2.png (53.3 KB ) - added by 3 days ago.
- контакт.png (32.5 KB ) - added by 3 days ago.
- контактнотиф.png (38.9 KB ) - added by 3 days ago.
Download all attachments as: .zip











