Commit 02d936d7 by bernard

Initial commit

parents
.DS_*
config.yml
composer.lock
vendor/
tests/tmp/
.idea
\ No newline at end of file
### Installation :
Go to extensions dir
Add this to composer.json in repositories part
```
{
"type": "composer",
"url": "https://packages.lab.appolo.fr/"
},
```
Run
> composer require appolo/bolt-extension-custom-queries
Add new field in content type like this :
```
list:
type: textarea
class: customQuery
group: customQuery
```
And add in the template where you want the filter result :
```
{{ record.list|queriesResults }}
```
\ No newline at end of file
{
"name": "appolo/bolt-extension-custom-queries",
"description": "",
"type": "bolt-extension",
"keywords": [
],
"version": "0.1.0",
"require": {
"bolt/bolt": "^3.0"
},
"require-dev": {
"phpunit/phpunit": "^4.7"
},
"license": "MIT",
"authors": [
{
"name": "Appolo",
"email": "contact@appolo.fr"
}
],
"minimum-stability": "dev",
"prefer-stable": true,
"autoload": {
"psr-4": {
"Bolt\\Extension\\Appolo\\CustomQueries\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Bolt\\Extension\\Appolo\\CustomQueries\\Tests\\": "tests",
"Bolt\\Tests\\": "vendor/bolt/bolt/tests/phpunit/unit/"
}
},
"extra": {
"bolt-assets": "web",
"bolt-class": "Bolt\\Extension\\Appolo\\CustomQueries\\CustomQueriesExtension"
}
}
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="tests/bootstrap.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false">
<testsuites>
<testsuite name="unit">
<directory>tests</directory>
</testsuite>
</testsuites>
<listeners>
<listener file="vendor/bolt/bolt/tests/phpunit/BoltListener.php" class="Bolt\Tests\BoltListener">
<arguments>
<!-- Configuration files. Can be either .yml or .yml.dist files -->
<!-- Locations can be relative to TEST_ROOT directory, the Bolt directory, or an absolute path -->
<array>
<element key="config">
<string>vendor/bolt/bolt/app/config/config.yml.dist</string>
</element>
<element key="contenttypes">
<string>vendor/bolt/bolt/app/config/contenttypes.yml.dist</string>
</element>
<element key="menu">
<string>vendor/bolt/bolt/app/config/menu.yml.dist</string>
</element>
<element key="permissions">
<string>vendor/bolt/bolt/app/config/permissions.yml.dist</string>
</element>
<element key="routing">
<string>vendor/bolt/bolt/app/config/routing.yml.dist</string>
</element>
<element key="taxonomy">
<string>vendor/bolt/bolt/app/config/taxonomy.yml.dist</string>
</element>
</array>
<!-- Theme directory. Can be relative to TEST_ROOT directory, the Bolt directory, or an absolute path -->
<array>
<element key="theme">
<string>theme/base-2014</string>
</element>
</array>
<!-- The Bolt SQLite database, leave empty to use the one bundled with Bolt's repository -->
<!-- Location can be relative to TEST_ROOT directory, the Bolt directory, or an absolute path -->
<array>
<element key="boltdb">
<string>vendor/bolt/bolt/tests/phpunit/unit/resources/db/bolt.db</string>
</element>
</array>
<!-- Reset the cache and test temporary directories -->
<boolean>true</boolean>
<!-- Create timer output in app/cache/phpunit-test-timer.txt -->
<boolean>true</boolean>
</arguments>
</listener>
</listeners>
</phpunit>
<?php
namespace Bolt\Extension\Appolo\CustomQueries\Controller\Backend;
use Bolt\Application;
use Bolt\Controller\Backend\BackendBase;
use Silex\ControllerCollection;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
class CustomQueriesController extends BackendBase {
/**
* @var Application
*/
private $application;
/**
* ConnectionController constructor.
* @param Application $application
* @internal param array $config
*/
public function __construct(Application $application)
{
$this->application = $application;
}
/**
* @param ControllerCollection $c
* @return ControllerCollection
*/
protected function addRoutes(ControllerCollection $c)
{
$c->match('/config', 'getConfig')
->bind('getConfig')
;
return $c;
}
/**
* @param Request $request
* @return JsonResponse
*/
public function getConfig(Request $request)
{
return new JsonResponse($this->application['config']->get('contenttypes'));
}
}
<?php
namespace Bolt\Extension\Appolo\CustomQueries;
use Bolt\Asset\File\JavaScript;
use Bolt\Asset\File\Stylesheet;
use Bolt\Controller\Zone;
use Bolt\Extension\Appolo\CustomQueries\Controller\Backend\CustomQueriesController;
use Bolt\Extension\SimpleExtension;
/**
* CustomQueries extension class.
*
* @author Your Name <you@example.com>
*/
class CustomQueriesExtension extends SimpleExtension
{
/**
* {@inheritdoc}
*/
protected function registerTwigFilters()
{
return [
'queriesResults' => ['getQueriesResults', ['is_safe' => ['html']]],
];
}
/**
* {@inheritdoc}
*/
protected function registerAssets()
{
return [
JavaScript::create()
->setFileName('js/app.js')
->setLate(true)
->setPriority(5)
->setZone(Zone::BACKEND),
Stylesheet::create()
->setFileName('css/app.css')
->setLate(true)
->setPriority(5)
->setZone(Zone::BACKEND)
];
}
/**
* Register Controller
* {@inheritdoc}
*/
protected function registerBackendControllers()
{
return [
'/custom-queries' => new CustomQueriesController($this->getContainer()),
];
}
/**
* Register twig paths for application
*/
protected function registerTwigPaths()
{
return ['templates'];
}
/**
* @return false|mixed
*/
public function getQueriesResults($input)
{
if($input == '') {
return '';
}
$json = json_decode($input, true);
if(!$json || empty($json)) {
return '';
}
$records = [];
$storage = $this->getContainer()['storage'];
foreach ($json as $contentType => $fields) {
$query = $this->_buildQuery($contentType, $fields);
if(!empty($query))
{
$rows = $this->getContainer()['db']->fetchAll($query);
$ids = array_map(function($rows) {
return $rows['id'];
}, $rows);
$contents = $storage->getContent($contentType, ['id' => implode(' || ', $ids), 'paging' => true, 'limit' => 99999]);
if(is_array($contents)) {
$records = array_merge($records, $contents);
} else {
$records = array_merge($records, [$contents]);
}
}
}
return $this->getContainer()['twig']->render("custom_queries.twig", ['records' => $records]);
}
/**
* @param array $fields
* @return string
*/
protected function _buildQuery($contentType, array $fields = []) {
if(!$contentType) {
return '';
}
$storage = $this->getContainer()['storage'];
$tableName = $storage->getContenttypeTablename($contentType);
if(!$tableName) {
return '';
}
$query = 'SELECT id FROM '.$tableName;
$where = $this->_buildWhere($fields);
if($where) {
$query .= $where;
}
return $query;
}
/**
* @param array $fields
* @return bool
*/
protected function _buildWhere(array $fields = []) {
$where = false;
foreach ($fields as $field => $value) {
if(!empty($value) && (!empty($value['include']) || !empty($value['exclude']))) {
$joinField = 'AND';
if(empty($value['joinFields']) || $value['joinFields'] == 'or') {
$joinField = 'OR';
}
if(!$where) {
$where = ' WHERE ';
} else {
$where .= ' AND ';
}
if(!empty($value['include'])) {
$where .= '(';
foreach ($value['include'] as $val) {
list($operator, $val) = $this->_getOperatorInclude($val);
$where .= $field.' '.$operator.' '.$val.' '.$joinField.' ';
}
$where = rtrim($where, ' '.$joinField.' ');
$where .= ')';
}
if(!empty($value['exclude'])) {
$where .= ' AND (';
foreach ($value['exclude'] as $val) {
list($operator, $val) = $this->_getOperatorExclude($val);
$where .= $field.' '.$operator.' '.$val.' AND ';
}
$where = rtrim($where, ' AND ');
$where .= ')';
}
}
}
return $where;
}
/**
* @param $value
* @return array
*/
protected function _getOperatorInclude($value) {
// Set the correct operator for the where clause
$operator = "=";
$first = substr($value, 0, 1);
if (substr($value, 0, 2) == "<=") {
$operator = "<=";
$value = substr($value, 2);
} elseif (substr($value, 0, 2) == ">=") {
$operator = ">=";
$value = substr($value, 2);
} elseif ($first == "<") {
$operator = "<";
$value = substr($value, 1);
} elseif ($first == ">") {
$operator = ">";
$value = substr($value, 1);
} elseif ($first == "%" || substr($value, -1) == "%") {
$operator = "LIKE";
}
$value = "'".$value."'";
return [$operator, $value];
}
/**
* @param $value
* @return array
*/
protected function _getOperatorExclude($value) {
// Set the correct operator for the where clause
$operator = "!=";
$first = substr($value, 0, 1);
if (substr($value, 0, 2) == "<=") {
$operator = "<=";
$value = substr($value, 2);
} elseif (substr($value, 0, 2) == ">=") {
$operator = ">=";
$value = substr($value, 2);
} elseif ($first == "<") {
$operator = "<";
$value = substr($value, 1);
} elseif ($first == ">") {
$operator = ">";
$value = substr($value, 1);
} elseif ($first == "%" || substr($value, -1) == "%") {
$operator = "NOT LIKE";
}
$value = "'".$value."'";
return [$operator, $value];
}
}
{% if records is defined %}
<ul>
{% for record in records %}
<li><a href="{{ record.link }}">{{ record.title }}</a></li>
{% endfor %}
</ul>
{% endif %}
\ No newline at end of file
.m-t-15 {
margin-top: 15px;
}
.border-none {
border: none !important;
}
.chooseField {
height: 26px !important;
}
.addContentTypeContainer {
margin-bottom: 30px;
}
.listContentTypeContainer .dashboardlisting tr {
background-color: transparent !important;
}
.listContentTypeContainer div.buic-listing table.listing tbody.striping_odd tr:nth-of-type(odd) {
background: #f2f4f3 !important;
}
.listContentTypeContainer .panel-heading .title {
font-size: 20px;
text-transform: uppercase;
font-weight: bold;
}
.switch {
position: relative;
width: 56px;
height: 26px;
border: 1px solid #5e9e11;
color: #5e9e11;
border-radius: 3px;
background-color: #e0e0e0;
float: right;
}
.quality {
position: relative;
display: inline-block;
width: 50%;
height: 100%;
line-height: 40px;
}
.quality:first-child label {
line-height: 26px;
}
.quality:last-child label {
line-height: 26px;
}
.quality label {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: pointer;
font-style: italic;
text-align: center;
transition: transform 0.4s, color 0.4s, background-color 0.4s;
}
.quality input[type="radio"] {
appearance: none;
width: 0;
height: 0;
opacity: 0;
}
.quality input[type="radio"]:focus {
outline: 0;
outline-offset: 0;
}
.quality input[type="radio"]:checked ~ label {
background-color: #5e9e11;
color: #ffffff;
}
.quality input[type="radio"]:active ~ label {
transform: scale(1.05);
}
class CustomQueries {
/**
* Parse Json
* @param json
* @returns {boolean}
*/
static parseJson(json) {
let parsedString = false;
try {
parsedString = JSON.parse(json);
} catch (e) {
return parsedString;
}
return parsedString;
}
/**
* Constructor
* @param element
*/
constructor(element) {
this.element = element;
this.parent = element.parent();
this.json = CustomQueries.parseJson(element.val());
this.name = element.attr('name');
this.usedContentType = [];
this.selectors = {
addContentTypeContainer: '.addContentTypeContainer-'+this.name,
addContentTypeBtn: '#addContentType-'+this.name,
validateContentTypeBtn: '#validateContentType-'+this.name,
cancelContentTypeBtn: '#cancelContentType-'+this.name,
selectContentType: '.addContentTypeContainer-'+this.name+' select',
listContentTypeContainer: '.listContentTypeContainer-'+this.name,
removeContentTypeBtn: '#removeContentType-'+this.name,
addFieldBtn: '#addField-'+this.name,
validateFieldBtn: '#validateField-'+this.name,
cancelFieldBtn: '#cancelField-'+this.name,
removeFieldBtn: '#removeField-'+this.name,
}
}
/**
* Init
*/
init() {
this.hideDefaultElement();
$.get('/bolt/custom-queries/config', (data) => {
this.config = data;
this.createContentTypeSelector();
this.loadOriginalContent();
});
}
/*##########################################
Load original content
##########################################*/
/**
* INIT JSON content for content type
*/
loadOriginalContent() {
let content = this.json;
for(let index in content) {
this.createContentTypeBlock(index);
this.loadOriginalField(index, content[index], content)
}
}
/**
* INIT JSON content for fields
* @param contentType
* @param fields
*/
loadOriginalField(contentType, fields, json) {
for(let index in fields) {
let text = index;
let joinField = 'and';
if(this.config[contentType] && this.config[contentType]['fields'] && this.config[contentType]['fields'][index] && this.config[contentType]['fields'][index].label) {
text = this.config[contentType]['fields'][index].label;
}
if(json[contentType] && json[contentType][index] && json[contentType][index]['joinFields']) {
joinField = json[contentType][index]['joinFields']
}
this.createFieldLine(contentType, index, text, joinField);
this.loadOriginalFieldContent(contentType, index, fields[index]);
}
}
/**
* INIT JSON content for fields content
* @param contentType
* @param field
* @param fieldContent
*/
loadOriginalFieldContent(contentType, field, fieldContent) {
for(let type in fieldContent) {
this.createInput(contentType, field, type, fieldContent[type]);
}
}
/*##########################################
Dom manipulation
##########################################*/
/**
* Create content type selection
*/
createContentTypeSelector() {
this.parent.append(`
<div class="addContentTypeContainer addContentTypeContainer-${this.name}">
<button class="btn btn-primary" type="button" id="addContentType-${this.name}"><i class="fa fa-plus"></i> Ajouter un type de contenu</button>
<div class="form-inline" style="display: none;">
<select class="form-control"></select>
<button type="button" class="btn btn-primary" id="validateContentType-${this.name}">Valider</button>
<button type="button" class="btn btn-danger" id="cancelContentType-${this.name}">Annuler</button>
</div>
</div>
<div class="listContentTypeContainer listContentTypeContainer-${this.name}"></div>
`);
this.initEventClickBtnAddContentType();
}
/**
* Create Content type block
* @param contentType
*/
createContentTypeBlock(contentType) {
if(!this.usedContentType[contentType] && contentType) {
this.usedContentType[contentType] = [];
$(this.selectors.listContentTypeContainer).append(
`<fieldset class="bolt-field-repeater">
<div class="repeater-slot contentType ${contentType}">
<div class="repeater-group panel panel-default">
<div class="panel-heading text-right">
<div class="pull-left title">
${contentType}
</div>
<div class="btn-group">
<div class="headField form-inline" style="display: none;">
<div>
<select class="form-control chooseField" id="chooseField-${this.name}-${contentType}"></select>
<button type="button" class="btn btn-sm btn-primary" id="validateField-${this.name}-${contentType}">Valider</button>
<button type="button" class="btn btn-sm btn-danger" id="cancelField-${this.name}-${contentType}">Annuler</button>
</div>
</div>
</div>
<div class="btn-group">
<button type="button" class="btn btn-sm btn-default" id="addField-${this.name}-${contentType}">
<i class="fa fa-plus"></i> Ajouter un champ
</button>
<button type="button" class="btn btn-sm btn-default" id="removeContentType-${this.name}-${contentType}">
<i class="fa fa-trash"></i> Supprimer le type de contenu
</button>
</div>
</div>
<div class="panel-body">
<div class="repeater-field border-none buic-listing" data-bolt-fieldset="text">
<table width="100%" class=" dashboardlisting listing">
<thead>
<tr>
<th width="30%">Champ</th>
<th width="5%"></th>
<th width="30%">Valeur inclue</th>
<th width="30%">Valeur exclu</th>
<th width="5%"></th>
</tr>
</thead>
<tbody class="striping_odd">
</tbody>
</table>
</div>
</div>
</div>
</div>
</fieldset>`
);
this.initEventClickBtnRemoveContentType(contentType);
this.initEventClickBtnAddField(contentType);
}
}
/**
* Fill content type value
* @param value
*/
fillContentTypeSelector(value) {
$(this.selectors.selectContentType).append('<option value="'+value+'">'+value+'</option>');
}
/**
* Create Field line in table
* @param contentType
* @param field
*/
createFieldLine(contentType, field, fieldText, joinField) {
if(!joinField || (joinField != 'or' && joinField != 'and')) {
joinField = 'and';
}
let andChecked = (joinField == 'and') ? 'checked': '';
let orChecked = (joinField == 'or') ? 'checked': '';
let container = $(this.selectors.listContentTypeContainer+' .contentType.'+contentType+' tbody');
container.append(`
<tr class="field-${field}">
<td>${fieldText}</td>
<td class="andor">
<div class='switch'><div class='quality'>
<input ${andChecked} id='and-${contentType}-${field}' name='${contentType}-${field}' type='radio' value='q1'>
<label for='and-${contentType}-${field}'>ET</label>
</div><div class='quality'>
<input ${orChecked} id='or-${contentType}-${field}' name='${contentType}-${field}' type='radio' value='q1'>
<label for='or-${contentType}-${field}'>OU</label>
</div>
</div>
</td>
<td class="include tags-select2"></td>
<td class="exclude tags-select2"></td>
<td class="action">
<button type="button" class="btn btn-sm btn-default" id="removeField-${this.name}-${contentType}-${field}"><i class="fa fa-trash"></i> </button>
</td>
</tr>
`);
this.usedContentType[contentType].push(field);
this.initEventClickBtnRemoveField(contentType, field);
this.initEventChaneToggleField(contentType, field);
}
/**
* Create Input in Field line
* @param contentType
* @param field
*/
createInput(contentType, field, type, values) {
let container = $(this.selectors.listContentTypeContainer+' .contentType.'+contentType+' tbody .field-'+field+' .'+type);
let input = '<select class="tags form-control" id="field-'+contentType+'-'+field+'-'+type+'" multiple="true">';
if(values) {
for(let index in values) {
input += '<option value="'+values[index]+'" selected>'+values[index]+'</option>';
}
}
input += '</select>';
container.append(input);
let selector = $('#field-'+contentType+'-'+field+'-'+type);
this.initEventSelectTags(selector);
}
/**
* Fill content type value
* @param contentType
* @param value
* @param text
*/
fillFieldSelector(contentType, value, text) {
if(this.usedContentType[contentType].indexOf(value) === -1) {
$('#chooseField-'+this.name+'-'+contentType).append('<option value="'+value+'">'+text+'</option>');
}
}
/**
* Hide default elements
*/
hideDefaultElement() {
this.element.hide();
this.parent.parent().find('label').hide();
}
/*##########################################
Events
##########################################*/
/**
* Add content type btn click event
*/
initEventClickBtnAddContentType() {
// Content type add btn
$(this.selectors.addContentTypeBtn).on('click', (e) => {
e.preventDefault();
$(this.selectors.addContentTypeBtn).hide();
$(this.selectors.selectContentType+' option').remove();
for(let index in this.config) {
if(!this.usedContentType[index]) {
this.fillContentTypeSelector(index);
}
}
$(this.selectors.selectContentType).parent().show();
});
// Content type validate btn
$(this.selectors.validateContentTypeBtn).on('click', (e) => {
e.preventDefault();
$(this.selectors.selectContentType).parent().hide();
$(this.selectors.addContentTypeBtn).show();
this.createContentTypeBlock($(this.selectors.selectContentType).val());
});
// Content type cancel btn
$(this.selectors.cancelContentTypeBtn).on('click', (e) => {
e.preventDefault();
$(this.selectors.selectContentType).parent().hide();
$(this.selectors.addContentTypeBtn).show();
});
}
/**
* Content Type remove event
* @param contentType
*/
initEventClickBtnRemoveContentType(contentType) {
$(this.selectors.removeContentTypeBtn+'-'+contentType).on('click', (e) => {
e.preventDefault();
this.usedContentType = this.usedContentType.slice(this.usedContentType.indexOf(contentType));
let index = this.usedContentType.indexOf(contentType);
this.usedContentType.splice(index, 1);
$('.listContentTypeContainer-'+this.name+' .contentType.'+contentType).remove();
this.rebuildJson();
});
}
/**
* Field add event
* @param contentType
*/
initEventClickBtnAddField(contentType) {
// Field add btn
$(this.selectors.addFieldBtn+'-'+contentType).on('click', (e) => {
e.preventDefault();
$(this.selectors.addFieldBtn+'-'+contentType).hide();
$(this.selectors.listContentTypeContainer+' .contentType.'+contentType+' .headField select option').remove();
if(this.config[contentType] && this.config[contentType]['fields']) {
/*console.log(this.config[contentType]['fields']);*/
for(let index in this.config[contentType]['fields']) {
let text = index;
if(this.config[contentType]['fields'][index].label) {
text = this.config[contentType]['fields'][index].label;
}
this.fillFieldSelector(contentType, index, text);
}
}
$(this.selectors.listContentTypeContainer+' .contentType.'+contentType+' .headField').show();
});
// Field validate btn
$(this.selectors.validateFieldBtn+'-'+contentType).on('click', (e) => {
e.preventDefault();
$(this.selectors.listContentTypeContainer+' .contentType.'+contentType+' .headField').hide();
$(this.selectors.addFieldBtn+'-'+contentType).show();
let field = $('#chooseField-'+this.name+'-'+contentType).val();
let fieldText = $('#chooseField-'+this.name+'-'+contentType+' option:selected').text();
this.createFieldLine(contentType, field, fieldText);
this.createInput(contentType, field, 'include');
this.createInput(contentType, field, 'exclude');
this.rebuildJson();
});
// Field cancel btn
$(this.selectors.cancelFieldBtn+'-'+contentType).on('click', (e) => {
e.preventDefault();
$(this.selectors.listContentTypeContainer+' .contentType.'+contentType+' .headField').hide();
$(this.selectors.addFieldBtn+'-'+contentType).show();
});
}
/**
* Field remove event
* @param contentType
* @param field
*/
initEventClickBtnRemoveField(contentType, field) {
$(this.selectors.removeFieldBtn+'-'+contentType+'-'+field).on('click', (e) => {
e.preventDefault();
$(this.selectors.listContentTypeContainer+' .contentType.'+contentType+' .field-'+field).remove();
// removed used field
this.rebuildJson();
});
}
/**
* Field select change event
* @param selector
*/
initEventSelectTags(selector) {
selector.select2({width:"100%",tags:true,minimumInputLength:1,tokenSeparators:[',']});
selector.on("select2:select", (e) => {
this.rebuildJson();
});
selector.on("select2:unselect", (e) => {
this.rebuildJson();
});
}
/**
* Toggle change event
* @param contentType
* @param field
*/
initEventChaneToggleField(contentType, field) {
$('#or-'+contentType+'-'+field).on('click', (e) => {
this.rebuildJson();
});
$('#and-'+contentType+'-'+field).on('click', (e) => {
this.rebuildJson();
});
}
/**
* Rebuild Json data
*/
rebuildJson() {
let json = {};
$('.tags').each(function () {
let id = $(this).attr('id').split('-');
let value = $(this).select2('val');
if(value) {
let joinFields = 'and';
if($('#or-'+id[1]+'-'+id[2]).is(":checked")) {
joinFields = 'or';
}
if(!json[id[1]]) {
json[id[1]] = {};
}
if(!json[id[1]][id[2]]) {
json[id[1]][id[2]] = {
joinFields: joinFields
};
}
json[id[1]][id[2]][id[3]] = value;
}
});
/*console.log(json);*/
this.element.val(JSON.stringify(json));
}
}
jQuery(document).ready(function ($) {
$('.customQuery').each(function () {
let customQueries = new CustomQueries($(this));
customQueries.init();
});
});
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment