Compare commits

..
5 Commits
19 changed files with 316 additions and 9 deletions
+1 -1
View File
@@ -30,6 +30,6 @@ Szkielet komponentu Joomla 6 do gromadzenia pytań testowych.
## Instalacja
W Joomla wybierz **System → Install → Install from Folder** i wskaż ten katalog. Manifestem instalacyjnym jest `com_testquestions.xml` (wersja 1.43.12).
W Joomla wybierz **System → Install → Install from Folder** i wskaż ten katalog. Manifestem instalacyjnym jest `com_testquestions.xml` (wersja 1.43.17).
Po instalacji komponent jest dostępny w panelu administratora jako **Pytania testowe**.
@@ -65,6 +65,7 @@ COM_TESTQUESTIONS_SET_REORDER_INFO="Przeciągnij pytania, aby zmienić ich kolej
COM_TESTQUESTIONS_SET_SAVE="Zapisz"
COM_TESTQUESTIONS_SET_SAVE_AND_CLOSE="Zapisz i zamknij"
COM_TESTQUESTIONS_SET_VARIANT_DURATION_INFO="Czas testu jest pobierany z zestawu nadrzędnego."
COM_TESTQUESTIONS_SET_VARIANT_STATUS_INHERITED="Status publikacji wariantu jest taki sam jak status zestawu nadrzędnego i zmienia się razem z nim."
COM_TESTQUESTIONS_AI_SETTINGS="Ustawienia AI"
COM_TESTQUESTIONS_AI_PROVIDER="Dostawca AI"
COM_TESTQUESTIONS_AI_API_KEY="Klucz API"
@@ -80,6 +81,13 @@ COM_TESTQUESTIONS_AI_USAGE_INPUT="Tokeny wejściowe"
COM_TESTQUESTIONS_AI_USAGE_OUTPUT="Tokeny wyjściowe"
COM_TESTQUESTIONS_AI_USAGE_TOTAL="Razem tokenów"
COM_TESTQUESTIONS_AI_USAGE_COST="Koszt zapisany"
COM_TESTQUESTIONS_PREVIEW_SELECTED="Podgląd zaznaczonych"
COM_TESTQUESTIONS_PREVIEW_TITLE="Podgląd pytań testowych"
COM_TESTQUESTIONS_PREVIEW_READONLY="Tryb podglądu — odpowiedzi są nieaktywne, test nie ma limitu czasu ani przycisku zakończenia."
COM_TESTQUESTIONS_PREVIEW_EMPTY="Nie znaleziono pytań do wyświetlenia."
COM_TESTQUESTIONS_PREVIEW_SELECT_QUESTIONS="Zaznacz pytania, które chcesz podejrzeć."
COM_TESTQUESTIONS_PREVIEW_BACK="Wróć do listy pytań"
COM_TESTQUESTIONS_PREVIEW_SET="Podgląd zestawu"
COM_TESTQUESTIONS_AI_SETTINGS_MENU="Konfiguracja AI"
COM_TESTQUESTIONS_AI_SETTINGS_SAVED="Ustawienia AI zapisano."
COM_TESTQUESTIONS_AI_SAVE_FAILED="Nie udało się zapisać ustawień AI."
@@ -0,0 +1 @@
-- No database changes required for version 1.43.13.
@@ -0,0 +1 @@
-- No database changes required for version 1.43.14.
@@ -0,0 +1 @@
-- No database changes required for version 1.43.15.
@@ -0,0 +1 @@
-- No database changes required for version 1.43.16.
@@ -0,0 +1 @@
-- Wydanie 1.43.17: zmieniono przyciski paska narzędzi podglądu zestawu.
@@ -4,7 +4,11 @@ namespace Komponent\Component\Testquestions\Administrator\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Controller\AdminController;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
class QuestionsController extends AdminController
{
@@ -14,4 +18,28 @@ class QuestionsController extends AdminController
{
return parent::getModel($name, $prefix, $config);
}
public function preview(): void
{
if (!Session::checkToken()) {
throw new \RuntimeException(Text::_('JINVALID_TOKEN'), 403);
}
$user = Factory::getApplication()->getIdentity();
if (!$user->authorise('core.edit', 'com_testquestions') && !$user->authorise('core.manage', 'com_testquestions')) {
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
$ids = array_values(array_unique(array_filter(array_map('intval', (array) $this->input->post->get('cid', [], 'array')))));
if (!$ids) {
$this->setRedirect(
Route::_('index.php?option=com_testquestions&view=questions', false),
Text::_('COM_TESTQUESTIONS_PREVIEW_SELECT_QUESTIONS'),
'warning'
);
return;
}
$this->setRedirect(Route::_('index.php?option=com_testquestions&view=preview&ids=' . implode(',', $ids), false));
}
}
@@ -0,0 +1,69 @@
<?php
namespace Komponent\Component\Testquestions\Administrator\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
class PreviewModel extends BaseDatabaseModel
{
public function getSet()
{
$setId = Factory::getApplication()->getInput()->getInt('set_id');
if ($setId < 1) {
return null;
}
$db = Factory::getContainer()->get('DatabaseDriver');
return $db->setQuery(
$db->getQuery(true)
->select(['id', 'title', 'test_content', 'image'])
->from($db->quoteName('#__testquestions_sets'))
->where($db->quoteName('id') . ' = ' . $setId)
)->loadObject();
}
public function getItems(): array
{
$input = Factory::getApplication()->getInput();
$setId = $input->getInt('set_id');
$db = Factory::getContainer()->get('DatabaseDriver');
if ($setId > 0) {
return $db->setQuery(
$db->getQuery(true)
->select(['q.id', 'q.question_type', 'q.question', 'q.answer_a', 'q.answer_b', 'q.answer_c', 'q.answer_d', 'q.answer_open', 'q.correct_answer', 'q.image'])
->from($db->quoteName('#__testquestions_set_questions', 'sq'))
->innerJoin($db->quoteName('#__testquestions_questions', 'q') . ' ON q.id = sq.question_id')
->where('sq.set_id = ' . $setId)
->where('q.published = 1')
->order('sq.ordering ASC, q.id ASC')
)->loadObjectList();
}
$ids = array_values(array_unique(array_filter(array_map('intval', explode(',', $input->getString('ids', ''))))));
if (!$ids) {
return [];
}
$rows = $db->setQuery(
$db->getQuery(true)
->select(['id', 'question_type', 'question', 'answer_a', 'answer_b', 'answer_c', 'answer_d', 'answer_open', 'correct_answer', 'image'])
->from($db->quoteName('#__testquestions_questions'))
->where($db->quoteName('id') . ' IN (' . implode(',', $ids) . ')')
)->loadObjectList('id');
$items = [];
foreach ($ids as $id) {
if (isset($rows[$id])) {
$items[] = $rows[$id];
}
}
return $items;
}
}
@@ -9,6 +9,26 @@ use Joomla\CMS\MVC\Model\AdminModel;
class SetModel extends AdminModel
{
public function publish(&$pks, $value = 1)
{
$ids = array_values(array_unique(array_filter(array_map('intval', (array) $pks))));
if (!parent::publish($pks, $value)) {
return false;
}
if ($ids) {
$db = Factory::getContainer()->get('DatabaseDriver');
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__testquestions_sets'))
->set($db->quoteName('published') . ' = ' . (int) $value)
->where($db->quoteName('parent_id') . ' IN (' . implode(',', $ids) . ')')
)->execute();
}
return true;
}
public function getTable($name = 'Set', $prefix = 'Administrator', $config = [])
{
return $this->getMVCFactory()->createTable($name, $prefix, $config);
@@ -136,6 +156,7 @@ class SetModel extends AdminModel
$parent = $this->getItem($parentId);
if ($parent) {
$data['duration'] = (int) $parent->duration;
$data['published'] = (int) $parent->published;
}
} elseif (!in_array(($data['set_type'] ?? 'ordinary'), ['ordinary', 'multi'], true)) {
$data['set_type'] = 'ordinary';
@@ -151,6 +172,15 @@ class SetModel extends AdminModel
$setId = (int) $this->getState($this->getName() . '.id');
$db = Factory::getContainer()->get('DatabaseDriver');
if ($parentId === 0) {
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__testquestions_sets'))
->set($db->quoteName('published') . ' = ' . (int) ($data['published'] ?? 1))
->where($db->quoteName('parent_id') . ' = ' . $setId)
)->execute();
}
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__testquestions_set_questions'))
@@ -0,0 +1,54 @@
<?php
namespace Komponent\Component\Testquestions\Administrator\View\Preview;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $items = [];
protected $set;
public function display($tpl = null)
{
$user = Factory::getApplication()->getIdentity();
if (!$user->authorise('core.edit', 'com_testquestions') && !$user->authorise('core.manage', 'com_testquestions')) {
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
$this->items = $this->get('Items');
$this->set = $this->get('Set');
$this->getDocument()->getWebAssetManager()->registerAndUseStyle(
'com_testquestions.exam',
'media/com_testquestions/egzamin.css'
);
ToolbarHelper::title(Text::_('COM_TESTQUESTIONS_PREVIEW_TITLE'));
if ($this->set) {
ToolbarHelper::link(
Route::_('index.php?option=com_testquestions&view=sets'),
'COM_TESTQUESTIONS_SETS',
'arrow-left'
);
ToolbarHelper::link(
Route::_('index.php?option=com_testquestions&task=set.edit&id=' . (int) $this->set->id),
'COM_TESTQUESTIONS_EDIT_SET',
'edit'
);
} else {
ToolbarHelper::link(
Route::_('index.php?option=com_testquestions&view=questions'),
'COM_TESTQUESTIONS',
'arrow-left'
);
}
return parent::display($tpl);
}
}
@@ -41,6 +41,7 @@ class HtmlView extends BaseHtmlView
}
if ($user->authorise('core.edit', 'com_testquestions')) {
ToolbarHelper::editList('question.edit', 'JTOOLBAR_EDIT');
ToolbarHelper::custom('questions.preview', 'eye', 'eye', 'COM_TESTQUESTIONS_PREVIEW_SELECTED', true);
}
if ($user->authorise('core.delete', 'com_testquestions')) {
ToolbarHelper::deleteList('', 'questions.delete', 'JTOOLBAR_DELETE');
@@ -47,6 +47,13 @@ class HtmlView extends BaseHtmlView
protected function addToolbar()
{
ToolbarHelper::title($this->item->id ? Text::_('COM_TESTQUESTIONS_EDIT_SET') : Text::_('COM_TESTQUESTIONS_NEW_SET'));
if (!empty($this->item->id) && ((string) $this->item->set_type !== 'multi' || $this->isVariant)) {
ToolbarHelper::link(
'index.php?option=com_testquestions&view=preview&set_id=' . (int) $this->item->id,
'COM_TESTQUESTIONS_PREVIEW_SET',
'eye'
);
}
ToolbarHelper::custom('set.save', 'save', 'save', 'COM_TESTQUESTIONS_SET_SAVE', false);
ToolbarHelper::custom('set.saveandclose', 'save', 'save', 'COM_TESTQUESTIONS_SET_SAVE_AND_CLOSE', false);
ToolbarHelper::cancel('set.cancel', 'JTOOLBAR_CANCEL');
@@ -0,0 +1,69 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
$resolveImageUrl = static function ($path) {
$path = trim((string) $path);
if ($path === '' || preg_match('#^https?://#i', $path)) {
return $path;
}
return Uri::root() . ltrim($path, '/');
};
?>
<div class="com-testquestions egzamin">
<p class="alert alert-info"><?php echo \Joomla\CMS\Language\Text::_('COM_TESTQUESTIONS_PREVIEW_READONLY'); ?></p>
<?php if ($this->set) : ?>
<header class="test-set-header">
<h1><?php echo htmlspecialchars($this->set->title, ENT_QUOTES, 'UTF-8'); ?></h1>
<?php if ($this->set->image) : ?>
<div class="foto_pyt"><img src="<?php echo htmlspecialchars($resolveImageUrl($this->set->image), ENT_QUOTES, 'UTF-8'); ?>" alt="" loading="lazy"></div>
<?php endif; ?>
<?php if ($this->set->test_content) : ?><div class="test-set-content"><?php echo $this->set->test_content; ?></div><?php endif; ?>
</header>
<?php endif; ?>
<?php if (!$this->items) : ?>
<div class="alert alert-warning"><?php echo \Joomla\CMS\Language\Text::_('COM_TESTQUESTIONS_PREVIEW_EMPTY'); ?></div>
<?php else : ?>
<?php foreach ($this->items as $number => $item) : ?>
<article class="testquestion mb-4">
<div class="pyt">
<div class="question-header">
<span class="question-number"><?php echo ($number + 1) . '.'; ?></span>
<div class="question-text"><?php echo $item->question; ?></div>
</div>
</div>
<?php if ($item->image) : ?>
<div class="foto_pyt"><img src="<?php echo htmlspecialchars($resolveImageUrl($item->image), ENT_QUOTES, 'UTF-8'); ?>" alt="" loading="lazy"></div>
<?php endif; ?>
<?php if ($item->question_type === 'open') : ?>
<div class="odp">
<strong><?php echo \Joomla\CMS\Language\Text::_('COM_TESTQUESTIONS_CORRECT_PATTERN'); ?>:</strong>
<?php echo htmlspecialchars((string) $item->answer_open, ENT_QUOTES, 'UTF-8'); ?>
</div>
<?php else : ?>
<ol type="A" class="answer-list">
<?php foreach (['answer_a', 'answer_b', 'answer_c', 'answer_d'] as $answerIndex => $answerField) : ?>
<?php $isCorrect = (int) $item->correct_answer === $answerIndex + 1; ?>
<li class="<?php echo $isCorrect ? 'odpp' : 'odp'; ?>">
<label>
<input type="radio" disabled <?php echo $isCorrect ? 'checked' : ''; ?>>
<strong><?php echo chr(65 + $answerIndex); ?>.</strong>
<span class="answer-text"><?php echo $item->$answerField; ?></span>
<?php if ($isCorrect) : ?><span class="text-success ms-1" title="<?php echo \Joomla\CMS\Language\Text::_('COM_TESTQUESTIONS_CORRECT_ANSWER'); ?>">✓</span><?php endif; ?>
</label>
</li>
<?php endforeach; ?>
</ol>
<?php endif; ?>
</article>
<?php endforeach; ?>
<?php endif; ?>
<a class="btn btn-secondary" href="<?php echo $this->set ? Route::_('index.php?option=com_testquestions&task=set.edit&id=' . (int) $this->set->id) : Route::_('index.php?option=com_testquestions&view=questions'); ?>">
<?php echo $this->set ? \Joomla\CMS\Language\Text::_('COM_TESTQUESTIONS_EDIT_SET') : \Joomla\CMS\Language\Text::_('COM_TESTQUESTIONS_PREVIEW_BACK'); ?>
</a>
</div>
@@ -28,7 +28,11 @@ HTMLHelper::_('behavior.formvalidator');
<?php echo $this->form->renderField('test_content'); ?>
<?php echo $this->form->renderField('image'); ?>
<?php echo $this->form->renderField('description'); ?>
<?php if ($this->isVariant) : ?>
<div class="alert alert-info"><?php echo \Joomla\CMS\Language\Text::_('COM_TESTQUESTIONS_SET_VARIANT_STATUS_INHERITED'); ?></div>
<?php else : ?>
<?php echo $this->form->renderField('published'); ?>
<?php endif; ?>
<?php if ((int) $this->item->id > 0 && (string) $this->item->set_type === 'multi') : ?>
<fieldset class="mb-3">
<legend><?php echo \Joomla\CMS\Language\Text::_('COM_TESTQUESTIONS_SET_VARIANTS'); ?></legend>
@@ -36,8 +40,13 @@ HTMLHelper::_('behavior.formvalidator');
<ul class="list-group mb-3">
<?php foreach ($this->variants as $variant) : ?>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span>
<a href="<?php echo Route::_('index.php?option=com_testquestions&task=set.edit&id=' . (int) $variant->id); ?>"><?php echo $this->escape($variant->title); ?></a>
<span class="badge <?php echo (int) $variant->published ? 'bg-success' : 'bg-secondary'; ?>"><?php echo (int) $variant->published ? \Joomla\CMS\Language\Text::_('JPUBLISHED') : \Joomla\CMS\Language\Text::_('JUNPUBLISHED'); ?></span>
</span>
<a class="btn btn-sm btn-outline-secondary" href="<?php echo Route::_('index.php?option=com_testquestions&view=preview&set_id=' . (int) $variant->id); ?>">
<?php echo \Joomla\CMS\Language\Text::_('COM_TESTQUESTIONS_PREVIEW_SET'); ?>
</a>
</li>
<?php endforeach; ?>
</ul>
@@ -54,7 +54,12 @@ HTMLHelper::_('behavior.multiselect');
<?php foreach ($this->items as $i => $item) : ?>
<tr>
<td><?php echo HTMLHelper::_('grid.id', $i, $item->id); ?></td>
<th scope="row"><a href="<?php echo Route::_('index.php?option=com_testquestions&task=set.edit&id=' . (int) $item->id); ?>"><?php echo (int) $item->parent_id > 0 ? '↳ ' : ''; ?><?php echo $this->escape($item->title); ?></a><?php if ((int) $item->parent_id > 0 && $item->parent_title) : ?> <small class="text-muted">(<?php echo $this->escape($item->parent_title); ?>)</small><?php endif; ?></th>
<th scope="row">
<a href="<?php echo Route::_('index.php?option=com_testquestions&task=set.edit&id=' . (int) $item->id); ?>"><?php echo (int) $item->parent_id > 0 ? '↳ ' : ''; ?><?php echo $this->escape($item->title); ?></a><?php if ((int) $item->parent_id > 0 && $item->parent_title) : ?> <small class="text-muted">(<?php echo $this->escape($item->parent_title); ?>)</small><?php endif; ?>
<?php if ($item->set_type === 'ordinary') : ?>
<a class="btn btn-sm btn-outline-secondary ms-2" href="<?php echo Route::_('index.php?option=com_testquestions&view=preview&set_id=' . (int) $item->id); ?>"><?php echo \Joomla\CMS\Language\Text::_('COM_TESTQUESTIONS_PREVIEW_SET'); ?></a>
<?php endif; ?>
</th>
<td><?php echo $this->escape($item->set_type === 'multi' ? \Joomla\CMS\Language\Text::_('COM_TESTQUESTIONS_SET_TYPE_MULTI') : ($item->set_type === 'variant' ? \Joomla\CMS\Language\Text::_('COM_TESTQUESTIONS_SET_TYPE_VARIANT') : \Joomla\CMS\Language\Text::_('COM_TESTQUESTIONS_SET_TYPE_ORDINARY'))); ?></td>
<td><?php echo $this->escape($item->subject_title ?: '—'); ?></td>
<td><?php echo (int) $item->question_count; ?></td>
+1 -1
View File
@@ -4,7 +4,7 @@
<creationDate>2026-10-14</creationDate>
<author>Komponent</author>
<license>GNU General Public License version 2 or later</license>
<version>1.43.12</version>
<version>1.43.17</version>
<description>COM_TESTQUESTIONS_DESCRIPTION</description>
<element>com_testquestions</element>
<namespace path="src">Komponent\Component\Testquestions</namespace>
@@ -3,6 +3,17 @@
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Uri\Uri;
$resolveImageUrl = static function ($path) {
$path = trim((string) $path);
if ($path === '' || preg_match('#^https?://#i', $path)) {
return $path;
}
return Uri::root() . ltrim($path, '/');
};
?>
<div class="com-testquestions egzamin">
<h1><?php echo \Joomla\CMS\Language\Text::_('COM_TESTQUESTIONS'); ?></h1>
@@ -43,7 +54,7 @@ use Joomla\CMS\HTML\HTMLHelper;
</div>
</div>
<?php if ($item->image) : ?>
<div class="foto_pyt"><img src="<?php echo htmlspecialchars($item->image, ENT_QUOTES, 'UTF-8'); ?>" alt="" loading="lazy"></div>
<div class="foto_pyt"><img src="<?php echo htmlspecialchars($resolveImageUrl($item->image), ENT_QUOTES, 'UTF-8'); ?>" alt="" loading="lazy"></div>
<?php endif; ?>
<?php if ($item->question_type === 'open') : ?>
<div class="<?php echo $this->results ? ($this->results['items'][$item->id]['correct'] ? 'odpup' : 'odpu') : 'odp'; ?>">
@@ -3,6 +3,17 @@
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Uri\Uri;
$resolveImageUrl = static function ($path) {
$path = trim((string) $path);
if ($path === '' || preg_match('#^https?://#i', $path)) {
return $path;
}
return Uri::root() . ltrim($path, '/');
};
?>
<div class="com-testquestions egzamin">
<?php if (!$this->started) : ?>
@@ -44,7 +55,7 @@ use Joomla\CMS\Factory;
<h1><?php echo htmlspecialchars($this->set->title, ENT_QUOTES, 'UTF-8'); ?></h1>
<?php $extraTime = Factory::getApplication()->getInput()->post->getInt('extra_time', 0); ?>
<?php if ((int) $this->set->duration > 0) : ?><div class="small text-muted"><?php echo \Joomla\CMS\Language\Text::_('COM_TESTQUESTIONS_SET_DURATION'); ?>: <?php echo (int) $this->set->duration + $extraTime; ?> min</div><?php endif; ?>
<?php if ($this->set->image) : ?><div class="foto_pyt"><img src="<?php echo htmlspecialchars($this->set->image, ENT_QUOTES, 'UTF-8'); ?>" alt="" loading="lazy"></div><?php endif; ?>
<?php if ($this->set->image) : ?><div class="foto_pyt"><img src="<?php echo htmlspecialchars($resolveImageUrl($this->set->image), ENT_QUOTES, 'UTF-8'); ?>" alt="" loading="lazy"></div><?php endif; ?>
<?php if ($this->set->test_content) : ?><div class="test-set-content"><?php echo $this->set->test_content; ?></div><?php endif; ?>
</header>
<?php if (!$this->results && ((int) $this->set->duration + $extraTime) > 0) : ?>
@@ -64,7 +75,7 @@ use Joomla\CMS\Factory;
<?php foreach ($this->items as $number => $item) : ?>
<article class="testquestion mb-4">
<div class="pyt"><div class="question-header"><span class="question-number"><?php echo ($number + 1) . '.'; ?></span><div class="question-text"><?php echo $item->question; ?></div></div></div>
<?php if ($item->image) : ?><div class="foto_pyt"><img src="<?php echo htmlspecialchars($item->image, ENT_QUOTES, 'UTF-8'); ?>" alt="" loading="lazy"></div><?php endif; ?>
<?php if ($item->image) : ?><div class="foto_pyt"><img src="<?php echo htmlspecialchars($resolveImageUrl($item->image), ENT_QUOTES, 'UTF-8'); ?>" alt="" loading="lazy"></div><?php endif; ?>
<?php if ($item->question_type === 'open') : ?>
<div class="<?php echo $this->results ? ($this->results['items'][$item->id]['correct'] ? 'odpup' : 'odpu') : 'odp'; ?>">
<?php if ($this->results) : ?>