From 1bed2d7dccedf3729d23e6b1552bcdc7188ee10a Mon Sep 17 00:00:00 2001 From: Wojtek Date: Thu, 15 Oct 2026 16:30:43 +0200 Subject: [PATCH] Add question set management --- README.md | 3 +- .../com_testquestions/forms/set.xml | 13 +++ .../language/en-GB/com_testquestions.ini | 8 ++ .../sql/install.mysql.utf8mb4.sql | 23 +++++ .../sql/uninstall.mysql.utf8mb4.sql | 2 + .../sql/updates/mysql/1.9.0.sql | 22 +++++ .../src/Controller/SetController.php | 20 ++++ .../src/Controller/SetsController.php | 17 ++++ .../com_testquestions/src/Model/SetModel.php | 98 +++++++++++++++++++ .../com_testquestions/src/Model/SetsModel.php | 40 ++++++++ .../com_testquestions/src/Table/SetTable.php | 23 +++++ .../src/View/Questions/HtmlView.php | 6 ++ .../src/View/Set/HtmlView.php | 37 +++++++ .../src/View/Sets/HtmlView.php | 54 ++++++++++ .../com_testquestions/tmpl/set/edit.php | 19 ++++ .../com_testquestions/tmpl/sets/default.php | 55 +++++++++++ com_testquestions.xml | 2 +- 17 files changed, 440 insertions(+), 2 deletions(-) create mode 100644 administrator/components/com_testquestions/forms/set.xml create mode 100644 administrator/components/com_testquestions/sql/updates/mysql/1.9.0.sql create mode 100644 administrator/components/com_testquestions/src/Controller/SetController.php create mode 100644 administrator/components/com_testquestions/src/Controller/SetsController.php create mode 100644 administrator/components/com_testquestions/src/Model/SetModel.php create mode 100644 administrator/components/com_testquestions/src/Model/SetsModel.php create mode 100644 administrator/components/com_testquestions/src/Table/SetTable.php create mode 100644 administrator/components/com_testquestions/src/View/Set/HtmlView.php create mode 100644 administrator/components/com_testquestions/src/View/Sets/HtmlView.php create mode 100644 administrator/components/com_testquestions/tmpl/set/edit.php create mode 100644 administrator/components/com_testquestions/tmpl/sets/default.php diff --git a/README.md b/README.md index 1ededb4..5e34f9e 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,11 @@ Szkielet komponentu Joomla 6 do gromadzenia pytań testowych. - wskazanie poprawnej odpowiedzi, - opcjonalne zdjęcie wybierane z Media Managera, - publikowanie, filtrowanie i usuwanie pytań w panelu administratora, +- tworzenie zestawów pytań w panelu administratora i przypisywanie do nich pytań, - tabela `#__testquestions_questions` tworzona podczas instalacji. ## Instalacja -W Joomla wybierz **System → Install → Install from Folder** i wskaż ten katalog. Manifestem instalacyjnym jest `com_testquestions.xml` (wersja 1.8.3). +W Joomla wybierz **System → Install → Install from Folder** i wskaż ten katalog. Manifestem instalacyjnym jest `com_testquestions.xml` (wersja 1.9.0). Po instalacji komponent jest dostępny w panelu administratora jako **Pytania testowe**. diff --git a/administrator/components/com_testquestions/forms/set.xml b/administrator/components/com_testquestions/forms/set.xml new file mode 100644 index 0000000..c00c396 --- /dev/null +++ b/administrator/components/com_testquestions/forms/set.xml @@ -0,0 +1,13 @@ + +
+
+ + + + + + + + +
+
diff --git a/administrator/components/com_testquestions/language/en-GB/com_testquestions.ini b/administrator/components/com_testquestions/language/en-GB/com_testquestions.ini index 1f9b436..3bcacb5 100644 --- a/administrator/components/com_testquestions/language/en-GB/com_testquestions.ini +++ b/administrator/components/com_testquestions/language/en-GB/com_testquestions.ini @@ -45,3 +45,11 @@ COM_TESTQUESTIONS_ERROR_QUESTION_TYPE="Wybierz poprawny typ pytania." COM_TESTQUESTIONS_ERROR_OPEN_PATTERN="Dla pytania otwartego wpisz wzorzec poprawnej odpowiedzi." COM_TESTQUESTIONS_ERROR_ALL_ANSWERS="Dla pytania zamkniętego uzupełnij wszystkie cztery odpowiedzi." COM_TESTQUESTIONS_ERROR_META="Wybierz poprawny czas i liczbę punktów." +COM_TESTQUESTIONS_SETS="Zestawy pytań" +COM_TESTQUESTIONS_NEW_SET="Nowy zestaw pytań" +COM_TESTQUESTIONS_EDIT_SET="Edytuj zestaw pytań" +COM_TESTQUESTIONS_SET_TITLE="Nazwa zestawu" +COM_TESTQUESTIONS_SET_DESCRIPTION="Opis zestawu" +COM_TESTQUESTIONS_SET_QUESTIONS="Pytania w zestawie" +COM_TESTQUESTIONS_SET_QUESTION_COUNT="Liczba pytań" +COM_TESTQUESTIONS_SET_SEARCH="Szukaj zestawów..." diff --git a/administrator/components/com_testquestions/sql/install.mysql.utf8mb4.sql b/administrator/components/com_testquestions/sql/install.mysql.utf8mb4.sql index 4f278fc..74b9617 100644 --- a/administrator/components/com_testquestions/sql/install.mysql.utf8mb4.sql +++ b/administrator/components/com_testquestions/sql/install.mysql.utf8mb4.sql @@ -37,3 +37,26 @@ CREATE TABLE IF NOT EXISTS `#__testquestions_categories` ( INSERT IGNORE INTO `#__testquestions_categories` (`id`, `title`, `published`, `ordering`) VALUES (1, 'Bez kategorii', 1, 0); + +CREATE TABLE IF NOT EXISTS `#__testquestions_sets` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `title` varchar(255) NOT NULL, + `description` text NULL, + `published` tinyint NOT NULL DEFAULT 1, + `ordering` int NOT NULL DEFAULT 0, + `created` datetime NULL DEFAULT NULL, + `created_by` int unsigned NOT NULL DEFAULT 0, + `modified` datetime NULL DEFAULT NULL, + `modified_by` int unsigned NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `idx_published` (`published`), + KEY `idx_ordering` (`ordering`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `#__testquestions_set_questions` ( + `set_id` int unsigned NOT NULL, + `question_id` int unsigned NOT NULL, + `ordering` int NOT NULL DEFAULT 0, + PRIMARY KEY (`set_id`, `question_id`), + KEY `idx_question_id` (`question_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci; diff --git a/administrator/components/com_testquestions/sql/uninstall.mysql.utf8mb4.sql b/administrator/components/com_testquestions/sql/uninstall.mysql.utf8mb4.sql index 25bb53c..010f238 100644 --- a/administrator/components/com_testquestions/sql/uninstall.mysql.utf8mb4.sql +++ b/administrator/components/com_testquestions/sql/uninstall.mysql.utf8mb4.sql @@ -1 +1,3 @@ +DROP TABLE IF EXISTS `#__testquestions_set_questions`; +DROP TABLE IF EXISTS `#__testquestions_sets`; DROP TABLE IF EXISTS `#__testquestions_questions`; diff --git a/administrator/components/com_testquestions/sql/updates/mysql/1.9.0.sql b/administrator/components/com_testquestions/sql/updates/mysql/1.9.0.sql new file mode 100644 index 0000000..e5c7348 --- /dev/null +++ b/administrator/components/com_testquestions/sql/updates/mysql/1.9.0.sql @@ -0,0 +1,22 @@ +CREATE TABLE IF NOT EXISTS `#__testquestions_sets` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `title` varchar(255) NOT NULL, + `description` text NULL, + `published` tinyint NOT NULL DEFAULT 1, + `ordering` int NOT NULL DEFAULT 0, + `created` datetime NULL DEFAULT NULL, + `created_by` int unsigned NOT NULL DEFAULT 0, + `modified` datetime NULL DEFAULT NULL, + `modified_by` int unsigned NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `idx_published` (`published`), + KEY `idx_ordering` (`ordering`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `#__testquestions_set_questions` ( + `set_id` int unsigned NOT NULL, + `question_id` int unsigned NOT NULL, + `ordering` int NOT NULL DEFAULT 0, + PRIMARY KEY (`set_id`, `question_id`), + KEY `idx_question_id` (`question_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci; diff --git a/administrator/components/com_testquestions/src/Controller/SetController.php b/administrator/components/com_testquestions/src/Controller/SetController.php new file mode 100644 index 0000000..b075807 --- /dev/null +++ b/administrator/components/com_testquestions/src/Controller/SetController.php @@ -0,0 +1,20 @@ +setRedirect('index.php?option=com_testquestions&view=sets'); + + return true; + } +} diff --git a/administrator/components/com_testquestions/src/Controller/SetsController.php b/administrator/components/com_testquestions/src/Controller/SetsController.php new file mode 100644 index 0000000..ca90108 --- /dev/null +++ b/administrator/components/com_testquestions/src/Controller/SetsController.php @@ -0,0 +1,17 @@ + true]) + { + return parent::getModel($name, $prefix, $config); + } +} diff --git a/administrator/components/com_testquestions/src/Model/SetModel.php b/administrator/components/com_testquestions/src/Model/SetModel.php new file mode 100644 index 0000000..8541963 --- /dev/null +++ b/administrator/components/com_testquestions/src/Model/SetModel.php @@ -0,0 +1,98 @@ +getMVCFactory()->createTable($name, $prefix, $config); + } + + public function getForm($data = [], $loadData = true) + { + return $this->loadForm('com_testquestions.set', 'set', ['control' => 'jform', 'load_data' => $loadData]); + } + + public function getItem($pk = null) + { + $item = parent::getItem($pk); + + if ($item && !empty($item->id)) { + $db = Factory::getContainer()->get('DatabaseDriver'); + $item->question_ids = $db->setQuery( + $db->getQuery(true) + ->select($db->quoteName('question_id')) + ->from($db->quoteName('#__testquestions_set_questions')) + ->where($db->quoteName('set_id') . ' = ' . (int) $item->id) + ->order($db->quoteName('ordering') . ' ASC') + )->loadColumn(); + } else { + $item->question_ids = []; + } + + return $item; + } + + protected function loadFormData() + { + return Factory::getApplication()->getUserState('com_testquestions.edit.set.data', []) ?: $this->getItem(); + } + + public function save($data) + { + $questionIds = array_values(array_unique(array_filter(array_map('intval', (array) ($data['question_ids'] ?? []))))); + unset($data['question_ids']); + + if (!parent::save($data)) { + return false; + } + + $setId = (int) $this->getState($this->getName() . '.id'); + $db = Factory::getContainer()->get('DatabaseDriver'); + $db->setQuery( + $db->getQuery(true) + ->delete($db->quoteName('#__testquestions_set_questions')) + ->where($db->quoteName('set_id') . ' = ' . $setId) + )->execute(); + + if ($questionIds) { + $query = $db->getQuery(true) + ->insert($db->quoteName('#__testquestions_set_questions')) + ->columns([$db->quoteName('set_id'), $db->quoteName('question_id'), $db->quoteName('ordering')]); + + foreach ($questionIds as $ordering => $questionId) { + $query->values(implode(',', [$setId, $questionId, $ordering])); + } + + $db->setQuery($query)->execute(); + } + + return true; + } + + public function delete(&$pks) + { + if (!parent::delete($pks)) { + return false; + } + + $db = Factory::getContainer()->get('DatabaseDriver'); + $ids = array_map('intval', (array) $pks); + + if ($ids) { + $db->setQuery( + $db->getQuery(true) + ->delete($db->quoteName('#__testquestions_set_questions')) + ->where($db->quoteName('set_id') . ' IN (' . implode(',', $ids) . ')') + )->execute(); + } + + return true; + } +} diff --git a/administrator/components/com_testquestions/src/Model/SetsModel.php b/administrator/components/com_testquestions/src/Model/SetsModel.php new file mode 100644 index 0000000..d294181 --- /dev/null +++ b/administrator/components/com_testquestions/src/Model/SetsModel.php @@ -0,0 +1,40 @@ +setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.published', $this->getUserStateFromRequest($this->context . '.filter.published', 'filter_published', '', 'string')); + } + + protected function getListQuery() + { + $db = Factory::getContainer()->get('DatabaseDriver'); + $query = $db->getQuery(true) + ->select(['s.*', 'COUNT(sq.question_id) AS question_count']) + ->from($db->quoteName('#__testquestions_sets', 's')) + ->leftJoin($db->quoteName('#__testquestions_set_questions', 'sq') . ' ON sq.set_id = s.id') + ->group('s.id'); + + $search = trim((string) $this->getState('filter.search')); + if ($search !== '') { + $query->where('s.title LIKE ' . $db->quote('%' . $db->escape($search, true) . '%', false)); + } + + $published = $this->getState('filter.published'); + if ($published !== '' && $published !== null) { + $query->where('s.published = ' . (int) $published); + } + + return $query->order('s.ordering ASC, s.title ASC'); + } +} diff --git a/administrator/components/com_testquestions/src/Table/SetTable.php b/administrator/components/com_testquestions/src/Table/SetTable.php new file mode 100644 index 0000000..7063324 --- /dev/null +++ b/administrator/components/com_testquestions/src/Table/SetTable.php @@ -0,0 +1,23 @@ +title = trim((string) $this->title); + + return $this->title !== '' && parent::check($updateNulls); + } +} diff --git a/administrator/components/com_testquestions/src/View/Questions/HtmlView.php b/administrator/components/com_testquestions/src/View/Questions/HtmlView.php index 3d25e59..6007765 100644 --- a/administrator/components/com_testquestions/src/View/Questions/HtmlView.php +++ b/administrator/components/com_testquestions/src/View/Questions/HtmlView.php @@ -51,5 +51,11 @@ class HtmlView extends BaseHtmlView 'COM_TESTQUESTIONS_MANAGE_CATEGORIES', 'list' ); + + ToolbarHelper::link( + 'index.php?option=com_testquestions&view=sets', + 'COM_TESTQUESTIONS_SETS', + 'list' + ); } } diff --git a/administrator/components/com_testquestions/src/View/Set/HtmlView.php b/administrator/components/com_testquestions/src/View/Set/HtmlView.php new file mode 100644 index 0000000..ec98694 --- /dev/null +++ b/administrator/components/com_testquestions/src/View/Set/HtmlView.php @@ -0,0 +1,37 @@ +form = $this->get('Form'); + $this->item = $this->get('Item'); + + if (count($errors = $this->get('Errors'))) { + throw new \RuntimeException(implode("\n", $errors)); + } + + $this->addToolbar(); + + return parent::display($tpl); + } + + protected function addToolbar() + { + ToolbarHelper::title($this->item->id ? Text::_('COM_TESTQUESTIONS_EDIT_SET') : Text::_('COM_TESTQUESTIONS_NEW_SET')); + ToolbarHelper::apply('set.apply', 'JTOOLBAR_APPLY'); + ToolbarHelper::save('set.save', 'JTOOLBAR_SAVE'); + ToolbarHelper::cancel('set.cancel', 'JTOOLBAR_CANCEL'); + } +} diff --git a/administrator/components/com_testquestions/src/View/Sets/HtmlView.php b/administrator/components/com_testquestions/src/View/Sets/HtmlView.php new file mode 100644 index 0000000..44e924e --- /dev/null +++ b/administrator/components/com_testquestions/src/View/Sets/HtmlView.php @@ -0,0 +1,54 @@ +items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + + if (count($errors = $this->get('Errors'))) { + throw new \RuntimeException(implode("\n", $errors)); + } + + $this->addToolbar(); + + return parent::display($tpl); + } + + protected function addToolbar() + { + $user = $this->getCurrentUser(); + $this->getDocument()->setTitle(Text::_('COM_TESTQUESTIONS_SETS')); + + if ($user->authorise('core.create', 'com_testquestions')) { + ToolbarHelper::addNew('set.add', 'JTOOLBAR_NEW'); + } + if ($user->authorise('core.edit', 'com_testquestions')) { + ToolbarHelper::editList('set.edit', 'JTOOLBAR_EDIT'); + } + if ($user->authorise('core.delete', 'com_testquestions')) { + ToolbarHelper::deleteList('', 'sets.delete', 'JTOOLBAR_DELETE'); + } + + ToolbarHelper::link( + 'index.php?option=com_testquestions&view=questions', + 'COM_TESTQUESTIONS_QUESTIONS', + 'list' + ); + } +} diff --git a/administrator/components/com_testquestions/tmpl/set/edit.php b/administrator/components/com_testquestions/tmpl/set/edit.php new file mode 100644 index 0000000..df01429 --- /dev/null +++ b/administrator/components/com_testquestions/tmpl/set/edit.php @@ -0,0 +1,19 @@ + +
+
+ form->renderField('title'); ?> + form->renderField('description'); ?> + form->renderField('published'); ?> + form->renderField('question_ids'); ?> +
+ + +
diff --git a/administrator/components/com_testquestions/tmpl/sets/default.php b/administrator/components/com_testquestions/tmpl/sets/default.php new file mode 100644 index 0000000..904191b --- /dev/null +++ b/administrator/components/com_testquestions/tmpl/sets/default.php @@ -0,0 +1,55 @@ + +
+
+
+ +
+
+ +
+
+ + +
+
+ + + + + + + + + + + + + items as $i => $item) : ?> + + + + + + + + + +
id); ?>escape($item->title); ?>question_count; ?>published, $i, 'sets.', true, 'cb'); ?>id; ?>
+ + pagination->getListFooter(); ?> + + + +
diff --git a/com_testquestions.xml b/com_testquestions.xml index 42b62e3..8388d53 100644 --- a/com_testquestions.xml +++ b/com_testquestions.xml @@ -4,7 +4,7 @@ 2026-10-14 Komponent GNU General Public License version 2 or later - 1.8.3 + 1.9.0 COM_TESTQUESTIONS_DESCRIPTION com_testquestions Komponent\Component\Testquestions