Skip to content
Snippets Groups Projects
Commit 0ed812fe authored by Stefan Galinski's avatar Stefan Galinski :video_game:
Browse files

Merge branch 'feature_4256-related-jobs' into 'master'

Feature 4256 related jobs

See merge request !38
parents e941701a fe4a0db0
No related branches found
No related tags found
1 merge request!38Feature 4256 related jobs
Showing
with 323 additions and 65 deletions
......@@ -335,6 +335,11 @@ class JoblistController extends ActionController {
$headTagService->execute();
}
$this->view->assign('job', $job);
$enableAutomaticRelatedJobs = (bool) $this->settings['enableAutomaticRelatedJobs'];
if ($enableAutomaticRelatedJobs) {
$relatedJobs = $this->jobRepository->findRelated($job);
$this->view->assign('relatedJobs', $relatedJobs);
}
} else {
$storagePid = (int) $this->configurationManager->getConfiguration(
ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK
......
......@@ -112,6 +112,12 @@ class Job extends AbstractEntity {
*/
protected $company;
/**
* @TYPO3\CMS\Extbase\Annotation\ORM\Lazy
* @var \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\SGalinski\SgJobs\Domain\Model\Job>
*/
protected $relatedJobs;
/**
* @var string $description
*/
......@@ -176,6 +182,7 @@ class Job extends AbstractEntity {
public function __construct() {
$this->attachment = new ObjectStorage();
$this->company = new ObjectStorage();
$this->relatedJobs = new ObjectStorage();
}
/**
......@@ -578,4 +585,47 @@ class Job extends AbstractEntity {
public function setApplyExternalLink(string $applyExternalLink) {
$this->applyExternalLink = $applyExternalLink;
}
/**
* @param ObjectStorage $relatedJobs
* @return void
*/
public function setRelatedJobs(ObjectStorage $relatedJobs): void {
$this->relatedJobs = $relatedJobs;
}
/**
* @return ObjectStorage
*/
public function getRelatedJobs() {
if ($this->relatedJobs instanceof LazyLoadingProxy) {
$this->relatedJobs->_loadRealInstance();
}
return $this->relatedJobs;
}
/**
* @param Job $relatedJobs
* @return void
*/
public function addRelatedJobs(Job $relatedJobs): void {
if ($this->relatedJobs instanceof LazyLoadingProxy) {
$this->relatedJobs->_loadRealInstance();
}
$this->relatedJobs->attach($relatedJobs);
}
/**
* @param Job $relatedJobs
* @return void
*/
public function removeRelatedJobs(Job $relatedJobs): void {
if ($this->relatedJobs instanceof LazyLoadingProxy) {
$this->relatedJobs->_loadRealInstance();
}
$this->relatedJobs->detach($relatedJobs);
}
}
......@@ -26,12 +26,16 @@ namespace SGalinski\SgJobs\Domain\Repository;
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use SGalinski\SgJobs\Domain\Model\Job;
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Query;
use TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbQueryParser;
use TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings;
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
use TYPO3\CMS\Extbase\Persistence\QueryResultInterface;
use TYPO3\CMS\Extbase\Persistence\QueryResultInterface as ExtbaseQueryResultInterface;
use TYPO3\CMS\Extbase\Persistence\Repository;
use TYPO3\CMS\Extbase\Utility\DebuggerUtility;
/**
* Job Repository
......@@ -259,6 +263,94 @@ class JobRepository extends Repository {
return $query->execute();
}
/**
* Finds related jobs by company & department.
* Tries to find at least $minRelatedJobs by first combining all constraints via AND.
* If we cannot find enough related jobs with these constraints, we repeat the query but with fewer constraints.
* E.g. we can only find 1 record which has the same country AND department -> fill the remaining space with
* jobs, which have either the same country OR department.
*
* @param Job $job
* @param int $limit
* @return array
*/
public function findRelated(Job $job, int $limit = 0): array {
// the minimum amount of related jobs, we should try to find before removing constraints
$minRelatedJobs = 2;
$relatedJobs = [];
$query = $this->prepareRelatedJobsQuery($job, 0);
if ($limit > 0) {
$query->setLimit($limit);
}
$resultCount = $query->count();
// fill $relatedJobs with more related jobs, by repeating the query, but with fewer constraints
if ($resultCount < $minRelatedJobs) {
$queryResult = $query->execute()->toArray();
array_push($relatedJobs, ...$queryResult);
$query = $this->prepareRelatedJobsQuery($job, 1);
$queryResult = $query->execute()->toArray();
array_push($relatedJobs, ...$queryResult);
$relatedJobs = array_unique($relatedJobs);
$relatedJobsCount = count($relatedJobs);
if ($relatedJobsCount < $minRelatedJobs) {
$query = $this->prepareRelatedJobsQuery($job, 2);
$queryResult = $query->execute()->toArray();
array_push($relatedJobs, ...$queryResult);
$relatedJobs = array_unique($relatedJobs);
}
} else {
$relatedJobs = $query->execute()->toArray();
}
return $relatedJobs;
}
/**
* Helper function, to avoid duplicate code.
* Returns a query, to be used within findRelated()
*
* @param Job $job
* @param int $iteration
* @return QueryInterface
*/
protected function prepareRelatedJobsQuery(Job $job, int $iteration): QueryInterface {
$query = $this->createQuery();
$constraints = [];
$storagePageIds = $query->getQuerySettings()->getStoragePageIds();
if (empty($storagePageIds)) {
// if no record storage page has been selected in the plugin, ignore it
$query->getQuerySettings()->setRespectStoragePage(FALSE);
}
$company = $job->getCompany();
$department = $job->getDepartment();
// look for jobs, which have the same company AND department
if ($iteration === 0) {
if ($company !== NULL) {
$constraints[] = $query->equals('company', $company->getUid());
}
if ($department !== NULL) {
$constraints[] = $query->equals('department', $department->getUid());
}
} else if (($iteration === 1) && $company !== NULL) {
// look for jobs, which have the same company
$constraints[] = $query->equals('company', $company->getUid());
} else if (($iteration === 2) && $department !== NULL) {
// look for jobs, which have the same department
$constraints[] = $query->equals('department', $department->getUid());
}
if (\count($constraints)) {
$query->matching($query->logicalAnd($constraints));
}
return $query;
}
/**
* Gets the amount of jobs filtered by companies
*
......
......@@ -54,7 +54,7 @@ $columns = [
'interface' => [],
'types' => [
'1' => [
'showitem' => '--palette--;;sysLanguageAndHidden,--palette--;;palette_title,--palette--;;palette_title_start_location,--palette--;;palette_apply_function,apply_external_link,department,experience_level,company,contact,--div--;LLL:EXT:sg_jobs/Resources/Private/Language/locallang_db.xlf:tca.qualification_tab,description,task,qualification,attachment,--div--;LLL:EXT:sg_jobs/Resources/Private/Language/locallang_db.xlf:tca.seo_tab,--palette--;;palette_location_specifications,employment_types,--palette--;;palette_seo_dates,--palette--;;palette_salary,--div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:tabs.access,starttime,endtime,--div--;LLL:EXT:languagevisibility/Resources/Private/Language/locallang_db.xml:tabname,tx_languagevisibility_visibility',
'showitem' => '--palette--;;sysLanguageAndHidden,--palette--;;palette_title,--palette--;;palette_title_start_location,--palette--;;palette_apply_function,apply_external_link,department,experience_level,company,contact,related_jobs,--div--;LLL:EXT:sg_jobs/Resources/Private/Language/locallang_db.xlf:tca.qualification_tab,description,task,qualification,attachment,--div--;LLL:EXT:sg_jobs/Resources/Private/Language/locallang_db.xlf:tca.seo_tab,--palette--;;palette_location_specifications,employment_types,--palette--;;palette_seo_dates,--palette--;;palette_salary,--div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:tabs.access,starttime,endtime,--div--;LLL:EXT:languagevisibility/Resources/Private/Language/locallang_db.xml:tabname,tx_languagevisibility_visibility',
],
],
'palettes' => [
......@@ -491,6 +491,19 @@ $columns = [
'eval' => 'trim'
],
],
'related_jobs' => [
'exclude' => TRUE,
'l10n_exclude' => TRUE,
'label' => 'LLL:EXT:sg_jobs/Resources/Private/Language/locallang_db.xlf:tx_sgjobs_domain_model_job.related_jobs',
'config' => [
'type' => 'group',
'internal_type' => 'db',
'allowed' => 'tx_sgjobs_domain_model_job',
'size' => 5,
'minitems' => 0,
'maxitems' => 99
],
],
],
];
......
......@@ -25,5 +25,7 @@ plugin.tx_sgjobs {
offersPage =
# cat=plugin.tx_sgjobs/other; type=int+; If set to 1, the application-form page will redirect to the overview if no jobId is passed
disallowUnsolicitedApplication = 0
# cat=plugin.tx_sgjobs/other; type=int+; If set to 1, the (automatic) output of related jobs (in regards to the location/department) is enabled
enableAutomaticRelatedJobs = 1
}
}
......@@ -25,6 +25,7 @@ plugin.tx_sgjobs {
privacyPolicyPage = {$plugin.tx_sgjobs.settings.privacyPolicyPage}
offersPage = {$plugin.tx_sgjobs.settings.offersPage}
disallowUnsolicitedApplication = {$plugin.tx_sgjobs.settings.disallowUnsolicitedApplication}
enableAutomaticRelatedJobs = {$plugin.tx_sgjobs.settings.enableAutomaticRelatedJobs}
}
features {
......
......@@ -202,6 +202,10 @@
<source><![CDATA[Highest achieved education *]]></source>
<target><![CDATA[Höchster Abschluss *]]></target>
</trans-unit>
<trans-unit id="frontend.apply.relatedJobs" approved="yes">
<source><![CDATA[Related Jobs]]></source>
<target><![CDATA[Verwandte Stellenanzeigen]]></target>
</trans-unit>
<trans-unit id="frontend.apply.email" approved="yes">
<source><![CDATA[Email *]]></source>
<target><![CDATA[E-Mail *]]></target>
......
......@@ -381,6 +381,10 @@
<source><![CDATA[Job title]]></source>
<target><![CDATA[Stellenbezeichnung]]></target>
</trans-unit>
<trans-unit id="tx_sgjobs_domain_model_job.related_jobs" approved="yes">
<source><![CDATA[Related Jobs]]></source>
<target><![CDATA[Verwandte Stellenanzeigen]]></target>
</trans-unit>
<trans-unit id="tx_sgjobs_domain_model_job.valid_through" approved="yes">
<source><![CDATA[Application Deadline]]></source>
<target><![CDATA[Bewerbungsschluss]]></target>
......
......@@ -213,6 +213,9 @@
<trans-unit id="frontend.apply.thank_you">
<source><![CDATA[Thank you for your application!]]></source>
</trans-unit>
<trans-unit id="frontend.apply.relatedJobs">
<source><![CDATA[Related Jobs]]></source>
</trans-unit>
<trans-unit id="frontend.apply.title">
<source><![CDATA[Job title *]]></source>
</trans-unit>
......
......@@ -294,6 +294,9 @@
<trans-unit id="tx_sgjobs_domain_model_job.title">
<source><![CDATA[Job title]]></source>
</trans-unit>
<trans-unit id="tx_sgjobs_domain_model_job.related_jobs">
<source><![CDATA[Related Jobs]]></source>
</trans-unit>
<trans-unit id="tx_sgjobs_domain_model_job.valid_through">
<source><![CDATA[Application Deadline]]></source>
</trans-unit>
......
<div class="col-md-6 col-sm-6 col-cs-12">
<div class="default-content-element">
<f:link.action class="sg-jobs-job sg-card sg-card-shadow" id="offer-{job.uid}" pageUid="{applyPageUid}" controller="Joblist" action="applyForm"
pluginName="JobApplication" arguments="{jobId: job.uid}">
<div class="default-content-element default-header-element sg-card-title">
<h2 class="h2">{job.title}</h2>
</div>
<div class="sgjobs-job-body">
<f:if condition="{job.description}">
<f:format.html parseFuncTSPath="lib.parseFunc_RTE">{job.description}</f:format.html>
</f:if>
</div>
<f:if condition="{job.qualification}">
<div class="sgjobs-highlight-area">
<ul class="default-list">
<li>
<f:format.raw><f:translate key="frontend.jobStart" /></f:format.raw>
<f:if condition="{job.alternativeStartDate}">
<f:then>
{job.alternativeStartDate}
</f:then>
<f:else>
<f:format.date date="{job.startDate}" format="d.m.Y" />
</f:else>
</f:if>
</li>
<li>
<f:if condition="!{job.telecommutePossible}">
<f:then>
<f:format.raw><f:translate key="frontend.locationLabel"/></f:format.raw><br>
{job.companies -> f:count()}&nbsp;<f:translate key="frontend.locationCountLabel"/>
</f:then>
<f:else>
<f:format.raw><f:translate key="frontend.jobLocationRemote"/></f:format.raw>
</f:else>
</f:if>
</li>
</ul>
</div>
</f:if>
<div class="default-content-element sg-cta sg-cta-with-icon sg-cta-stretch">
<div class="btn btn-warning btn-lg">
<f:format.raw><f:translate key="frontend.jobDetailsCta" /></f:format.raw>
</div>
</div>
</f:link.action>
</div>
</div>
......@@ -659,6 +659,35 @@
</div>
</div>
</f:if>
<f:if condition="{job.relatedJobs}">
<f:then>
<div class="row">
<div class="container">
<h3><f:translate key="frontend.apply.relatedJobs"/></h3>
<div class="row default-content-element equal-height-columns stretch-first-child">
<f:for each="{job.relatedJobs}" as="relatedJob">
<f:render partial="Teaser" arguments="{job: relatedJob, applyPageUid: settings.applyPage}"/>
</f:for>
</div>
</div>
</div>
</f:then>
<f:else>
<f:if condition="{settings.enableAutomaticRelatedJobs}">
<div class="row">
<div class="container">
<h3><f:translate key="frontend.apply.relatedJobs"/></h3>
<div class="row default-content-element equal-height-columns stretch-first-child">
<f:for each="{relatedJobs}" as="relatedJob">
<f:render partial="Teaser" arguments="{job: relatedJob, applyPageUid: settings.applyPage}"/>
</f:for>
</div>
</div>
</div>
</f:if>
</f:else>
</f:if>
</f:section>
<f:section name="privacyPolicyCheckboxLink">
......
CREATE TABLE tx_sgjobs_domain_model_job (
path_segment text DEFAULT '' NOT NULL,
title text DEFAULT '' NOT NULL,
job_id varchar(30) DEFAULT '' NOT NULL,
attachment int(11) DEFAULT '0' NOT NULL,
task text DEFAULT '' NOT NULL,
qualification text DEFAULT '' NOT NULL,
alternative_start_date text DEFAULT '' NOT NULL,
start_date int(11) unsigned DEFAULT '0' NOT NULL,
location text DEFAULT '' NOT NULL,
company text DEFAULT '' NOT NULL,
telecommute_possible tinyint(4) unsigned DEFAULT '0' NOT NULL,
office_work_possible tinyint(4) unsigned DEFAULT '1' NOT NULL,
employment_types text DEFAULT '' NOT NULL,
date_posted int(11) unsigned DEFAULT '0' NOT NULL,
valid_through int(11) unsigned DEFAULT '0' NOT NULL,
salary_currency varchar(3) DEFAULT 'EUR' NOT NULL,
base_salary varchar(15) DEFAULT '' NOT NULL,
max_salary varchar(15) DEFAULT '' NOT NULL,
salary_unit varchar(5) DEFAULT '' NOT NULL,
description text DEFAULT '' NOT NULL,
department int(11) DEFAULT '0' NOT NULL,
experience_level int(11) DEFAULT '0' NOT NULL,
contact int(11) unsigned DEFAULT '0' NOT NULL,
featured_offer tinyint(4) unsigned DEFAULT '0' NOT NULL,
hide_apply_by_email tinyint(4) unsigned DEFAULT '0' NOT NULL,
hide_apply_by_postal tinyint(4) unsigned DEFAULT '0' NOT NULL,
apply_external_link varchar(512) DEFAULT '' NOT NULL
path_segment text DEFAULT '' NOT NULL,
title text DEFAULT '' NOT NULL,
job_id varchar(30) DEFAULT '' NOT NULL,
attachment int(11) DEFAULT '0' NOT NULL,
task text DEFAULT '' NOT NULL,
qualification text DEFAULT '' NOT NULL,
alternative_start_date text DEFAULT '' NOT NULL,
start_date int(11) unsigned DEFAULT '0' NOT NULL,
location text DEFAULT '' NOT NULL,
company text DEFAULT '' NOT NULL,
telecommute_possible tinyint(4) unsigned DEFAULT '0' NOT NULL,
office_work_possible tinyint(4) unsigned DEFAULT '1' NOT NULL,
employment_types text DEFAULT '' NOT NULL,
date_posted int(11) unsigned DEFAULT '0' NOT NULL,
valid_through int(11) unsigned DEFAULT '0' NOT NULL,
salary_currency varchar(3) DEFAULT 'EUR' NOT NULL,
base_salary varchar(15) DEFAULT '' NOT NULL,
max_salary varchar(15) DEFAULT '' NOT NULL,
salary_unit varchar(5) DEFAULT '' NOT NULL,
description text DEFAULT '' NOT NULL,
department int(11) DEFAULT '0' NOT NULL,
experience_level int(11) DEFAULT '0' NOT NULL,
contact int(11) unsigned DEFAULT '0' NOT NULL,
featured_offer tinyint(4) unsigned DEFAULT '0' NOT NULL,
hide_apply_by_email tinyint(4) unsigned DEFAULT '0' NOT NULL,
hide_apply_by_postal tinyint(4) unsigned DEFAULT '0' NOT NULL,
apply_external_link varchar(512) DEFAULT '' NOT NULL,
related_jobs varchar(255) DEFAULT '' NOT NULL
);
CREATE TABLE tx_sgjobs_domain_model_department (
......@@ -37,48 +38,48 @@ CREATE TABLE tx_sgjobs_domain_model_experience_level (
);
CREATE TABLE tx_sgjobs_domain_model_company (
city varchar(255) DEFAULT '' NOT NULL,
zip varchar(255) DEFAULT '' NOT NULL,
name varchar(255) DEFAULT '' NOT NULL,
street varchar(255) DEFAULT '' NOT NULL,
state varchar(255) DEFAULT '' NOT NULL,
country varchar(255) DEFAULT '' NOT NULL,
description text DEFAULT '' NOT NULL,
contact int(11) unsigned DEFAULT '0' NOT NULL,
job_id text DEFAULT '' NOT NULL,
identifying_url varchar(255) DEFAULT '' NOT NULL
city varchar(255) DEFAULT '' NOT NULL,
zip varchar(255) DEFAULT '' NOT NULL,
name varchar(255) DEFAULT '' NOT NULL,
street varchar(255) DEFAULT '' NOT NULL,
state varchar(255) DEFAULT '' NOT NULL,
country varchar(255) DEFAULT '' NOT NULL,
description text DEFAULT '' NOT NULL,
contact int(11) unsigned DEFAULT '0' NOT NULL,
job_id text DEFAULT '' NOT NULL,
identifying_url varchar(255) DEFAULT '' NOT NULL
);
CREATE TABLE tx_sgjobs_domain_model_contact (
title varchar(255) DEFAULT '' NOT NULL,
first_name varchar(255) DEFAULT '' NOT NULL,
last_name varchar(255) DEFAULT '' NOT NULL,
email varchar(255) DEFAULT '' NOT NULL,
phone varchar(255) DEFAULT '' NOT NULL,
image int(11) unsigned DEFAULT '0',
title varchar(255) DEFAULT '' NOT NULL,
first_name varchar(255) DEFAULT '' NOT NULL,
last_name varchar(255) DEFAULT '' NOT NULL,
email varchar(255) DEFAULT '' NOT NULL,
phone varchar(255) DEFAULT '' NOT NULL,
image int(11) unsigned DEFAULT '0',
);
CREATE TABLE tx_sgjobs_domain_model_job_application (
job_id text DEFAULT '' NOT NULL,
job int(11) unsigned DEFAULT '0' NOT NULL,
job_title text DEFAULT '' NOT NULL,
company text DEFAULT '' NOT NULL,
gender varchar(30) DEFAULT '' NOT NULL,
first_name text DEFAULT '' NOT NULL,
last_name text DEFAULT '' NOT NULL,
street text DEFAULT '' NOT NULL,
city text DEFAULT '' NOT NULL,
zip varchar(255) DEFAULT '' NOT NULL,
country text DEFAULT '' NOT NULL,
nationality text DEFAULT '' NOT NULL,
education text DEFAULT '' NOT NULL,
birth_date text DEFAULT '' NOT NULL,
phone text DEFAULT '' NOT NULL,
mobile text DEFAULT '' NOT NULL,
email text DEFAULT '' NOT NULL,
message text DEFAULT '' NOT NULL,
cover_letter int(11) unsigned DEFAULT '0' NOT NULL,
certificate int(11) unsigned DEFAULT '0' NOT NULL,
cv int(11) unsigned DEFAULT '0' NOT NULL,
job_id text DEFAULT '' NOT NULL,
job int(11) unsigned DEFAULT '0' NOT NULL,
job_title text DEFAULT '' NOT NULL,
company text DEFAULT '' NOT NULL,
gender varchar(30) DEFAULT '' NOT NULL,
first_name text DEFAULT '' NOT NULL,
last_name text DEFAULT '' NOT NULL,
street text DEFAULT '' NOT NULL,
city text DEFAULT '' NOT NULL,
zip varchar(255) DEFAULT '' NOT NULL,
country text DEFAULT '' NOT NULL,
nationality text DEFAULT '' NOT NULL,
education text DEFAULT '' NOT NULL,
birth_date text DEFAULT '' NOT NULL,
phone text DEFAULT '' NOT NULL,
mobile text DEFAULT '' NOT NULL,
email text DEFAULT '' NOT NULL,
message text DEFAULT '' NOT NULL,
cover_letter int(11) unsigned DEFAULT '0' NOT NULL,
certificate int(11) unsigned DEFAULT '0' NOT NULL,
cv int(11) unsigned DEFAULT '0' NOT NULL,
privacy_policy tinyint(4) unsigned DEFAULT '0' NOT NULL
);
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment