/// <summary> /// Gets the active threads visible to the user. /// </summary> /// <param name="accessableForums">A list of accessable forums IDs, which the user has permission to access.</param> /// <param name="hoursThreshold">The hours threshold for the query to fetch the active threads. All threads within this threshold's period of time (in hours) /// are fetched.</param> /// <param name="forumsWithThreadsFromOthers">The forums for which the calling user can view other users' threads. Can be null</param> /// <param name="userId">The userid of the calling user.</param> /// <returns>a list with objects representing the Active threads</returns> public static async Task <List <AggregatedThreadRow> > GetActiveThreadsAggregatedData(List <int> accessableForums, short hoursThreshold, List <int> forumsWithThreadsFromOthers, int userId) { if (accessableForums == null || accessableForums.Count <= 0) { return(null); } var qf = new QueryFactory(); var q = qf.Create() .Select <AggregatedThreadRow>(ThreadGuiHelper.BuildQueryProjectionForAllThreadsWithStatsWithForumName(qf).ToArray()) .From(ThreadGuiHelper.BuildFromClauseForAllThreadsWithStats(qf) .InnerJoin(qf.Forum).On(ThreadFields.ForumID.Equal(ForumFields.ForumID))) .Where(ThreadFields.ForumID.In(accessableForums) .And(ThreadFields.IsClosed.Equal(false)) .And(ThreadFields.MarkedAsDone.Equal(false)) .And(MessageFields.PostingDate.Source("LastMessage").GreaterEqual(DateTime.Now.AddHours((double)0 - hoursThreshold))) .And(ThreadGuiHelper.CreateThreadFilter(forumsWithThreadsFromOthers, userId))) .OrderBy(MessageFields.PostingDate.Source("LastMessage").Ascending()); using (var adapter = new DataAccessAdapter()) { return(await adapter.FetchQueryAsync(q).ConfigureAwait(false)); } }
/// <summary> /// Gets the active threads. /// </summary> /// <param name="accessableForums">A list of accessable forums IDs, which the user has permission to access.</param> /// <param name="hoursThreshold">The hours threshold for the query to fetch the active threads. All threads within this threshold's period of time (in hours) /// are fetched.</param> /// <param name="forumsWithOnlyOwnThreads">The forums for which the calling user can view other users' threads. Can be null</param> /// <param name="userID">The userid of the calling user.</param> /// <returns>a dataView of Active threads</returns> public static DataView GetActiveThreadsAsDataView(List <int> accessableForums, short hoursThreshold, List <int> forumsWithThreadsFromOthers, int userID) { if (accessableForums == null || accessableForums.Count <= 0) { return(null); } var qf = new QueryFactory(); var q = qf.Create() .Select(new List <object>(ThreadGuiHelper.BuildQueryProjectionForAllThreadsWithStats(qf)) { ForumFields.ForumName } .ToArray()) .From(ThreadGuiHelper.BuildFromClauseForAllThreadsWithStats(qf) .InnerJoin(qf.Forum).On(ThreadFields.ForumID == ForumFields.ForumID)) .Where((ThreadFields.ForumID == accessableForums) .And(ThreadFields.IsClosed == false) .And(ThreadFields.MarkedAsDone == false) .And(ThreadFields.ThreadLastPostingDate >= DateTime.Now.AddHours((double)0 - hoursThreshold)) .And(ThreadGuiHelper.CreateThreadFilter(forumsWithThreadsFromOthers, userID))) .OrderBy(ThreadFields.ThreadLastPostingDate.Ascending()); var dao = new TypedListDAO(); var activeThreads = dao.FetchAsDataTable(q); return(activeThreads.DefaultView); }
/// <summary> /// Gets the threads and accompanying statistics info, in the supportqueue specified. Only the threads which are in the forums in the list of /// accessable forums are returned. /// </summary> /// <param name="accessableForums">A list of accessable forums IDs, which the user has permission to access.</param> /// <param name="supportQueueID">The ID of the support queue to retrieve the threads for.</param> /// <returns>a dataView of Active threads</returns> public static DataView GetAllThreadsInSupportQueueAsDataView(List <int> accessableForums, int supportQueueID) { // return null, if the user does not have a valid list of forums to access if (accessableForums == null || accessableForums.Count <= 0) { return(null); } var qf = new QueryFactory(); var q = qf.Create(); var projectionFields = new List <object>(ThreadGuiHelper.BuildQueryProjectionForAllThreadsWithStats(qf)); projectionFields.AddRange(new[] { ForumFields.ForumName, UserFields.NickName.Source("PlacedInQueueUser").As("NickNamePlacedInQueue"), SupportQueueThreadFields.PlacedInQueueByUserID, SupportQueueThreadFields.PlacedInQueueOn, UserFields.NickName.Source("ClaimedThreadUser").As("NickNameClaimedThread"), SupportQueueThreadFields.ClaimedByUserID, SupportQueueThreadFields.ClaimedOn }); q.Select(projectionFields.ToArray()); q.From(ThreadGuiHelper.BuildFromClauseForAllThreadsWithStats(qf) .InnerJoin(qf.Forum).On(ThreadFields.ForumID == ForumFields.ForumID) .InnerJoin(qf.SupportQueueThread).On(ThreadFields.ThreadID == SupportQueueThreadFields.ThreadID) .InnerJoin(qf.User.As("PlacedInQueueUser")) .On(SupportQueueThreadFields.PlacedInQueueByUserID == UserFields.UserID.Source("PlacedInQueueUser")) .LeftJoin(qf.User.As("ClaimedThreadUser")) .On(SupportQueueThreadFields.ClaimedByUserID == UserFields.UserID.Source("ClaimedThreadUser"))); q.Where((ThreadFields.ForumID == accessableForums).And(SupportQueueThreadFields.QueueID == supportQueueID)); q.OrderBy(ThreadFields.ThreadLastPostingDate.Ascending()); TypedListDAO dao = new TypedListDAO(); var threadsInQueue = dao.FetchAsDataTable(q); return(threadsInQueue.DefaultView); }
/// <summary> /// Creates the attachment filter. The filter filters on attachments with the specified approvalstate in the threads viewable by the calling user. /// </summary> /// <param name="accessableForums">The accessable forums.</param> /// <param name="forumsWithApprovalRight">The forums with approval right.</param> /// <param name="forumsWithThreadsFromOthers">The forums with threads from others.</param> /// <param name="userID">The user ID.</param> /// <param name="filter">The filter.</param> /// <returns>ready to use predicate expression for fetch actions on attachments with the approval state specified in the threads /// matching the forumID's.</returns> private static PredicateExpression CreateAttachmentFilter(List <int> accessableForums, List <int> forumsWithApprovalRight, List <int> forumsWithThreadsFromOthers, int userID, bool approvalState) { PredicateExpression filter = new PredicateExpression(); // specify the filter for the accessable forums. Do this by a fieldcomparerange predicate and filter on Thread.ForumID. As 'accessableForums' is a list // the following statement will create a fieldcomparerange predicate for us. if (accessableForums.Count == 1) { // optimization, specify the only value instead of the range, so we won't get a WHERE Field IN (@param) query which is slow on some // databases, but we'll get a WHERE Field == @param filter.Add(ThreadFields.ForumID == accessableForums[0]); } else { filter.Add(ThreadFields.ForumID == accessableForums); } // specify the filter for the forums with approval rights: if (forumsWithApprovalRight.Count == 1) { // optimization, specify the only value instead of the range, so we won't get a WHERE Field IN (@param) query which is slow on some // databases, but we'll get a WHERE Field == @param filter.Add(ThreadFields.ForumID == forumsWithApprovalRight[0]); } else { filter.Add(ThreadFields.ForumID == forumsWithApprovalRight); } // Also filter on the threads viewable by the passed in userid, which is the caller of the method. If a forum isn't in the list of // forumsWithThreadsFromOthers, only the sticky threads and the threads started by userid should be counted / taken into account. filter.AddWithAnd(ThreadGuiHelper.CreateThreadFilter(forumsWithApprovalRight, userID)); // as last filter, we'll add a filter to only get the data for attachments which aren't approved yet. filter.AddWithAnd(AttachmentFields.Approved == false); return(filter); }
/// <summary> /// Subscribes the user specified to the thread specified for notifications. A transaction can be specified to save the information inside the /// transaction specified. If the user is already subscribed to this thread, nothing is done /// </summary> /// <param name="threadId">The thread ID.</param> /// <param name="userId">The user ID.</param> /// <param name="adapter">The live adapter with an active transaction. Can be null, in which case a local adapter is used.</param> /// <returns></returns> public static async Task <bool> AddThreadToSubscriptionsAsync(int threadId, int userId, IDataAccessAdapter adapter) { var localAdapter = adapter == null; var adapterToUse = adapter ?? new DataAccessAdapter(); try { // check if this user is already subscribed to this thread. If not, add a new subscription. if (await ThreadGuiHelper.GetThreadSubscriptionAsync(threadId, userId, adapterToUse) == null) { // user isn't yet subscribed, add the subscription return(await adapterToUse.SaveEntityAsync(new ThreadSubscriptionEntity { UserID = userId, ThreadID = threadId }).ConfigureAwait(false)); } // already subscribed, no-op. return(true); } finally { if (localAdapter) { adapterToUse.Dispose(); } } }
/// <summary> /// Gets the active threads with statistics. /// </summary> /// <param name="accessableForums">A list of accessable forums IDs, which the user has permission to access.</param> /// <param name="hoursThreshold">The hours threshold for the query to fetch the active threads. All threads within this threshold's period of time (in hours) /// are fetched.</param> /// <param name="forumsWithOnlyOwnThreads">The forums for which the calling user can view other users' threads. Can be null</param> /// <param name="userID">The userid of the calling user.</param> /// <returns> /// a dataTable of Active threads with statistics /// </returns> public static DataTable GetActiveThreadsStatisticsAsDataTable(List <int> accessableForums, short hoursThreshold, List <int> forumsWithThreadsFromOthers, int userID) { // return null, if the user does not have a valid list of forums to access if (accessableForums == null || accessableForums.Count <= 0) { return(null); } var qf = new QueryFactory(); var q = qf.Create() .Select(ThreadFields.ThreadID.CountDistinct().As("AmountThreads"), MessageFields.MessageID.Count().As("AmountPostings"), ThreadFields.ThreadLastPostingDate.Max().As("LastPostingDate")) .From(qf.Thread.InnerJoin(qf.Message).On(ThreadFields.ThreadID == MessageFields.ThreadID)) .Where((ThreadFields.ForumID == accessableForums) .And(ThreadFields.IsClosed == false) .And(ThreadFields.MarkedAsDone == false) .And(ThreadFields.ThreadLastPostingDate >= DateTime.Now.AddHours((double)0 - hoursThreshold)) .And(ThreadGuiHelper.CreateThreadFilter(forumsWithThreadsFromOthers, userID))); var dao = new TypedListDAO(); return(dao.FetchAsDataTable(q)); //// create dyn. list and pull statistics using that list. //ResultsetFields fields = new ResultsetFields(3); //fields.DefineField(ThreadFields.ThreadID, 0, "AmountThreads", string.Empty, AggregateFunction.CountDistinct); //fields.DefineField(MessageFields.MessageID, 1, "AmountPostings", string.Empty, AggregateFunction.Count); //fields.DefineField(ThreadFields.ThreadLastPostingDate, 2, "LastPostingDate", string.Empty, AggregateFunction.Max); //RelationCollection relations = new RelationCollection(); //relations.Add(ThreadEntity.Relations.MessageEntityUsingThreadID); //PredicateExpression filter = new PredicateExpression(); //// only the forums the user has access to //filter.Add(ThreadFields.ForumID == accessableForums.ToArray()); //// only the threads which are not closed //filter.AddWithAnd(ThreadFields.IsClosed == false); //// only the threads which are active (== not done) //filter.AddWithAnd(ThreadFields.MarkedAsDone == false); //// only threads which have been updated in the last Globals.HoursForActiveThreadsTreshold hours //filter.AddWithAnd(ThreadFields.ThreadLastPostingDate >= DateTime.Now.AddHours((double)0 - hoursThreshold)); //// Also filter on the threads viewable by the passed in userid, which is the caller of the method. If a forum isn't in the list of //// forumsWithThreadsFromOthers, only the sticky threads and the threads started by userid should be counted / taken into account. //IPredicateExpression threadFilter = ThreadGuiHelper.CreateThreadFilter(forumsWithThreadsFromOthers, userID); //filter.AddWithAnd(threadFilter); //TypedListDAO dao = new TypedListDAO(); //DataTable toReturn = new DataTable(); //dao.GetMultiAsDataTable(fields, toReturn, 0, null, filter, relations, true, null, null, 0, 0); //return toReturn; }
/// <summary> /// Marks the thread as done. /// </summary> /// <param name="threadID">Thread ID.</param> /// <returns></returns> public static bool MarkThreadAsDone(int threadID) { // load the entity from the database ThreadEntity thread = ThreadGuiHelper.GetThread(threadID); if (thread == null) { // not found return(false); } // get the support queue the thread is in (if any) SupportQueueEntity containingSupportQueue = SupportQueueGuiHelper.GetQueueOfThread(threadID); thread.MarkedAsDone = true; // if the thread is in a support queue, the thread has to be removed from that queue. This is a multi-entity action and therefore we've to start a // transaction if that's the case. If not, we can use the easy route and simply save the thread and be done with it. if (containingSupportQueue == null) { // not in a queue, simply save the thread. return(thread.Save()); } // in a queue, so remove from the queue and save the entity. Transaction trans = new Transaction(IsolationLevel.ReadCommitted, "MarkThreadDone"); trans.Add(thread); try { // save the thread bool result = thread.Save(); if (result) { // save succeeded, so remove from queue, pass the current transaction to the method so the action takes place inside this transaction. SupportQueueManager.RemoveThreadFromQueue(threadID, trans); } trans.Commit(); return(true); } catch { // rollback transaction trans.Rollback(); throw; } finally { trans.Dispose(); } }
/// <summary> /// Unsubscribes the specified user from the specified thread. /// </summary> /// <param name="threadID">The thread ID.</param> /// <param name="userID">The user ID.</param> /// <returns>true if delete succeeded, false otherwise</returns> public static bool RemoveSingleSubscription(int threadID, int userID) { ThreadSubscriptionEntity subscription = ThreadGuiHelper.GetThreadSubscription(threadID, userID); if (subscription != null) { // there's a subscription, delete it return(subscription.Delete()); } else { return(true); } }
/// <summary> /// Unsubscribes the specified user from the specified thread. /// </summary> /// <param name="threadId">The thread ID.</param> /// <param name="userId">The user ID.</param> /// <returns>true if delete succeeded, false otherwise</returns> public static async Task <bool> RemoveSingleSubscriptionAsync(int threadId, int userId) { var subscription = await ThreadGuiHelper.GetThreadSubscriptionAsync(threadId, userId); if (subscription != null) { // there's a subscription, delete it using (var adapter = new DataAccessAdapter()) { return(await adapter.DeleteEntityAsync(subscription).ConfigureAwait(false)); } } return(true); }
/// <summary> /// Marks the thread as done. /// </summary> /// <param name="threadId">Thread ID.</param> /// <returns></returns> public static async Task <bool> MarkThreadAsDoneAsync(int threadId) { var thread = await ThreadGuiHelper.GetThreadAsync(threadId); if (thread == null) { // not found return(false); } var containingSupportQueue = await SupportQueueGuiHelper.GetQueueOfThreadAsync(threadId); thread.MarkedAsDone = true; using (var adapter = new DataAccessAdapter()) { // if the thread is in a support queue, the thread has to be removed from that queue. This is a multi-entity action and therefore we've to start a // transaction if that's the case. If not, we can use the easy route and simply save the thread and be done with it. if (containingSupportQueue == null) { // not in a queue, simply save the thread. return(await adapter.SaveEntityAsync(thread).ConfigureAwait(false)); } // in a queue, so remove from the queue and save the entity. await adapter.StartTransactionAsync(IsolationLevel.ReadCommitted, "MarkThreadDone").ConfigureAwait(false); try { // save the thread var result = await adapter.SaveEntityAsync(thread).ConfigureAwait(false); if (result) { // save succeeded, so remove from queue, pass the current adapter to the method so the action takes place inside this transaction. await SupportQueueManager.RemoveThreadFromQueueAsync(threadId, adapter); } adapter.Commit(); return(true); } catch { // rollback transaction adapter.Rollback(); throw; } } }
/// <summary> /// Updates the memo field for the given thread /// </summary> /// <param name="threadID">Thread ID.</param> /// <param name="memo">Memo.</param> /// <returns></returns> public static bool UpdateMemo(int threadID, string memo) { // load the entity from the database ThreadEntity thread = ThreadGuiHelper.GetThread(threadID); if (thread == null) { // not found return(false); } thread.Memo = memo; //update the entity in the database return(thread.Save()); }
/// <summary> /// Moves the given thread to the given forum. /// </summary> /// <param name="threadId">ID of thread to move</param> /// <param name="newForumId">ID of forum to move the thread to</param> /// <returns></returns> public static async Task <bool> MoveThreadAsync(int threadId, int newForumId) { var thread = await ThreadGuiHelper.GetThreadAsync(threadId); if (thread == null) { // not found return(false); } thread.ForumID = newForumId; using (var adapter = new DataAccessAdapter()) { return(await adapter.SaveEntityAsync(thread).ConfigureAwait(false)); } }
/// <summary> /// Moves the given thread to the given forum. /// </summary> /// <param name="threadID">ID of thread to move</param> /// <param name="newForumID">ID of forum to move the thread to</param> /// <returns></returns> public static bool MoveThread(int threadID, int newForumID) { // load the entity from the database ThreadEntity thread = ThreadGuiHelper.GetThread(threadID); if (thread == null) { // not found return(false); } // update the ForumID field with the new value thread.ForumID = newForumID; // save the updated entity back to the database return(thread.Save()); }
/// <summary> /// Gets the bookmarks with statistics for the user specified. /// </summary> /// <param name="userID">User ID.</param> /// <returns></returns> public static async Task <List <AggregatedThreadRow> > GetBookmarksAggregatedDataAsync(int userID) { var qf = new QueryFactory(); var q = qf.Create() .Select <AggregatedThreadRow>(ThreadGuiHelper.BuildQueryProjectionForAllThreadsWithStatsWithForumName(qf).ToArray()) .From(ThreadGuiHelper.BuildFromClauseForAllThreadsWithStats(qf) .InnerJoin(qf.Forum).On(ThreadFields.ForumID.Equal(ForumFields.ForumID))) .Where(ThreadFields.ThreadID.In(qf.Create() .Select(BookmarkFields.ThreadID) .Where(BookmarkFields.UserID.Equal(userID)))) .OrderBy(MessageFields.PostingDate.Source("LastMessage").Descending()); using (var adapter = new DataAccessAdapter()) { return(await adapter.FetchQueryAsync(q).ConfigureAwait(false)); } }
/// <summary> /// Gets the last pageSize threads in which the user specified participated with one or more messages for the page specified. /// Threads which aren't visible for the calling user are filtered out. If pageNumber is 0, pageSize is used to limit the list to the pageSize /// </summary> /// <param name="accessableForums">A list of accessable forums IDs, which the user calling the method has permission to access.</param> /// <param name="participantUserId">The participant user ID of the user of which the threads have to be obtained.</param> /// <param name="forumsWithThreadsFromOthers">The forums with threads from others.</param> /// <param name="callingUserId">The calling user ID.</param> /// <param name="pageSize">Size of the page.</param> /// <param name="pageNumber">The page number to fetch.</param> /// <returns>a list with objects representing the last threads for the user</returns> public static async Task <List <AggregatedThreadRow> > GetLastThreadsForUserAggregatedDataAsync(List <int> accessableForums, int participantUserId, List <int> forumsWithThreadsFromOthers, int callingUserId, int pageSize, int pageNumber) { // return null, if the user does not have a valid list of forums to access if (accessableForums == null || accessableForums.Count <= 0) { return(null); } var numberOfThreadsToFetch = pageSize; if (numberOfThreadsToFetch <= 0) { numberOfThreadsToFetch = 25; } var qf = new QueryFactory(); var q = qf.Create() .Select <AggregatedThreadRow>(ThreadGuiHelper.BuildQueryProjectionForAllThreadsWithStatsWithForumName(qf).ToArray()) .From(ThreadGuiHelper.BuildFromClauseForAllThreadsWithStats(qf) .InnerJoin(qf.Forum).On(ThreadFields.ForumID.Equal(ForumFields.ForumID))) .Where(ThreadFields.ForumID.In(accessableForums) .And(ThreadFields.ThreadID.In(qf.Create() .Select(MessageFields.ThreadID) .Where(MessageFields.PostedByUserID.Equal(participantUserId)))) .And(ThreadGuiHelper.CreateThreadFilter(forumsWithThreadsFromOthers, callingUserId))) .OrderBy(MessageFields.PostingDate.Source("LastMessage").Descending()); if (pageNumber <= 0) { // no paging // get the last numberOfThreadsToFetch, so specify a limit equal to the numberOfThreadsToFetch specified q.Limit(numberOfThreadsToFetch); } else { // use paging q.Page(pageNumber, numberOfThreadsToFetch); } using (var adapter = new DataAccessAdapter()) { return(await adapter.FetchQueryAsync(q).ConfigureAwait(false)); } }
/// <summary> /// Updates the memo field for the given thread /// </summary> /// <param name="threadId">Thread ID.</param> /// <param name="memo">Memo.</param> /// <returns></returns> public static async Task <bool> UpdateMemoAsync(int threadId, string memo) { // load the entity from the database var thread = await ThreadGuiHelper.GetThreadAsync(threadId); if (thread == null) { // not found return(false); } using (var adapter = new DataAccessAdapter()) { thread.Memo = memo; return(await adapter.SaveEntityAsync(thread).ConfigureAwait(false)); } }
/// <summary> /// Gets the bookmarks with statistics for the user specified. /// </summary> /// <param name="userID">User ID.</param> /// <returns></returns> public static DataView GetBookmarksAsDataView(int userID) { var qf = new QueryFactory(); var q = qf.Create() .Select(new List <object>(ThreadGuiHelper.BuildQueryProjectionForAllThreadsWithStats(qf)) { ForumFields.ForumName, ForumFields.SectionID }.ToArray()) .From(ThreadGuiHelper.BuildFromClauseForAllThreadsWithStats(qf) .InnerJoin(qf.Forum).On(ThreadFields.ForumID == ForumFields.ForumID)) .Where(ThreadFields.ThreadID.In(qf.Create().Select(BookmarkFields.ThreadID).Where(BookmarkFields.UserID == userID))) .OrderBy(ThreadFields.ThreadLastPostingDate.Descending()); var dao = new TypedListDAO(); var bookmarkedThreads = dao.FetchAsDataTable(q); return(bookmarkedThreads.DefaultView); }
/// <summary> /// Modifies the given properties of the given thread. /// </summary> /// <param name="threadID">The threadID of the thread which properties should be changed</param> /// <param name="subject">The new subject for this thread</param> /// <param name="isSticky">The new value for IsSticky</param> /// <param name="isClosed">The new value for IsClosed</param> /// <returns></returns> public static bool ModifyThreadProperties(int threadID, string subject, bool isSticky, bool isClosed) { // load the entity from the database ThreadEntity thread = ThreadGuiHelper.GetThread(threadID); if (thread == null) { // not found return(false); } // update the fields with new values thread.Subject = subject; thread.IsSticky = isSticky; thread.IsClosed = isClosed; // save the updated entity back to the database return(thread.Save()); }
/// <summary> /// Gets the last pageSize threads in which the user specified participated with one or more messages for the page specified. /// Threads which aren't visible for the calling user are filtered out. If pageNumber is 0, pageSize is used to limit the list to the pageSize /// </summary> /// <param name="accessableForums">A list of accessable forums IDs, which the user calling the method has permission to access.</param> /// <param name="participantUserID">The participant user ID of the user of which the threads have to be obtained.</param> /// <param name="forumsWithThreadsFromOthers">The forums with threads from others.</param> /// <param name="callingUserID">The calling user ID.</param> /// <param name="pageSize">Size of the page.</param> /// <param name="pageNumber">The page number to fetch.</param> /// <returns>a dataView of the threads requested</returns> public static DataView GetLastThreadsForUserAsDataView(List <int> accessableForums, int participantUserID, List <int> forumsWithThreadsFromOthers, int callingUserID, int pageSize, int pageNumber) { // return null, if the user does not have a valid list of forums to access if (accessableForums == null || accessableForums.Count <= 0) { return(null); } int numberOfThreadsToFetch = pageSize; if (numberOfThreadsToFetch <= 0) { numberOfThreadsToFetch = 25; } var qf = new QueryFactory(); var q = qf.Create() .Select(ThreadGuiHelper.BuildQueryProjectionForAllThreadsWithStats(qf)) .From(ThreadGuiHelper.BuildFromClauseForAllThreadsWithStats(qf)) .Where((ThreadFields.ForumID == accessableForums) .And(ThreadFields.ThreadID.In(qf.Create() .Select(MessageFields.ThreadID) .Where(MessageFields.PostedByUserID == participantUserID))) .And(ThreadGuiHelper.CreateThreadFilter(forumsWithThreadsFromOthers, callingUserID))) .OrderBy(ThreadFields.ThreadLastPostingDate.Descending()); if (pageNumber <= 0) { // no paging // get the last numberOfThreadsToFetch, so specify a limit equal to the numberOfThreadsToFetch specified q.Limit(numberOfThreadsToFetch); } else { // use paging q.Page(pageNumber, numberOfThreadsToFetch); } var dao = new TypedListDAO(); var lastThreads = dao.FetchAsDataTable(q); return(lastThreads.DefaultView); }
/// <summary> /// Marks the thread as un-done, and add it to the default queue of the forum. /// </summary> /// <param name="threadID">Thread ID</param> /// <returns></returns> public static bool UnMarkThreadAsDone(int threadID, int userID) { // load the entity from the database ThreadEntity thread = ThreadGuiHelper.GetThread(threadID); if (thread == null) { // not found return(false); } thread.MarkedAsDone = false; ForumEntity forum = new ForumEntity(thread.ForumID); // Save the thread and add it to the default queue of the forum. Transaction trans = new Transaction(IsolationLevel.ReadCommitted, "MarkThreadUnDone"); trans.Add(thread); try { thread.Save(); if ((forum.Fields.State == EntityState.Fetched) && (forum.DefaultSupportQueueID.HasValue)) { // not in a queue, and the forum has a default queue. Add the thread to the queue of the forum SupportQueueManager.AddThreadToQueue(threadID, forum.DefaultSupportQueueID.Value, userID, trans); } trans.Commit(); return(true); } catch { // rollback transaction trans.Rollback(); throw; } finally { trans.Dispose(); } }
/// <summary> /// Subscribes the user specified to the thread specified for notifications. A transaction can be specified to save the information inside the /// transaction specified. If the user is already subscribed to this thread, nothing is done /// </summary> /// <param name="threadID">The thread ID.</param> /// <param name="userID">The user ID.</param> /// <param name="transactionToUse">The transaction to use. If no transaction is specified, no transaction is created</param> /// <returns></returns> public static bool AddThreadToSubscriptions(int threadID, int userID, Transaction transactionToUse) { // check if this user is already subscribed to this thread. If not, add a new subscription. ThreadSubscriptionEntity subscription = ThreadGuiHelper.GetThreadSubscription(threadID, userID, transactionToUse); if (subscription == null) { // user isn't yet subscribed, add the subscription subscription = new ThreadSubscriptionEntity(); subscription.UserID = userID; subscription.ThreadID = threadID; if (transactionToUse != null) { transactionToUse.Add(subscription); } return(subscription.Save()); } // already subscribed, no-op. return(true); }
/// <summary> /// Gets the row count for the set of threads in which the user specified participated with one or more messages for the page specified. /// Threads which aren't visible for the calling user are filtered out. /// </summary> /// <param name="accessableForums">A list of accessable forums IDs, which the user calling the method has permission to access.</param> /// <param name="participantUserID">The participant user ID of the user of which the threads have to be obtained.</param> /// <param name="forumsWithThreadsFromOthers">The forums with threads from others.</param> /// <param name="callingUserID">The calling user ID.</param> /// <returns>a dataView of the threads requested</returns> public static int GetRowCountLastThreadsForUserAsDataView(List <int> accessableForums, int participantUserID, List <int> forumsWithThreadsFromOthers, int callingUserID) { // return null, if the user does not have a valid list of forums to access if (accessableForums == null || accessableForums.Count <= 0) { return(0); } var qf = new QueryFactory(); var q = qf.Create() .Select(ThreadGuiHelper.BuildQueryProjectionForAllThreadsWithStats(qf)) .From(ThreadGuiHelper.BuildFromClauseForAllThreadsWithStats(qf)) .Where((ThreadFields.ForumID == accessableForums) .And(ThreadFields.ThreadID.In(qf.Create() .Select(MessageFields.ThreadID) .Where(MessageFields.PostedByUserID == participantUserID))) .And(ThreadGuiHelper.CreateThreadFilter(forumsWithThreadsFromOthers, callingUserID))); var dao = new TypedListDAO(); return(dao.GetScalar <int>(qf.Create().Select(Functions.CountRow()).From(q), null)); }
/// <summary> /// Marks the thread as un-done, and add it to the default queue of the forum. /// </summary> /// <param name="threadId">Thread ID</param> /// <param name="userId">the user adding the thread to a queue by unmarking it as done </param> /// <returns></returns> public static async Task <bool> UnMarkThreadAsDoneAsync(int threadId, int userId) { var thread = await ThreadGuiHelper.GetThreadAsync(threadId); if (thread == null) { // not found return(false); } thread.MarkedAsDone = false; using (var adapter = new DataAccessAdapter()) { // Save the thread and add it to the default queue of the forum. await adapter.StartTransactionAsync(IsolationLevel.ReadCommitted, "MarkThreadUnDone").ConfigureAwait(false); try { await adapter.SaveEntityAsync(thread, true).ConfigureAwait(false); var q = new QueryFactory().Forum.Where(ForumFields.ForumID.Equal(thread.ForumID)); var forum = await adapter.FetchFirstAsync(q).ConfigureAwait(false); if (forum?.DefaultSupportQueueID != null) { // not in a queue, and the forum has a default queue. Add the thread to the queue of the forum await SupportQueueManager.AddThreadToQueueAsync(threadId, forum.DefaultSupportQueueID.Value, userId, adapter); } adapter.Commit(); return(true); } catch { // rollback transaction adapter.Rollback(); throw; } } }
/// <summary> /// Gets the threads and accompanying statistics info, in the supportqueues specified. Only the threads which are in the forums in the list of /// accessable forums are returned. /// </summary> /// <param name="accessableForums">A list of accessable forums IDs, which the user has permission to access.</param> /// <param name="supportQueueIds">The support queue IDs to obtain the threads info for.</param> /// <returns> /// a list of aggregated support queue contents rows, one per thread, or an empty list if no forums were accessible. /// </returns> public static async Task <List <AggregatedSupportQueueContentsRow> > GetAllThreadsInSpecifiedSupportQueuesAsync(List <int> accessableForums, int[] supportQueueIds) { // return null, if the user does not have a valid list of forums to access if (accessableForums == null || accessableForums.Count <= 0) { return(new List <AggregatedSupportQueueContentsRow>()); } var qf = new QueryFactory(); var projectionFields = new List <object>(ThreadGuiHelper.BuildQueryProjectionForAllThreadsWithStatsWithForumName(qf)); projectionFields.AddRange(new[] { SupportQueueThreadFields.QueueID, UserFields.NickName.Source("PlacedInQueueUser").As("PlacedInQueueByNickName"), SupportQueueThreadFields.PlacedInQueueByUserID, SupportQueueThreadFields.PlacedInQueueOn, UserFields.NickName.Source("ClaimedThreadUser").As("ClaimedByNickName"), SupportQueueThreadFields.ClaimedByUserID, SupportQueueThreadFields.ClaimedOn }); var q = qf.Create() .Select <AggregatedSupportQueueContentsRow>(projectionFields.ToArray()) .From(ThreadGuiHelper.BuildFromClauseForAllThreadsWithStats(qf) .InnerJoin(qf.Forum).On(ThreadFields.ForumID.Equal(ForumFields.ForumID)) .InnerJoin(qf.SupportQueueThread).On(ThreadFields.ThreadID.Equal(SupportQueueThreadFields.ThreadID)) .InnerJoin(qf.User.As("PlacedInQueueUser")) .On(SupportQueueThreadFields.PlacedInQueueByUserID.Equal(UserFields.UserID.Source("PlacedInQueueUser"))) .LeftJoin(qf.User.As("ClaimedThreadUser")) .On(SupportQueueThreadFields.ClaimedByUserID.Equal(UserFields.UserID.Source("ClaimedThreadUser")))) .Where(ThreadFields.ForumID.In(accessableForums.ToArray()).And(SupportQueueThreadFields.QueueID.In(supportQueueIds))) .OrderBy(SupportQueueThreadFields.QueueID.Ascending(), MessageFields.PostingDate.Source("LastMessage").Ascending()); using (var adapter = new DataAccessAdapter()) { return(await adapter.FetchQueryAsync(q).ConfigureAwait(false)); } }
/// <summary> /// Returns a list with aggregated data objects, one per thread, for the requested forum and page /// </summary> /// <param name="forumId">ID of Forum for which the Threadlist is required</param> /// <param name="pageNumber">The page number to fetch, which is used to fetch non-sticky posts</param> /// <param name="pageSize">The number of rows to fetch for the page. </param> /// <param name="canViewNormalThreadsStartedByOthers">If set to true, the user calling the method has the right to view threads started by others. /// Otherwise only the threads started by the user calling the method are returned.</param> /// <param name="userId">The userid of the user calling the method.</param> /// <returns>List with all the thread info, aggregated. Sticky threads are sorted to the top.</returns> public static async Task <List <AggregatedThreadRow> > GetAllThreadsInForumAggregatedDataAsync(int forumId, int pageNumber, int pageSize, bool canViewNormalThreadsStartedByOthers, int userId) { // create a query which always fetches the sticky threads, and besides those the threads which are visible to the user. // then sort the sticky threads at the top and page through the resultset. var qf = new QueryFactory(); var offsetStart = pageSize * (pageNumber - 1); if (offsetStart < 0) { offsetStart = 0; } var q = qf.Create() .From(ThreadGuiHelper.BuildFromClauseForAllThreadsWithStats(qf)) .Where(ThreadFields.ForumID.Equal(forumId)) .OrderBy(ThreadFields.IsSticky.Descending(), MessageFields.PostingDate.Source("LastMessage").Descending()) .Select <AggregatedThreadRow>(ThreadGuiHelper.BuildQueryProjectionForAllThreadsWithStats(qf).ToArray()) .Offset(offsetStart) // skip the pages we don't need. .Limit(pageSize + 1); // fetch 1 row extra, which we can use to determine whether there are more pages left. // if the user can't view threads started by others, filter out threads started by users different from userID. Otherwise just filter on forumid and stickyness. if (!canViewNormalThreadsStartedByOthers) { // caller can't view threads started by others: add a filter so that threads not started by calling user aren't enlisted. // however sticky threads are always returned so the filter contains a check so the limit is only applied on threads which aren't sticky // add a filter for sticky threads, add it with 'OR', so sticky threads are always accepted. The whole expression is and-ed to the already existing // expression q.AndWhere((ThreadFields.StartedByUserID.Equal(userId)).Or(ThreadFields.IsSticky.Equal(true))); } using (var adapter = new DataAccessAdapter()) { var toReturn = await adapter.FetchQueryAsync(q).ConfigureAwait(false); return(toReturn); } }
/// <summary> /// Modifies the given properties of the given thread. /// </summary> /// <param name="threadId">The threadID of the thread which properties should be changed</param> /// <param name="subject">The new subject for this thread</param> /// <param name="isSticky">The new value for IsSticky</param> /// <param name="isClosed">The new value for IsClosed</param> public static async Task ModifyThreadPropertiesAsync(int threadId, string subject, bool isSticky, bool isClosed) { var thread = await ThreadGuiHelper.GetThreadAsync(threadId); if (thread == null) { // not found return; } // update the fields with new values thread.Subject = subject; thread.IsSticky = isSticky; thread.IsClosed = isClosed; using (var adapter = new DataAccessAdapter()) { await adapter.StartTransactionAsync(IsolationLevel.ReadCommitted, "ModifyThreadProperties"); try { var result = await adapter.SaveEntityAsync(thread).ConfigureAwait(false); if (result) { // save succeeded, so remove from queue, pass the current transaction to the method so the action takes place inside this transaction. await SupportQueueManager.RemoveThreadFromQueueAsync(threadId, adapter); } adapter.Commit(); } catch { adapter.Rollback(); throw; } } }
/// <summary> /// Does the search using MS Full text search /// </summary> /// <param name="searchString">Search string.</param> /// <param name="forumIDs">Forum Ids of forums to search into.</param> /// <param name="orderFirstElement">Order first element setting.</param> /// <param name="orderSecondElement">Order second element setting.</param> /// <param name="forumsWithThreadsFromOthers">The forums with threads from others.</param> /// <param name="userID">The userid of the calling user.</param> /// <param name="targetToSearch">The target to search.</param> /// <returns> /// TypedList filled with threads matching the query. /// </returns> public static SearchResultTypedList DoSearch(string searchString, List <int> forumIDs, SearchResultsOrderSetting orderFirstElement, SearchResultsOrderSetting orderSecondElement, List <int> forumsWithThreadsFromOthers, int userID, SearchTarget targetToSearch) { // the search utilizes full text search. It performs a CONTAINS upon the MessageText field of the Message entity. string searchTerms = PrepareSearchTerms(searchString); bool searchMessageText = (targetToSearch == SearchTarget.MessageText) || (targetToSearch == SearchTarget.MessageTextAndThreadSubject); bool searchSubject = (targetToSearch == SearchTarget.ThreadSubject) || (targetToSearch == SearchTarget.MessageTextAndThreadSubject); if (!(searchSubject || searchMessageText)) { // no target specified, select message searchMessageText = true; } PredicateExpression searchTermFilter = new PredicateExpression(); if (searchMessageText) { // Message contents filter searchTermFilter.Add(new FieldCompareSetPredicate(ThreadFields.ThreadID, MessageFields.ThreadID, SetOperator.In, new FieldFullTextSearchPredicate(MessageFields.MessageText, FullTextSearchOperator.Contains, searchTerms))); } if (searchSubject) { // Thread subject filter if (searchMessageText) { searchTermFilter.AddWithOr(new FieldFullTextSearchPredicate(ThreadFields.Subject, FullTextSearchOperator.Contains, searchTerms)); } else { searchTermFilter.Add(new FieldFullTextSearchPredicate(ThreadFields.Subject, FullTextSearchOperator.Contains, searchTerms)); } } IPredicateExpression mainFilter = searchTermFilter .And(ForumFields.ForumID == forumIDs) .And(ThreadGuiHelper.CreateThreadFilter(forumsWithThreadsFromOthers, userID)); ISortExpression sorter = new SortExpression(); // add first element sorter.Add(CreateSearchSortClause(orderFirstElement)); if (orderSecondElement != orderFirstElement) { sorter.Add(CreateSearchSortClause(orderSecondElement)); } SearchResultTypedList results = new SearchResultTypedList(false); try { // get the data from the db. results.Fill(500, sorter, false, mainFilter); } catch { // probably an error with the search words / user error. Swallow for now, which will result in an empty resultset. } return(results); }
/// <summary> /// Retrieves for all available sections all forums with all relevant statistical information. This information is stored per forum in an /// AggregatedForumRow instance. The forum instances are indexed under their sectionid. Only forums which are vieable by the user are returned. /// </summary> /// <param name="availableSections">SectionCollection with all available sections</param> /// <param name="accessableForums">List of accessable forums IDs.</param> /// <param name="forumsWithThreadsFromOthers">The forums for which the calling user can view other users' threads. Can be null</param> /// <param name="userId">The userid of the calling user.</param> /// <returns> /// MultiValueHashtable with per key (sectionID) a set of AggregatedForumRow instance, one row per forum in the section. If a section contains no forums /// displayable to the user it's not present in the returned hashtable. /// </returns> public static async Task <MultiValueHashtable <int, AggregatedForumRow> > GetAllAvailableForumsAggregatedData(EntityCollection <SectionEntity> availableSections, List <int> accessableForums, List <int> forumsWithThreadsFromOthers, int userId) { var toReturn = new MultiValueHashtable <int, AggregatedForumRow>(); // return an empty list, if the user does not have a valid list of forums to access if (accessableForums == null || accessableForums.Count <= 0) { return(toReturn); } // fetch all forums with statistics in a dynamic list, while filtering on the list of accessable forums for this user. // Create the filter separate of the query itself, as it's re-used multiple times. var threadFilter = ThreadGuiHelper.CreateThreadFilter(forumsWithThreadsFromOthers, userId); var qf = new QueryFactory(); var q = qf.Create() .Select(() => new AggregatedForumRow() { ForumID = ForumFields.ForumID.ToValue <int>(), ForumName = ForumFields.ForumName.ToValue <string>(), ForumDescription = ForumFields.ForumDescription.ToValue <string>(), ForumLastPostingDate = ForumFields.ForumLastPostingDate.ToValue <DateTime?>(), // add a scalar query which retrieves the # of threads in the specific forum. // this will result in the query: // ( // SELECT COUNT(ThreadID) FROM Thread // WHERE ForumID = Forum.ForumID AND threadfilter. // ) As AmountThreads AmountThreads = qf.Create() .Select(ThreadFields.ThreadID.Count()) .CorrelatedOver(ThreadFields.ForumID.Equal(ForumFields.ForumID)) .Where(threadFilter) .ToScalar().As("AmountThreads").ToValue <int>(), // add a scalar query which retrieves the # of messages in the threads of this forum. // this will result in the query: // ( // SELECT COUNT(MessageID) FROM Message // WHERE ThreadID IN // ( // SELECT ThreadID FROM Thread WHERE ForumID = Forum.ForumID AND threadfilter // ) // ) AS AmountMessages AmountMessages = qf.Create() .Select(MessageFields.MessageID.Count()) .Where(MessageFields.ThreadID.In( qf.Create() .Select(ThreadFields.ThreadID) .CorrelatedOver(ThreadFields.ForumID.Equal(ForumFields.ForumID)) .Where(threadFilter))) .ToScalar().As("AmountMessages").ToValue <int>(), HasRSSFeed = ForumFields.HasRSSFeed.ToValue <bool>(), SectionID = ForumFields.SectionID.ToValue <int>() }) .Where(ForumFields.ForumID.In(accessableForums)) .OrderBy(ForumFields.OrderNo.Ascending(), ForumFields.ForumName.Ascending()) .CacheResultset(Globals.DefaultCacheDurationOfResultsets); using (var adapter = new DataAccessAdapter()) { var results = await adapter.FetchQueryAsync(q).ConfigureAwait(false); foreach (var forum in results) { toReturn.Add(forum.SectionID, forum); } } return(toReturn); }
/// <summary> /// Sends email to all users subscribed to a specified thread, except the user who initiated the thread update. /// </summary> /// <param name="threadId">The thread that was updated.</param> /// <param name="initiatedByUserId">The user who initiated the update (who will not receive notification).</param> /// <param name="emailData">The email data.</param> private static async Task SendThreadReplyNotifications(int threadId, int initiatedByUserId, Dictionary <string, string> emailData) { // get list of subscribers to thread, minus the initiator. Do this by fetching the subscriptions plus the related user entity entity instances. // The related user entities are loaded using a prefetch path. var q = new QueryFactory().ThreadSubscription .Where((ThreadSubscriptionFields.ThreadID.Equal(threadId)).And(ThreadSubscriptionFields.UserID.NotEqual(initiatedByUserId))) .WithPath(ThreadSubscriptionEntity.PrefetchPathUser); var subscriptions = new EntityCollection <ThreadSubscriptionEntity>(); using (var adapter = new DataAccessAdapter()) { await adapter.FetchQueryAsync(q, subscriptions).ConfigureAwait(false); if (subscriptions.Count <= 0) { // no subscriptions, nothing to do return; } } // now collect all email addresses into an array so we can pass that to the email routine. var toAddresses = new string[subscriptions.Count]; for (var i = 0; i < subscriptions.Count; i++) { toAddresses[i] = subscriptions[i].User.EmailAddress; } // use template to construct message to send. var emailTemplate = emailData.GetValue("emailTemplate") ?? string.Empty; var mailBody = SD.HnD.Utility.StringBuilderCache.Acquire(emailTemplate.Length + 256); mailBody.Append(emailTemplate); var applicationURL = emailData.GetValue("applicationURL") ?? string.Empty; if (!string.IsNullOrEmpty(emailTemplate)) { // Use the existing template to format the body var siteName = emailData.GetValue("siteName") ?? string.Empty; var thread = await ThreadGuiHelper.GetThreadAsync(threadId); if (thread == null) { // thread doesn't exist, exit return; } var threadSubject = thread.Subject; var threadUrl = string.Format("{0}Thread/{1}", applicationURL, thread.ThreadID); mailBody.Replace("[SiteURL]", applicationURL); mailBody.Replace("[SiteName]", siteName); mailBody.Replace("[ThreadSubject]", threadSubject); mailBody.Replace("[ThreadURL]", threadUrl); } // format the subject var subject = (emailData.GetValue("emailThreadNotificationSubject") ?? string.Empty); var fromAddress = emailData.GetValue("defaultFromEmailAddress") ?? string.Empty; try { //send message await HnDGeneralUtils.SendEmail(subject, SD.HnD.Utility.StringBuilderCache.GetStringAndRelease(mailBody), fromAddress, toAddresses, emailData) .ConfigureAwait(false); } catch (SmtpFailedRecipientsException) { // swallow as it shouldn't have any effect on further operations } catch (SmtpException) { // swallow, as there's nothing we can do } catch (SmtpCommandException) { // recipient didn't exist, forget it. } // rest: problematic, so bubble upwards. }
/// <summary> /// Returns a DataView object that contains a complete list of threads list for /// the requested forum and required date & time interval /// </summary> /// <param name="forumID">ID of Forum for which the Threadlist is required</param> /// <param name="limiter">Limits the Threadlist to between now and; last 48 Hrs, Last Week, Last Month, Last Year</param> /// <param name="minNumberOfThreadsToFetch">The minimum number of threads to fetch if there are less threads available in the limiter interval</param> /// <param name="minNumberOfNonStickyVisibleThreads">The minimum number of non-sticky visible threads to show. If the # of threads is lower than /// this number (due to the limiter value), the minNumberOfThreadsToFetch are fetched</param> /// <param name="canViewNormalThreadsStartedByOthers">If set to true, the user calling the method has the right to view threads started by others. /// Otherwise only the threads started by the user calling the method are returned.</param> /// <param name="userID">The userid of the user calling the method.</param> /// <returns>DataView with all the threads</returns> public static DataView GetAllThreadsInForumAsDataView(int forumID, ThreadListInterval limiter, short minNumberOfThreadsToFetch, short minNumberOfNonStickyVisibleThreads, bool canViewNormalThreadsStartedByOthers, int userID) { DateTime limiterDate; // convert the limiter enum to a datetime which we can use in the filters on the thread data, where we'll use the limiter date // as a filter for the last posting date of a post in a given thread. switch (limiter) { case ThreadListInterval.Last24Hours: limiterDate = DateTime.Today.AddHours(-24); break; case ThreadListInterval.Last48Hours: limiterDate = DateTime.Today.AddHours(-48); break; case ThreadListInterval.LastWeek: limiterDate = DateTime.Today.AddDays(-7); break; case ThreadListInterval.LastMonth: limiterDate = DateTime.Today.AddMonths(-1); break; case ThreadListInterval.LastYear: limiterDate = DateTime.Today.AddYears(-1); break; default: limiterDate = DateTime.Today.AddHours(-48); break; } var qf = new QueryFactory(); var q = qf.Create(); q.Select(ThreadGuiHelper.BuildQueryProjectionForAllThreadsWithStats(qf)); q.From(ThreadGuiHelper.BuildFromClauseForAllThreadsWithStats(qf)); q.Where(((ThreadFields.IsSticky == true).Or(ThreadFields.ThreadLastPostingDate >= limiterDate)).And(ThreadFields.ForumID == forumID)); // if the user can't view threads started by others, filter out threads started by users different from userID if (!canViewNormalThreadsStartedByOthers) { // caller can't view threads started by others: add a filter so that threads not started by calling user aren't enlisted. // however sticky threads are always returned so the filter contains a check so the limit is only applied on threads which aren't sticky // add a filter for sticky threads, add it with 'OR', so sticky threads are always accepted q.AndWhere((ThreadFields.StartedByUserID == userID).Or(ThreadFields.IsSticky == true)); } q.OrderBy(ThreadFields.IsSticky.Descending(), ThreadFields.IsClosed.Ascending(), ThreadFields.ThreadLastPostingDate.Descending()); var dao = new TypedListDAO(); var threads = dao.FetchAsDataTable(q); // count # non-sticky threads. If it's below a given minimum, refetch everything, but now don't fetch on date filtered but at least the // set minimum. Do this ONLY if the user can view other user's threads. If that's NOT the case, don't refetch anything. DataView stickyThreads = new DataView(threads, ThreadFieldIndex.IsSticky.ToString() + "=false", "", DataViewRowState.CurrentRows); if ((stickyThreads.Count < minNumberOfNonStickyVisibleThreads) && canViewNormalThreadsStartedByOthers) { // not enough threads available, fetch again, // first fetch the sticky threads. q = qf.Create(); q.Select(ThreadGuiHelper.BuildQueryProjectionForAllThreadsWithStats(qf)); q.From(ThreadGuiHelper.BuildFromClauseForAllThreadsWithStats(qf)); q.Where((ThreadFields.IsSticky == true).And(ThreadFields.ForumID == forumID)); q.OrderBy(ThreadFields.ThreadLastPostingDate.Descending()); threads = dao.FetchAsDataTable(q); // then fetch the rest. Fetch it into the same datatable object to append the rows to the already fetched sticky threads (if any) q = qf.Create(); q.Select(ThreadGuiHelper.BuildQueryProjectionForAllThreadsWithStats(qf)); q.From(ThreadGuiHelper.BuildFromClauseForAllThreadsWithStats(qf)); q.Where((ThreadFields.IsSticky == false).And(ThreadFields.ForumID == forumID)); q.Limit(minNumberOfThreadsToFetch); q.OrderBy(ThreadFields.ThreadLastPostingDate.Descending()); dao.FetchAsDataTable(q, threads); // sort closed threads to the bottom. Do this in-memory as it's a sort operation after projection. Doing it on the server would mean // a sort operation before projection. return(new DataView(threads, string.Empty, ThreadFieldIndex.IsClosed.ToString() + " ASC", DataViewRowState.CurrentRows)); } else { return(threads.DefaultView); } }