Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Forum Thread paging #2473

Merged
merged 25 commits into from
Jul 27, 2018
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
62b7fa1
Basic Infinite Scroll
scopeInfinity Jul 16, 2018
eab7c91
Merge branch 'master' of https://github.com/Submitty/Submitty into fo…
scopeInfinity Jul 17, 2018
8b6a488
DB query refactoring
scopeInfinity Jul 18, 2018
43a903f
Reload threads on scroll down
scopeInfinity Jul 21, 2018
0206b19
Merge branch 'master' of https://github.com/Submitty/Submitty into fo…
scopeInfinity Jul 22, 2018
8d07973
Pull all threads at once for MergeThread
scopeInfinity Jul 22, 2018
15d4f64
Bug fixes
scopeInfinity Jul 22, 2018
b9dbe08
Filter status within sql query
scopeInfinity Jul 22, 2018
78e2edc
Thread status in cookies
scopeInfinity Jul 22, 2018
fa0903d
Minor bug fix
scopeInfinity Jul 22, 2018
e51f8ec
input_forum_data.py changes due to Thread Status PR
scopeInfinity Jul 22, 2018
2f3fd90
Load merge thread list when needed
scopeInfinity Jul 23, 2018
89ef166
JS optimization
scopeInfinity Jul 23, 2018
f23bdad
Bug Fixes
scopeInfinity Jul 25, 2018
edbbc8d
Fixed active thread on next page bug
scopeInfinity Jul 25, 2018
91448f1
Spinner on loading
scopeInfinity Jul 25, 2018
14da976
Bug Fixed
scopeInfinity Jul 26, 2018
c00bc18
Merge branch 'master' of https://github.com/Submitty/Submitty into fo…
scopeInfinity Jul 26, 2018
4b260b0
Fixed bug related to edit post
scopeInfinity Jul 26, 2018
0d94e8e
Merge branch 'master' into forum_thread_paging
andrewaikens87 Jul 27, 2018
d5aa3a8
Better validation
scopeInfinity Jul 27, 2018
e2f8edd
Merge branch 'forum_thread_paging' of https://github.com/Submitty/Sub…
scopeInfinity Jul 27, 2018
6d201b8
Merge branch 'master' into forum_thread_paging
andrewaikens87 Jul 27, 2018
9ee5af3
Merge branch 'master' into forum_thread_paging
bmcutler Jul 27, 2018
257ba71
Merge branch 'master' into forum_thread_paging
bmcutler Jul 27, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .setup/bin/input_forum_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,6 @@
os.system("""PGPASSWORD='{}' psql --host={} --username={} --dbname={} -c \"INSERT INTO threads (title, created_by, pinned, deleted, merged_thread_id, merged_post_id, is_visible) VALUES (\'{:s}\', \'{:s}\', false, false, -1, -1, true)\" > /dev/null""".format(*variables, "Thread{:d}".format(i+1), "aphacker"))
os.system("""PGPASSWORD='{}' psql --host={} --username={} --dbname={} -c \"INSERT INTO thread_categories (thread_id, category_id) VALUES ({:d}, 1)\" > /dev/null""".format(*variables, i+1))
for pid in range(posts):
os.system("""PGPASSWORD='{}' psql --host={} --username={} --dbname={} -c \"INSERT INTO posts (thread_id, parent_id, author_user_id, content, timestamp, anonymous, deleted, resolved, type, has_attachment) VALUES ({}, {}, {}, {}, \'{}\', false, false, false, 0, false)\" > /dev/null""".format(*variables, i+1, -1 if pid == 0 else i*posts + pid, "'aphacker'", "'Post{:d}'".format(i*posts + pid+1), datetime.now()))
os.system("""PGPASSWORD='{}' psql --host={} --username={} --dbname={} -c \"INSERT INTO posts (thread_id, parent_id, author_user_id, content, timestamp, anonymous, deleted, type, has_attachment) VALUES ({}, {}, {}, {}, \'{}\', false, false, 0, false)\" > /dev/null""".format(*variables, i+1, -1 if pid == 0 else i*posts + pid, "'aphacker'", "'Post{:d}'".format(i*posts + pid+1), datetime.now()))


115 changes: 55 additions & 60 deletions site/app/controllers/forum/ForumController.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ public function run() {
case 'show_stats':
$this->showStats();
break;
case 'get_threads_before':
$this->getThreadsBefore();
break;
case 'merge_thread':
$this->mergeThread();
break;
Expand Down Expand Up @@ -545,42 +548,14 @@ private function editPost(){
return null;
}

private function getSortedThreads($categories_ids, $max_thread, $show_deleted = false){
private function getSortedThreads($categories_ids, $max_thread, $show_deleted, $thread_status, $blockNumber = 1){
$blockSize = 10;
$current_user = $this->core->getUser()->getId();
if($this->isValidCategories($categories_ids)) {
$announce_threads = $this->core->getQueries()->loadAnnouncements($categories_ids, $show_deleted);
$reg_threads = $this->core->getQueries()->loadThreads($categories_ids, $show_deleted);
} else {
$announce_threads = $this->core->getQueries()->loadAnnouncementsWithoutCategory($show_deleted);
$reg_threads = $this->core->getQueries()->loadThreadsWithoutCategory($show_deleted);
}
$favorite_threads = $this->core->getQueries()->loadPinnedThreads($current_user);

$ordered_threads = array();
// Order : Favourite and Announcements => Announcements only => Favourite only => Others
foreach ($announce_threads as $thread) {
if(in_array($thread['id'], $favorite_threads)) {
$thread['favorite'] = true;
$ordered_threads[] = $thread;
}
}
foreach ($announce_threads as $thread) {
if(!in_array($thread['id'], $favorite_threads)) {
$ordered_threads[] = $thread;
}
}
foreach ($reg_threads as $thread) {
if(in_array($thread['id'], $favorite_threads)) {
$thread['favorite'] = true;
$ordered_threads[] = $thread;
}
}
foreach ($reg_threads as $thread) {
if(!in_array($thread['id'], $favorite_threads)) {
$ordered_threads[] = $thread;
}
if(!$this->isValidCategories($categories_ids)) {
// No filter for category
$categories_ids = array();
}

$ordered_threads = $this->core->getQueries()->loadThreadBlock($categories_ids, $thread_status, $show_deleted, $current_user, $blockSize, $blockNumber);
foreach ($ordered_threads as &$thread) {
$list = array();
foreach(explode("|", $thread['categories_ids']) as $id ) {
Expand All @@ -594,58 +569,61 @@ private function getSortedThreads($categories_ids, $max_thread, $show_deleted =
}

public function getThreads(){

$pageNumber = !empty($_GET["page_number"]) && is_numeric($_GET["page_number"]) ? (int)$_GET["page_number"] : -1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be validating the lower bound of this page number, i.e. what if the page number is input as -15 (this can be done by simply changing the next_page html attribute). This will throw an exception since the offset in the query can't be negative.

$show_deleted = $this->showDeleted();
$currentCourse = $this->core->getConfig()->getCourse();
$categories_ids = array_key_exists('thread_categories', $_POST) && !empty($_POST["thread_categories"]) ? explode("|", $_POST['thread_categories']) : array();
$thread_status = array_key_exists('thread_status', $_POST) && ($_POST["thread_status"] === "0" || !empty($_POST["thread_status"])) ? explode("|", $_POST['thread_status']) : array();
if(empty($categories_ids) && !empty($_COOKIE[$currentCourse . '_forum_categories'])){
$categories_ids = explode("|", $_COOKIE[$currentCourse . '_forum_categories']);
}
if(empty($thread_status) && !empty($_COOKIE['forum_thread_status'])){
$thread_status = explode("|", $_COOKIE['forum_thread_status']);
}
foreach ($categories_ids as &$id) {
$id = (int)$id;
}
$thread_status = array_key_exists('thread_status', $_POST) && ($_POST["thread_status"] === "0" || !empty($_POST["thread_status"])) ? explode("|", $_POST['thread_status']) : array();
foreach ($thread_status as &$status) {
$status = (int)$status;
}
$max_thread = 0;
$threads = $this->getSortedThreads($categories_ids, $max_thread, $show_deleted);
// Filter thread list
if(!empty($thread_status)) {
$filtered = array();
foreach ($threads as &$thread) {
if(in_array($thread['status'], $thread_status)) {
$filtered[] = $thread;
}
}
$threads = $filtered;
}
$currentCategoriesIds = array_key_exists('currentCategoriesId', $_POST) ? explode("|", $_POST["currentCategoriesId"]) : array();
$threads = $this->getSortedThreads($categories_ids, $max_thread, $show_deleted, $thread_status, $pageNumber);
$currentCategoriesIds = (!empty($_POST['currentCategoriesId'])) ? explode("|", $_POST["currentCategoriesId"]) : array();
$currentThreadId = array_key_exists('currentThreadId', $_POST) && !empty($_POST["currentThreadId"]) && is_numeric($_POST["currentThreadId"]) ? (int)$_POST["currentThreadId"] : -1;
$thread_data = array();
$current_thread_title = "";
$activeThread = false;
$this->core->getOutput()->renderOutput('forum\ForumThread', 'showAlteredDisplayList', $threads, true, $currentThreadId, $currentCategoriesIds);
$this->core->getOutput()->useHeader(false);
$this->core->getOutput()->useFooter(false);
return $this->core->getOutput()->renderJson(array("html" => $this->core->getOutput()->getOutput()));
return $this->core->getOutput()->renderJson(array(
"html" => $this->core->getOutput()->getOutput(),
"count" => count($threads)
));
}

public function showThreads(){
$user = $this->core->getUser()->getId();
$currentCourse = $this->core->getConfig()->getCourse();
$category_id = in_array('thread_category', $_POST) ? array($_POST['thread_category']) : -1;
$currentCourse = $this->core->getConfig()->getCourse();
$category_id = in_array('thread_category', $_POST) ? $_POST['thread_category'] : -1;
$category_id = array($category_id);
if(!empty($_COOKIE[$currentCourse . '_forum_categories']) && $category_id[0] == -1 ) {
$category_id = explode('|', $_COOKIE[$currentCourse . '_forum_categories']);
}
foreach ($category_id as &$id) {
$thread_status = array();
if(!empty($_COOKIE[$currentCourse . '_forum_categories']) && $category_id[0] == -1 ) {
$category_id = explode('|', $_COOKIE[$currentCourse . '_forum_categories']);
}
if(!empty($_COOKIE['forum_thread_status'])){
$thread_status = explode("|", $_COOKIE['forum_thread_status']);
}
foreach ($category_id as &$id) {
$id = (int)$id;
}

$max_thread = 0;
}
foreach ($thread_status as &$status) {
$status = (int)$status;
}

$max_thread = 0;
$show_deleted = $this->showDeleted();
$threads = $this->getSortedThreads($category_id, $max_thread, $show_deleted);
$threads = $this->getSortedThreads($category_id, $max_thread, $show_deleted, $thread_status, 1);

$current_user = $this->core->getUser()->getId();

Expand All @@ -669,7 +647,7 @@ public function showThreads(){
if(empty($_REQUEST["thread_id"]) || empty($posts)) {
$posts = $this->core->getQueries()->getPostsForThread($current_user, -1, $show_deleted);
}

$this->core->getOutput()->renderOutput('forum\ForumThread', 'showForumThreads', $user, $posts, $threads, $show_deleted, $option, $max_thread);
}

Expand Down Expand Up @@ -785,6 +763,23 @@ public function showStats(){
$this->core->getOutput()->renderOutput('forum\ForumThread', 'statPage', $users);
}

public function getThreadsBefore(){
$output = array();
if($this->core->getUser()->getGroup() <= 2){
if(!empty($_POST["current_thead_date"])){
$current_thead_date = $_POST["current_thead_date"];
$merge_thread_list = $this->core->getQueries()->getThreadsBefore($current_thead_date, 1);
$output["content"] = $merge_thread_list;
} else {
$output["error"] = "No date provided. Please try again.";
}
} else {
$output["error"] = "You do not have permissions to do that.";
}
$this->core->getOutput()->renderJson($output);
return $output;
}

public function mergeThread(){
$parent_thread_id = $_POST["merge_thread_parent"];
$child_thread_id = $_POST["merge_thread_child"];
Expand Down
77 changes: 45 additions & 32 deletions site/app/libraries/database/DatabaseQueries.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,39 +115,46 @@ public function insertSubmittyUser(User $user) {
throw new NotImplementedException();
}

public function loadAnnouncements($categories_ids, $show_deleted = false){
assert(count($categories_ids) > 0);
$query_multiple_qmarks = "?".str_repeat(",?", count($categories_ids)-1);
$query_parameters = array_merge( array(count($categories_ids)), $categories_ids );
$query_delete = $show_deleted?"true":"deleted = false";
$query_delete .= " and merged_thread_id = -1";
/**
* Returns thread list along with their category information
* Filter based on categories, thread status and deleted status
* Order: Favourite and Announcements => Announcements only => Favourite only => Others
*
* @return ordered threads after filter
*/
public function loadThreadBlock($categories_ids, $thread_status, $show_deleted, $current_user, $blockSize, $blockNumber){
// $blockNumber is 1 based index
$query_offset = ($blockNumber-1) * $blockSize;

$this->course_db->query("SELECT t.*, array_to_string(array_agg(e.category_id),'|') as categories_ids, array_to_string(array_agg(w.category_desc),'|') as categories_desc, array_to_string(array_agg(w.color),'|') as categories_color FROM threads t, thread_categories e, categories_list w WHERE {$query_delete} and pinned = true and t.id = e.thread_id and e.category_id = w.category_id GROUP BY t.id HAVING ? = (SELECT count(*) FROM thread_categories tc WHERE tc.thread_id = t.id and category_id IN (".$query_multiple_qmarks.")) ORDER BY t.id DESC", $query_parameters);
return $this->course_db->rows();
}
// Query Generation
if(count($categories_ids) == 0) {
$query_multiple_qmarks = "NULL";
} else {
$query_multiple_qmarks = "?".str_repeat(",?", count($categories_ids)-1);
}
if(count($thread_status) == 0) {
$query_status = "true";
} else {
$query_status = "status in (?".str_repeat(",?", count($thread_status)-1).")";
}

public function loadAnnouncementsWithoutCategory($show_deleted = false){
$query_delete = $show_deleted?"true":"deleted = false";
$query_delete .= " and merged_thread_id = -1";
$this->course_db->query("SELECT t.*, array_to_string(array_agg(e.category_id),'|') as categories_ids, array_to_string(array_agg(w.category_desc),'|') as categories_desc, array_to_string(array_agg(w.color),'|') as categories_color FROM threads t, thread_categories e, categories_list w WHERE {$query_delete} and pinned = true and t.id = e.thread_id and e.category_id = w.category_id GROUP BY t.id ORDER BY t.id DESC");
return $this->course_db->rows();
}
$query_select_categories = "SELECT thread_id, array_to_string(array_agg(w.category_id),'|') as categories_ids, array_to_string(array_agg(w.category_desc),'|') as categories_desc, array_to_string(array_agg(w.color),'|') as categories_color FROM categories_list w JOIN thread_categories e ON e.category_id = w.category_id GROUP BY e.thread_id";

public function loadThreadsWithoutCategory($show_deleted = false){
$query_delete = $show_deleted?"true":"deleted = false";
$query_delete .= " and merged_thread_id = -1";
$this->course_db->query("SELECT t.*, array_to_string(array_agg(e.category_id),'|') as categories_ids, array_to_string(array_agg(w.category_desc),'|') as categories_desc, array_to_string(array_agg(w.color),'|') as categories_color FROM threads t, thread_categories e, categories_list w WHERE {$query_delete} and pinned = false and t.id = e.thread_id and e.category_id = w.category_id GROUP BY t.id ORDER BY t.id DESC");
return $this->course_db->rows();
}
$query = "SELECT t.*, categories_ids, categories_desc, categories_color, (case when sf.user_id is NULL then false else true end) as favorite FROM threads t JOIN ({$query_select_categories}) AS QSC ON QSC.thread_id = t.id LEFT JOIN student_favorites sf ON sf.thread_id = t.id and sf.user_id = ? WHERE {$query_delete} and ? = (SELECT count(*) FROM thread_categories tc WHERE tc.thread_id = t.id and category_id IN ({$query_multiple_qmarks})) and {$query_status} ORDER BY pinned DESC, favorite DESC, t.id DESC LIMIT ? OFFSET ?";

public function loadThreads($categories_ids, $show_deleted = false) {
assert(count($categories_ids) > 0);
$query_multiple_qmarks = "?".str_repeat(",?", count($categories_ids)-1);
$query_parameters = array_merge( array(count($categories_ids)), $categories_ids );
$query_delete = $show_deleted?"true":"deleted = false";
$query_delete .= " and merged_thread_id = -1";
// Parameters
$query_parameters = array();
$query_parameters[] = $current_user;
$query_parameters[] = count($categories_ids);
$query_parameters = array_merge($query_parameters, $categories_ids);
$query_parameters = array_merge($query_parameters, $thread_status);
$query_parameters[] = $blockSize;
$query_parameters[] = $query_offset;

$this->course_db->query("SELECT t.*, array_to_string(array_agg(e.category_id),'|') as categories_ids, array_to_string(array_agg(w.category_desc),'|') as categories_desc, array_to_string(array_agg(w.color),'|') as categories_color FROM threads t, thread_categories e, categories_list w WHERE {$query_delete} and pinned = false and t.id = e.thread_id and e.category_id = w.category_id GROUP BY t.id HAVING ? = (SELECT count(*) FROM thread_categories tc WHERE tc.thread_id = t.id and category_id IN (".$query_multiple_qmarks.")) ORDER BY t.id DESC", $query_parameters);
// Execute
$this->course_db->query($query, $query_parameters);
return $this->course_db->rows();
}

Expand Down Expand Up @@ -239,9 +246,15 @@ public function createThread($user, $title, $content, $anon, $prof_pinned, $stat
return array("thread_id" => $id, "post_id" => $post_id);
}

public function getThreadsBefore($timestamp, $page) {
// TODO: Handle request page wise
$this->course_db->query("SELECT t.id as id, title from threads t JOIN posts p on p.thread_id = t.id and parent_id = -1 WHERE timestamp < ? and t.deleted = false", array($timestamp));
return $this->course_db->rows();
}

public function getThread($thread_id) {
$this->course_db->query("SELECT * from threads where id = ?", array($thread_id));
return $this->course_db->rows();
$this->course_db->query("SELECT * from threads where id = ?", array($thread_id));
return $this->course_db->rows();
}

public function getThreadTitle($thread_id){
Expand Down Expand Up @@ -2164,8 +2177,9 @@ public function existsPost($thread_id, $post_id){
return count($result) > 0;
}

public function existsAnnouncements(){
$this->course_db->query("SELECT MAX(id) FROM threads where deleted = false AND pinned = true");
public function existsAnnouncements($show_deleted = false){
$query_delete = $show_deleted?"true":"deleted = false";
$this->course_db->query("SELECT MAX(id) FROM threads where {$query_delete} AND pinned = true");
$result = $this->course_db->rows();
return empty($result[0]["max"]) ? -1 : $result[0]["max"];
}
Expand Down Expand Up @@ -2239,7 +2253,7 @@ public function getCategories(){
public function getPostsForThread($current_user, $thread_id, $show_deleted = false, $option = "tree"){
$query_delete = $show_deleted?"true":"deleted = false";
if($thread_id == -1) {
$announcement_id = $this->existsAnnouncements();
$announcement_id = $this->existsAnnouncements($show_deleted);
if($announcement_id == -1){
$this->course_db->query("SELECT MAX(id) as max from threads WHERE {$query_delete} and pinned = false");
$thread_id = $this->course_db->rows()[0]["max"];
Expand All @@ -2255,7 +2269,6 @@ public function getPostsForThread($current_user, $thread_id, $show_deleted = fal
}

$result_rows = $this->course_db->rows();

if(count($result_rows) > 0){
$this->course_db->query("INSERT INTO viewed_responses(thread_id,user_id,timestamp) SELECT ?, ?, current_timestamp WHERE NOT EXISTS (SELECT 1 FROM viewed_responses WHERE thread_id=? AND user_id=?)", array($thread_id, $current_user, $thread_id, $current_user));
}
Expand Down
7 changes: 5 additions & 2 deletions site/app/templates/forum/FilterForm.twig
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
{% endif %}
}
$( document ).ready(function() {
$('#thread_category option').mousedown(function(e) {
$('#thread_category option, #thread_status_select option').mousedown(function(e) {
e.preventDefault();
var current_selection = $(this).prop('selected');
$(this).prop('selected', !current_selection);
Expand All @@ -34,6 +34,9 @@
{% for category in cookie_selected_categories %}
$('#thread_category option[value="{{ category }}"]').prop('selected', true);
{% endfor %}
{% for status in cookie_selected_thread_status %}
$('#thread_status_select option[value="{{ status }}"]').prop('selected', true);
{% endfor %}

$("#tree").prop("checked", true);
{% if display_option in ['tree', 'time', 'alpha'] %}
Expand All @@ -48,6 +51,6 @@
</form>
{% endblock %}
{% block buttons %}
<a class="btn btn-default" title="Clear Filter" onclick="$('#thread_category option, #thread_status_select option').prop('selected', false);{$onChange};$('#category_wrapper').css('display', 'none');"><i class="fa fa-eraser"></i> Clear Filter</a>
<a class="btn btn-default" title="Clear Filter" onclick="$('#thread_category option, #thread_status_select option').prop('selected', false);updateThreads();$('#category_wrapper').css('display', 'none');"><i class="fa fa-eraser"></i> Clear Filter</a>
<a class="btn btn-default" title="Close Popup" onclick="$('#category_wrapper').css('display', 'none');">Close</a>
{% endblock %}
Loading