Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
38 changes: 1 addition & 37 deletions migration/migrator/data/course_tables.sql
Original file line number Diff line number Diff line change
Expand Up @@ -898,31 +898,10 @@ CREATE TABLE public.forum_attachments (
post_id integer NOT NULL,
file_name character varying NOT NULL,
version_added integer DEFAULT 1 NOT NULL,
version_deleted integer DEFAULT 0 NOT NULL,
id integer NOT NULL
version_deleted integer DEFAULT 0 NOT NULL
);


--
-- Name: forum_attachments_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--

CREATE SEQUENCE public.forum_attachments_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;


--
-- Name: forum_attachments_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--

ALTER SEQUENCE public.forum_attachments_id_seq OWNED BY public.forum_attachments.id;


--
-- Name: forum_posts_history; Type: TABLE; Schema: public; Owner: -
--
Expand Down Expand Up @@ -1979,13 +1958,6 @@ ALTER TABLE ONLY public.course_materials_access ALTER COLUMN id SET DEFAULT next
ALTER TABLE ONLY public.course_materials_sections ALTER COLUMN id SET DEFAULT nextval('public.course_materials_sections_id_seq'::regclass);


--
-- Name: forum_attachments id; Type: DEFAULT; Schema: public; Owner: -
--

ALTER TABLE ONLY public.forum_attachments ALTER COLUMN id SET DEFAULT nextval('public.forum_attachments_id_seq'::regclass);


--
-- Name: grade_inquiries id; Type: DEFAULT; Schema: public; Owner: -
--
Expand Down Expand Up @@ -2192,14 +2164,6 @@ ALTER TABLE ONLY public.electronic_gradeable
ADD CONSTRAINT electronic_gradeable_g_id_pkey PRIMARY KEY (g_id);


--
-- Name: forum_attachments forum_attachments_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--

ALTER TABLE ONLY public.forum_attachments
ADD CONSTRAINT forum_attachments_pkey PRIMARY KEY (id);


--
-- Name: forum_upducks forum_upducks_user_id_post_id_key; Type: CONSTRAINT; Schema: public; Owner: -
--
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Migration for a given Submitty course database."""

import json
from pathlib import Path

def up(config, database, semester, course):
"""
Run up migration.
:param config: Object holding configuration details about Submitty
:type config: migrator.config.Config
:param database: Object for interacting with given database for environment
:type database: migrator.db.Database
:param semester: Semester of the course being migrated
:type semester: str
:param course: Code of course being migrated
:type course: str
"""

database.execute(
"""
CREATE TABLE IF NOT EXISTS chatrooms (
id SERIAL PRIMARY KEY,
host_id character varying NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
host_name character varying,
title text NOT NULL,
description text,
is_active BOOLEAN DEFAULT false NOT NULL,
allow_anon BOOLEAN DEFAULT true NOT NULL
);

CREATE TABLE IF NOT EXISTS chatroom_messages (
id SERIAL PRIMARY KEY,
chatroom_id integer NOT NULL REFERENCES chatrooms(id) ON DELETE CASCADE,
user_id character varying NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
display_name character varying,
is_pinned boolean,
who_pinned character varying,
role character varying,
content text NOT NULL,
timestamp timestamp(0) with time zone NOT NULL
);
"""
)
course_dir = Path(config.submitty['submitty_data_dir'], 'courses', semester, course)
# add boolean to course config
config_file = Path(course_dir, 'config', 'config.json')
if config_file.is_file():
with open(config_file, 'r') as in_file:
j = json.load(in_file)
j['course_details']['chat_enabled'] = False

with open(config_file, 'w') as out_file:
json.dump(j, out_file, indent=4)

def down(config, database, semester, course):
"""
Run down migration (rollback).
:param config: Object holding configuration details about Submitty
:type config: migrator.config.Config
:param database: Object for interacting with given database for environment
:type database: migrator.db.Database
:param semester: Semester of the course being migrated
:type semester: str
:param course: Code of course being migrated
:type course: str
"""
database.execute("DROP TABLE IF EXISTS chatrooms cascade")
database.execute("DROP TABLE IF EXISTS chatroom_messages")

pass
230 changes: 230 additions & 0 deletions site/app/controllers/ChatroomController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
<?php

namespace app\controllers;

use app\libraries\response\WebResponse;
use app\libraries\response\JsonResponse;
use app\libraries\response\RedirectResponse;
use app\entities\chat\Chatroom;
use app\entities\chat\Message;
use app\libraries\routers\AccessControl;
use app\libraries\routers\Enabled;
use Symfony\Component\Routing\Annotation\Route;

/**
* @Enabled("chat")
*/
class ChatroomController extends AbstractController {
/**
* @Route("/courses/{_semester}/{_course}/chat", methods={"GET"})
*/
public function showChatroomssPage(): WebResponse {
$repo = $this->core->getCourseEntityManager()->getRepository(Chatroom::class);
$chatrooms = $repo->findBy([], ['id' => 'ASC']);

return new WebResponse(
'Chatroom',
'showAllChatrooms',
$chatrooms
);
}

/**
* @Route("/courses/{_semester}/{_course}/chat/new", methods={"POST"})
* @AccessControl(role="INSTRUCTOR")
*/
public function addChatroom(): RedirectResponse {
$em = $this->core->getCourseEntityManager();

$hostId = $this->core->getUser()->getId();
$hostName = $this->core->getUser()->getDisplayFullName();
$chatroom = new Chatroom($hostId, $hostName, $_POST['title'], $_POST['description']);
if (!isset($_POST['allow-anon'])) {
$chatroom->setAllowAnon(false);
}

$em->persist($chatroom);
$em->flush();

$this->core->addSuccessMessage("Chatroom successfully added");
return new RedirectResponse($this->core->buildCourseUrl(['chat']));
}

/**
* @Route("/courses/{_semester}/{_course}/chat/{chatroom_id}", methods={"GET"})
* @param string $chatroom_id
* @return RedirectResponse|WebResponse
*/
public function getChatroom(string $chatroom_id): WebResponse|RedirectResponse {
if (!is_numeric($chatroom_id)) {
$this->core->addErrorMessage("Invalid Chatroom ID");
return new RedirectResponse($this->core->buildCourseUrl(['chat']));
}

$repo = $this->core->getCourseEntityManager()->getRepository(Chatroom::class);
$chatroom = $repo->find($chatroom_id);

if ($chatroom == null) {
$this->core->addErrorMessage("chatroom not found");
return new RedirectResponse($this->core->buildCourseUrl(['chat']));
}

return new WebResponse(
'Chatroom',
'showChatroom',
$chatroom,
);
}

/**
* @Route("/courses/{_semester}/{_course}/chat/{chatroom_id}/anonymous", methods={"GET"})
* @param string $chatroom_id
* @return RedirectResponse|WebResponse
*/
public function getChatroomAnon(string $chatroom_id): WebResponse|RedirectResponse {
if (!is_numeric($chatroom_id)) {
$this->core->addErrorMessage("Invalid Chatroom ID");
return new RedirectResponse($this->core->buildCourseUrl(['chat']));
}

$repo = $this->core->getCourseEntityManager()->getRepository(Chatroom::class);
$chatroom = $repo->find($chatroom_id);

return new WebResponse(
'Chatroom',
'showChatroom',
$chatroom,
true,
);
}

/**
* @Route("/courses/{_semester}/{_course}/chat/delete", methods={"POST"})
* @AccessControl(role="INSTRUCTOR")
*/
public function deleteChatroom(): JsonResponse {
$chatroom_id = intval($_POST['chatroom_id'] ?? -1);
$em = $this->core->getCourseEntityManager();

$repo = $em->getRepository(Chatroom::class);

$chatroom = $repo->find($chatroom_id);
if ($chatroom === null) {
return JsonResponse::getFailResponse('Invalid Chatroom ID');
}
foreach ($chatroom->getMessages() as $message) {
$em->remove($message);
}
$em->remove($chatroom);
$em->flush();
return JsonResponse::getSuccessResponse();
}

/**
* @Route("/courses/{_semester}/{_course}/chat/{chatroom_id}/edit", methods={"POST"})
* @AccessControl(role="INSTRUCTOR")
*/
public function editChatroom(string $chatroom_id): RedirectResponse {
$em = $this->core->getCourseEntityManager();
$chatroom = $em->getRepository(Chatroom::class)->find($chatroom_id);

if (!$chatroom) {
$this->core->addErrorMessage("Chatroom not found");
return new RedirectResponse($this->core->buildCourseUrl(['chat']));
}

if (isset($_POST['title'])) {
$chatroom->setTitle($_POST['title']);
}
if (isset($_POST['description'])) {
$chatroom->setDescription($_POST['description']);
}
if (isset($_POST['allow-anon'])) {
$chatroom->setAllowAnon(true);
}
else {
$chatroom->setAllowAnon(false);
}

$em->flush();

$this->core->addSuccessMessage("Chatroom successfully updated");
return new RedirectResponse($this->core->buildCourseUrl(['chat']));
}

/**
* @Route("/courses/{_semester}/{_course}/chat/{chatroom_id}/toggleOnOff", methods={"POST"})
* @AccessControl(role="INSTRUCTOR")
*/
public function toggleChatroomOnOff(string $chatroom_id): RedirectResponse {
$em = $this->core->getCourseEntityManager();
$chatroom = $em->getRepository(Chatroom::class)->find($chatroom_id);

if ($chatroom === null) {
$this->core->addErrorMessage("Chatroom not found");
return new RedirectResponse($this->core->buildCourseUrl(['chat']));
}

$chatroom->toggle_on_off();

$em->flush();

return new RedirectResponse($this->core->buildCourseUrl(['chat']));
}

/**
* @Route("/courses/{_semester}/{_course}/chat/{chatroom_id}/messages", methods={"GET"})
*/
public function fetchMessages(string $chatroom_id): JsonResponse {
$em = $this->core->getCourseEntityManager();
$messages = $em->getRepository(Message::class)->findBy(['chatroom' => $chatroom_id], ['timestamp' => 'ASC']);

$formattedMessages = array_map(function ($message) {
return [
'id' => $message->getId(),
'content' => $message->getContent(),
'timestamp' => $message->getTimestamp()->format('Y-m-d H:i:s'),
'user_id' => $message->getUserId(),
'display_name' => $message->getDisplayName(),
'role' => $message->getRole(),
'is_pinned' => $message->isPinned(),
'pinned_by' => $message->getWhoPinned()
];
}, $messages);

return JsonResponse::getSuccessResponse($formattedMessages);
}

/**
* @Route("/courses/{_semester}/{_course}/chat/{chatroom_id}/send", methods={"POST"})
*/
public function addMessage(string $chatroom_id): JsonResponse {
$em = $this->core->getCourseEntityManager();
$content = $_POST['content'];
$userId = $_POST['user_id'];
$displayName = $_POST['display_name'] ?? '';
$role = $_POST['role'] ?? 'student';
$chatroom = $em->getRepository(Chatroom::class)->find($chatroom_id);

$message = new Message($userId, $displayName, $role, $content, $chatroom);

$em->persist($message);
$em->flush();

return JsonResponse::getSuccessResponse($message);
}

/**
* @Route("/courses/{_semester}/{_course}/chat/{chatroom_id}/{message_id}/pin", methods={"POST"})
*/
public function pinMessage(string $chatroom_id, string $message_id): JsonResponse {
$em = $this->core->getCourseEntityManager();
$chatroom = $em->getRepository(Chatroom::class)->find($chatroom_id);
$message = $chatroom->getRepository(Message::class)->find($message_id);

$em->persist($message);
$em->flush();

return JsonResponse::getSuccessResponse($message);
}
}
9 changes: 9 additions & 0 deletions site/app/controllers/GlobalController.php
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,15 @@ public function prep_course_sidebar(&$sidebar_buttons, $unread_notifications_cou
]);
}

if ($this->core->getConfig()->isChatEnabled()) {
$sidebar_buttons[] = new NavButton($this->core, [
"href" => $this->core->buildCourseUrl(['chat']),
"title" => "Live Lecture Chat",
"id" => "nav-sidebar-chat",
"icon" => "fa-regular fa-keyboard"
]);
}

$course_path = $this->core->getConfig()->getCoursePath();
$course_materials_path = $course_path . "/uploads/course_materials";
$empty = FileUtils::isEmptyDir($course_materials_path);
Expand Down
6 changes: 4 additions & 2 deletions site/app/controllers/admin/ConfigurationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ public function viewConfiguration(): MultiResponse {
'seek_message_enabled' => $this->core->getConfig()->isSeekMessageEnabled(),
'seek_message_instructions' => $this->core->getConfig()->getSeekMessageInstructions(),
'queue_announcement_message' => $this->core->getConfig()->getQueueAnnouncementMessage(),
'polls_enabled' => $this->core->getConfig()->isPollsEnabled()
'polls_enabled' => $this->core->getConfig()->isPollsEnabled(),
'chat_enabled' => $this->core->getCOnfig()->isChatEnabled()
];
$seating_options = $this->getGradeableSeatingOptions();
$admin_in_course = false;
Expand Down Expand Up @@ -149,7 +150,8 @@ public function updateConfiguration(): MultiResponse {
'seating_only_for_instructor',
'queue_enabled',
'seek_message_enabled',
'polls_enabled'
'polls_enabled',
'chat_enabled'
]
)
) {
Expand Down
Loading