Release 2.6.0 (#4472)
- Added: calendar entry title combination for customer, project and activity - Added: show not_invoiced and not_exported data in detail screens - Added: force logout if user is disabled - Added: reduced amount of database queries on several screens - Fixed: open-close status on work-contract screen for users without configuration - Fixed: failsafe order/orderBy in query if manually manipulated to be null - Fixed: unify statistic calculation (not_invoiced and not_exported) across screens - Tech: bump packages - Tech: Symfony 6.4
This commit is contained in:
@@ -31,6 +31,8 @@ use App\Form\Type\ActivityType;
|
||||
use App\Repository\ActivityRateRepository;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use App\Repository\Query\TeamQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
@@ -135,6 +137,22 @@ final class ActivityController extends AbstractController
|
||||
$defaultTeam = null;
|
||||
$now = $this->getDateTimeFactory()->createDateTime();
|
||||
|
||||
$exportUrl = null;
|
||||
$invoiceUrl = null;
|
||||
$params = ['customers[]' => '', 'projects[]' => '', 'activities[]' => $activity->getId(), 'daterange' => '', 'exported' => TimesheetQuery::STATE_NOT_EXPORTED, 'billable' => true];
|
||||
if ($activity->getProject() !== null) {
|
||||
$params['projects[]'] = $activity->getProject()->getId();
|
||||
if ($activity->getProject()->getCustomer() !== null) {
|
||||
$params['customers[]'] = $activity->getProject()->getCustomer()->getId();
|
||||
}
|
||||
}
|
||||
if ($this->isGranted('create_export')) {
|
||||
$exportUrl = $this->generateUrl('export', array_merge($params, ['preview' => true]));
|
||||
}
|
||||
if ($this->isGranted('view_invoice')) {
|
||||
$invoiceUrl = $this->generateUrl('invoice', $params);
|
||||
}
|
||||
|
||||
if ($this->isGranted('edit', $activity)) {
|
||||
if ($this->isGranted('create_team')) {
|
||||
$defaultTeam = $teamRepository->findOneBy(['name' => $activity->getName()]);
|
||||
@@ -147,7 +165,9 @@ final class ActivityController extends AbstractController
|
||||
}
|
||||
|
||||
if ($this->isGranted('permissions', $activity) || $this->isGranted('details', $activity) || $this->isGranted('view_team')) {
|
||||
$teams = $activity->getTeams();
|
||||
$query = new TeamQuery();
|
||||
$query->addActivity($activity);
|
||||
$teams = $teamRepository->getTeamsForQuery($query);
|
||||
}
|
||||
|
||||
// additional boxes by plugins
|
||||
@@ -168,7 +188,9 @@ final class ActivityController extends AbstractController
|
||||
'team' => $defaultTeam,
|
||||
'teams' => $teams,
|
||||
'now' => $now,
|
||||
'boxes' => $boxes
|
||||
'boxes' => $boxes,
|
||||
'export_url' => $exportUrl,
|
||||
'invoice_url' => $invoiceUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@ namespace App\Controller\Auth;
|
||||
use App\Configuration\SamlConfigurationInterface;
|
||||
use App\Saml\SamlAuthFactory;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\SecurityRequestAttributes;
|
||||
|
||||
#[Route(path: '/saml')]
|
||||
final class SamlController extends AbstractController
|
||||
@@ -32,7 +32,7 @@ final class SamlController extends AbstractController
|
||||
}
|
||||
|
||||
$session = $request->getSession();
|
||||
$authErrorKey = Security::AUTHENTICATION_ERROR;
|
||||
$authErrorKey = SecurityRequestAttributes::AUTHENTICATION_ERROR;
|
||||
|
||||
$error = null;
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use App\Repository\Query\TeamQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
@@ -307,6 +309,15 @@ final class CustomerController extends AbstractController
|
||||
$rates = [];
|
||||
$now = $this->getDateTimeFactory()->createDateTime();
|
||||
|
||||
$exportUrl = null;
|
||||
$invoiceUrl = null;
|
||||
if ($this->isGranted('create_export')) {
|
||||
$exportUrl = $this->generateUrl('export', ['customers[]' => $customer->getId(), 'projects[]' => '', 'daterange' => '', 'exported' => TimesheetQuery::STATE_NOT_EXPORTED, 'preview' => true, 'billable' => true]);
|
||||
}
|
||||
if ($this->isGranted('view_invoice')) {
|
||||
$invoiceUrl = $this->generateUrl('invoice', ['customers[]' => $customer->getId(), 'projects[]' => '', 'daterange' => '', 'exported' => TimesheetQuery::STATE_NOT_EXPORTED, 'billable' => true]);
|
||||
}
|
||||
|
||||
if ($this->isGranted('edit', $customer)) {
|
||||
if ($this->isGranted('create_team')) {
|
||||
$defaultTeam = $teamRepository->findOneBy(['name' => $customer->getName()]);
|
||||
@@ -328,7 +339,9 @@ final class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
if ($this->isGranted('permissions', $customer) || $this->isGranted('details', $customer) || $this->isGranted('view_team')) {
|
||||
$teams = $customer->getTeams();
|
||||
$query = new TeamQuery();
|
||||
$query->addCustomer($customer);
|
||||
$teams = $teamRepository->getTeamsForQuery($query);
|
||||
}
|
||||
|
||||
// additional boxes by plugins
|
||||
@@ -353,7 +366,9 @@ final class CustomerController extends AbstractController
|
||||
'customer_now' => new \DateTime('now', $timezone),
|
||||
'rates' => $rates,
|
||||
'now' => $now,
|
||||
'boxes' => $boxes
|
||||
'boxes' => $boxes,
|
||||
'export_url' => $exportUrl,
|
||||
'invoice_url' => $invoiceUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -248,7 +248,7 @@ final class ProfileController extends AbstractController
|
||||
public function preferencesAction(User $profile, Request $request, EventDispatcherInterface $dispatcher, UserRepository $userRepository): Response
|
||||
{
|
||||
// we need to prepare the user preferences, which is done via an EventSubscriber
|
||||
$event = new PrepareUserEvent($profile);
|
||||
$event = new PrepareUserEvent($profile, false);
|
||||
$dispatcher->dispatch($event);
|
||||
|
||||
$form = $this->createPreferencesForm($profile);
|
||||
|
||||
@@ -36,6 +36,8 @@ use App\Repository\ProjectRateRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use App\Repository\Query\TeamQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Utils\Context;
|
||||
use App\Utils\DataTable;
|
||||
@@ -334,6 +336,15 @@ final class ProjectController extends AbstractController
|
||||
$rates = [];
|
||||
$now = $this->getDateTimeFactory()->createDateTime();
|
||||
|
||||
$exportUrl = null;
|
||||
$invoiceUrl = null;
|
||||
if ($this->isGranted('create_export') && $project->getCustomer() !== null) {
|
||||
$exportUrl = $this->generateUrl('export', ['customers[]' => $project->getCustomer()->getId(), 'projects[]' => $project->getId(), 'daterange' => '', 'exported' => TimesheetQuery::STATE_NOT_EXPORTED, 'preview' => true, 'billable' => true]);
|
||||
}
|
||||
if ($this->isGranted('view_invoice') && $project->getCustomer() !== null) {
|
||||
$invoiceUrl = $this->generateUrl('invoice', ['customers[]' => $project->getCustomer()->getId(), 'projects[]' => $project->getId(), 'daterange' => '', 'exported' => TimesheetQuery::STATE_NOT_EXPORTED, 'billable' => true]);
|
||||
}
|
||||
|
||||
if ($this->isGranted('edit', $project)) {
|
||||
if ($this->isGranted('create_team')) {
|
||||
$defaultTeam = $teamRepository->findOneBy(['name' => $project->getName()]);
|
||||
@@ -351,7 +362,9 @@ final class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
if ($this->isGranted('permissions', $project) || $this->isGranted('details', $project) || $this->isGranted('view_team')) {
|
||||
$teams = $project->getTeams();
|
||||
$query = new TeamQuery();
|
||||
$query->addProject($project);
|
||||
$teams = $teamRepository->getTeamsForQuery($query);
|
||||
}
|
||||
|
||||
// additional boxes by plugins
|
||||
@@ -375,7 +388,9 @@ final class ProjectController extends AbstractController
|
||||
'teams' => $teams,
|
||||
'rates' => $rates,
|
||||
'now' => $now,
|
||||
'boxes' => $boxes
|
||||
'boxes' => $boxes,
|
||||
'export_url' => $exportUrl,
|
||||
'invoice_url' => $invoiceUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,14 +30,15 @@ final class ProjectDateRangeController extends AbstractController
|
||||
$dateFactory = $this->getDateTimeFactory();
|
||||
$user = $this->getUser();
|
||||
|
||||
$query = new ProjectDaterangeQuery($dateFactory->getStartOfMonth(), $user);
|
||||
$defaultStart = $dateFactory->getStartOfMonth();
|
||||
$query = new ProjectDaterangeQuery($defaultStart, $user);
|
||||
$form = $this->createFormForGetRequest(ProjectDateRangeForm::class, $query, [
|
||||
'timezone' => $user->getTimezone()
|
||||
]);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
$dateRange = new DateRange(true);
|
||||
$dateRange->setBegin($query->getMonth());
|
||||
$dateRange->setBegin($query->getMonth() ?? $defaultStart);
|
||||
$dateRange->setEnd($dateFactory->getEndOfMonth($dateRange->getBegin()));
|
||||
|
||||
$projects = $service->findProjectsForDateRange($query, $dateRange);
|
||||
|
||||
@@ -64,7 +64,7 @@ final class PasswordResetController extends AbstractController
|
||||
$username = $request->request->get('username');
|
||||
$user = $this->userService->findUserByUsernameOrEmail($username);
|
||||
|
||||
if (null !== $user && !$user->isPasswordRequestNonExpired($this->configuration->getPasswordResetRetryLifetime())) {
|
||||
if (!$user->isPasswordRequestNonExpired($this->configuration->getPasswordResetRetryLifetime())) {
|
||||
if (!$user->isInternalUser()) {
|
||||
throw $this->createAccessDeniedException(
|
||||
sprintf('The user "%s" tried to reset the password, but it is registered as "%s" auth-type.', $user->getUserIdentifier(), $user->getAuth())
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace App\Controller;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\User;
|
||||
use App\Event\PrepareUserEvent;
|
||||
use App\Event\UserPreferenceDisplayEvent;
|
||||
use App\Export\Spreadsheet\UserExporter;
|
||||
use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
|
||||
@@ -112,7 +113,7 @@ final class UserController extends AbstractController
|
||||
|
||||
#[Route(path: '/create', name: 'admin_user_create', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('create_user')]
|
||||
public function createAction(Request $request, SystemConfiguration $config, UserRepository $userRepository): Response
|
||||
public function createAction(Request $request, SystemConfiguration $config, UserRepository $userRepository, EventDispatcherInterface $dispatcher): Response
|
||||
{
|
||||
$user = $this->createNewDefaultUser($config);
|
||||
$editForm = $this->getCreateUserForm($user);
|
||||
@@ -126,6 +127,14 @@ final class UserController extends AbstractController
|
||||
$userRepository->saveUser($user);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
try {
|
||||
$event = new PrepareUserEvent($user, false);
|
||||
$dispatcher->dispatch($event);
|
||||
$userRepository->saveUser($user);
|
||||
} catch (\Exception $ex) {
|
||||
// it should be no problem, if creating default user preferences fails
|
||||
}
|
||||
|
||||
return $this->redirectToRouteAfterCreate('user_profile_edit', ['username' => $user->getUserIdentifier()]);
|
||||
}
|
||||
|
||||
|
||||
@@ -76,9 +76,9 @@ final class WizardController extends AbstractController
|
||||
$userService->updateUser($user);
|
||||
|
||||
if ($data['reload'] === '1') {
|
||||
return $this->redirectToRoute('wizard', ['wizard' => 'profile', '_locale' => $data['language']]);
|
||||
return $this->redirectToRoute('wizard', ['wizard' => 'profile', '_locale' => $user->getLanguage()]);
|
||||
} else {
|
||||
return $this->redirectToRoute('wizard', ['wizard' => $next, '_locale' => $data['language']]);
|
||||
return $this->redirectToRoute('wizard', ['wizard' => $next, '_locale' => $user->getLanguage()]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user