Skip to content

Commit 7143b02

Browse files
committed
fix #38: implement feature selection page
also: start porting templating to twig
1 parent b83cac3 commit 7143b02

10 files changed

+427
-112
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ composer.lock
77
vendor
88
.proxies
99
.cache
10+
uploads/*

composer.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,14 @@
66
"mpratt/embera": "*",
77
"nyholm/psr7": "*",
88
"symfony/cache": "*",
9+
"symfony/form": "*",
910
"symfony/http-client": "*",
11+
"symfony/http-foundation": "*",
1012
"symfony/mailer": "*",
11-
"symfony/mailgun-mailer": "*"
13+
"symfony/mailgun-mailer": "*",
14+
"symfony/translation": "*",
15+
"symfony/twig-bridge": "*",
16+
"twig/twig": "*"
1217
},
1318
"autoload": {
1419
"psr-4": {"": "entities"}

entities/ProjGroup.php

+6
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ class ProjGroup
8282
/** @Column */
8383
public string $patch_submission = 'https://...';
8484

85+
/** @Column(length=40) */
86+
public string $hash_proposal_file = '';
87+
88+
/** @Column */
89+
public string $url_proposal = '';
90+
8591
/** @Column */
8692
public DateTimeImmutable $allow_modifications_date;
8793

index.php

+176-1
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,27 @@
99
require 'github.php';
1010
require 'validation.php';
1111

12+
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
13+
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
14+
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
15+
1216
$page = $_REQUEST['page'] ?? '';
1317
$file = "pages/$page.php";
1418

19+
use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationExtension;
20+
use Symfony\Component\Form\Forms;
21+
22+
$formFactory = Forms::createFormFactoryBuilder()
23+
->addExtension(new HttpFoundationExtension())
24+
->getFormFactory();
25+
$form = null;
26+
$select_form = null;
27+
$embed_file = null;
28+
$success_message = null;
29+
$table = null;
30+
31+
$request = \Symfony\Component\HttpFoundation\Request::createFromGlobals();
32+
1533
try {
1634
if (ctype_alpha($page) && file_exists($file)) {
1735
require $file;
@@ -26,4 +44,161 @@
2644
}
2745
}
2846

29-
html_footer();
47+
function terminate($error_message = null) {
48+
global $page, $table, $form, $select_form, $embed_file, $success_message;
49+
50+
$appvar = new \ReflectionClass('\Symfony\Bridge\Twig\AppVariable');
51+
$loader = new \Twig\Loader\FilesystemLoader([
52+
__DIR__ . '/templates',
53+
dirname($appvar->getFileName()) . '/Resources/views/Form'
54+
]);
55+
56+
$options = [
57+
'debug' => !IN_PRODUCTION,
58+
'cache' => __DIR__ . '/.cache/twig',
59+
];
60+
$twig = new \Twig\Environment($loader, $options);
61+
$formEngine = new \Symfony\Bridge\Twig\Form\TwigRendererEngine(
62+
['bootstrap_5_layout.html.twig'], $twig);
63+
$twig->addRuntimeLoader(new \Twig\RuntimeLoader\FactoryRuntimeLoader([
64+
Symfony\Component\Form\FormRenderer::class => function () use ($formEngine) {
65+
return new \Symfony\Component\Form\FormRenderer($formEngine);
66+
}
67+
]));
68+
$twig->addExtension(new \Symfony\Bridge\Twig\Extension\FormExtension());
69+
$twig->addExtension(new \Symfony\Bridge\Twig\Extension\TranslationExtension());
70+
71+
$pages = [
72+
['dashboard', 'Statistics', ROLE_STUDENT],
73+
['profile', 'Edit profile', ROLE_STUDENT],
74+
['listprojects', 'Projects', ROLE_STUDENT],
75+
['bugs', 'Bugs', ROLE_STUDENT],
76+
['features', 'Features', ROLE_STUDENT],
77+
['patches', 'Patches', ROLE_STUDENT],
78+
['shifts', 'Shifts', ROLE_PROF],
79+
['deadlines', 'Deadlines', ROLE_PROF],
80+
['changerole', 'Change Role', ROLE_PROF],
81+
['impersonate', 'Impersonate', ROLE_SUDO],
82+
['cron', 'Cron', ROLE_PROF],
83+
['phpinfo', 'PHP Info', ROLE_PROF],
84+
];
85+
$navbar = [];
86+
$title = '';
87+
88+
foreach ($pages as $p) {
89+
if ($p[0] === $page)
90+
$title = $p[1];
91+
92+
if (auth_at_least($p[2]))
93+
$navbar[] = [
94+
'url' => dourl($p[0]),
95+
'name' => $p[1]
96+
];
97+
}
98+
99+
$user = get_user();
100+
101+
$content = [
102+
'title' => $title,
103+
'navbar' => $navbar,
104+
'name' => $user->name,
105+
'email' => $user->email,
106+
'user_id' => $user->id,
107+
'role' => get_role_string(),
108+
'photo' => $user->getPhoto(),
109+
'embed_file' => $embed_file,
110+
'success_message' => $error_message ? '' : $success_message,
111+
'error_message' => $error_message,
112+
'table' => $table,
113+
];
114+
115+
if ($form)
116+
$content['form'] = $form->createView();
117+
if ($select_form)
118+
$content['select_form'] = $select_form->createView();
119+
120+
echo $twig->render('main.html.twig', $content);
121+
db_flush();
122+
exit();
123+
}
124+
125+
function filter_by($filters) {
126+
global $page, $request, $formFactory, $select_form;
127+
$select_form = $formFactory->createNamedBuilder('',
128+
Symfony\Component\Form\Extension\Core\Type\FormType::class);
129+
130+
$select_form->add('page', HiddenType::class, [
131+
'data' => $page,
132+
]);
133+
134+
$selected_year = $request->query->get('year', db_get_group_years()[0]['year']);
135+
$selected_shift = $request->query->get('shift', null);
136+
$own_shifts_only = $request->query->get('own_shifts', false) ? true : false;
137+
$selected_group = $request->query->get('group', 'all');
138+
$selected_repo = $request->query->get('repo', 'all');
139+
$return = null;
140+
141+
$selected_shift_obj = $selected_shift && $selected_shift != 'all'
142+
? db_fetch_shift_id($selected_shift) : null;
143+
144+
if (in_array('year', $filters)) {
145+
foreach (db_get_group_years() as $year) {
146+
$years[$year['year']] = $year['year'];
147+
}
148+
$select_form->add('year', ChoiceType::class, [
149+
'label' => 'Year',
150+
'choices' => $years,
151+
'data' => $selected_year,
152+
]);
153+
$return = $selected_year;
154+
}
155+
if (in_array('shift', $filters)) {
156+
$shifts = ['All' => 'all'];
157+
foreach (db_fetch_shifts($selected_year) as $shift) {
158+
if (!has_shift_permissions($shift))
159+
continue;
160+
if ($own_shifts_only && $shift->prof != get_user())
161+
continue;
162+
$shifts[$shift->name] = $shift->id;
163+
}
164+
$select_form->add('shift', ChoiceType::class, [
165+
'label' => 'Shift',
166+
'choices' => $shifts,
167+
'data' => $selected_shift,
168+
]);
169+
}
170+
if (in_array('group', $filters)) {
171+
$groups = ['All' => 'all'];
172+
$return = [];
173+
foreach (db_fetch_groups($selected_year) as $group) {
174+
if (!has_group_permissions($group))
175+
continue;
176+
if ($own_shifts_only && $group->prof() != get_user())
177+
continue;
178+
if ($selected_shift_obj && $group->shift != $selected_shift_obj)
179+
continue;
180+
if ($selected_repo != 'all' && $group->getRepositoryId() != $selected_repo)
181+
continue;
182+
183+
if ($selected_group == 'all' || $group->id == $selected_group)
184+
$return[] = $group;
185+
$groups[$group->group_number] = $group->id;
186+
}
187+
$select_form->add('group', ChoiceType::class, [
188+
'label' => 'Group',
189+
'choices' => $groups,
190+
'data' => $selected_group,
191+
]);
192+
}
193+
if (in_array('own_shifts', $filters)) {
194+
$select_form->add('own_shifts', CheckboxType::class, [
195+
'label' => 'Show only own shifts',
196+
'data' => $own_shifts_only,
197+
]);
198+
}
199+
$select_form = $select_form->getForm();
200+
$select_form->handleRequest($request);
201+
return $return;
202+
}
203+
204+
terminate();

pages/dashboard.php

+1-5
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,7 @@
22
// Copyright (c) 2022-present Instituto Superior Técnico.
33
// Distributed under the MIT license that can be found in the LICENSE file.
44

5-
html_header('Dashboard');
6-
7-
do_start_form('dashboard');
8-
$selected_year = do_year_selector();
9-
echo "</form>\n";
5+
$selected_year = filter_by(['year']);
106

117
foreach (db_get_merged_patch_stats() as $entry) {
128
$years[] = '"' . get_term_for($entry['year']) . '"';

pages/features.php

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
// Copyright (c) 2022-present Instituto Superior Técnico.
3+
// Distributed under the MIT license that can be found in the LICENSE file.
4+
5+
use Symfony\Component\Form\Extension\Core\Type\FormType;
6+
use Symfony\Component\Form\Extension\Core\Type\FileType;
7+
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
8+
use Symfony\Component\Form\Extension\Core\Type\UrlType;
9+
use Symfony\Component\HttpFoundation\File\UploadedFile;
10+
11+
$user = get_user();
12+
$group = $user->getGroup();
13+
if (!$group && $user->role === ROLE_STUDENT) {
14+
terminate('Student is not in a group');
15+
}
16+
17+
if (!empty($_GET['download'])) {
18+
$group = db_fetch_group_id($_GET['download']);
19+
if (has_group_permissions($group)) {
20+
header('Content-Type: application/pdf');
21+
header('Content-Disposition: inline; filename="feature_proposal_' .
22+
$group->group_number . '.pdf"');
23+
readfile(__DIR__ . "/../uploads/{$group->hash_proposal_file}");
24+
exit();
25+
} else {
26+
die("No permissions");
27+
}
28+
}
29+
30+
if ($user->role === ROLE_STUDENT) {
31+
$form = $formFactory->createBuilder(FormType::class)
32+
->add('url', UrlType::class, [
33+
'label' => 'Issue URL (if applicable)',
34+
'required' => false,
35+
'data' => $group->url_proposal,
36+
])
37+
->add('file', FileType::class, [
38+
'label' => 'Upload PDF',
39+
'attr' => ['accept' => '.pdf'],
40+
])
41+
->add('submit', SubmitType::class, ['label' => 'Upload'])
42+
->getForm();
43+
44+
$form->handleRequest($request);
45+
46+
if ($form->isSubmitted() && $form->isValid()) {
47+
$file = $form->get('file')->getData();
48+
if ($file instanceof UploadedFile) {
49+
if ($file->getSize() > 5 * 1024 * 1024) {
50+
terminate('Error: File size exceeds 5 MB');
51+
}
52+
if ($file->getClientMimeType() !== 'application/pdf' ||
53+
(new finfo())->file($file->getPathname(), FILEINFO_MIME_TYPE)
54+
!== 'application/pdf') {
55+
terminate('Error: Only PDF files are allowed');
56+
}
57+
58+
$hash = hash_file('sha1', $file->getPathname());
59+
$file->move(__DIR__ . '/../uploads', $hash);
60+
$success_message = "File uploaded successfully!";
61+
62+
if ($group->hash_proposal_file && $group->hash_proposal_file !== $hash) {
63+
unlink(__DIR__ . "/../uploads/{$group->hash_proposal_file}");
64+
}
65+
$group->hash_proposal_file = $hash;
66+
$group->url_proposal = check_url($form->get('url')->getData());
67+
}
68+
}
69+
}
70+
71+
if (auth_at_least(ROLE_TA)) {
72+
$groups = filter_by(['group', 'year', 'shift', 'own_shifts']);
73+
74+
foreach ($groups as $group) {
75+
$table[] = [
76+
'Group' => $group->group_number,
77+
'Issue URL' => $group->url_proposal
78+
? ['label' => 'link', 'url' => $group->url_proposal] : '',
79+
'PDF' => $group->hash_proposal_file
80+
? ['label' => 'link',
81+
'url' => dourl('features', ['download' => $group->id], '&')]
82+
: '',
83+
];
84+
}
85+
if (sizeof($groups) === 1) {
86+
$group = $groups[0];
87+
}
88+
}
89+
90+
if ($group && $group->hash_proposal_file) {
91+
$embed_file = dourl('features', ['download' => $group->id], '&');
92+
}

0 commit comments

Comments
 (0)