├── .gitignore
├── _config.php
├── CONTRIBUTING.md
├── .gitattributes
├── CHANGELOG.md
├── _config
└── config.yml
├── phpunit.xml.dist
├── .github
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
└── workflows
│ └── ci.yml
├── .editorconfig
├── composer.json
├── package.json
├── README.md
├── src
├── Controller
│ └── FAQPageController.php
├── Page
│ └── FAQPage.php
├── Model
│ ├── FAQTopic.php
│ └── FAQ.php
├── Admin
│ └── FAQAdmin.php
└── BulkLoader
│ └── FAQBulkLoader.php
├── gulpfile.js
└── LICENSE.md
/.gitignore:
--------------------------------------------------------------------------------
1 | npm-debug.log
2 | node_modules/
3 |
--------------------------------------------------------------------------------
/_config.php:
--------------------------------------------------------------------------------
1 |
2 |
3 | tests
4 |
5 |
6 |
7 |
8 | src/
9 |
10 | tests/
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: 'FEATURE '
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # For more information about the properties used in
2 | # this file, please see the EditorConfig documentation:
3 | # http://editorconfig.org/
4 |
5 | root = true
6 |
7 | [*]
8 | charset = utf-8
9 | end_of_line = lf
10 | indent_size = 4
11 | indent_style = tab
12 | insert_final_newline = true
13 | trim_trailing_whitespace = true
14 |
15 | # Docs say 80 ideally, 100 ok, no more than 120
16 | # http://doc.silverstripe.org/en/getting_started/coding_conventions/
17 | max_line_length = 100
18 |
19 | [*.md]
20 | trim_trailing_whitespace = false
21 |
22 | [*.yml]
23 | indent_size = 2
24 | indent_style = space
25 |
26 | #PSR 2
27 | [**.php]
28 | indent_style = space
29 | indent_size = 4
30 |
31 | [{.travis.yml,package.json}]
32 | # The indent size used in the `package.json` file cannot be changed
33 | # https://github.com/npm/npm/pull/3180#issuecomment-16336516
34 | indent_size = 2
35 | indent_style = space
36 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dynamic/dynamic-faqs",
3 | "description": "filterable list of FAQs grouped by topic",
4 | "authors": [
5 | {
6 | "name": "Dynamic",
7 | "email": "dev@dynamicagency.com",
8 | "homepage": "https://www.dynamicagency.com"
9 | }
10 | ],
11 | "keywords": [
12 | "silverstripe"
13 | ],
14 | "type": "silverstripe-vendormodule",
15 | "license": "BSD-3-Clause",
16 | "require": {
17 | "dynamic/silverstripe-collection": "^2.0",
18 | "dynamic/viewable-dataobject": "^2.0",
19 | "silverstripe/recipe-cms": "^4.0",
20 | "symbiote/silverstripe-gridfieldextensions": "^3.0"
21 | },
22 | "require-dev": {
23 | "phpunit/phpunit": "^5.7"
24 | },
25 | "config": {
26 | "process-timeout": 600
27 | },
28 | "minimum-stability": "dev",
29 | "prefer-stable": true,
30 | "extra": {
31 | "branch-alias": {
32 | "dev-master": "2.x-dev"
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: 'BUG '
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dynamic-faqs",
3 | "version": "1.0.0",
4 | "description": "filterable list of FAQs grouped by topic",
5 | "main": "./public/src/main.js",
6 | "license": "BSD-3-Clause",
7 | "author": "Dynamic",
8 | "contributors": [
9 | {
10 | "name": "Dynamic",
11 | "email": "dev@dynamicagency.com"
12 | }
13 | ],
14 | "engines": {
15 | "node": "^4.2.0"
16 | },
17 | "devDependencies": {
18 | "babel-core": "^6.4.0",
19 | "babel-jest": "^6.0.1",
20 | "babel-preset-es2015": "^6.3.13",
21 | "babelify": "^7.2.0",
22 | "browserify": "^12.0.1",
23 | "gulp": "^3.9.0",
24 | "gulp-sass": "^2.0.4",
25 | "jest-cli": "^0.8.2",
26 | "semver": "^5.0.3",
27 | "vinyl-source-stream": "^1.1.0"
28 | },
29 | "scripts": {
30 | "build": "gulp",
31 | "test": "jest"
32 | },
33 | "babel": {
34 | "presets": [
35 | "es2015"
36 | ]
37 | },
38 | "jest": {
39 | "scriptPreprocessor": "/node_modules/babel-jest",
40 | "testDirectoryName": "tests/javascript"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # dynamic-faqs
2 | [](https://travis-ci.com/dynamic/dynamic-faqs)
3 | [](https://scrutinizer-ci.com/g/dynamic/dynamic-faqs/?branch=master)
4 | [](https://scrutinizer-ci.com/g/dynamic/dynamic-faqs/?branch=master)
5 | [](https://codecov.io/gh/dynamic/dynamic-faqs)
6 |
7 | filterable list of FAQs grouped by topic
8 |
9 | ## Requirements
10 |
11 | - SilverStripe 3.2
12 |
13 | ## Installation
14 |
15 | This is how you install dynamic-faqs.
16 |
17 | ## Example usage
18 |
19 | You use dynamic-faqs like this.
20 |
21 | ## Documentation
22 |
23 | See the [docs/en](docs/en/index.md) folder.
24 |
--------------------------------------------------------------------------------
/src/Controller/FAQPageController.php:
--------------------------------------------------------------------------------
1 | latestParam('ID');
26 |
27 | if (!$object = FAQ::get()->filter('URLSegment', $urlSegment)->first()) {
28 | return $this->httpError(404, "The FAQ you're looking for doesn't seem to be here.");
29 | }
30 |
31 | return $this->customise(new ArrayData([
32 | 'Object' => $object,
33 | 'Title' => $object->Title,
34 | 'MetaTags' => $object->MetaTags(),
35 | 'Breadcrumbs' => $object->Breadcrumbs(),
36 | ]))->renderWith([
37 | 'FAQ',
38 | 'Page',
39 | ]);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp'),
2 | browserify = require('browserify'),
3 | babelify = require('babelify'),
4 | source = require('vinyl-source-stream'),
5 | sass = require('gulp-sass'),
6 | packageJSON = require('./package.json'),
7 | semver = require('semver');
8 |
9 | // Make sure the Node.js version is valid.
10 | if (!semver.satisfies(process.versions.node, packageJSON.engines.node)) {
11 | console.error('Invalid Node.js version. You need to be using ' + packageJSON.engines.node);
12 | process.exit(1);
13 | }
14 |
15 | gulp.task('js', function () {
16 | browserify({
17 | entries: './javascript/src/main.js',
18 | extensions: ['.js'],
19 | debug: true
20 | })
21 | .transform(babelify)
22 | .bundle()
23 | .pipe(source('bundle.js'))
24 | .pipe(gulp.dest('./javascript/dist'));
25 | });
26 |
27 | gulp.task('sass', function () {
28 | gulp.src('./scss/main.scss')
29 | .pipe(sass().on('error', sass.logError))
30 | .pipe(gulp.dest('./css'));
31 | });
32 |
33 | gulp.task('js:watch', function () {
34 | gulp.watch('./javascript/**/*.js', ['js']);
35 | });
36 |
37 | gulp.task('sass:watch', function () {
38 | gulp.watch('./scss/**/*.scss', ['sass']);
39 | });
40 |
41 | gulp.task('default', ['js', 'sass', 'js:watch', 'sass:watch']);
42 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2017, Dynamic
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5 |
6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7 |
8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9 |
10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
11 |
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
13 |
--------------------------------------------------------------------------------
/src/Page/FAQPage.php:
--------------------------------------------------------------------------------
1 | beforeUpdateCMSFields(function (FieldList $fields) {
35 | if ($this->ID) {
36 | $config = GridFieldConfig_RecordEditor::create();
37 | $config->addComponents([
38 | new GridFieldOrderableRows('SortOrder'),
39 | //new GridFieldAddExistingSearchButton(),
40 | ])
41 | ->removeComponentsByType([
42 | GridFieldAddExistingAutocompleter::class,
43 | ]);
44 |
45 | $faqs = GridField::create(
46 | 'FAQs',
47 | 'FAQs',
48 | FAQ::get()->sort('SortOrder'),
49 | $config
50 | );
51 | $fields->addFieldsToTab('Root.FAQs', [
52 | $faqs,
53 | ]);
54 | }
55 | });
56 |
57 | return parent::getCMSFields();
58 | }
59 |
60 | /**
61 | * @return DataList
62 | */
63 | public function getFAQList()
64 | {
65 | return FAQ::get()->sort('SortOrder');
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/Model/FAQTopic.php:
--------------------------------------------------------------------------------
1 | 'Varchar(255)',
26 | 'Content' => 'HTMLText',
27 | );
28 |
29 | /**
30 | * @var array
31 | */
32 | private static $belongs_many_many = array(
33 | 'FAQs' => FAQ::class,
34 | );
35 |
36 | /**
37 | * @var string
38 | */
39 | private static $table_name = "FAQTopic";
40 |
41 | /**
42 | * @var string
43 | */
44 | private static $default_sort = 'Title';
45 |
46 | public function getCMSFields()
47 | {
48 | $fields = parent::getCMSFields();
49 |
50 | if ($this->ID) {
51 | $faqs = $fields->dataFieldByName('FAQs');
52 | $config = $faqs->getConfig();
53 | $config->removeComponentsByType('GridFieldAddExistingAutocompleter');
54 | $config->addComponent(new GridFieldAddExistingSearchButton());
55 | $config->removeComponentsByType('GridFieldAddNewButton');
56 | }
57 |
58 | return $fields;
59 | }
60 |
61 | /**
62 | * @param null $member
63 | * @return bool|int
64 | */
65 | public function canCreate($member = null, $context = [])
66 | {
67 | return Permission::check('FAQ_CREATE', 'any', $member);
68 | }
69 |
70 | /**
71 | * @param null $member
72 | * @return bool|int
73 | */
74 | public function canEdit($member = null)
75 | {
76 | return Permission::check('FAQ_EDIT', 'any', $member);
77 | }
78 |
79 | /**
80 | * @param null $member
81 | * @return bool|int
82 | */
83 | public function canDelete($member = null)
84 | {
85 | return Permission::check('FAQ_DELETE', 'any', $member);
86 | }
87 |
88 | /**
89 | * @param null $member
90 | * @return bool
91 | */
92 | public function canView($member = null)
93 | {
94 | return true;
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | # https://help.github.com/en/categories/automating-your-workflow-with-github-actions
2 |
3 | on:
4 | push:
5 | branches: [ '**' ]
6 | pull_request:
7 | branches: [ '**' ]
8 |
9 | name: "CI"
10 |
11 | jobs:
12 | tests:
13 | name: "Tests"
14 |
15 | runs-on: "ubuntu-latest"
16 |
17 | env:
18 | php_extensions: ctype, dom, fileinfo, hash, intl, mbstring, session, simplexml, tokenizer, xml, pdo, mysqli, gd, zip
19 |
20 | services:
21 | mysql:
22 | image: "mysql:5.7"
23 | env:
24 | MYSQL_ALLOW_EMPTY_PASSWORD: true
25 | MYSQL_ROOT_PASSWORD:
26 | MYSQL_DATABASE: test_db
27 | ports:
28 | - 3306/tcp
29 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
30 |
31 | strategy:
32 | fail-fast: false
33 | matrix:
34 | php-version:
35 | - "7.3"
36 | - "7.4"
37 |
38 | steps:
39 | - name: "Checkout"
40 | uses: "actions/checkout@v2"
41 |
42 | - name: "Install PHP with extensions"
43 | uses: "shivammathur/setup-php@v2"
44 | with:
45 | php-version: "${{ matrix.php-version }}"
46 | extensions: "${{ env.php_extensions }}"
47 | coverage: "xdebug"
48 |
49 | - name: "Start mysql service"
50 | run: "sudo /etc/init.d/mysql start"
51 |
52 | - name: "Cache dependencies installed with composer"
53 | uses: "actions/cache@v1"
54 | with:
55 | path: "~/.composer/cache"
56 | key: "php${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('**/composer.json') }}"
57 | restore-keys: "php${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-"
58 |
59 | - name: "Authorize private packagist"
60 | env:
61 | COMPOSER_TOKEN: ${{ secrets.COMPOSER_TOKEN }}
62 | run: "if [[ $COMPOSER_TOKEN ]]; then composer config --global --auth http-basic.repo.packagist.com token $COMPOSER_TOKEN; fi"
63 |
64 | - name: "Install dependencies with composer"
65 | run: "composer install --no-ansi --no-interaction --no-progress"
66 |
67 | - name: "Run tests with phpunit/phpunit"
68 | env:
69 | SS_DATABASE_PORT: ${{ job.services.mysql.ports['3306'] }}
70 | #run: "vendor/bin/phpunit --coverage-html=build/logs/coverage --coverage-xml=build/logs/coverage"
71 | run: "vendor/bin/phpunit"
72 |
73 | - name: "Run tests with squizlabs/php_codesniffer"
74 | run: "vendor/bin/phpcs -s --report=summary --standard=phpcs.xml.dist --extensions=php,inc --ignore=autoload.php --ignore=vendor/ app/src/ app/tests/ || exit 0"
75 |
76 |
77 |
--------------------------------------------------------------------------------
/src/Admin/FAQAdmin.php:
--------------------------------------------------------------------------------
1 | FAQBulkLoader::class,
26 | );
27 |
28 | /**
29 | * @var string
30 | */
31 | private static $url_segment = 'faqs';
32 |
33 | /**
34 | * @var string
35 | */
36 | private static $menu_title = 'FAQs';
37 |
38 | /**
39 | * @return \SilverStripe\ORM\Search\SearchContext
40 | */
41 | public function getSearchContext()
42 | {
43 | $context = parent::getSearchContext();
44 | $params = $this->request->requestVar('q');
45 |
46 | if ($this->modelClass == 'FAQ') {
47 | $fields = $context->getFields();
48 |
49 | $fields->removeByName('q[LastEdited]');
50 | $fields->push(new CheckboxField('q[Last6Months]', 'Records Requiring Update'));
51 | }
52 |
53 | return $context;
54 | }
55 |
56 | /**
57 | * @return \SilverStripe\ORM\DataList
58 | */
59 | public function getList()
60 | {
61 | $list = parent::getList();
62 |
63 | $params = $this->request->requestVar('q'); // use this to access search parameters
64 |
65 | if ($this->modelClass == 'FAQ' && isset($params['Last6Months']) && $params['Last6Months']) {
66 | $list = $list->exclude('LastEdited:GreaterThan', date('Y-m-d', strtotime('-6 months')));
67 | }
68 |
69 | return $list;
70 | }
71 |
72 | /**
73 | * @return array|string[]
74 | */
75 | public function getExportFields()
76 | {
77 | if ($this->modelClass == 'FAQ') {
78 | return array(
79 | 'ID' => 'ID',
80 | 'Title' => 'Title',
81 | 'URLSegment' => 'URLSegment',
82 | 'Type' => 'Type',
83 | 'Content' => 'Content',
84 | 'URL' => 'URL',
85 | 'Popularity' => 'Popularity',
86 | 'SilverCloud_ID' => 'SilverCloud_ID',
87 | 'TopicNames' => 'Categories',
88 | 'Keywords' => 'Keywords',
89 | 'ShowInResults' => 'ShowInResults',
90 | );
91 | }
92 |
93 | return parent::getExportFields();
94 | }
95 |
96 | /**
97 | * @param null $id
98 | * @param null $fields
99 | * @return \SilverStripe\Forms\Form
100 | */
101 | public function getEditForm($id = null, $fields = null)
102 | {
103 | $form = parent::getEditForm($id, $fields);
104 |
105 | $gridFieldName = 'FAQ';
106 | $gridField = $form->Fields()->fieldByName($gridFieldName);
107 |
108 | if ($gridField) {
109 | $gridField->getConfig()->getComponentByType('GridFieldPrintButton')->setPrintColumns($columns = array(
110 | 'ID' => 'ID',
111 | 'Title' => 'Title',
112 | 'Content' => 'Content',
113 | ));
114 | }
115 |
116 | return $form;
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/Model/FAQ.php:
--------------------------------------------------------------------------------
1 | 'Varchar(255)',
40 | 'Content' => 'HTMLText',
41 | 'SortOrder' => 'Int',
42 | );
43 |
44 | /**
45 | * @var array
46 | */
47 | private static $many_many = array(
48 | 'Topics' => FAQTopic::class,
49 | );
50 |
51 | /**
52 | * @var array
53 | */
54 | private static $many_many_extraFields = array(
55 | 'Topics' => array(
56 | 'Sort' => 'Int',
57 | ),
58 | );
59 |
60 | /**
61 | * @var string
62 | */
63 | private static $default_sort = 'SortOrder';
64 |
65 | /**
66 | * @var array
67 | */
68 | private static $extensions = [
69 | Versioned::class,
70 | ];
71 |
72 | /**
73 | * @var array
74 | */
75 | private static $summary_fields = array(
76 | 'Title' => 'Title',
77 | 'TopicNames' => 'Topics',
78 | );
79 |
80 | /**
81 | * @var array
82 | */
83 | private static $searchable_fields = array(
84 | 'Title' => [
85 | 'title' => 'Title',
86 | ],
87 | 'Content' => [
88 | 'title' => 'Content',
89 | ],
90 | 'Topics.ID' => [
91 | 'title' => 'Topic',
92 | ],
93 | );
94 |
95 | /**
96 | * @var string
97 | */
98 | private static $table_name = "FAQ";
99 |
100 | /**
101 | * @return FieldList
102 | */
103 | public function getCMSFields()
104 | {
105 | $this->beforeUpdateCMSFields(function (FieldList $fields) {
106 | $fields->removeByName([
107 | 'SortOrder',
108 | ]);
109 |
110 | if ($this->ID) {
111 | // Topics
112 | $config = GridFieldConfig_RelationEditor::create();
113 | $config->addComponent(new GridFieldOrderableRows('Sort'));
114 | $config->removeComponentsByType(GridFieldAddExistingAutocompleter::class);
115 | $config->addComponent(new GridFieldAddExistingSearchButton());
116 | $topics = $this->Topics()->sort('Sort');
117 | $topicsField = GridField::create('Topics', 'Topics', $topics, $config);
118 |
119 | $fields->addFieldsToTab('Root.Topics', array(
120 | $topicsField,
121 | ));
122 | }
123 | });
124 |
125 | return parent::getCMSFields();
126 | }
127 |
128 | /**
129 | * @return ValidationResult
130 | */
131 | public function validate()
132 | {
133 | $result = parent::validate();
134 | if (!$this->Title) {
135 | $result->addError('A Title is required before you can save');
136 | }
137 |
138 | return $result;
139 | }
140 |
141 | /**
142 | * @return string
143 | */
144 | public function getTopicNames()
145 | {
146 | if ($this->Topics()->exists()) {
147 | $list = '';
148 | $ct = 1;
149 | foreach ($this->Topics() as $topic) {
150 | $list .= $topic->Title;
151 | if ($ct < $this->Topics()->Count()) {
152 | $list .= ', ';
153 | }
154 | ++$ct;
155 | }
156 |
157 | return $list;
158 | }
159 |
160 | return '';
161 | }
162 |
163 | /**
164 | * @return mixed
165 | */
166 | public function getTopicName()
167 | {
168 | return $this->Topics()->first()->getTitle();
169 | }
170 |
171 | /**
172 | * @return SearchContext
173 | */
174 | public function getCustomSearchContext()
175 | {
176 | $fields = $this->scaffoldSearchFields(array(
177 | 'restrictFields' => array('Title', 'Topics.ID')
178 | ));
179 |
180 | $filters = array(
181 | 'Title' => new PartialMatchFilter('Title'),
182 | 'Topics.ID' => new ExactMatchFilter('Topics.ID'),
183 | );
184 |
185 | return new SearchContext(
186 | $this->class,
187 | $fields,
188 | $filters
189 | );
190 | }
191 |
192 | /**
193 | * @return string
194 | */
195 | public function getParentPage()
196 | {
197 | return FAQPage::get()->first();
198 | }
199 |
200 | /**
201 | * @return string
202 | */
203 | public function getViewAction()
204 | {
205 | return 'view';
206 | }
207 |
208 | /**
209 | * @return array
210 | */
211 | public function providePermissions()
212 | {
213 | return array(
214 | 'FAQ_EDIT' => 'Edit a FAQ',
215 | 'FAQ_DELETE' => 'Delete a FAQ',
216 | 'FAQ_CREATE' => 'Create a FAQ',
217 | );
218 | }
219 |
220 | /**
221 | * @param null $member
222 | * @return bool|int
223 | */
224 | public function canCreate($member = null, $context = [])
225 | {
226 | return Permission::check('FAQ_CREATE', 'any', $member);
227 | }
228 |
229 | /**
230 | * @param null $member
231 | * @return bool|int
232 | */
233 | public function canEdit($member = null)
234 | {
235 | return Permission::check('FAQ_EDIT', 'any', $member);
236 | }
237 |
238 | /**
239 | * @param null $member
240 | * @return bool|int
241 | */
242 | public function canDelete($member = null)
243 | {
244 | return Permission::check('FAQ_DELETE', 'any', $member);
245 | }
246 |
247 | /**
248 | * @param null $member
249 | * @return bool
250 | */
251 | public function canView($member = null)
252 | {
253 | return true;
254 | }
255 | }
256 |
--------------------------------------------------------------------------------
/src/BulkLoader/FAQBulkLoader.php:
--------------------------------------------------------------------------------
1 | 'Title',
15 | 'Type' => 'Type',
16 | 'Content' => '->getContent',
17 | 'Categories' => '->getTopic',
18 | 'Keywords' => 'Keywords',
19 | );
20 |
21 | /**
22 | * @var string[]
23 | */
24 | public $duplicateChecks = array(
25 | 'SilverCloud_ID',
26 | 'ID',
27 | );
28 |
29 | /**
30 | * getter functions to use in custom importer methods below
31 | *
32 | * @param $val
33 | * @return bool
34 | */
35 | public function getBoolean($val)
36 | {
37 | return $val == 'Y';
38 | }
39 |
40 | /**
41 | * @param $val
42 | * @return string
43 | */
44 | public function getDate($val)
45 | {
46 | return date('Y-m-d', strtotime($val));
47 | }
48 |
49 | /**
50 | * @param $val
51 | * @return string
52 | */
53 | public function getTime($val)
54 | {
55 | return date('H:i:s', strtotime($val));
56 | }
57 |
58 | /**
59 | * @param $val
60 | * @return array|string|string[]|null
61 | */
62 | public function getEscape($val)
63 | {
64 | $val = str_replace('_x000D_', '', $val);
65 | return preg_replace("/\r|\n/", '', $val);
66 | }
67 |
68 | /**
69 | * @param $obj
70 | * @param $val
71 | * @param $record
72 | */
73 | public function getContent(&$obj, $val, $record)
74 | {
75 | if ($val) {
76 | $content = $this->getEscape($val);
77 | if ($obj->Type == 'Link' && $obj->URL == '') {
78 | $obj->URL = $content;
79 | } else {
80 | $obj->Content = $content;
81 | }
82 | }
83 | }
84 |
85 | /**
86 | * @param $obj
87 | * @param $val
88 | * @param $record
89 | */
90 | public function getTopic(&$obj, $val, $record)
91 | {
92 | if ($val) {
93 | $topics = explode(', ', $this->getEscape($val));
94 | //SS_Log::log($obj->Title . " parent category = " . $parent, SS_Log::WARN);
95 | $ct = 1;
96 | foreach ($topics as $topic) {
97 | $top = FAQTopic::get()->filter('Title', $topic)->First();
98 | if (!$top) {
99 | $top = FAQTopic::create();
100 | $top->Title = $topic;
101 | $top->write();
102 | }
103 | $obj->Topics()->add($top);
104 | ++$ct;
105 | }
106 | }
107 | }
108 |
109 | /**
110 | * @param $obj
111 | * @param $val
112 | * @param $record
113 | */
114 | public function getTag(&$obj, $val, $record)
115 | {
116 | if ($val) {
117 | $tags = explode(', ', $this->getEscape($val));
118 | //SS_Log::log($obj->Title . " parent category = " . $parent, SS_Log::WARN);
119 | $ct = 1;
120 | foreach ($tags as $tag) {
121 | $tg = FAQTag::get()->filter('Title', $tag)->First();
122 | if (!$tg) {
123 | $tg = FAQTag::create();
124 | $tg->Title = $tag;
125 | $tg->write();
126 | }
127 | $obj->Tags()->add($tg);
128 | ++$ct;
129 | }
130 | }
131 | }
132 |
133 | /**
134 | * @todo Better messages for relation checks and duplicate detection
135 | * Note that columnMap isn't used.
136 | *
137 | * @param array $record
138 | * @param array $columnMap
139 | * @param BulkLoader_Result $results
140 | * @param bool $preview
141 | *
142 | * @return int
143 | */
144 | /*
145 | protected function processRecord($record, $columnMap, &$results, $preview = false)
146 | {
147 | $class = $this->objectClass;
148 |
149 | // find existing object, or create new one
150 | $existingObj = $this->findExistingObject($record, $columnMap);
151 | $obj = ($existingObj) ? $existingObj : new $class();
152 |
153 | // first run: find/create any relations and store them on the object
154 | // we can't combine runs, as other columns might rely on the relation being present
155 | $relations = array();
156 | foreach ($record as $fieldName => $val) {
157 | // don't bother querying of value is not set
158 | if ($this->isNullValue($val)) {
159 | continue;
160 | }
161 |
162 | // checking for existing relations
163 | if (isset($this->relationCallbacks[$fieldName])) {
164 | // trigger custom search method for finding a relation based on the given value
165 | // and write it back to the relation (or create a new object)
166 | $relationName = $this->relationCallbacks[$fieldName]['relationname'];
167 | if ($this->hasMethod($this->relationCallbacks[$fieldName]['callback'])) {
168 | $relationObj = $this->{$this->relationCallbacks[$fieldName]['callback']}($obj, $val, $record);
169 | } elseif ($obj->hasMethod($this->relationCallbacks[$fieldName]['callback'])) {
170 | $relationObj = $obj->{$this->relationCallbacks[$fieldName]['callback']}($val, $record);
171 | }
172 | if (!$relationObj || !$relationObj->exists()) {
173 | $relationClass = $obj->has_one($relationName);
174 | $relationObj = new $relationClass();
175 | //write if we aren't previewing
176 | if (!$preview) {
177 | $relationObj->write();
178 | }
179 | }
180 | $obj->{"{$relationName}ID"} = $relationObj->ID;
181 | //write if we are not previewing
182 | if (!$preview) {
183 | $obj->write();
184 | $obj->flushCache(); // avoid relation caching confusion
185 | }
186 | } elseif (strpos($fieldName, '.') !== false) {
187 | // we have a relation column with dot notation
188 | list($relationName, $columnName) = explode('.', $fieldName);
189 | // always gives us an component (either empty or existing)
190 | $relationObj = $obj->getComponent($relationName);
191 | if (!$preview) {
192 | $relationObj->write();
193 | }
194 | $obj->{"{$relationName}ID"} = $relationObj->ID;
195 |
196 | //write if we are not previewing
197 | if (!$preview) {
198 | $obj->write();
199 | $obj->flushCache(); // avoid relation caching confusion
200 | }
201 | }
202 | }
203 |
204 | // second run: save data
205 |
206 | foreach ($record as $fieldName => $val) {
207 | // break out of the loop if we are previewing
208 | if ($preview) {
209 | break;
210 | }
211 |
212 | // look up the mapping to see if this needs to map to callback
213 | $mapped = $this->columnMap && isset($this->columnMap[$fieldName]);
214 |
215 | if ($mapped && strpos($this->columnMap[$fieldName], '->') === 0) {
216 | $funcName = substr($this->columnMap[$fieldName], 2);
217 |
218 | $this->$funcName($obj, $val, $record);
219 | } elseif ($obj->hasMethod("import{$fieldName}")) {
220 | $obj->{"import{$fieldName}"}($val, $record);
221 | } else {
222 | $obj->update(array($fieldName => $val));
223 | }
224 | }
225 |
226 | // write record
227 | $id = ($preview) ? 0 : $obj->write();
228 |
229 | if ($preview) {
230 | $id = 0;
231 | } else {
232 | $id = $obj->writeToStage('Stage');
233 | // now publish it
234 | //$obj->publish("Stage", "Live");
235 | }
236 |
237 | // @todo better message support
238 | $message = '';
239 |
240 | // save to results
241 | if ($existingObj) {
242 | $results->addUpdated($obj, $message);
243 | } else {
244 | $results->addCreated($obj, $message);
245 | }
246 |
247 | $objID = $obj->ID;
248 |
249 | $obj->destroy();
250 |
251 | // memory usage
252 | unset($existingObj);
253 | unset($obj);
254 |
255 | return $objID;
256 | }
257 | */
258 | }
259 |
--------------------------------------------------------------------------------