public void CanPostAskForHelpEvent() { // Arrange var log = new AskForHelpEvent { SenderId = 1, SolutionName = "Dummy Solution", Code = "dummy code", CourseId = 1, UserComment = "dummy user comment" }; // Act var result = Posts.SaveEvent(log); // Assert using (var connection = new SqlConnection(StringConstants.ConnectionString)) { var savedlog = connection.Query <AskForHelpEvent>( "select b.EventLogId, a.EventTypeId, b.Code, a.SenderId, b.SolutionName, b.UserComment, a.CourseId " + "from EventLogs a " + "inner join AskForHelpEvents b on b.EventLogId=a.Id and a.Id=@id", new { @Id = result }).SingleOrDefault(); Assert.IsTrue(savedlog != null); Assert.IsTrue(savedlog.SenderId == log.SenderId); Assert.IsTrue(savedlog.EventType == log.EventType); Assert.IsTrue(savedlog.Code == log.Code); Assert.IsTrue(savedlog.SolutionName == log.SolutionName); Assert.IsTrue(savedlog.UserComment == log.UserComment); Assert.IsTrue(savedlog.CourseId == log.CourseId); } }
public void ShowAskForHelp(object sender, EventArgs e) { var cacheItem = _cache[StringConstants.AuthenticationCacheKey]; if (cacheItem != null && string.IsNullOrEmpty(cacheItem.ToString())) { MessageBox.Show("You must be logged into OSBLE+ in order to access this window."); return; } var vm = new AskForHelpViewModel(); //find current text selection var dte = (DTE2)GetService(typeof(SDTE)); if (dte != null) { dynamic selection = dte.ActiveDocument.Selection; if (selection != null) { try { vm.Code = selection.Text; } catch (Exception) { // ignored } } } //AC: Restrict "ask for help" to approx 20 lines if (vm.Code.Length > 750) { vm.Code = vm.Code.Substring(0, 750); } //show message dialog var result = AskForHelpForm.ShowModalDialog(vm); if (result == MessageBoxResult.OK) { var generator = EventGenerator.GetInstance(); var evt = new AskForHelpEvent { Code = vm.Code, UserComment = vm.UserText }; generator.SubmitEvent(evt); MessageBox.Show("Your question has been logged and will show up in the activity stream shortly."); } }
public EventLog SubmitLog(EventLog log, OsbideUser user) { LocalErrorLog errorLogger = new LocalErrorLog(); errorLogger.SenderId = user.Id; errorLogger.Content = "About to save log " + log.LogType + " to DB. "; errorLogger.LogDate = DateTime.Now; log.Sender = null; log.SenderId = user.Id; log.DateReceived = DateTime.UtcNow; log.EventTypeId = Convert.ToInt32(Enum.Parse(typeof(EventTypes), log.LogType)); //insert into the DB Db.EventLogs.Add(log); try { Db.SaveChanges(); errorLogger.Content += "Item saved. "; } catch (System.Data.Entity.Validation.DbEntityValidationException dbEx) { errorLogger.Content += "Error saving: "; foreach (var validationErrors in dbEx.EntityValidationErrors) { foreach (var validationError in validationErrors.ValidationErrors) { System.Diagnostics.Trace.TraceInformation("Property: {0} Error: {1}", validationError.PropertyName, validationError.ErrorMessage); errorLogger.Content += string.Format("Property: {0} Error: {1}", validationError.PropertyName, validationError.ErrorMessage); } } Db.LocalErrorLogs.Add(errorLogger); Db.SaveChanges(); return(null); } catch (Exception ex) { errorLogger.Content += "Error saving: " + ex.Message; System.Diagnostics.Trace.TraceInformation(ex.Message); Db.LocalErrorLogs.Add(errorLogger); Db.SaveChanges(); return(null); } //Tease apart log information and insert into the appropriate DB table IOsbideEvent evt = null; try { errorLogger.Content += "About to unzip event."; evt = EventFactory.FromZippedBinary(log.Data.BinaryData, new OsbideDeserializationBinder()); evt.EventLogId = log.Id; } catch (Exception) { errorLogger.Content += "Error unzipping event."; Db.LocalErrorLogs.Add(errorLogger); Db.SaveChanges(); return(null); } var hashtags = string.Empty; var usertags = string.Empty; if (log.LogType == AskForHelpEvent.Name) { Db.AskForHelpEvents.Add((AskForHelpEvent)evt); AskForHelpEvent ask = evt as AskForHelpEvent; //send email to interested parties //find all of this user's subscribers and send them an email List <OsbideUser> observers = new List <OsbideUser>(); observers = (from subscription in Db.UserSubscriptions join dbUser in Db.Users on new { InstitutionId = subscription.ObserverInstitutionId, SchoolId = subscription.ObserverSchoolId } equals new { InstitutionId = user.InstitutionId, SchoolId = user.SchoolId } where subscription.SubjectSchoolId == user.SchoolId && subscription.SubjectInstitutionId == user.InstitutionId && dbUser.ReceiveEmailOnNewFeedPost == true select dbUser).ToList(); if (observers.Count > 0) { string url = StringConstants.GetActivityFeedDetailsUrl(log.Id); string body = "Greetings,<br />{0} asked for help regarding the following item:<br />\"{1}\"<br />To view this " + "conversation online, please visit {2} or visit your OSBIDE user profile.<br /><br />Thanks,<br />OSBIDE<br /><br />" + "These automated messages can be turned off by editing your user profile."; body = string.Format(body, user.FirstAndLastName, ask.UserComment, url); List <MailAddress> to = new List <MailAddress>(); foreach (OsbideUser observer in observers) { to.Add(new MailAddress(observer.Email)); } Email.Send("[OSBIDE] Someone has asked for help!", body, to); } } else if (log.LogType == BuildEvent.Name) { BuildEvent build = (BuildEvent)evt; Db.BuildEvents.Add(build); string pattern = "error ([^:]+)"; //strip out non-critical errors List <BuildEventErrorListItem> errorItems = build.ErrorItems.Where(e => e.ErrorListItem.CriticalErrorName.Length > 0).ToList(); build.ErrorItems.Clear(); build.ErrorItems = errorItems; //log all errors in their own DB for faster search List <string> errors = new List <string>(); Dictionary <string, ErrorType> errorTypes = new Dictionary <string, ErrorType>(); foreach (BuildEventErrorListItem item in build.ErrorItems) { Match match = Regex.Match(item.ErrorListItem.Description, pattern); //ignore bad matches if (match.Groups.Count == 2) { string errorCode = match.Groups[1].Value.ToLower().Trim(); ErrorType type = Db.ErrorTypes.Where(t => t.Name == errorCode).FirstOrDefault(); if (type == null) { if (errorTypes.ContainsKey(errorCode) == false) { type = new ErrorType() { Name = errorCode }; Db.ErrorTypes.Add(type); } else { type = errorTypes[errorCode]; } } if (errorCode.Length > 0 && errors.Contains(errorCode) == false) { errors.Add(errorCode); } errorTypes[errorCode] = type; } } Db.SaveChanges(); foreach (string errorType in errors) { Db.BuildErrors.Add(new BuildError() { BuildErrorTypeId = errorTypes[errorType].Id, LogId = log.Id }); } } else if (log.LogType == CutCopyPasteEvent.Name) { Db.CutCopyPasteEvents.Add((CutCopyPasteEvent)evt); } else if (log.LogType == DebugEvent.Name) { Db.DebugEvents.Add((DebugEvent)evt); } else if (log.LogType == EditorActivityEvent.Name) { Db.EditorActivityEvents.Add((EditorActivityEvent)evt); } else if (log.LogType == ExceptionEvent.Name) { Db.ExceptionEvents.Add((ExceptionEvent)evt); } else if (log.LogType == FeedPostEvent.Name) { hashtags = string.Join(",", ParseHashtags(((FeedPostEvent)evt).Comment)); usertags = string.Join(",", ParseUserTags(((FeedPostEvent)evt).Comment)); Db.FeedPostEvents.Add((FeedPostEvent)evt); } else if (log.LogType == HelpfulMarkGivenEvent.Name) { Db.HelpfulMarkGivenEvents.Add((HelpfulMarkGivenEvent)evt); } else if (log.LogType == LogCommentEvent.Name) { Db.LogCommentEvents.Add((LogCommentEvent)evt); } else if (log.LogType == SaveEvent.Name) { Db.SaveEvents.Add((SaveEvent)evt); } else if (log.LogType == SubmitEvent.Name) { errorLogger.Content += "Submit event detected. "; Db.SubmitEvents.Add((SubmitEvent)evt); } try { errorLogger.Content += "Attempting to save to DB. "; Db.SaveChanges(); /* * if(hashtags.Length >= 0 || usertags.Length >= 0 ) * * using (var context = new OsbideProcs()) * { * context.InsertPostTags(log.Id, usertags, hashtags); * } * */ } catch (System.Data.Entity.Validation.DbEntityValidationException dbEx) { errorLogger.Content += "Error saving to DB: "; foreach (var validationErrors in dbEx.EntityValidationErrors) { foreach (var validationError in validationErrors.ValidationErrors) { System.Diagnostics.Trace.TraceInformation("Property: {0} Error: {1}", validationError.PropertyName, validationError.ErrorMessage); errorLogger.Content += string.Format("Property: {0} Error: {1}", validationError.PropertyName, validationError.ErrorMessage); Db.LocalErrorLogs.Add(errorLogger); Db.SaveChanges(); } } return(null); } catch (Exception ex) { errorLogger.Content += "Error saving to DB: " + ex.Message; Db.LocalErrorLogs.Add(errorLogger); Db.SaveChanges(); System.Diagnostics.Trace.TraceInformation(ex.Message); return(null); } //Db.LocalErrorLogs.Add(errorLogger); //Db.SaveChanges(); return(log); }
/// <summary> /// Gets comment details for a particular student /// </summary> /// <param name="id"></param> /// <returns></returns> public ActionResult Student(int id) { /* * to fetch: * For each comment: * Comment * Option to view entire thread * Chris, Adam, Carla content coding info * Adam's student EC coding info * size of previous / next save * time of prevous / last save * NPSM state * */ List<CommentTimelineViewModel> viewModel = new List<CommentTimelineViewModel>(); //id is nice for MVC, but not very descriptive, switch to studentId in body. int studentId = id; int[] interestingEventIds = { 1 //ask for help , 2 //build event , 7 //feed post , 9 //log comment , 10 //save }; //only social events int[] socialEventIds = {1, 7, 9}; //check to see if we have cached results in the DB var cachedResults = Db .CommentTimelines .Include(c => c.ProgrammingState) .Include(c => c.QuestionCodings.Select(q => q.QuestionCoding.Post)) .Include(c => c.AnswerCodings.Select(q => q.AnswerCoding.Answer)) .Include(c => c.ExpertCoding) .Include(c => c.TimelineCodeDocuments) .Where(c => c.AuthorId == studentId) ; if (cachedResults.Count() > 0) { var authorQuery = from user in OsbideDb.Users where user.Id == studentId select user; OsbideUser student = authorQuery.FirstOrDefault(); student = (student == null) ? new OsbideUser() : student; var eventLogQuery = from log in OsbideDb.EventLogs where log.SenderId == studentId && socialEventIds.Contains(log.EventTypeId) select log; Dictionary<int, EventLog> allEventLogs = new Dictionary<int, EventLog>(); foreach(EventLog log in eventLogQuery) { allEventLogs.Add(log.Id, log); } foreach(CommentTimeline timeline in cachedResults) { CommentTimelineViewModel nextViewModel = new CommentTimelineViewModel(timeline); nextViewModel.Log = allEventLogs[timeline.EventLogId]; nextViewModel.Author = student; List<TimelineCodeDocument> beforeDocuments = timeline.TimelineCodeDocuments.Where(t => t.isBeforeComment == true).ToList(); List<TimelineCodeDocument> afterDocuments = timeline.TimelineCodeDocuments.Where(t => t.isBeforeComment == false).ToList(); nextViewModel.CodeBeforeComment = new Dictionary<string, TimelineCodeDocument>(); nextViewModel.CodeAfterComment = new Dictionary<string, TimelineCodeDocument>(); foreach(TimelineCodeDocument tcd in beforeDocuments) { nextViewModel.CodeBeforeComment.Add(tcd.DocumentName, tcd); } foreach(TimelineCodeDocument tcd in afterDocuments) { nextViewModel.CodeAfterComment.Add(tcd.DocumentName, tcd); } viewModel.Add(nextViewModel); } } else { //no cached results, do it the long way... //this query pulls most questions (ask for help excluded?) from the analytics DB //and should be faster than loading all questions from the OSBIDE DB var commentQuery = from comment in Db.Posts where comment.AuthorId == studentId select comment; //convert into dictionary for lookup by logsQuery Dictionary<int, Post> posts = new Dictionary<int, Post>(); foreach (Post post in commentQuery) { posts.Add(post.OsbideId, post); } //This query will pull down all content coding questions, organized by Osbide user ID var contentCodingQuery = from code in Db.ContentCodings where code.AuthorId == studentId select code; SortedList<DateTime, ContentCoding> expertCodings = new SortedList<DateTime, ContentCoding>(); foreach (ContentCoding coding in contentCodingQuery) { //I was getting key mismatch (probably difference in milliseconds). My solution was to create //a new date using only coarser measures DateTime dateKey = new DateTime(coding.Date.Year, coding.Date.Month, coding.Date.Day, coding.Date.Hour, coding.Date.Minute, coding.Date.Second, DateTimeKind.Utc); expertCodings.Add(dateKey, coding); } //This query will pull down information obtained from my crowd-sourced content coding. //AnswerCodings have FK reference to the original question as well as the answer. If a Post //is in the AnswerCodings table, it must be an answer var answeredQuestionsQuery = from answer in Db.AnswerCodings .Include(c => c.Answer) .Include(c => c.Question) where answer.Answer.AuthorId == studentId || answer.Question.AuthorId == studentId select answer; var allPostsQuery = from question in Db.QuestionCodings .Include(c => c.Post) where question.Post.AuthorId == studentId select question; Dictionary<int, PostCoding> crowdCodings = new Dictionary<int, PostCoding>(); foreach (QuestionCoding coding in allPostsQuery) { if (crowdCodings.ContainsKey(coding.Post.OsbideId) == false) { crowdCodings.Add(coding.Post.OsbideId, new PostCoding()); crowdCodings[coding.Post.OsbideId].OsbidePostId = coding.Post.OsbideId; } crowdCodings[coding.Post.OsbideId].Codings.Add(coding); } foreach (AnswerCoding coding in answeredQuestionsQuery) { if (crowdCodings.ContainsKey(coding.Question.OsbideId) == false) { crowdCodings.Add(coding.Question.OsbideId, new PostCoding()); crowdCodings[coding.Question.OsbideId].OsbidePostId = coding.Question.OsbideId; } crowdCodings[coding.Question.OsbideId].Responses.Add(coding); } //grab all save and build events for this user Dictionary<int, SaveEvent> allSaves = new Dictionary<int, SaveEvent>(); Dictionary<int, BuildEvent> allBuilds = new Dictionary<int, BuildEvent>(); var savesQuery = from save in OsbideDb.SaveEvents .Include(s => s.Document) join log in OsbideDb.EventLogs on save.EventLogId equals log.Id where log.SenderId == studentId select save; foreach (SaveEvent saveEvent in savesQuery) { allSaves[saveEvent.EventLogId] = saveEvent; } var buildsQuery = from build in OsbideDb.BuildEvents .Include(b => b.Documents.Select(d => d.Document)) join log in OsbideDb.EventLogs on build.EventLogId equals log.Id where log.SenderId == studentId select build; foreach (BuildEvent buildEvent in buildsQuery) { allBuilds[buildEvent.EventLogId] = buildEvent; } //this query pulls data directly from event logs. var logsQuery = from log in OsbideDb.EventLogs where log.SenderId == studentId && interestingEventIds.Contains(log.EventTypeId) select log; List<EventLog> eventLogs = logsQuery.ToList(); Stack<EventLog> saveEvents = new Stack<EventLog>(); List<EventLog> socialEvents = new List<EventLog>(); foreach (EventLog log in eventLogs) { //holds the next entry into the view model CommentTimelineViewModel nextViewModel = new CommentTimelineViewModel(); //if we have a document save event, remember for later until we get a social event if (log.LogType == SaveEvent.Name || log.LogType == BuildEvent.Name) { saveEvents.Push(log); } else { //social event detected //1: grab previous edit information string solutionName = ""; Dictionary<string, CodeDocument> previousDocuments = new Dictionary<string, CodeDocument>(); //Start with saves as they will contain more up-to-date information than last build while (saveEvents.Count > 0 && saveEvents.Peek().LogType != BuildEvent.Name) { EventLog next = saveEvents.Pop(); if (allSaves.ContainsKey(next.Id)) { SaveEvent save = allSaves[next.Id]; if (solutionName.Length == 0) { solutionName = save.SolutionName; } if (save.SolutionName == solutionName) { if (previousDocuments.ContainsKey(save.Document.FileName) == false) { previousDocuments[save.Document.FileName] = save.Document; } } } } //at this point, saveEvents should be empty or we should be at a build event. //Finish off the snapshot with documents transferred with last build if (saveEvents.Count > 0) { EventLog top = saveEvents.Pop(); if (allBuilds.ContainsKey(top.Id)) { BuildEvent build = allBuilds[top.Id]; if (solutionName.Length == 0) { solutionName = build.SolutionName; } if (build.SolutionName == solutionName) { foreach (BuildDocument doc in build.Documents) { if (previousDocuments.ContainsKey(doc.Document.FileName) == false) { previousDocuments[doc.Document.FileName] = doc.Document; } } } } } //store final result in view model foreach(CodeDocument document in previousDocuments.Values) { TimelineCodeDocument tcd = new TimelineCodeDocument() { CodeDocumentId = document.Id, CommentTimeline = nextViewModel.Timeline, DocumentContent = document.Content, DocumentName = document.FileName, isBeforeComment = true }; if(nextViewModel.Timeline.TimelineCodeDocuments == null) { nextViewModel.Timeline.TimelineCodeDocuments = new List<TimelineCodeDocument>(); } nextViewModel.Timeline.TimelineCodeDocuments.Add(tcd); } //2: grab next edit information (will have to be done on 2nd pass) //grab expert content coding info DateTime dateKey = new DateTime(log.DateReceived.Year, log.DateReceived.Month, log.DateReceived.Day, log.DateReceived.Hour, log.DateReceived.Minute, log.DateReceived.Second, DateTimeKind.Utc); //I was getting key mismatch (probably difference in milliseconds). My solution was to create //a new date using only coarser measures if (expertCodings.ContainsKey(dateKey)) { nextViewModel.Timeline.ExpertCoding = expertCodings[dateKey]; nextViewModel.Timeline.ExpertCodingId = expertCodings[dateKey].Id; } //grab crowd coding info var crowd = crowdCodings.Where(cc => cc.Key == log.Id).Select(k => k.Value).FirstOrDefault(); if (crowd != null) { foreach (var question in crowd.Codings) { TimelineQuestionCoding questionCode = new TimelineQuestionCoding() { CommentTimeline = nextViewModel.Timeline, QuestionCoding = question, QuestionCodingId = question.Id }; nextViewModel.Timeline.QuestionCodings.Add(questionCode); } foreach (var answer in crowd.Responses) { TimelineAnswerCoding responseCoding = new TimelineAnswerCoding() { CommentTimeline = nextViewModel.Timeline, AnswerCoding = answer, AnswerCodingId = answer.Id }; nextViewModel.Timeline.AnswerCodings.Add(responseCoding); } } //grab NPSM state info //we want the most recent NPSM state that occurred before the comment was made var npsmQuery = from npsm in Db.TimelineStates where npsm.StartTime <= log.DateReceived && npsm.IsSocialEvent == false && npsm.UserId == log.SenderId && npsm.State != "--" orderby npsm.Id ascending select npsm; TimelineState state = npsmQuery.Take(1).FirstOrDefault(); if (state != null) { nextViewModel.Timeline.ProgrammingState = state; nextViewModel.Timeline.ProgrammingStateId = state.Id; } //add in comment information if (posts.ContainsKey(log.Id) == true) { nextViewModel.Timeline.Comment = posts[log.Id].Content; } else { //not found in pre-query. Pull manually if (log.LogType == FeedPostEvent.Name) { FeedPostEvent feedPost = OsbideDb.FeedPostEvents.Where(fpe => fpe.EventLogId == log.Id).FirstOrDefault(); if (feedPost != null) { nextViewModel.Timeline.Comment = feedPost.Comment; } } else if (log.LogType == LogCommentEvent.Name) { LogCommentEvent logComment = OsbideDb.LogCommentEvents.Where(fpe => fpe.EventLogId == log.Id).FirstOrDefault(); if (logComment != null) { nextViewModel.Timeline.Comment = logComment.Content; } } else if (log.LogType == AskForHelpEvent.Name) { AskForHelpEvent ask = OsbideDb.AskForHelpEvents.Where(fpe => fpe.EventLogId == log.Id).FirstOrDefault(); if (ask != null) { nextViewModel.Timeline.Comment = ask.UserComment + "\n" + ask.Code; } } } nextViewModel.Log = log; nextViewModel.Timeline.EventLogId = log.Id; nextViewModel.Timeline.AuthorId = log.SenderId; nextViewModel.Author = log.Sender; if(nextViewModel.Timeline.ExpertCoding == null) { nextViewModel.Timeline.ExpertCoding = new ContentCoding(); } if(nextViewModel.Timeline.ProgrammingState == null) { nextViewModel.Timeline.ProgrammingState = new TimelineState() { EndTime = new DateTime(2016, 01, 01), StartTime = new DateTime(2016, 01, 01), State = "not available" }; } viewModel.Add(nextViewModel); } } //2nd pass: find code modifications made after comment. for (int i = 0; i < viewModel.Count; i++) { CommentTimelineViewModel current = viewModel[i] as CommentTimelineViewModel; CommentTimelineViewModel next = new CommentTimelineViewModel(); if (i + 1 < viewModel.Count) { next = viewModel[i + 1] as CommentTimelineViewModel; } else { next.Log = eventLogs.Last(); } List<EventLog> logsBetween = eventLogs .Where(l => l.DateReceived >= current.Log.DateReceived) .Where(l => l.DateReceived <= next.Log.DateReceived) .ToList(); Dictionary<string, CodeDocument> nextDocuments = new Dictionary<string, CodeDocument>(); string solutionName = ""; foreach (EventLog log in logsBetween) { if (log.LogType == SaveEvent.Name) { if (allSaves.ContainsKey(log.Id)) { SaveEvent save = allSaves[log.Id]; if (solutionName.Length == 0) { solutionName = save.SolutionName; } if (save.SolutionName == solutionName) { if (nextDocuments.ContainsKey(save.Document.FileName) == false) { nextDocuments[save.Document.FileName] = save.Document; } } } } else if (log.LogType == BuildEvent.Name) { if (allBuilds.ContainsKey(log.Id)) { BuildEvent build = allBuilds[log.Id]; if (solutionName.Length == 0) { solutionName = build.SolutionName; } if (build.SolutionName == solutionName) { foreach (BuildDocument doc in build.Documents) { if (nextDocuments.ContainsKey(doc.Document.FileName) == false) { nextDocuments[doc.Document.FileName] = doc.Document; } } } } } } //store after documents foreach (CodeDocument document in nextDocuments.Values) { TimelineCodeDocument tcd = new TimelineCodeDocument() { CodeDocumentId = document.Id, CommentTimeline = current.Timeline, DocumentContent = document.Content, DocumentName = document.FileName, isBeforeComment = false }; if(current.Timeline.TimelineCodeDocuments == null) { current.Timeline.TimelineCodeDocuments = new List<TimelineCodeDocument>(); } current.Timeline.TimelineCodeDocuments.Add(tcd); } } //try jamming all this crap into DB Db.CommentTimelines.AddRange(viewModel.Select(m => m.Timeline).ToList()); Db.SaveChanges(); } return View(viewModel); }