wiki:UseCaseImplementationsFinal

Version 12 (modified by 231067, 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
    });
}

Овој код ја конфигурира OAuth2 автентикацијата преку Google. ClientId и ClientSecret се вчитуваат од конфигурацијата на апликацијата. Доколку вредностите се валидни, се регистрира Google authentication middleware кој овозможува редирекција кон Google, како и обработка на callback барањето по успешна автентикација. CallbackPath ја дефинира патеката каде Google ќе го врати корисникот, додека 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);
}

Методот ExternalLogin иницира процес на најава преку надворешен OAuth2 провајдер. Се генерира redirect URL кон методот ExternalLoginCallback , по што се враќа ChallengeResult кој го пренасочува корисникот кон избраниот провајдер (Google, Microsoft или GitHub).

// 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();
        }
    }

Методот ExternalLoginConfirmation се повикува по успешна OAuth2 автентикација. Неговата функција е да креира нов корисник во системот и да го поврзе со надворешниот login провајдер.

Методот најпрво ги вчитува информациите од надворешниот провајдер. Креира нов објект од тип Student или Mentor , во зависност од избраниот тип на корисник, па го зачувува корисникот во базата преку UserManager.CreateAsync . Го поврзува надворешниот login со локалниот корисник преку UserManager.AddLoginAsync и ги зачувува предметите и темите поврзани со корисникот, доколку се внесени.

[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 });
}


Методот LinkLogin овозможува поврзување на дополнителен OAuth2 провајдер со веќе постоечки кориснички профил. Ова му овозможува на корисникот да се најавува преку повеќе провајдери.

Методот LinkLoginCallback го финализира процесот на поврзување на надворешниот login со тековниот корисник. Доколку операцијата е успешна, новиот login провајдер се додава во корисничкиот профил.

Ментор

Поставување на предлог теми за проект

Менторот може да постави предлог теми за проект на својот профил. Најпрво треба да го отвори прозорецот за уредување на својот профил и да го најде следното поле:

Откако ќе додаде тема, ќе може да ја уреди, избрише или додели на некој студент, притоа студентот мора да стапил во контакт со менторот претходно на самата страна и да има договор помеѓу студентот и менторот за таа тема. Доколку темата е доделена, менторот може да ја одземе.

// 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 });
}

Методот AddTopic овозможува креирање на нов предлог за тема од страна на менторот. Се проверува идентитетот на тековниот корисник и валидноста на внесените податоци. Доколку валидацијата е успешна, се креира нов објект од тип TopicSuggestion и се зачувува во базата. Како резултат се враќа JSON одговор со податоците за новата тема.

Методот EditTopic овозможува измена на постоечки предлог за тема, а методот DeleteTopic овозможува бришење на предлог тема, пришто темата може да се избрише само доколку не е веќе доделена на студент Ова ограничување спречува губење на податоци поврзани со активни менторства.

Доделување теми на студент

Менторот може да додели тема на студент преку 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 });
}

Методот AssignTopic овозможува доделување на тема на студент. Се врши проверка дали темата постои, дали припаѓа на тековниот ментор и дали не е веќе доделена. Доколку условите се исполнети, темата се означува како доделена и се зачувува ID на студентот. Потоа се креира нотификација во системскиот inbox, и се испраќа e-mail нотификација до студентот со истата содржина.

Методот UnassignTopic овозможува отстранување на доделена тема од студент. Се ажурира статусот на темата и се отстранува врската со студентот. Дополнително се испраќа системска и e-mail нотификација до студентот.

Секој корисник може да ги види предлог темите на менторот на неговиот профил. Може да види наслов, опис, доколку се доделени или не. Ако е менторот, или пак студентот на кој е доделена темата, тогаш има посебно поле „Доделена на студент:“

// 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);
}

Методот GetMentorTopics враќа листа на сите недоделени теми на одреден ментор. Податоците се враќаат во JSON формат и се користат за прикажување на достапните теми во корисничкиот интерфејс.

Методот GetTopicCandidates враќа листа на студенти кои имаат воспоставено контакт со менторот. Оваа листа се користи како кандидати при доделување на тема.

Студент

Контакт со ментор (променет)

Контактирање на ментор од страна на студент останува исто, само е додадено поле за избор на предлог тема, која студентот може да ја избере но е опционална.

Студентот стиска на копчето за контакт кај профилот на менторот и се отвара следниот прозорец:

По испраќање на контакт барање на менторот, менторот добива нотификација како и претходно, само што сега е излистана и темата која ја одбрал студентот. Ова е со цел да знае професорот на кого да ја додели темата понатаму, но тоа мора да го направи преку својот профил, во случај да има повеќе заинтересирани студенти.

Нотификации преку 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)

Download all attachments as: .zip

Note: See TracWiki for help on using the wiki.