├── .bowerrc ├── .gitignore ├── Gruntfile.js ├── LICENSE ├── README.md ├── bower.json ├── composer.json ├── crudkit.php ├── demo ├── fixtures │ ├── chinook.sqlite │ └── chinook_pristine.sqlite ├── html_page.php ├── mysql_basic.php ├── sql_basic.php ├── sql_basic_with_id.php └── test.php ├── package.json ├── phpunit.xml ├── src ├── CrudKit │ ├── Controllers │ │ ├── BaseController.php │ │ └── MainController.php │ ├── CrudKitApp.php │ ├── Data │ │ ├── ArrayDataProvider.php │ │ ├── BaseDataProvider.php │ │ ├── BaseSQLDataProvider.php │ │ ├── DataProvider.php │ │ ├── SQL │ │ │ ├── ExternalColumn.php │ │ │ ├── ForeignColumn.php │ │ │ ├── PrimaryColumn.php │ │ │ ├── SQLColumn.php │ │ │ └── ValueColumn.php │ │ └── SQLDataProvider.php │ ├── Form │ │ ├── BaseFormItem.php │ │ ├── DateTimeFormItem.php │ │ ├── HasManyItem.php │ │ ├── HorizontalItem.php │ │ ├── ManyToOneItem.php │ │ ├── NumberFormItem.php │ │ ├── OneToManyItem.php │ │ └── TextFormItem.php │ ├── Laravel │ │ ├── CrudKitServiceProvider.php │ │ ├── LaravelApp.php │ │ └── LaravelTablePage.php │ ├── Pages │ │ ├── BasePage.php │ │ ├── BaseSQLDataPage.php │ │ ├── BasicDataPage.php │ │ ├── BasicLoginPage.php │ │ ├── DummyPage.php │ │ ├── HtmlPage.php │ │ ├── MySQLTablePage.php │ │ ├── Page.php │ │ ├── SQLServerTablePage.php │ │ └── SQLiteTablePage.php │ └── Util │ │ ├── FlashBag.php │ │ ├── FormHelper.php │ │ ├── Installer.php │ │ ├── LoggingHelper.php │ │ ├── RouteGenerator.php │ │ ├── TwigUtil.php │ │ ├── UrlHelper.php │ │ └── ValueBag.php ├── static │ ├── build │ │ ├── css │ │ │ └── crudkit.min.css │ │ ├── fonts │ │ │ ├── FontAwesome.otf │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.svg │ │ │ ├── fontawesome-webfont.ttf │ │ │ ├── fontawesome-webfont.woff │ │ │ ├── fontawesome-webfont.woff2 │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ └── glyphicons-halflings-regular.woff2 │ │ ├── img │ │ │ ├── boxed-bg.jpg │ │ │ ├── boxed-bg.png │ │ │ ├── default-50x50.gif │ │ │ └── icons.png │ │ └── js │ │ │ └── crudkit.min.js │ ├── extra │ │ ├── ui-bootstrap-custom-0.13.0.min.js │ │ └── ui-bootstrap-custom-tpls-0.13.0.min.js │ ├── js │ │ ├── app-react.js │ │ ├── app.js │ │ └── utils.js │ └── less │ │ └── crudkit.less └── templates │ ├── core.twig │ ├── includes │ ├── requires.twig │ └── requires_prod.twig │ ├── layout.twig │ ├── main_page.twig │ ├── pages │ ├── base.twig │ ├── basicdata.twig │ ├── basicdata │ │ ├── edit_item.twig │ │ └── view_item.twig │ ├── dummy.twig │ ├── html.twig │ └── login.twig │ └── util │ ├── form.twig │ └── form_inline.twig └── tests ├── ArrayDataProviderFactory.php ├── SqlDataProviderFactory.php ├── Unit ├── CrudKitTest.php └── Data │ ├── ArrayDataProviderTest.php │ └── SQLDataProviderTest.php ├── bootstrap.php └── fixtures └── chinook.sqlite /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory":"src/static/vendor" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | build/html-coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 28 | node_modules 29 | 30 | /vendor/ 31 | 32 | .idea 33 | temp 34 | bower_components 35 | vendor 36 | .idea/* 37 | composer.lock 38 | 39 | adminer.php 40 | 41 | demo/crudkit 42 | demo/ 43 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | var vendorRoot = "src/static/vendor/"; 4 | var buildRoot = "src/static/build/"; 5 | var tempRoot = "src/static/temp/"; 6 | grunt.initConfig({ 7 | watch: { 8 | lessFiles: { 9 | files: ["src/static/less/*.less"], 10 | tasks: ['less:main'] 11 | } 12 | }, 13 | copy: { 14 | temp: { 15 | files: [{ 16 | expand: true, 17 | cwd: vendorRoot + 'bootstrap/dist/', 18 | src: ["fonts/*"], 19 | dest: buildRoot 20 | }, { 21 | expand: true, 22 | cwd: vendorRoot + 'adminlte/dist/', 23 | src: [ 24 | "img/boxed-bg.png", 25 | "img/boxed-bg.jpg", 26 | "img/default-50x50.gif", 27 | "img/icons.png" 28 | ], 29 | dest: buildRoot 30 | }, { 31 | expand: true, 32 | cwd: vendorRoot + 'fontawesome/', 33 | src: ["fonts/*"], 34 | dest: buildRoot 35 | }, ] 36 | }, 37 | build: { 38 | files: [{ 39 | expand: true, 40 | cwd: vendorRoot + 'bootstrap/dist/', 41 | src: ["fonts/*"], 42 | dest: tempRoot 43 | }, { 44 | expand: true, 45 | cwd: vendorRoot + 'adminlte/dist/', 46 | src: ["img/*"], 47 | dest: tempRoot 48 | }, { 49 | expand: true, 50 | cwd: vendorRoot + 'fontawesome/', 51 | src: ["fonts/*"], 52 | dest: tempRoot 53 | }] 54 | } 55 | }, 56 | less: { 57 | main: { 58 | files: { 59 | "src/static/temp/crudkit.css": "src/static/less/crudkit.less" 60 | } 61 | } 62 | }, 63 | concat: { 64 | build_css: { 65 | files: { 66 | "src/static/build/css/crudkit.min.css": [ 67 | "src/static/temp/css/crudkit-libs.min.css", 68 | "src/static/temp/crudkit.css" 69 | ] 70 | } 71 | }, 72 | build_js: { 73 | options: { 74 | separator: ";\n" 75 | }, 76 | files: { 77 | "src/static/build/js/crudkit.min.js": [ 78 | "src/static/temp/js/crudkit-libs.min.js", 79 | "src/static/js/app.js" 80 | ] 81 | } 82 | }, 83 | css: { 84 | files: { 85 | "src/static/temp/css/crudkit-libs.min.css": [ 86 | vendorRoot + "bootstrap/dist/css/bootstrap.min.css", 87 | vendorRoot + "angular-busy/dist/angular-busy.min.css", 88 | vendorRoot + "adminlte/dist/css/AdminLTE.min.css", 89 | vendorRoot + "adminlte/dist/css/skins/skin-blue.css", 90 | vendorRoot + "fontawesome/css/font-awesome.min.css", 91 | vendorRoot + "bootstrap3-dialog/dist/css/bootstrap-dialog.min.css" 92 | ] 93 | } 94 | }, 95 | js: { 96 | options: { 97 | separator: ";\n" 98 | }, 99 | files: { 100 | "src/static/temp/js/crudkit-libs.min.js": [ 101 | vendorRoot + "jquery/dist/jquery.min.js", 102 | vendorRoot + "bootstrap/dist/js/bootstrap.min.js", 103 | vendorRoot + "lodash/lodash.min.js", 104 | vendorRoot + "angularjs/angular.min.js", 105 | vendorRoot + "angular-animate/angular-animate.min.js", 106 | vendorRoot + "angular-busy/dist/angular-busy.min.js", 107 | vendorRoot + "angular-filter/dist/angular-filter.min.js", 108 | vendorRoot + "bootstrap3-dialog/dist/js/bootstrap-dialog.min.js", 109 | vendorRoot + "jsurl/url.min.js", 110 | vendorRoot + "moment/min/moment.min.js", 111 | vendorRoot + "moment-timezone/builds/moment-timezone-with-data.min.js", 112 | vendorRoot + "adminlte/dist/js/app.min.js", 113 | "src/static/extra/ui-bootstrap-custom-0.13.0.min.js", 114 | "src/static/extra/ui-bootstrap-custom-tpls-0.13.0.min.js" 115 | ] 116 | } 117 | } 118 | }, 119 | clean: { 120 | 121 | }, 122 | replace: { 123 | sourceMaps: { 124 | src: ["src/static/temp/js/crudkit-libs.min.js", "src/static/temp/css/crudkit-libs.min.css"], 125 | overwrite: true, 126 | replacements: [{ 127 | from: "sourceMappingURL", 128 | to: "" 129 | }] 130 | } 131 | } 132 | }); 133 | 134 | grunt.loadNpmTasks('grunt-contrib-watch'); 135 | grunt.loadNpmTasks('grunt-contrib-copy'); 136 | grunt.loadNpmTasks('grunt-contrib-less'); 137 | grunt.loadNpmTasks('grunt-contrib-concat'); 138 | grunt.loadNpmTasks('grunt-contrib-clean'); 139 | grunt.loadNpmTasks('grunt-text-replace'); 140 | 141 | grunt.registerTask('build', ['less', 'concat:js', 'concat:css', 'replace', 'copy:temp']); 142 | grunt.registerTask('release', ['build', 'concat:build_css', 'concat:build_js', 'copy:build']); 143 | 144 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Anirudh Sanjeev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CrudKit 2 | 3 | [![GitHub license](https://img.shields.io/github/license/mashape/apistatus.svg)](https://github.com/skyronic/crudkit/blob/master/LICENSE) 4 | 5 | ----------- 6 | 7 | A Toolkit to quickly build powerful mobile-friendly CRUD (create/read/update/delete) interfaces. http://crudkit.com 8 | 9 | ## Build locally 10 | Additionally from having PHP installed, make sure you have [nodejs](https://nodejs.org/) installed. 11 | 12 | Once you have the basic requirements, follow the below instructions: 13 | 14 | 1. Clone the repository `$ git clone git@github.com:skyronic/crudkit.git` 15 | 16 | 2. Install [composer](https://getcomposer.org). 17 | 18 | 3. Install / Update dependencies, `$ composer update`. 19 | 20 | 4. Install `grunt` and other node related packages using: `$ npm install` (You might need to use `sudo`) 21 | 22 | 5. Install client-side dependencies using: `$ bower install` 23 | 24 | ## Demos 25 | Demos are listed in `demo/`. Inorder to see the demos in action follow the steps listed below: 26 | 27 | - Create a symlink of `crudkit` using: `$ ln -s /home//path/to/crudkit demo/crudkit` 28 | - Build static files: `$ grunt buildStatic` 29 | - Start the server by running `$ php -S 0.0.0.0:8080` from the root of the project 30 | - Navigate to http://localhost:8080/demo/sql_basic.php 31 | 32 | ## Laravel Support 33 | 34 | Check out [markpurser/laravel-crudkit](https://github.com/markpurser/laravel-crudkit) for a similar project (no code shared with this project) with out of the box support for eloquent and relying on blade templates and Laravel for routing. 35 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crudkit", 3 | "version": "0.0.0", 4 | "homepage": "https://github.com/crudkit/crudkit", 5 | "authors": [ 6 | "Anirudh Sanjeev " 7 | ], 8 | "description": "PHP Admin generator", 9 | "moduleType": [ 10 | "amd" 11 | ], 12 | "directory": "./vendor/bower_components", 13 | "license": "MIT", 14 | "ignore": [ 15 | "**/.*", 16 | "node_modules", 17 | "bower_components", 18 | "vendor", 19 | "test", 20 | "tests" 21 | ], 22 | "dependencies": { 23 | "angularjs": "~1.3.15", 24 | "jquery": "~2.1.1", 25 | "kendo-ui-core": "*", 26 | "bootstrap": "~3.3.4", 27 | "angular-bootstrap": "~0.12.1", 28 | "open-sans-fontface": "~1.4.0", 29 | "react": "~0.13.2", 30 | "jsurl": "*", 31 | "angular-busy": "~4.1.3", 32 | "angular-filter": "~0.5.4", 33 | "lodash": "~3.7.0", 34 | "adminlte": "~2.3.2", 35 | "lato": "~0.3.0", 36 | "fontawesome": "~4.3.0", 37 | "bootstrap3-dialog": "~1.34.4", 38 | "moment": "~2.10.3", 39 | "moment-timezone": "^0.5.1" 40 | }, 41 | "resolutions": { 42 | "angular": "1.3.15" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "skyronic/crudkit", 3 | "description": "Toolkit to quickly build powerful, mobile-friendly CRUD (create/read/update/delete) and admin interfaces. Integrates with Laravel and has support for other MVC Frameworks. Works with MySQL and other databases.", 4 | "license": "MIT", 5 | "version": "0.3.3-beta", 6 | "homepage": "http://www.crudkit.com/", 7 | "authors": [ 8 | { 9 | "name": "Anirudh Sanjeev", 10 | "email": "skyronic@gmail.com", 11 | "homepage": "http://www.skyronic.com/" 12 | } 13 | ], 14 | "support": { 15 | "docs": "http://www.crudkit.com/docs/" 16 | }, 17 | "autoload": { 18 | "psr-4": { 19 | "CrudKit\\": "src/CrudKit/" 20 | } 21 | }, 22 | "autoload-dev": { 23 | "psr-4" : { 24 | "CrudKitTests\\": "tests" 25 | } 26 | }, 27 | "keywords": [ 28 | "crud", 29 | "database", 30 | "gui", 31 | "admin", 32 | "laravel", 33 | "backend" 34 | ], 35 | "minimum-stability": "beta", 36 | "require": { 37 | "doctrine/dbal": "2.5.1", 38 | "twig/twig": "1.18.0", 39 | "league/url": "3.3.1", 40 | "filp/whoops": "~1.1", 41 | "nesbot/carbon": "~1.19" 42 | }, 43 | "require-dev": { 44 | "phpunit/phpunit": "4.8.*@dev" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /crudkit.php: -------------------------------------------------------------------------------- 1 | setName ("My First Page"); // setName is available for all pages. 13 | $page->setInnerHtml ("This is page # 1"); // You can set the HTML of this page, a feature supported by HtmlPage 14 | $app->addPage ($page); 15 | 16 | $page2 = new HtmlPage("mypage2"); 17 | $page2->setName ("My Second Page"); 18 | $page2->setInnerHtml ("This is page # 2"); 19 | $app->addPage ($page2); 20 | 21 | // Render the app. This will display the HTML 22 | $app->render (); 23 | 24 | ?> -------------------------------------------------------------------------------- /demo/mysql_basic.php: -------------------------------------------------------------------------------- 1 | "UTF8")); 11 | $invoice->setName("Invoice") 12 | ->setPrimaryColumnWithId("a0", "InvoiceId") 13 | ->setTableName("Invoice") 14 | ->addColumnWithId("a1", "BillingCity", "City") 15 | ->addColumnWithId("a2", "BillingCountry", "Country") 16 | ->addColumnWithId("a3", "Total", "Total") 17 | ->addColumnWithId("a4", "InvoiceDate", "Date", array( 18 | )) 19 | ->setSummaryColumns(["a1", "a2", "a3", "a4"]); 20 | $app->addPage($invoice); 21 | 22 | $page = new MySQLTablePage("mysql1", "user2","hunter2", "Chinook", array('charset' => "UTF8")); 23 | $page->setName("Customer Management") 24 | ->setTableName("Customer") 25 | ->setPrimaryColumn("CustomerId") 26 | ->addColumn("FirstName", "First Name") 27 | ->addColumn("LastName", "Last Name") 28 | ->addColumn("City", "City") 29 | ->addColumn("Country", "Country") 30 | ->addColumn("Email", "E-mail") 31 | ->setSummaryColumns(array("FirstName", "Country")); 32 | $app->addPage($page); 33 | 34 | // Render the app. This will display the HTML 35 | $app->render (); 36 | 37 | ?> -------------------------------------------------------------------------------- /demo/sql_basic.php: -------------------------------------------------------------------------------- 1 | setAppName ("Admin Panel"); 11 | 12 | $login = new BasicLoginPage (); 13 | if ($login->userTriedLogin ()) { 14 | $username = $login->getUserName (); 15 | $password = $login->getPassword (); 16 | 17 | // TODO: you should use a better way to validate the password. Even if 18 | if ($username === 'admin' && $password === 'demo') { 19 | $login->success (); 20 | } 21 | else if ($username === 'user' && $password === 'demo') { 22 | $app->setReadOnly (true); 23 | $login->success (); 24 | } 25 | else { 26 | $login->fail ("Please check your password"); 27 | } 28 | } 29 | $app->useLogin ($login); 30 | 31 | $page = new SQLiteTablePage ("sqlite2", "fixtures/chinook.sqlite"); 32 | $page->setName("Customer Management") 33 | ->setTableName("Customer") 34 | ->setRowsPerPage (20) 35 | ->setPrimaryColumn("CustomerId") 36 | ->addColumn("FirstName", "First Name", array( 37 | 'required' => true 38 | )) 39 | ->addColumn("LastName", "Last Name") 40 | ->addColumn("City", "City", array( 41 | 'required' => true 42 | )) 43 | ->addColumn("Country", "Country") 44 | ->addColumn("Email", "E-mail") 45 | ->setSummaryColumns(array("FirstName", "Country")); 46 | $app->addPage($page); 47 | 48 | $invoice = new SQLiteTablePage ("sqlite1", "fixtures/chinook.sqlite"); 49 | $invoice->setName("Invoice") 50 | ->setPrimaryColumnWithId("a0", "InvoiceId") 51 | ->setTableName("Invoice") 52 | ->addColumnWithId("a1", "BillingCity", "City") 53 | ->addColumnWithId("a2", "BillingCountry", "Country") 54 | ->addColumnWithId("a3", "Total", "Total") 55 | ->addColumnWithId("a4", "InvoiceDate", "Date", array( 56 | )) 57 | ->setSummaryColumns(["a1", "a2", "a3", "a4"]); 58 | $app->addPage($invoice); 59 | 60 | // Render the app. This will display the HTML 61 | $app->render (); 62 | -------------------------------------------------------------------------------- /demo/sql_basic_with_id.php: -------------------------------------------------------------------------------- 1 | setName("Customer Management") 12 | ->setTableName("Customer") 13 | ->setPrimaryColumnWithId("c1", "CustomerId") 14 | ->addColumnWithId("c2", "FirstName", "First Name") 15 | ->addColumnWithId("c3", "LastName", "Last Name") 16 | ->addColumnWithId("c4", "City", "City") 17 | ->addColumnWithId("c5", "Country", "Country") 18 | ->addColumnWithId("c6", "Email", "E-mail") 19 | ->setSummaryColumns(array("c1", "c5")); 20 | $app->addPage($page); 21 | 22 | // Render the app. This will display the HTML 23 | $app->render (); 24 | 25 | ?> -------------------------------------------------------------------------------- /demo/test.php: -------------------------------------------------------------------------------- 1 | setStaticRoot("/src/static"); 13 | 14 | 15 | $page4 = new BasicDataPage('dummy4'); 16 | $page4->setName("SQLITE PAGE"); 17 | $sqliteProvider = new SQLiteDataProvider("fixtures/chinook.sqlite"); 18 | $sqliteProvider->setTable("Customer"); 19 | $sqliteProvider->setPrimaryColumn("a0", "CustomerId"); 20 | $sqliteProvider->addColumn("a1", "FirstName", "First Name"); 21 | $sqliteProvider->addColumn("a2", "LastName", "Last Name"); 22 | $sqliteProvider->addColumn("a3", "City", "City"); 23 | $sqliteProvider->addColumn("a4", "Country", "Country"); 24 | $sqliteProvider->addColumn("a5", "Email", "Email"); 25 | $sqliteProvider->setSummaryColumns(array("a1", "a2")); 26 | $sqliteProvider->manyToOne("a6", "SupportRepId", "Employee", "EmployeeId", "FirstName", "Support Rep"); 27 | $page4->setDataProvider($sqliteProvider); 28 | $crud->addPage($page4); 29 | 30 | $page5 = new BasicDataPage('dummy5'); 31 | $page5->setName("Employees"); 32 | 33 | $empProvider = new SQLiteDataProvider("fixtures/chinook.sqlite"); 34 | $empProvider->setTable("Employee"); 35 | $empProvider->setPrimaryColumn("b0", "EmployeeId"); 36 | $empProvider->addColumn("b1", "FirstName", "First Name"); 37 | $empProvider->addColumn("b2", "LastName", "Last Name"); 38 | $empProvider->manyToOne("b3", "ReportsTo", "Employee", "EmployeeId", "FirstName", "Reports To"); 39 | $empProvider->oneToMany ("b4", $sqliteProvider, "SupportRepId", "b0", "Customers"); 40 | $empProvider->setSummaryColumns(array("b1", "b2")); 41 | 42 | $page5->setDataProvider($empProvider); 43 | $crud->addPage($page5); 44 | 45 | 46 | $invProvider = new SQLiteDataProvider("fixtures/chinook.sqlite"); 47 | $invProvider->setTable("Invoice"); 48 | $invProvider->setPrimaryColumn("b0", "InvoiceId"); 49 | $invProvider->addColumn("b1", "BillingCountry", "Country123"); 50 | $invProvider->addColumn("b2", "Total", "Total123"); 51 | $invProvider->addColumn("b3", "InvoiceDate", "InvoiceDate123"); 52 | $invProvider->setSummaryColumns(array("b1", "b2", "b3")); 53 | 54 | $page6 = new BasicDataPage('dummy6'); 55 | $page6->setName ("Custom Data Types"); 56 | $page6->setDataProvider($invProvider); 57 | $crud->addPage($page6); 58 | 59 | 60 | 61 | 62 | $crud->render(); 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crudkit", 3 | "version": "1.0.0", 4 | "description": "PHP library to generate crud interfaces. ", 5 | "directories": { 6 | "test": "test" 7 | }, 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/crudkit/crudkit.git" 14 | }, 15 | "author": "Anirudh Sanjeev ", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/crudkit/crudkit/issues" 19 | }, 20 | "homepage": "https://github.com/crudkit/crudkit", 21 | "dependencies": { 22 | "bower": "^1.4.1", 23 | "grunt": "^0.4.5", 24 | "grunt-bower-requirejs": "^2.0.0", 25 | "grunt-contrib-clean": "^0.6.0", 26 | "grunt-contrib-concat": "^0.5.1", 27 | "grunt-contrib-copy": "^0.8.0", 28 | "grunt-contrib-less": "^1.0.1", 29 | "grunt-contrib-watch": "^0.6.1", 30 | "grunt-text-replace": "^0.4.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | 17 | ./tests 18 | 19 | 20 | 21 | 22 | 23 | 24 | 33 | 34 | 35 | 36 | 37 | ./ 38 | 39 | tests 40 | vendor 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/CrudKit/Controllers/BaseController.php: -------------------------------------------------------------------------------- 1 | app = $app; 47 | $this->url = new UrlHelper(); 48 | $this->routeGen = new RouteGenerator(); 49 | $this->twig = new TwigUtil(); 50 | } 51 | 52 | public function handle () { 53 | $action = $this->url->get("action", "default"); 54 | 55 | $whoops = new \Whoops\Run(); 56 | if($this->url->get("ajax", false)) { 57 | $whoops->pushHandler (new \Whoops\Handler\JsonResponseHandler()); 58 | } 59 | else { 60 | $whoops->pushHandler (new \Whoops\Handler\PrettyPageHandler()); 61 | } 62 | $whoops->register(); 63 | $result = null; 64 | if(method_exists($this, "handle_".$action)) { 65 | $result = call_user_func(array($this, "handle_". $action)); 66 | } 67 | else { 68 | throw new Exception ("Unknown action"); 69 | } 70 | $output = ""; 71 | 72 | if(is_string($result)) { 73 | $newResult = array( 74 | 'type' => 'transclude', 75 | 'content' => $result 76 | ); 77 | 78 | $result = $newResult; 79 | } 80 | 81 | switch($result['type']) { 82 | case "template": 83 | $output = $this->twig->renderTemplateToString($result['template'], $result['data']); 84 | break; 85 | case "json": 86 | $this->app->setJsonResponse(true); 87 | $data = $result['data']; 88 | $data['flashbag'] = FlashBag::getFlashes(); 89 | $output = json_encode($data); 90 | break; 91 | case "redirect": 92 | $this->app->_requestRedirect ($result['url']); 93 | return; 94 | break; 95 | case "transclude": 96 | $pageMap = []; 97 | /** @var Page $pageItem */ 98 | foreach($this->app->getPages() as $pageItem) { 99 | $pageMap []= array( 100 | 'id' => $pageItem->getId(), 101 | 'name' => $pageItem->getName() 102 | ); 103 | } 104 | ValueBag::set("flashbag", FlashBag::getFlashes()); 105 | $data = array( 106 | 'valueBag' => json_encode(ValueBag::getValues()), 107 | 'staticRoot' => $this->app->getStaticRoot(), 108 | 'pageMap' => $pageMap, 109 | 'defaultUrl' => $this->routeGen->defaultRoute(), 110 | 'title' => $this->app->getAppName (), 111 | 'userParams' => $this->app->getUserParams (), 112 | 'pageTitle' => '' 113 | ); 114 | if($this->page !== null) { 115 | $data['page'] = $this->page; 116 | $data['currentId'] = $this->page->getId(); 117 | $data['pageTitle'] = $this->page->getName(); 118 | } 119 | else { 120 | $data['currentId'] = -1; 121 | } 122 | $data['page_content'] = $result['content']; 123 | $data['dev'] = false; // change to true to load unminified js 124 | 125 | $output = $this->twig->renderTemplateToString("main_page.twig", $data); 126 | break; 127 | default: 128 | throw new Exception ("Unknown result type"); 129 | } 130 | 131 | return $output; 132 | 133 | } 134 | } -------------------------------------------------------------------------------- /src/CrudKit/Controllers/MainController.php: -------------------------------------------------------------------------------- 1 | app->getDefaultPage (); 11 | if($firstPage !== null) { 12 | return array( 13 | 'type' => 'redirect', 14 | 'url' => $this->routeGen->openPage ($firstPage->getId()) 15 | ); 16 | } 17 | else { 18 | return ""; 19 | } 20 | } 21 | public function handle_view_page () { 22 | // Handle the view page action 23 | $pageId = $this->url->get('page'); 24 | $this->page = $this->app->getPageById($pageId); 25 | $this->page->init($this->app); 26 | ValueBag::set("pageId", $this->page->getId()); 27 | 28 | return $this->page->render(); 29 | } 30 | 31 | public function handle_page_function () { 32 | $pageId = $this->url->get('page'); 33 | $this->page = $this->app->getPageById($pageId); 34 | $this->page->init($this->app); 35 | $func = $this->url->get("func"); 36 | ValueBag::set("pageId", $this->page->getId()); 37 | 38 | if(method_exists($this->page, "handle_".$func)) 39 | { 40 | return call_user_func(array($this->page, "handle_".$func)); 41 | } 42 | else { 43 | throw new \Exception("Unknown method"); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/CrudKit/CrudKitApp.php: -------------------------------------------------------------------------------- 1 | staticRoot = $staticRoot; 34 | } 35 | 36 | public function getUserParams () { 37 | if ($this->login !== null) { 38 | return [ 39 | 'username' => $this->login->getLoggedInUser(), 40 | 'logout_link' => $this->login->createLogoutLink () 41 | ]; 42 | } 43 | else { 44 | return null; 45 | } 46 | } 47 | 48 | protected $login = null; 49 | public function useLogin ($login) { 50 | $this->login = $login; 51 | $login->preprocess ($this); 52 | if (!$login->check ()) { 53 | $this->addPage ($login); 54 | $this->render (); 55 | } 56 | } 57 | 58 | /** 59 | * @param $page Page 60 | */ 61 | public function addPage ($page) { 62 | $this->pages []= $page; 63 | $this->pageById[$page->getId()] = $page; 64 | } 65 | 66 | public function setAppName ($title) { 67 | $this->title = $title; 68 | } 69 | 70 | public function getAppName () { 71 | return $this->title; 72 | } 73 | 74 | public function getPages () { 75 | return $this->pages; 76 | } 77 | 78 | public function getStaticRoot () { 79 | return $this->staticRoot; 80 | } 81 | 82 | public function getDefaultPage () { 83 | if(isset($this->pages[0])) 84 | return $this->pages [0]; 85 | else 86 | return null; 87 | } 88 | 89 | protected $readOnly = false; 90 | public function setReadOnly ($readOnlyFlag) { 91 | $this->readOnly = $readOnlyFlag; 92 | } 93 | 94 | public function isReadOnly () { 95 | return $this->readOnly; 96 | } 97 | 98 | 99 | /** 100 | * Render your CrudKit app and return it as a string 101 | */ 102 | public function renderToString () { 103 | if(!isset($this->staticRoot)) { 104 | throw new \Exception("Please set static root using `setStaticRoot`"); 105 | } 106 | 107 | $controller = new MainController($this); 108 | return $controller->handle(); 109 | } 110 | 111 | /** 112 | * @param $id 113 | * @return Page 114 | */ 115 | public function getPageById ($id) { 116 | if(isset($this->pageById[$id])) { 117 | return $this->pageById[$id]; 118 | } else { 119 | return null; 120 | } 121 | } 122 | 123 | /** 124 | * Render your CrudKit app and output to HTML 125 | */ 126 | public function render () { 127 | $content = $this->renderToString(); 128 | 129 | if ($this->redirect !== null) { 130 | header("Location: ".$this->redirect); 131 | session_write_close(); 132 | exit(); 133 | return; 134 | } 135 | 136 | // Headers are also calculated in render to string 137 | if($this->isJsonResponse()) { 138 | header("Content-type: application/json;"); 139 | } 140 | 141 | 142 | echo $content; 143 | exit (); 144 | } 145 | 146 | protected $jsonResponse = false; 147 | 148 | /** 149 | * @return boolean 150 | */ 151 | public function isJsonResponse() 152 | { 153 | return $this->jsonResponse; 154 | } 155 | 156 | protected $redirect = null; 157 | 158 | public function _requestRedirect ($url) { 159 | $this->redirect = $url; 160 | } 161 | 162 | /** 163 | * @param boolean $jsonResponse 164 | */ 165 | public function setJsonResponse($jsonResponse) 166 | { 167 | $this->jsonResponse = $jsonResponse; 168 | } 169 | 170 | public function __construct () { 171 | session_start(); 172 | } 173 | 174 | } -------------------------------------------------------------------------------- /src/CrudKit/Data/ArrayDataProvider.php: -------------------------------------------------------------------------------- 1 | schema = $schema; 29 | $this->summaryColumns = $summaryCols; 30 | foreach($data as $key => $values) { 31 | $this->setRow($key, $values); 32 | } 33 | } 34 | 35 | public function getSchema() 36 | { 37 | return $this->schema; 38 | } 39 | 40 | public function getSummaryColumns() 41 | { 42 | $summaryCols = []; 43 | foreach($this->summaryColumns as $column) { 44 | $columnData = $this->schema[$column]; 45 | $summaryCols[] = [ 46 | 'key' => $column, 47 | 'label' => $columnData['label'], 48 | 'renderType' => empty($columnData['type']) ? 'string' : $columnData['type'] 49 | ]; 50 | } 51 | return $summaryCols; 52 | } 53 | 54 | public function getRowCount(array $params = []) 55 | { 56 | $data = $this->getData($params); 57 | return count($data); 58 | } 59 | 60 | public function getEditForm() 61 | { 62 | $form = new FormHelper(); 63 | foreach($this->schema as $columnId => $columnOptions) { 64 | $this->addFormItemFromSchema($form, $columnId, $columnOptions); 65 | } 66 | return $form; 67 | } 68 | 69 | private function addFormItemFromSchema(FormHelper $form, $columnId, array $columnOptions) 70 | { 71 | $type = empty($columnOptions['type']) ? 'string' : $columnOptions['type']; 72 | $config = [ 73 | 'label' => $columnOptions['label'] 74 | ]; 75 | switch($type) { 76 | case 'text': 77 | case 'string': 78 | $item = new TextFormItem($form, $columnId, $config); 79 | break; 80 | case 'number': 81 | $item = new NumberFormItem($form, $columnId, $config); 82 | break; 83 | case 'datetime': 84 | $item = new DateTimeFormItem($form, $columnId, $config); 85 | break; 86 | default: 87 | throw new \Exception("The Column Type [$type] is not valid."); 88 | } 89 | $form->addItem($item); 90 | } 91 | 92 | public function getEditFormOrder() 93 | { 94 | return array_keys($this->schema); 95 | } 96 | 97 | public function getData(array $params = []) 98 | { 99 | if(isset($params['filters_json'])) { 100 | throw new \InvalidArgumentException('JSON Filters are currently not supported'); 101 | } 102 | $skip = isset($params['skip']) ? $params['skip'] : 0; 103 | $take = isset($params['take']) ? $params['take'] : 10; 104 | 105 | $data = array_values($this->data); 106 | return array_slice($data, $skip, $take); 107 | } 108 | 109 | public function getRow($id = null) 110 | { 111 | $id = (int) $id; 112 | return isset($this->data[$id]) ? $this->data[$id] : []; 113 | } 114 | 115 | public function setRow($id = null, array $values = []) 116 | { 117 | $errors = $this->validateRow($values); 118 | if(!empty($errors)) { 119 | return false; 120 | } 121 | $this->data[ (int) $id ] = $values; 122 | return true; 123 | } 124 | 125 | /** 126 | * @param array $values 127 | * @return int 128 | */ 129 | public function createItem(array $values) 130 | { 131 | foreach($values as $formKey => $formValue) { 132 | if(!$this->isFieldInSchema($formKey)) { 133 | throw new \InvalidArgumentException ("The Column [$formKey] is not defined."); 134 | } 135 | } 136 | $this->data[] = $values; 137 | end($this->data); 138 | return key($this->data); 139 | } 140 | 141 | /** 142 | * @param mixed $rowId 143 | * @return bool 144 | */ 145 | public function deleteItem($rowId) 146 | { 147 | $rowId = (int) $rowId; 148 | if(isset($this->data[$rowId])) { 149 | unset($this->data[$rowId]); 150 | return true; 151 | } 152 | return false; 153 | } 154 | 155 | /** 156 | * @param array $ids 157 | * @return bool 158 | */ 159 | public function deleteMultipleItems(array $ids) 160 | { 161 | $deleted = 0; 162 | foreach($ids as $id) { 163 | $deleted += (int) $this->deleteItem($id); 164 | } 165 | return $deleted; 166 | } 167 | 168 | /** 169 | * @param $id 170 | * @param $foreign_key 171 | * @return array 172 | */ 173 | public function getRelationshipValues($id, $foreign_key) 174 | { 175 | return [ 176 | 'type' => 'json', 177 | 'data' => [ 178 | 'values' => [] 179 | ] 180 | ]; 181 | } 182 | 183 | /** 184 | * Returns true if a field exists as part of this source's schema 185 | * 186 | * @param string $formKey 187 | * @return bool 188 | */ 189 | protected function isFieldInSchema($formKey) 190 | { 191 | return array_key_exists($formKey, $this->schema); 192 | } 193 | 194 | /** 195 | * Returns a callable validator for this field if it exists, and null otherwise 196 | * 197 | * @param string $formKey 198 | * @return callable|null 199 | */ 200 | protected function getValidatorForField($formKey) 201 | { 202 | $validator = null; 203 | if( isset($this->schema[$formKey]['options']['validator']) ) { 204 | $validator = $this->schema[$formKey]['options']['validator']; 205 | if(!is_callable($validator)) { 206 | $validator = null; 207 | } 208 | } 209 | return $validator; 210 | } 211 | 212 | /** 213 | * Returns an array of require field names 214 | * 215 | * @return string[] 216 | */ 217 | protected function getRequiredFields() 218 | { 219 | $required = []; 220 | foreach($this->schema as $field => $fieldSchema) { 221 | if(isset($fieldSchema['options']['required']) && $fieldSchema['options']['required']) { 222 | $required[] = $field; 223 | } 224 | } 225 | return $required; 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/CrudKit/Data/BaseDataProvider.php: -------------------------------------------------------------------------------- 1 | initQueue as $item) { 19 | $item->init(); 20 | } 21 | } 22 | 23 | public function getEditForm() 24 | { 25 | return new FormHelper([], $this->getEditFormConfig()); 26 | } 27 | 28 | public function getEditFormConfig() 29 | { 30 | return []; 31 | } 32 | 33 | public function setPage(Page $page) 34 | { 35 | $this->page = $page; 36 | } 37 | 38 | public function validateRow(array $values = []) 39 | { 40 | $failed = []; 41 | foreach ($values as $formKey => $formValue) { 42 | if (!$this->isFieldInSchema($formKey)) { 43 | throw new \InvalidArgumentException ("The Column [$formKey] is not defined."); 44 | } 45 | $validator = $this->getValidatorForField($formKey); 46 | if ($validator && !$validator($formValue)) { 47 | $failed[$formKey] = $formValue; 48 | } 49 | } 50 | return $failed; 51 | } 52 | 53 | public function validateRequiredRow(array $values = []) 54 | { 55 | $failed = []; 56 | $requiredFields = $this->getRequiredFields(); 57 | foreach ($values as $formKey => $formValue) { 58 | if (!$this->isFieldInSchema($formKey)) { 59 | throw new \InvalidArgumentException ("The Column [$formKey] is not defined."); 60 | } 61 | } 62 | foreach ($requiredFields as $requiredField) { 63 | if (empty($values[$requiredField])) { 64 | $failed[$requiredField] = 'missing'; 65 | } 66 | } 67 | return $failed; 68 | } 69 | 70 | /** 71 | * Returns true if a field exists as part of this source's schema 72 | * 73 | * @param string $formKey 74 | * @return bool 75 | */ 76 | protected abstract function isFieldInSchema($formKey); 77 | 78 | /** 79 | * Returns a callable validator for this field if it exists, and null otherwise 80 | * 81 | * @param string $formKey 82 | * @return callable|null 83 | */ 84 | protected abstract function getValidatorForField($formKey); 85 | 86 | /** 87 | * Returns an array of require field names 88 | * 89 | * @return string[] 90 | */ 91 | protected abstract function getRequiredFields(); 92 | } 93 | -------------------------------------------------------------------------------- /src/CrudKit/Data/BaseSQLDataProvider.php: -------------------------------------------------------------------------------- 1 | options['type'] === self::HAS_MANY ) { 26 | $item = new HasManyItem ($form, $this->id, array( 27 | 'label' => $this->options['label'] 28 | )); 29 | $form->addItem($item); 30 | } 31 | } 32 | 33 | public function getSchema() 34 | { 35 | return array(); 36 | } 37 | 38 | public function getExpr() 39 | { 40 | return null; 41 | } 42 | 43 | public function getSummaryConfig() 44 | { 45 | return array(); 46 | } 47 | } -------------------------------------------------------------------------------- /src/CrudKit/Data/SQL/ForeignColumn.php: -------------------------------------------------------------------------------- 1 | options['fk_type'] === "manyToOne") { 19 | $item = new ManyToOneItem($form, $this->id, array( 20 | 'label' => $this->options['label'] 21 | )); 22 | $form->addItem($item); 23 | $form->addRelationship($this->id, $this->options['fk_type']); 24 | } 25 | else if($this->options['fk_type'] === "oneToMany") { 26 | $item = new OneToManyItem($form, $this->id, array( 27 | 'label' => $this->options['label'], 28 | 'fk_provider' => $this->options['fk_provider'] 29 | )); 30 | $form->addItem($item); 31 | $form->addRelationship($this->id, $this->options['fk_type']); 32 | } 33 | } 34 | 35 | public function getSchema() 36 | { 37 | return array(); 38 | } 39 | 40 | public function getExpr() 41 | { 42 | if(isset($this->options['expr'])) { 43 | return $this->options['expr']; 44 | } 45 | else { 46 | return null; 47 | } 48 | } 49 | 50 | public function getSummaryConfig() 51 | { 52 | return array(); 53 | } 54 | } -------------------------------------------------------------------------------- /src/CrudKit/Data/SQL/PrimaryColumn.php: -------------------------------------------------------------------------------- 1 | $this->typeName, 23 | 'label' => "Primary", 24 | 'primaryFlag' => true, 25 | 'key' => $this->id 26 | ); 27 | } 28 | 29 | public function getExpr() 30 | { 31 | return $this->options['expr']; 32 | } 33 | 34 | public function getSummaryConfig() 35 | { 36 | return array( 37 | 'key' => $this->id, 38 | 'name' => "Primary", 39 | 'renderType' => $this->typeName 40 | ); 41 | } 42 | } -------------------------------------------------------------------------------- /src/CrudKit/Data/SQL/SQLColumn.php: -------------------------------------------------------------------------------- 1 | id = $id; 41 | $this->category = $category; 42 | $this->options = $options; 43 | } 44 | 45 | public function doctrineColumnLookup ($col_lookup) { 46 | if(isset($this->options['expr']) && isset($col_lookup[$this->options['expr']])) 47 | { 48 | /** 49 | * @var $col Column 50 | */ 51 | $col = $col_lookup[$this->options['expr']]; 52 | $this->type = $col->getType(); 53 | $this->typeName = self::simplifyTypeName($this->type->getName()); 54 | } 55 | } 56 | 57 | public function cleanValue ($value) { 58 | switch($this->typeName) { 59 | case "number": 60 | return floatval($value); 61 | break; 62 | case "string": 63 | return "".$value; 64 | break; 65 | case "datetime": 66 | $timezone = isset($this->options['timezone']) ? $this->options['timezone'] : "UTC"; 67 | // Assuming that the value that client has given is in UTC 68 | $timeObject = Carbon::createFromTimestamp(intval($value), "UTC"); 69 | 70 | // Now convert this into the target timezone 71 | $timeObject->setTimezone($timezone); 72 | return $timeObject; 73 | break; 74 | case "date": 75 | $date = date_create($value); 76 | $date = date_format($date,'m-d-Y'); 77 | return $date; 78 | break; 79 | default: 80 | throw new \Exception("Unknown type {$this->typeName}"); 81 | } 82 | } 83 | 84 | public function prepareForClient ($value) { 85 | switch($this->typeName) { 86 | case "number": 87 | return "".floatval($value); 88 | break; 89 | case "string": 90 | return "".$value; 91 | break; 92 | case "datetime": 93 | $timezone = isset($this->options['timezone']) ? $this->options['timezone'] : "UTC"; 94 | $timeObject = Carbon::parse($value, $timezone); 95 | // Convert that into UTC 96 | $timeObject->setTimezone("UTC"); 97 | return $timeObject->getTimestamp(); 98 | break; 99 | case "date": 100 | $date = date_create($value); 101 | $date = date_format($date,'m-d-Y'); 102 | return $date; 103 | break; 104 | default: 105 | throw new \Exception("Unknown type {$this->typeName}"); 106 | } 107 | } 108 | 109 | /** 110 | * @param $builder QueryBuilder 111 | * @param $expr ExpressionBuilder 112 | * @param $type string 113 | * @param $value 114 | * @param bool $orFlag 115 | * @return string 116 | * @throws \Exception 117 | */ 118 | public function addFilterToBuilder($builder, $expr, $type, $value, $orFlag = false) 119 | { 120 | switch ($type){ 121 | case "contains": 122 | case "like": 123 | return $expr->like($this->getExpr(), $builder->createNamedParameter("%".$value."%")); 124 | case "sw": // starts with 125 | return $expr->like($this->getExpr(), $builder->createNamedParameter($value."%")); 126 | case "ew": // starts with 127 | return $expr->like($this->getExpr(), $builder->createNamedParameter("%".$value)); 128 | case "eq": 129 | return $expr->eq($this->getExpr(), $builder->createNamedParameter($value)); 130 | case "gt": 131 | return $expr->gt ($this->getExpr(), $builder->createNamedParameter($value)); 132 | case "gte": 133 | return $expr->gte ($this->getExpr(), $builder->createNamedParameter($value)); 134 | case "lt": 135 | return $expr->gt ($this->getExpr(), $builder->createNamedParameter($value)); 136 | case "lte": 137 | return $expr->gte ($this->getExpr(), $builder->createNamedParameter($value)); 138 | default: 139 | throw new \Exception("Unkown filter type $type"); 140 | } 141 | } 142 | 143 | public static function simplifyTypeName ($typeName) { 144 | switch($typeName) { 145 | case "integer": 146 | case "float": 147 | case "smallint": 148 | case "bigint": 149 | case "decimal": 150 | case "numeric": 151 | case "smallint": 152 | case "bigint": 153 | return self::TYPE_NUMBER; 154 | case "string": 155 | case "text": 156 | return self::TYPE_STRING; 157 | case "datetime": 158 | return self::TYPE_DATETIME; 159 | case "date": 160 | return self::TYPE_DATE; 161 | default: 162 | throw new \Exception("Unknown type $typeName"); 163 | } 164 | } 165 | 166 | public function setOptions($values) { 167 | $this->options = array_merge($this->options, $values); 168 | } 169 | 170 | public function getExprAs() { 171 | if(is_null($this->getExpr())) 172 | { 173 | // Some columns don't get expressions. In those case return null 174 | return null; 175 | } 176 | 177 | return $this->getExpr()." AS ".$this->id; 178 | } 179 | 180 | public function init () { 181 | // A child will override this 182 | } 183 | 184 | /** 185 | * @param $form FormHelper 186 | * @return mixed 187 | */ 188 | public abstract function updateForm ($form); 189 | public abstract function getSchema (); 190 | public abstract function getExpr (); 191 | public abstract function getSummaryConfig (); 192 | 193 | } 194 | -------------------------------------------------------------------------------- /src/CrudKit/Data/SQL/ValueColumn.php: -------------------------------------------------------------------------------- 1 | typeName) { 25 | case "string": 26 | $item = new TextFormItem($form, $this->id, array( 27 | 'label' => $this->options['label'] 28 | )); 29 | break; 30 | case "datetime": 31 | $item = new DateTimeFormItem($form, $this->id, array( 32 | 'label' => $this->options['label'] 33 | )); 34 | break; 35 | case "number": 36 | $item = new NumberFormItem($form, $this->id, array( 37 | 'label' => $this->options['label'] 38 | )); 39 | break; 40 | } 41 | $form->addItem($item); 42 | } 43 | 44 | public function getSchema() 45 | { 46 | return array( 47 | 'type' => $this->typeName, 48 | 'label' => $this->options['label'] 49 | ); 50 | } 51 | 52 | public function getExpr() 53 | { 54 | return $this->options['expr']; 55 | } 56 | 57 | public function getSummaryConfig() 58 | { 59 | $summaryConf = array( 60 | 'key' => $this->id, 61 | 'name' => $this->options['label'], 62 | 'renderType' => $this->typeName 63 | ); 64 | 65 | if(isset($this->options['primaryColumn'])) { 66 | $summaryConf['primaryColumn'] = $this->options['primaryColumn']; 67 | $summaryConf['renderType'] = "primaryLink"; 68 | } 69 | 70 | return $summaryConf; 71 | } 72 | 73 | 74 | public function doctrineColumnLookup($col_lookup) 75 | { 76 | parent::doctrineColumnLookup($col_lookup); 77 | 78 | } 79 | } -------------------------------------------------------------------------------- /src/CrudKit/Data/SQLDataProvider.php: -------------------------------------------------------------------------------- 1 | setConn($connection); 65 | //TODO: Make these fields required (after refactoring Page classes) since init() fails without them 66 | if($table) { 67 | $this->setTable($table); 68 | } 69 | if($primaryColumn) { 70 | $this->setPrimaryColumn($primaryColumn, $primaryColumn); 71 | } 72 | if(!empty($summaryCols)) { 73 | $this->setSummaryColumns($summaryCols); 74 | } 75 | } 76 | 77 | public function setConn(Connection $connection) 78 | { 79 | $this->conn = $connection; 80 | } 81 | 82 | public function addColumn ($id, $expr, $label, $options = array()) { 83 | $options['label'] = $label; 84 | $options['expr'] = $expr; 85 | 86 | $this->internalAddColumn(SQLColumn::CATEGORY_VALUE, $id, $options); 87 | } 88 | 89 | public function hasMany ($id, $label, $page, $options) { 90 | $this->internalAddColumn(SQLColumn::CATEGORY_EXTERNAL, $id, array( 91 | 'page_id' => $page->getId (), 92 | 'label' => $label, 93 | 'type' => ExternalColumn::HAS_MANY, 94 | 'foreign_key' => $options['foreign_key'], 95 | 'local_key' => $options['local_key'] 96 | )); 97 | } 98 | 99 | public function setPrimaryColumn ($id, $expr) { 100 | $this->internalAddColumn(SQLColumn::CATEGORY_PRIMARY, $id, array( 101 | 'name' => "Primary", 102 | 'expr' => $expr 103 | )); 104 | $this->primary_col = $id; 105 | } 106 | 107 | /** 108 | * @return SQLColumn 109 | */ 110 | protected function getPrimaryColumn() 111 | { 112 | return $this->columns[$this->primary_col]; 113 | } 114 | 115 | public function manyToOne ($id, $foreignKey, $extTable, $primary, $nameColumn, $label) { 116 | $this->internalAddColumn(SQLColumn::CATEGORY_FOREIGN, $id, array( 117 | 'fk_type' => 'manyToOne', 118 | 'fk_table' => $extTable, 119 | 'fk_primary' => $primary, 120 | 'fk_name_col' => $nameColumn, 121 | 'expr' => $foreignKey, 122 | 'label' => $label 123 | )); 124 | } 125 | 126 | public function init () { 127 | parent::init(); 128 | 129 | $this->processColumns(); 130 | $this->postProcessColumns(); 131 | } 132 | 133 | /** 134 | * @param array $summary_cols 135 | */ 136 | public function setSummaryColumns($summary_cols) 137 | { 138 | $this->summary_cols = $summary_cols; 139 | } 140 | 141 | /** 142 | * Converts columns from raw objects to more powerful cool objects 143 | */ 144 | protected function processColumns () { 145 | // Get a schema manager and get the list of columns 146 | $sm = $this->conn->getSchemaManager(); 147 | $columns = $sm->listTableColumns($this->tableName); 148 | 149 | $type_lookup = array(); 150 | 151 | /** 152 | * @var $col Column 153 | */ 154 | foreach($columns as $col) { 155 | $type_lookup[$col->getName()] = $col; 156 | } 157 | 158 | foreach($this->colDefs as $item) { 159 | $id = $item['id']; 160 | $category = $item['category']; 161 | $opts = $item['options']; 162 | 163 | /** 164 | * @var $target SQLColumn 165 | */ 166 | $target = null; 167 | 168 | switch($category) { 169 | case SQLColumn::CATEGORY_VALUE: 170 | $target = new ValueColumn($id, $category, $opts); 171 | break; 172 | // case "foreign": 173 | // $target = new ForeignColumn($id, $category, $opts); 174 | // break; 175 | case SQLColumn::CATEGORY_PRIMARY: 176 | $target = new PrimaryColumn($id, $category, $opts); 177 | break; 178 | case SQLColumn::CATEGORY_EXTERNAL: 179 | $target = new ExternalColumn ($id, $category, $opts); 180 | break; 181 | default: 182 | //TODO: Throw library-specific exceptions 183 | throw new \Exception("Unknown category for column $category"); 184 | } 185 | 186 | $target->doctrineColumnLookup($type_lookup); 187 | $target->init (); 188 | 189 | $this->columns[$id] = $target; 190 | } 191 | } 192 | 193 | protected function postProcessColumns () { 194 | // Set the first summary column as a primary column 195 | /** 196 | * @var SQLColumn $sumColObj 197 | */ 198 | $sumColObj = $this->columns[$this->summary_cols[0]]; 199 | $sumColObj->setOptions(array( 200 | 'primaryColumn' => $this->getPrimaryColumn()->id 201 | )); 202 | // TODO: Improve primary handling 203 | } 204 | 205 | /** 206 | * Super cool and useful function to query columns and get a reduced 207 | * subset 208 | * 209 | * @param $queryType 210 | * @param $queryValues 211 | * @param $valueType 212 | * @param bool $keyValue 213 | * @return array 214 | * @throws \Exception 215 | */ 216 | protected function queryColumns ($queryType, $queryValues, $valueType, $keyValue = false, $ignoreNull = false) { 217 | $target_columns = array(); 218 | 219 | if($queryType === "col_list") { 220 | // The caller has already specified a list of columns 221 | $target_columns = $queryValues; 222 | } 223 | else { 224 | /** 225 | * @var $key 226 | * @var SQLColumn $col 227 | */ 228 | foreach($this->columns as $key => $col) { 229 | if($queryType === "category") { 230 | if(in_array($col->category, $queryValues)) { 231 | $target_columns []= $key; 232 | } 233 | } 234 | else if ($queryType === "all") { 235 | $target_columns []= $key; 236 | } 237 | } 238 | } 239 | 240 | $results = array(); 241 | 242 | foreach($target_columns as $colKey) { 243 | /** 244 | * @var $column SQLColumn 245 | */ 246 | $column = $this->columns[$colKey]; 247 | $resultItem = null; 248 | switch($valueType) { 249 | case "id": 250 | $resultItem = $colKey; 251 | break; 252 | case "expr": 253 | $resultItem = $column->getExpr(); 254 | break; 255 | case "exprAs": 256 | $resultItem = $column->getExprAs(); 257 | break; 258 | case "object": 259 | $resultItem = $column; 260 | break; 261 | case "schema": 262 | $resultItem = $column->getSchema(); 263 | break; 264 | case "summary": 265 | $resultItem = $column->getSummaryConfig(); 266 | break; 267 | default: 268 | //TODO: Throw library-specific exceptions 269 | throw new \Exception("Unknown value type $valueType"); 270 | } 271 | 272 | if(is_null($resultItem) && $ignoreNull) { 273 | continue; 274 | } 275 | 276 | if($keyValue) { 277 | $results[$colKey] = $resultItem; 278 | } 279 | else { 280 | $results []= $resultItem; 281 | } 282 | } 283 | 284 | return $results; 285 | } 286 | 287 | protected function internalAddColumn ($category, $id, $options = array()) { 288 | $this->colDefs []= array( 289 | 'id' => $id, 290 | 'category' => $category, 291 | 'options' => $options 292 | ); 293 | } 294 | 295 | protected function prepareObjectForClient ($object) { 296 | $result = array(); 297 | foreach($object as $key => $value) { 298 | /** 299 | * @var $col SQLColumn 300 | */ 301 | $col = $this->columns[$key]; 302 | $result[$key] = $col->prepareForClient($value); 303 | } 304 | 305 | return $result; 306 | } 307 | 308 | /** 309 | * @param $table string 310 | */ 311 | public function setTable ($table) { 312 | $this->tableName = $table; 313 | } 314 | 315 | public function getData(array $params = array()) 316 | { 317 | $skip = isset($params['skip']) ? $params['skip'] : 0; 318 | $take = isset($params['take']) ? $params['take'] : 10; 319 | $builder = $this->conn->createQueryBuilder(); 320 | $builder->select($this->queryColumns('all', array(), 'exprAs', false, true)) 321 | ->from($this->tableName) 322 | ->setFirstResult($skip) 323 | ->setMaxResults($take); 324 | 325 | if(isset($params['filters_json'])) { 326 | $filters = json_decode($params['filters_json'], true); 327 | if(count($filters) > 0) { 328 | $this->addConditionsToBuilder($builder, $filters); 329 | } 330 | } 331 | LoggingHelper::logBuilder($builder); 332 | $exec = $builder->execute(); 333 | 334 | return $exec->fetchAll(\PDO::FETCH_ASSOC); 335 | } 336 | 337 | /** 338 | * @param $builder QueryBuilder 339 | * @param $filters 340 | */ 341 | protected function addConditionsToBuilder ($builder, $filters) { 342 | foreach($filters as $filterItem) { 343 | $id = $filterItem['id']; 344 | if($id === "_ck_all_summary") { 345 | $target_cols = $this->summary_cols; 346 | 347 | $exprList = array(); 348 | foreach($target_cols as $colKey) { 349 | /** 350 | * @var $col SQLColumn 351 | */ 352 | $col = $this->columns[$colKey]; 353 | $val = $col->cleanValue($filterItem['value']); 354 | $exprString = $col->addFilterToBuilder ($builder, $builder->expr(), $filterItem['type'], $val); 355 | $exprList []= $exprString; 356 | $composite = call_user_func_array(array($builder->expr(), "orX"), $exprList); 357 | $builder->andWhere($composite); 358 | } 359 | } 360 | if(isset($this->columns[$id])) { 361 | /** 362 | * @var $col SQLColumn 363 | */ 364 | $col = $this->columns[$id]; 365 | $val = $col->cleanValue($filterItem['value']); 366 | $exprString = $col->addFilterToBuilder ($builder, $builder->expr(), $filterItem['type'], $val); 367 | $builder->andWhere($exprString); 368 | } 369 | } 370 | } 371 | 372 | public function getSchema() 373 | { 374 | return $this->queryColumns("category", array(SQLColumn::CATEGORY_VALUE, SQLColumn::CATEGORY_PRIMARY), "schema", true); 375 | } 376 | 377 | public function getRowCount(array $params = []) 378 | { 379 | $builder = $this->conn->createQueryBuilder(); 380 | $builder->select(array("COUNT(".$this->getPrimaryColumn()->getExpr().") AS row_count")) 381 | ->from($this->tableName); 382 | 383 | if(isset($params['filters_json'])) { 384 | $filters = json_decode($params['filters_json'], true); 385 | if(count($filters) > 0) { 386 | $this->addConditionsToBuilder($builder, $filters); 387 | } 388 | } 389 | 390 | LoggingHelper::logBuilder($builder); 391 | $exec = $builder->execute(); 392 | 393 | $countResult = $exec->fetchAll(\PDO::FETCH_ASSOC); 394 | return $countResult[0]['row_count']; 395 | } 396 | 397 | public function oneToMany($id, $dataProvider, $externalKey, $localKey, $name) 398 | { 399 | // Make sure data provider gets inited properly 400 | $this->initQueue []= $dataProvider; 401 | 402 | $this->internalAddColumn(SQLColumn::CATEGORY_FOREIGN, $id, array( 403 | 'fk_type' => 'oneToMany', 404 | 'fk_provider' => $dataProvider, 405 | 'fk_extKey' => $externalKey, 406 | 'fk_localKey' => $localKey, 407 | 'label' => $name 408 | )); 409 | } 410 | 411 | public function getSummaryColumns() 412 | { 413 | return $this->queryColumns("col_list", $this->summary_cols, "summary"); 414 | } 415 | 416 | public function getEditFormOrder() 417 | { 418 | return $this->queryColumns("category", 419 | array(SQLColumn::CATEGORY_VALUE, SQLColumn::CATEGORY_PRIMARY), 420 | "id"); 421 | } 422 | 423 | public function getRow($id = null) 424 | { 425 | $pk = $this->getPrimaryColumn ()->getExpr(); 426 | $builder = $this->conn->createQueryBuilder(); 427 | $exec = $builder->select($this->queryColumns('all', array(), 'exprAs', false, true)) 428 | ->from($this->tableName) 429 | ->where("$pk = ".$builder->createNamedParameter($id)) 430 | ->execute(); 431 | 432 | LoggingHelper::logBuilder($builder); 433 | $values = $exec->fetch(PDO::FETCH_ASSOC); 434 | return $this->prepareObjectForClient($values); 435 | } 436 | 437 | public function setRow($id = null, array $values = []) 438 | { 439 | $builder = $this->conn->createQueryBuilder(); 440 | $pk = $this->getPrimaryColumn()->getExpr(); 441 | $builder->update($this->tableName); 442 | foreach($values as $formKey => $formValue) { 443 | /** @var SQLColumn $col */ 444 | $col = null; 445 | if(!isset($this->columns[$formKey])) { 446 | //TODO: Throw library-specific exceptions 447 | throw new \Exception ("Unknown column"); 448 | } 449 | $col = $this->columns[$formKey]; 450 | $val = $col->cleanValue ($values[$formKey]); 451 | $builder->set($col->getExpr(), $builder->createNamedParameter($val)); 452 | } 453 | 454 | LoggingHelper::logBuilder($builder); 455 | $builder->where("$pk = ".$builder->createNamedParameter($id)) 456 | ->execute(); 457 | return true; 458 | } 459 | 460 | public function deleteItem($rowId) 461 | { 462 | $builder = $this->conn->createQueryBuilder(); 463 | $pk = $this->getPrimaryColumn()->getExpr(); 464 | $status = $builder->delete($this->tableName) 465 | ->where("$pk = ".$builder->createNamedParameter($rowId)) 466 | ->execute(); 467 | 468 | return $status; 469 | } 470 | 471 | public function createItem(array $values) 472 | { 473 | $builder = $this->conn->createQueryBuilder(); 474 | $builder->insert($this->tableName); 475 | foreach($values as $formKey => $formValue) { 476 | /** @var SQLColumn $col */ 477 | $col = null; 478 | if(!isset($this->columns[$formKey])) { 479 | //TODO: Throw library-specific exceptions 480 | throw new \Exception ("Unknown column"); 481 | } 482 | $col = $this->columns[$formKey]; 483 | $builder->setValue($col->getExpr(), $builder->createNamedParameter($values[$formKey])); 484 | } 485 | 486 | LoggingHelper::logBuilder($builder); 487 | $builder->execute(); 488 | return $this->conn->lastInsertId(); 489 | 490 | } 491 | 492 | public function deleteMultipleItems(array $ids) 493 | { 494 | $builder = $this->conn->createQueryBuilder(); 495 | $pk = $this->getPrimaryColumn()->getExpr(); 496 | $expr = $builder->expr(); 497 | 498 | $builder->delete($this->tableName) 499 | ->where($expr->in($pk, $ids)); 500 | LoggingHelper::logBuilder($builder); 501 | $status = $builder->execute(); 502 | 503 | return $status; 504 | } 505 | 506 | protected function isFieldInSchema($formKey) 507 | { 508 | return isset($this->columns[$formKey]); 509 | } 510 | 511 | protected function getValidatorForField($formKey) 512 | { 513 | $options = $this->columns[$formKey]->options; 514 | if(isset($options['validator']) && is_callable($options["validator"])) { 515 | return $options['validator']; 516 | } 517 | return null; 518 | } 519 | 520 | protected function getRequiredFields() 521 | { 522 | $required = []; 523 | foreach($this->columns as $key => $column) { 524 | if(isset($column->options['required']) && $column->options['required']) { 525 | $required[] = $key; 526 | } 527 | } 528 | return $required; 529 | } 530 | 531 | public function getEditForm ($id = null) { 532 | $form = new FormHelper(); 533 | 534 | $formColumns = $this->queryColumns("category", array(SQLColumn::CATEGORY_VALUE, SQLColumn::CATEGORY_EXTERNAL), 'object'); 535 | 536 | /** 537 | * @var $col SQLColumn 538 | */ 539 | foreach($formColumns as $col) { 540 | $col->updateForm($form); 541 | } 542 | 543 | return $form; 544 | } 545 | 546 | public function getRelationshipValues ($id, $foreign) { 547 | 548 | $builder = $this->conn->createQueryBuilder(); 549 | $forColumn = $this->columns[$foreign]; 550 | $forOpts = $forColumn->options; 551 | if($forOpts['fk_type'] === "manyToOne") { 552 | $statement = $builder->select(array($forOpts['fk_name_col']." AS label", $forOpts['fk_primary']." AS id")) 553 | ->from($forOpts['fk_table']) 554 | ->setMaxResults(100) 555 | ->execute(); 556 | 557 | return $statement->fetchAll(PDO::FETCH_ASSOC); 558 | } 559 | else if($forOpts['fk_type'] === "oneToMany") { 560 | /** 561 | * @var $extProvider SQLiteDataProvider 562 | */ 563 | $extProvider = $forOpts['fk_provider']; 564 | return $extProvider->getForeignValues($forOpts['fk_extKey'], $id); 565 | } 566 | else { 567 | //TODO: Throw library-specific exceptions 568 | throw new \Exception("Unknown relationship value"); 569 | } 570 | return array(); 571 | } 572 | 573 | public function getForeignValues ($localKey, $value) { 574 | $builder = $this->conn->createQueryBuilder(); 575 | $exec = $builder->select($this->queryColumns('all', array(), 'exprAs', false, true)) 576 | ->from($this->tableName) 577 | ->where("$localKey = ".$builder->createNamedParameter($value)) 578 | ->setMaxResults(10) // TODO fix this 579 | ->execute(); 580 | 581 | return $exec->fetchAll(\PDO::FETCH_ASSOC); 582 | } 583 | } 584 | -------------------------------------------------------------------------------- /src/CrudKit/Form/BaseFormItem.php: -------------------------------------------------------------------------------- 1 | config = $config; 19 | $this->key = $key; 20 | $this->form = $form; 21 | $this->id = "control-{$this->key}"; 22 | } 23 | 24 | public abstract function render (); 25 | public abstract function renderInline (); 26 | } -------------------------------------------------------------------------------- /src/CrudKit/Form/DateTimeFormItem.php: -------------------------------------------------------------------------------- 1 | getAngularDirectives (); 10 | 11 | return << 13 | 14 | 15 | COMP; 16 | } 17 | } -------------------------------------------------------------------------------- /src/CrudKit/Form/HasManyItem.php: -------------------------------------------------------------------------------- 1 | form->getNgModel(); 12 | $label = $this->config['label']; 13 | 14 | return << 16 |

${label}

17 |
18 |

Has Many will come here

19 |
20 | 21 | RENDER; 22 | } 23 | 24 | public function renderInline() 25 | { 26 | return ""; 27 | } 28 | } -------------------------------------------------------------------------------- /src/CrudKit/Form/HorizontalItem.php: -------------------------------------------------------------------------------- 1 | config['label']; 8 | $content = $this->renderInternal(); 9 | return << 11 | 12 |
13 | $content 14 |
15 | 16 | RENDER; 17 | 18 | } 19 | 20 | public function getAngularDirectives () { 21 | $ngModel = $this->form->getNgModel(); 22 | return "ng-change=\"{$this->changeFunc}('{$this->key}')\" ng-model=\"$ngModel.{$this->key}\""; 23 | } 24 | 25 | public function renderInline () { 26 | return $this->render(); 27 | } 28 | 29 | public abstract function renderInternal (); 30 | } -------------------------------------------------------------------------------- /src/CrudKit/Form/ManyToOneItem.php: -------------------------------------------------------------------------------- 1 | form->getNgModel(); 10 | $directives = $this->getAngularDirectives (); 11 | return << 13 | 14 | COMP; 15 | } 16 | 17 | public function renderInline () { 18 | // don't render a fkey item inline 19 | return ""; 20 | } 21 | } -------------------------------------------------------------------------------- /src/CrudKit/Form/NumberFormItem.php: -------------------------------------------------------------------------------- 1 | getAngularDirectives (); 10 | $ngModel = $this->form->getNgModel(); 11 | $value = isset($this->config['value']) ? $this->config['value'] : ""; 12 | 13 | return << 15 | 16 | 17 | COMP; 18 | } 19 | } -------------------------------------------------------------------------------- /src/CrudKit/Form/OneToManyItem.php: -------------------------------------------------------------------------------- 1 | form->getNgModel(); 13 | $label = $this->config['label']; 14 | 15 | /** 16 | * @var $provider DataProvider 17 | */ 18 | $provider = $this->config['fk_provider']; 19 | $providerForm = $provider->getEditForm(); 20 | $providerForm->setNgModel("inlineItem"); 21 | $inlineContent = $providerForm->renderInline(); 22 | 23 | return << 25 |

${label}

26 |
27 | $inlineContent 28 |
29 | 30 | RENDER; 31 | } 32 | 33 | public function renderInline() 34 | { 35 | return ""; 36 | } 37 | } -------------------------------------------------------------------------------- /src/CrudKit/Form/TextFormItem.php: -------------------------------------------------------------------------------- 1 | getAngularDirectives (); 10 | $value = isset($this->config['value']) ? $this->config['value'] : ""; 11 | return << 13 | COMP; 14 | } 15 | } -------------------------------------------------------------------------------- /src/CrudKit/Laravel/CrudKitServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 13 | __DIR__.'/../../static/' => public_path('vendor/crudkit/'), 14 | ], 'public'); 15 | } 16 | 17 | public function register() 18 | { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/CrudKit/Laravel/LaravelApp.php: -------------------------------------------------------------------------------- 1 | renderToString(); 12 | 13 | if ($this->redirect !== null) { 14 | $url = ''.$this->redirect; 15 | return new RedirectResponse ($url); 16 | } 17 | $response = new Response (); 18 | // Headers are also calculated in render to string 19 | if($this->isJsonResponse()) { 20 | $response->header("Content-type", "application/json;"); 21 | } 22 | $response->setContent ($content); 23 | 24 | return $response; 25 | } 26 | 27 | public function __construct () { 28 | // configure to laravel default 29 | $this->setStaticRoot ("/vendor/crudkit/"); 30 | } 31 | } -------------------------------------------------------------------------------- /src/CrudKit/Laravel/LaravelTablePage.php: -------------------------------------------------------------------------------- 1 | getPdo(); 12 | $params = array( 13 | 'pdo' => $pdo 14 | ); 15 | 16 | $conn = DriverManager::getConnection($params); 17 | $this->preInit($id, $conn); 18 | 19 | return $this; 20 | } 21 | } -------------------------------------------------------------------------------- /src/CrudKit/Pages/BasePage.php: -------------------------------------------------------------------------------- 1 | name = $name; 16 | return $this; 17 | } 18 | public function getName () { 19 | return $this->name; 20 | } 21 | 22 | protected $app = null; 23 | public function init ($app = null) { 24 | if ($app == null) { 25 | die ("app is null"); 26 | } 27 | $this->app = $app; 28 | } 29 | 30 | /** 31 | * @return null 32 | */ 33 | public function getId() 34 | { 35 | return $this->id; 36 | } 37 | 38 | protected function setId ($id) { 39 | $this->id = $id; 40 | } 41 | } -------------------------------------------------------------------------------- /src/CrudKit/Pages/BaseSQLDataPage.php: -------------------------------------------------------------------------------- 1 | setId($id); 16 | $this->sqlProvider = new SQLDataProvider($conn); 17 | } 18 | 19 | 20 | /** 21 | * Set the name of the table to work with. 22 | * 23 | * @param $tableName 24 | * @return $this 25 | */ 26 | public function setTableName ($tableName) { 27 | $this->sqlProvider->setTable($tableName); 28 | return $this; 29 | } 30 | 31 | /** 32 | * Add a column to edit 33 | * 34 | * @param string $column_name 35 | * @param string $label 36 | * @param array $options 37 | * @return $this 38 | */ 39 | public function addColumn ($column_name, $label, $options = array()) { 40 | $this->sqlProvider->addColumn($column_name, $column_name, $label, $options); 41 | return $this; 42 | } 43 | 44 | public function hasMany ($id, $name, $columns) { 45 | $this->sqlProvider->hasMany($id, $name, $columns, []); 46 | return $this; 47 | } 48 | 49 | /** 50 | * Add a column to edit with a unique ID 51 | * 52 | * @param string $id 53 | * @param string $column_name 54 | * @param string $label 55 | * @param array $options 56 | * @return $this 57 | */ 58 | public function addColumnWithId ($id, $column_name, $label, $options = array()) { 59 | $this->sqlProvider->addColumn($id, $column_name, $label, $options); 60 | return $this; 61 | } 62 | 63 | /** 64 | * Set the columns to display in the summary table 65 | * 66 | * @param $summaryColumns 67 | * @return $this 68 | */ 69 | public function setSummaryColumns ($summaryColumns) { 70 | $this->sqlProvider->setSummaryColumns($summaryColumns); 71 | return $this; 72 | } 73 | 74 | /** 75 | * Set the primary column 76 | * @param $primaryColumn 77 | * @return $this 78 | */ 79 | public function setPrimaryColumn ($primaryColumn) { 80 | $this->sqlProvider->setPrimaryColumn($primaryColumn, $primaryColumn); 81 | return $this; 82 | } 83 | 84 | /** 85 | * @param $id 86 | * @param $primaryColumn 87 | * @return $this 88 | */ 89 | public function setPrimaryColumnWithId ($id, $primaryColumn) { 90 | $this->sqlProvider->setPrimaryColumn($id, $primaryColumn); 91 | return $this; 92 | } 93 | 94 | public function init ($app = null) { 95 | $this->setDataProvider($this->sqlProvider); 96 | parent::init ($app); 97 | } 98 | } -------------------------------------------------------------------------------- /src/CrudKit/Pages/BasicDataPage.php: -------------------------------------------------------------------------------- 1 | app->isReadOnly (); 23 | ValueBag::set ("writable", $writableFlag); 24 | ValueBag::set ("rowsPerPage", $this->rowsPerPage); 25 | return $twig->renderTemplateToString("pages/basicdata.twig", array( 26 | 'route' => new RouteGenerator(), 27 | 'page' => $this, 28 | 'writable' => $writableFlag, 29 | 'name' => $this->name 30 | )); 31 | } 32 | 33 | /** 34 | * Get the column specification and send to the client 35 | * @return array 36 | */ 37 | public function handle_get_colSpec () { 38 | $url = new UrlHelper (); 39 | $filters = $url->get("filters_json", "[]"); 40 | 41 | $params = array( 42 | 'filters_json' => $filters 43 | ); 44 | 45 | return array( 46 | 'type' => 'json', 47 | 'data' => array ( 48 | 'count' => $this->dataProvider->getRowCount($params), 49 | 'schema' => $this->dataProvider->getSchema(), 50 | 'columns' => $this->dataProvider->getSummaryColumns() 51 | ) 52 | ); 53 | } 54 | 55 | /** 56 | * Get Data 57 | * @return array 58 | */ 59 | public function handle_get_data() { 60 | $url = new UrlHelper (); 61 | $pageNumber = $url->get('pageNumber', 1); 62 | $perPage = $url->get('perPage', 10); 63 | $filters = $url->get("filters_json", "[]"); 64 | 65 | $params = array( 66 | 'skip' => ($pageNumber - 1) * $perPage, 67 | 'take' => $perPage, 68 | 'filters_json' => $filters 69 | ); 70 | return array( 71 | 'type' => 'json', 72 | 'data' => array ( 73 | 'rows' => $this->dataProvider->getData($params) 74 | ) 75 | ); 76 | } 77 | 78 | public function handle_delete_items () { 79 | if ($this->app->isReadOnly ()) { 80 | throw new Exception ("Read Only"); 81 | } 82 | $url = new UrlHelper (); 83 | $delete_ids = json_decode($url->get('delete_ids', "[]"), true); 84 | $this->dataProvider->deleteMultipleItems($delete_ids); 85 | 86 | return array ( 87 | 'type' => 'json', 88 | 'data' => array ( 89 | 'success' => true 90 | ) 91 | ); 92 | } 93 | 94 | public function handle_view_item () { 95 | $twig = new TwigUtil(); 96 | 97 | $url = new UrlHelper(); 98 | $rowId = $url->get("item_id", null); 99 | 100 | $route = new RouteGenerator(); 101 | $deleteUrl = ($route->itemFunc($this->getId(), $rowId, "delete_item")); 102 | $editUrl = ($route->itemFunc($this->getId(), $rowId, "edit_item")); 103 | $writable = !$this->app->isReadOnly (); 104 | 105 | $summaryKey = $this->dataProvider->getSummaryColumns()[0]['key']; 106 | $rowData = $this->dataProvider->getRow ($rowId); 107 | $rowName = $rowData[$summaryKey]; 108 | 109 | $templateData = array( 110 | 'page' => $this, 111 | 'name' => $this->name, 112 | 'rowId' => $rowId, 113 | 'writable' => $writable, 114 | 'deleteUrl' => $deleteUrl, 115 | 'editUrl' => $editUrl, 116 | 'schema' => $this->dataProvider->getSchema (), 117 | 'rowName' => $rowName, 118 | 'row' => $rowData, 119 | ); 120 | 121 | return $twig->renderTemplateToString("pages/basicdata/view_item.twig", $templateData); 122 | } 123 | 124 | public function handle_edit_item () { 125 | if ($this->app->isReadOnly ()) { 126 | throw new Exception ("Read Only"); 127 | } 128 | $twig = new TwigUtil(); 129 | 130 | $url = new UrlHelper(); 131 | $rowId = $url->get("item_id", null); 132 | $form = $this->dataProvider->getEditForm(); 133 | $form->setPageId($this->getId()); 134 | $form->setItemId($rowId); 135 | 136 | $route = new RouteGenerator(); 137 | $deleteUrl = ($route->itemFunc($this->getId(), $rowId, "delete_item")); 138 | 139 | $formContent = $form->render($this->dataProvider->getEditFormOrder()); 140 | $templateData = array( 141 | 'page' => $this, 142 | 'name' => $this->name, 143 | 'editForm' => $formContent, 144 | 'rowId' => $rowId, 145 | 'canDelete' => true, 146 | 'deleteUrl' => $deleteUrl 147 | ); 148 | 149 | return $twig->renderTemplateToString("pages/basicdata/edit_item.twig", $templateData); 150 | } 151 | 152 | public function handle_delete_item () { 153 | if ($this->app->isReadOnly ()) { 154 | throw new Exception ("Read Only"); 155 | } 156 | $url = new UrlHelper(); 157 | $rowId = $url->get("item_id", null); 158 | $route = new RouteGenerator(); 159 | $summaryKey = $this->dataProvider->getSummaryColumns()[0]['key']; 160 | $rowData = $this->dataProvider->getRow ($rowId); 161 | $rowName = $rowData[$summaryKey]; 162 | 163 | $status = $this->dataProvider->deleteItem ($rowId); 164 | FlashBag::add("alert", "Item $rowName has been deleted", "success"); 165 | 166 | // Redirect back to the pageme 167 | return array( 168 | 'type' => 'redirect', 169 | 'url' => $route->openPage($this->getId()) 170 | ); 171 | } 172 | 173 | public function handle_new_item () { 174 | if ($this->app->isReadOnly ()) { 175 | throw new Exception ("Read Only"); 176 | } 177 | $twig = new TwigUtil(); 178 | 179 | $form = $this->dataProvider->getEditForm(); 180 | 181 | $form->setPageId($this->getId()); 182 | $form->setNewItem(); 183 | 184 | $formContent = $form->render($this->dataProvider->getEditFormOrder()); 185 | $templateData = array( 186 | 'page' => $this, 187 | 'name' => $this->name, 188 | 'editForm' => $formContent 189 | ); 190 | 191 | return $twig->renderTemplateToString("pages/basicdata/edit_item.twig", $templateData); 192 | } 193 | 194 | public function handle_get_form_values () { 195 | $url = new UrlHelper (); 196 | $item_id = $url->get("item_id", null); 197 | if($item_id === "_ck_new"){ 198 | return array( 199 | 'type' => 'json', 200 | 'data' => array ( 201 | 'schema' => $this->dataProvider->getSchema(), 202 | 'values' => array() 203 | ) 204 | ); 205 | } 206 | return array( 207 | 'type' => 'json', 208 | 'data' => array ( 209 | 'schema' => $this->dataProvider->getSchema(), 210 | 'values' => $this->dataProvider->getRow($item_id) 211 | ) 212 | ); 213 | } 214 | 215 | public function handle_get_foreign () { 216 | $url = new UrlHelper(); 217 | $foreign_key = $url->get("foreign_key", null); 218 | $item_id = $url->get("item_id", null); 219 | 220 | return array( 221 | 'type' => 'json', 222 | 'data' => array ( 223 | 'values' => $this->dataProvider->getRelationshipValues($item_id, $foreign_key) 224 | ) 225 | ); 226 | } 227 | 228 | public function handle_create_item () { 229 | if ($this->app->isReadOnly ()) { 230 | throw new Exception ("Read Only"); 231 | } 232 | $url = new UrlHelper(); 233 | 234 | $values = json_decode($url->get("values_json", "{}"), true); 235 | 236 | //We have to check that all required fields are in request AND all provided data meet requirements 237 | $failedValues = array_merge( 238 | $this->dataProvider->validateRow($values), 239 | $this->dataProvider->validateRequiredRow($values)); 240 | if(empty($failedValues)){ 241 | $new_pk = $this->dataProvider->createItem($values); 242 | FlashBag::add("alert", "Item $new_pk has been created", "success"); 243 | return array( 244 | 'type' => 'json', 245 | 'data' => array( 246 | 'success' => true, 247 | 'newItemId' => $new_pk 248 | ) 249 | ); 250 | } 251 | else { 252 | FlashBag::add("alert", "Could not set certain fields", "error"); 253 | 254 | return array( 255 | 'type' => 'json', 256 | 'data' => array( 257 | 'success' => true, 258 | 'dataValid' => false, 259 | 'failedValues' => $failedValues 260 | ) 261 | ); 262 | //throw new \Exception("Cannot validate values"); 263 | } 264 | 265 | } 266 | 267 | public function handle_set_form_values () { 268 | if ($this->app->isReadOnly ()) { 269 | throw new Exception ("Read Only"); 270 | } 271 | $form = new FormHelper(array(), $this->dataProvider->getEditFormConfig()); 272 | $url = new UrlHelper(); 273 | 274 | $values = json_decode($url->get("values_json", "{}"), true); 275 | if(empty($values)) { 276 | return array( 277 | 'type' => 'json', 278 | 'data' => array( 279 | 'success' => true 280 | ) 281 | ); 282 | } 283 | 284 | //validate 285 | $failedValues = $this->dataProvider->validateRow($values); 286 | if(empty($failedValues)) { 287 | $url = new UrlHelper(); 288 | $rowId = $url->get("item_id", null); 289 | $summaryKey = $this->dataProvider->getSummaryColumns()[0]['key']; 290 | $rowData = $this->dataProvider->getRow ($rowId); 291 | $rowName = $rowData[$summaryKey]; 292 | 293 | $this->dataProvider->setRow($url->get("item_id", null), $values); 294 | FlashBag::add("alert", "Item $rowName has been updated", "success"); 295 | return array( 296 | 'type' => 'json', 297 | 'data' => array( 298 | 'success' => true, 299 | 'dataValid' => true 300 | ) 301 | ); 302 | } 303 | else { 304 | FlashBag::add("alert", "Could not update certain fields", "error"); 305 | 306 | return array( 307 | 'type' => 'json', 308 | 'data' => array( 309 | 'success' => true, 310 | 'dataValid' => false, 311 | 'failedValues' => $failedValues 312 | ) 313 | ); 314 | //throw new \Exception("Cannot validate values"); 315 | } 316 | } 317 | 318 | // TODO: start organizing parameters 319 | protected $rowsPerPage = 10; 320 | public function setRowsPerPage ($rows = 10) { 321 | $this->rowsPerPage = $rows; 322 | return $this; 323 | } 324 | 325 | /** 326 | * @var DataProvider 327 | */ 328 | protected $dataProvider = null; 329 | 330 | /** 331 | * @return DataProvider 332 | */ 333 | public function getDataProvider() 334 | { 335 | return $this->dataProvider; 336 | } 337 | 338 | /** 339 | * @param DataProvider $dataProvider 340 | */ 341 | public function setDataProvider($dataProvider) 342 | { 343 | $this->dataProvider = $dataProvider; 344 | $this->dataProvider->setPage($this); 345 | } 346 | 347 | public function init ($app = null) { 348 | parent::init($app); 349 | $this->dataProvider->init(); 350 | } 351 | } 352 | 353 | -------------------------------------------------------------------------------- /src/CrudKit/Pages/BasicLoginPage.php: -------------------------------------------------------------------------------- 1 | setId ("__ck_basic_login"); 14 | $this->url = new UrlHelper (); 15 | $this->route = new RouteGenerator(); 16 | } 17 | 18 | public function userTriedLogin () { 19 | return $this->url->has ('username') && $this->url->has ('password'); 20 | } 21 | 22 | public function getUserName () { 23 | return $this->url->get ('username'); 24 | } 25 | 26 | public function getPassword () { 27 | return $this->url->get ('password'); 28 | } 29 | 30 | // TODO: Refactor this into a session helper 31 | public function check () { 32 | return isset($_SESSION['__ck_logged_in']) && $_SESSION['__ck_logged_in'] == true; 33 | } 34 | 35 | public function doLogin () { 36 | $_SESSION['__ck_logged_in'] = true; 37 | } 38 | 39 | public function doLogout ($username = null) { 40 | unset($_SESSION['__ck_logged_in']); 41 | unset($_SESSION['__ck_username']); 42 | } 43 | 44 | 45 | protected $loginQueued = false; 46 | 47 | public function queueLogin () { 48 | $this->loginQueued = true; 49 | } 50 | 51 | public function success () { 52 | // Don't do the login writing to the session just yet 53 | // since we might be on a different page id than expected, 54 | // we need to do a clean redirect 55 | $this->queueLogin (); 56 | $_SESSION['__ck_username'] = $this->getUserName (); 57 | } 58 | 59 | public function getLoggedInUser () { 60 | return $_SESSION['__ck_username']; 61 | } 62 | 63 | public function createLogoutLink () { 64 | return $this->url->resetGetParams (array('__ckLogout' => true)); 65 | } 66 | 67 | public function preprocess ($app) { 68 | if ($this->url->get ('__ckLogout', false) !== false) { 69 | $this->doLogout (); 70 | } 71 | } 72 | 73 | protected $error = null; 74 | public function fail ($error) { 75 | $this->error = $error; 76 | } 77 | 78 | protected $welcomeMessage = "Please Log In"; 79 | public function setWelcomeMessage ($message) { 80 | $this->welcomeMessage = $message; 81 | } 82 | 83 | function render() 84 | { 85 | if ($this->loginQueued) { 86 | FlashBag::add ('success', "Login Success"); 87 | $this->doLogin (); 88 | return array ( 89 | 'type' => 'redirect', 90 | 'url' => $this->route->root () 91 | ); 92 | } 93 | 94 | return array ( 95 | 'type' => 'template', 96 | 'template' => 'pages/login.twig', 97 | 'data' => [ 98 | 'staticRoot' => $this->app->getStaticRoot(), 99 | 'title' => $this->app->getAppName (), 100 | 'welcomeMessage' => $this->welcomeMessage, 101 | 'endpoint' => $this->route->root (), 102 | 'valueBag' => json_encode(ValueBag::getValues()), 103 | 'bodyclass' => 'login-page', 104 | 'error' => $this->error, 105 | 'username' => $this->url->get ('username', '') 106 | ] 107 | ); 108 | } 109 | } -------------------------------------------------------------------------------- /src/CrudKit/Pages/DummyPage.php: -------------------------------------------------------------------------------- 1 | renderTemplateToString("pages/dummy.twig", array( 15 | 'name' => $this->name, 16 | 'content' => $this->content 17 | )); 18 | } 19 | 20 | /** 21 | * @return string 22 | */ 23 | public function getContent() 24 | { 25 | return $this->content; 26 | } 27 | 28 | /** 29 | * @param string $content 30 | */ 31 | public function setContent($content) 32 | { 33 | $this->content = $content; 34 | } 35 | } -------------------------------------------------------------------------------- /src/CrudKit/Pages/HtmlPage.php: -------------------------------------------------------------------------------- 1 | renderTemplateToString("pages/html.twig", array( 15 | 'page' => $this, 16 | 'name' => $this->name, 17 | 'content' => $this->content 18 | )); 19 | } 20 | 21 | public function __construct ($id) { 22 | $this->setId($id); 23 | return $this; 24 | } 25 | 26 | 27 | /** 28 | * @param string $content 29 | */ 30 | public function setInnerHtml($content) 31 | { 32 | $this->content = $content; 33 | } 34 | } -------------------------------------------------------------------------------- /src/CrudKit/Pages/MySQLTablePage.php: -------------------------------------------------------------------------------- 1 | 'string', 16 | 'set' => 'string' 17 | ]; 18 | 19 | public function __construct($id, $user, $pass, $db, $extra = []) 20 | { 21 | $params = [ 22 | 'driver' => 'pdo_mysql', 23 | 'user' => $user, 24 | 'password' => $pass, 25 | 'dbname' => $db, 26 | ]; 27 | 28 | if (isset($extra['host'])) { 29 | $params['host'] = $extra['host']; 30 | } 31 | if (isset($extra['port'])) { 32 | $params['port'] = $extra['port']; 33 | } 34 | if (isset($extra['charset'])) { 35 | $params['charset'] = $extra['charset']; 36 | } 37 | $conn = DriverManager::getConnection($params); 38 | 39 | $platform = $conn->getDatabasePlatform(); 40 | foreach($this->additionalTypeMappings as $typeName => $type) { 41 | $platform->registerDoctrineTypeMapping($typeName, $type); 42 | } 43 | 44 | $this->preInit($id, $conn); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/CrudKit/Pages/Page.php: -------------------------------------------------------------------------------- 1 | 'sqlsrv', 12 | 'user' => $user, 13 | 'password' => $pass, 14 | 'dbname' => $db 15 | ); 16 | 17 | if(isset($extra['host'])) { 18 | $params['host'] = $extra['host']; 19 | } 20 | if(isset($extra['port'])) { 21 | $params['port'] = $extra['port']; 22 | } 23 | if(isset($extra['charset'])) { 24 | $params['charset'] = $extra['charset']; 25 | } 26 | $conn = DriverManager::getConnection($params); 27 | $this->preInit($id, $conn); 28 | 29 | return $this; 30 | } 31 | } -------------------------------------------------------------------------------- /src/CrudKit/Pages/SQLiteTablePage.php: -------------------------------------------------------------------------------- 1 | 'pdo_sqlite', 12 | 'path' => $path 13 | ); 14 | $conn = DriverManager::getConnection($params); 15 | $this->preInit($id, $conn); 16 | 17 | return $this; 18 | } 19 | } -------------------------------------------------------------------------------- /src/CrudKit/Util/FlashBag.php: -------------------------------------------------------------------------------- 1 | $message, 17 | 'extra' => $extra 18 | ); 19 | } 20 | 21 | public static function getFlashes () { 22 | if(!isset($_SESSION['_ck_flash'])) { 23 | return array(); 24 | } 25 | $flashItems = $_SESSION['_ck_flash']; 26 | 27 | $_SESSION['_ck_flash'] = array(); 28 | return $flashItems; 29 | } 30 | } -------------------------------------------------------------------------------- /src/CrudKit/Util/FormHelper.php: -------------------------------------------------------------------------------- 1 | formItems []= $item; 22 | } 23 | 24 | public function setPageId ($page) { 25 | $this->params['pageId'] = $page; 26 | $this->jsParams['pageId'] = $page; 27 | } 28 | 29 | public function setItemId ($itemId) { 30 | $this->params['itemId'] = $itemId; 31 | $this->jsParams['itemId'] = $itemId; 32 | } 33 | 34 | public function setNewItem () { 35 | $this->params['newItem'] = true; 36 | $this->jsParams['newItem'] = true; 37 | } 38 | 39 | public function setDeleteUrl ($url) { 40 | $this->params['canDelete'] = true; 41 | $this->params['deleteUrl'] = $url; 42 | } 43 | 44 | public function addRelationship ($fKey, $type) { 45 | $this->jsParams['hasRelationships'] = true; 46 | if(!isset($this->jsParams['relationships'])) { 47 | $this->jsParams['relationships'] = array(); 48 | } 49 | $this->jsParams['relationships'] []= array( 50 | 'type' => $type, 51 | 'key' => $fKey 52 | ); 53 | } 54 | 55 | protected $ngModel = "formItems"; 56 | 57 | protected $relationships = array(); 58 | 59 | protected $formItems = array(); 60 | 61 | public function render ($order) { 62 | $twig = new TwigUtil(); 63 | 64 | $this->params['formItems'] = $this->formItems; 65 | $this->params['id'] = $this->id; 66 | 67 | ValueBag::set($this->id, $this->jsParams); 68 | 69 | return $twig->renderTemplateToString("util/form.twig", $this->params); 70 | } 71 | 72 | public function renderInline () { 73 | $twig = new TwigUtil(); 74 | 75 | $this->params['formItems'] = $this->formItems; 76 | // $this->params['id'] = $this->id; 77 | 78 | // ValueBag::set($this->id, $this->jsParams); 79 | 80 | return $twig->renderTemplateToString("util/form_inline.twig", $this->params); 81 | } 82 | 83 | /** 84 | * @return string 85 | */ 86 | public function getNgModel() 87 | { 88 | return $this->ngModel; 89 | } 90 | 91 | /** 92 | * @param string $ngModel 93 | */ 94 | public function setNgModel($ngModel) 95 | { 96 | $this->ngModel = $ngModel; 97 | } 98 | } -------------------------------------------------------------------------------- /src/CrudKit/Util/Installer.php: -------------------------------------------------------------------------------- 1 | isDir()) { 19 | $target = $dest . DIRECTORY_SEPARATOR . $iterator->getSubPathName(); 20 | if (!is_dir($target)) 21 | mkdir($target); 22 | } else { 23 | echo "Copying:\nSource:". $item."\nDestination:".$dest . DIRECTORY_SEPARATOR . $iterator->getSubPathName()."\n\n"; 24 | copy($item, $dest . DIRECTORY_SEPARATOR . $iterator->getSubPathName()); 25 | } 26 | } 27 | } 28 | public static function copyFiles (Event $event) { 29 | $composer = $event->getComposer (); 30 | 31 | $vendorPath = $composer->getConfig()->get('vendor-dir'); 32 | $root = dirname($vendorPath); 33 | $ds = DIRECTORY_SEPARATOR; 34 | 35 | $src = $vendorPath.$ds."skyronic".$ds."crudkit".$ds."src".$ds."static".$ds."build".$ds; 36 | $dest = $root.$ds."static".$ds."crudkit"; 37 | 38 | echo "Copying crudkit static files... \n\n"; 39 | self::recursiveCopy ($src, $dest); 40 | echo "Done!\n\n"; 41 | } 42 | } -------------------------------------------------------------------------------- /src/CrudKit/Util/LoggingHelper.php: -------------------------------------------------------------------------------- 1 | getSQL(); 22 | $params = $builder->getParameters(); 23 | 24 | self::log("Running Query: ".$sql. " with params:"); 25 | self::logObject($params); 26 | } 27 | } -------------------------------------------------------------------------------- /src/CrudKit/Util/RouteGenerator.php: -------------------------------------------------------------------------------- 1 | urlHelper = new UrlHelper(); 13 | } 14 | 15 | public function openPage($pageId) { 16 | return $this->urlHelper->resetGetParams(array('page' => $pageId, 'action' => 'view_page')); 17 | } 18 | 19 | public function pageFunc ($pageId, $func) { 20 | return $this->urlHelper->resetGetParams(array('page' => $pageId, 'action' => 'page_function', 'func' => $func)); 21 | } 22 | 23 | public function itemFunc ($pageId, $rowId, $func) { 24 | return $this->urlHelper->resetGetParams(array('page' => $pageId, 'action' => 'page_function', 'func' => $func, 'item_id' => $rowId)); 25 | } 26 | 27 | public function newItem ($pageId) { 28 | return $this->urlHelper->resetGetParams(array('page' => $pageId, 'action' => 'page_function', 'func' => "new_item")); 29 | } 30 | 31 | public function root () { 32 | return $this->urlHelper->resetGetParams(); 33 | } 34 | 35 | public function defaultRoute () { 36 | return $this->urlHelper->resetGetParams(array()); 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /src/CrudKit/Util/TwigUtil.php: -------------------------------------------------------------------------------- 1 | basePath = dirname(dirname(dirname(__FILE__)))."/templates/"; 16 | } 17 | 18 | public function renderTemplateToString ($name, $dictionary) { 19 | Twig_Autoloader::register(); 20 | $loader = new Twig_Loader_Filesystem($this->basePath); 21 | $twig = new Twig_Environment($loader, array( 22 | 'debug' => true 23 | )); 24 | $twig->addExtension(new Twig_Extension_Debug()); 25 | 26 | $data = array_merge($dictionary, array( 27 | 'url' => new UrlHelper(), 28 | 'route' => new RouteGenerator() 29 | )); 30 | return $twig->render($name, $data); 31 | } 32 | } -------------------------------------------------------------------------------- /src/CrudKit/Util/UrlHelper.php: -------------------------------------------------------------------------------- 1 | getQuery(); 23 | $currentParams->modify($params); 24 | 25 | return self::$url->setQuery($currentParams); 26 | } 27 | 28 | public function resetGetParams ($params = array()) { 29 | return self::$url->setQuery($params); 30 | } 31 | 32 | public function has ($key) { 33 | return $this->get ($key, '__ck_undefined') !== "__ck_undefined"; 34 | } 35 | 36 | public function get ($key, $default = null) { 37 | $postdata = file_get_contents("php://input", 'rb'); 38 | $json_post = array(); 39 | 40 | try { 41 | $json_post = json_decode($postdata, true); 42 | } 43 | catch (Exception $e) { 44 | // Don't do anything this is what's expected if json serialization fails 45 | } 46 | 47 | if(isset($_GET[$key])) { 48 | return $_GET[$key]; 49 | } 50 | else if(isset($_POST[$key])) { 51 | return $_POST[$key]; 52 | } 53 | else if(isset($json_post[$key])) { 54 | return $json_post[$key]; 55 | } 56 | else { 57 | return $default; 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /src/CrudKit/Util/ValueBag.php: -------------------------------------------------------------------------------- 1 | t?e.selectPage(t):i.$render()})},this.calculateTotalPages=function(){var t=this.itemsPerPage<1?1:Math.ceil(e.totalItems/this.itemsPerPage);return Math.max(t||0,1)},this.render=function(){e.page=parseInt(i.$viewValue,10)||1},e.selectPage=function(t,a){e.page!==t&&t>0&&t<=e.totalPages&&(a&&a.target&&a.target.blur(),i.$setViewValue(t),i.$render())},e.getText=function(t){return e[t+"Text"]||n.config[t+"Text"]},e.noPrevious=function(){return 1===e.page},e.noNext=function(){return e.page===e.totalPages}}]).constant("paginationConfig",{itemsPerPage:10,boundaryLinks:!1,directionLinks:!0,firstText:"First",previousText:"Previous",nextText:"Next",lastText:"Last",rotate:!0}).directive("pagination",["$parse","paginationConfig",function(e,t){return{restrict:"EA",scope:{totalItems:"=",firstText:"@",previousText:"@",nextText:"@",lastText:"@"},require:["pagination","?ngModel"],controller:"PaginationController",templateUrl:"template/pagination/pagination.html",replace:!0,link:function(a,n,i,r){function o(e,t,a){return{number:e,text:t,active:a}}function s(e,t){var a=[],n=1,i=t,r=angular.isDefined(u)&&t>u;r&&(c?(n=Math.max(e-Math.floor(u/2),1),i=n+u-1,i>t&&(i=t,n=i-u+1)):(n=(Math.ceil(e/u)-1)*u+1,i=Math.min(n+u-1,t)));for(var s=n;i>=s;s++){var l=o(s,s,s===e);a.push(l)}if(r&&!c){if(n>1){var g=o(n-1,"...",!1);a.unshift(g)}if(t>i){var p=o(i+1,"...",!1);a.push(p)}}return a}var l=r[0],g=r[1];if(g){var u=angular.isDefined(i.maxSize)?a.$parent.$eval(i.maxSize):t.maxSize,c=angular.isDefined(i.rotate)?a.$parent.$eval(i.rotate):t.rotate;a.boundaryLinks=angular.isDefined(i.boundaryLinks)?a.$parent.$eval(i.boundaryLinks):t.boundaryLinks,a.directionLinks=angular.isDefined(i.directionLinks)?a.$parent.$eval(i.directionLinks):t.directionLinks,l.init(g,t),i.maxSize&&a.$parent.$watch(e(i.maxSize),function(e){u=parseInt(e,10),l.render()});var p=l.render;l.render=function(){p(),a.page>0&&a.page<=a.totalPages&&(a.pages=s(a.page,a.totalPages))}}}}}]).constant("pagerConfig",{itemsPerPage:10,previousText:"« Previous",nextText:"Next »",align:!0}).directive("pager",["pagerConfig",function(e){return{restrict:"EA",scope:{totalItems:"=",previousText:"@",nextText:"@"},require:["pager","?ngModel"],controller:"PaginationController",templateUrl:"template/pagination/pager.html",replace:!0,link:function(t,a,n,i){var r=i[0],o=i[1];o&&(t.align=angular.isDefined(n.align)?t.$parent.$eval(n.align):e.align,r.init(o,e))}}}]); -------------------------------------------------------------------------------- /src/static/extra/ui-bootstrap-custom-tpls-0.13.0.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * angular-ui-bootstrap 3 | * http://angular-ui.github.io/bootstrap/ 4 | 5 | * Version: 0.13.0 - 2015-05-02 6 | * License: MIT 7 | */ 8 | angular.module("ui.bootstrap",["ui.bootstrap.tpls","ui.bootstrap.pagination"]),angular.module("ui.bootstrap.tpls",["template/pagination/pager.html","template/pagination/pagination.html"]),angular.module("ui.bootstrap.pagination",[]).controller("PaginationController",["$scope","$attrs","$parse",function(e,t,a){var n=this,i={$setViewValue:angular.noop},r=t.numPages?a(t.numPages).assign:angular.noop;this.init=function(o,l){i=o,this.config=l,i.$render=function(){n.render()},t.itemsPerPage?e.$parent.$watch(a(t.itemsPerPage),function(t){n.itemsPerPage=parseInt(t,10),e.totalPages=n.calculateTotalPages()}):this.itemsPerPage=l.itemsPerPage,e.$watch("totalItems",function(){e.totalPages=n.calculateTotalPages()}),e.$watch("totalPages",function(t){r(e.$parent,t),e.page>t?e.selectPage(t):i.$render()})},this.calculateTotalPages=function(){var t=this.itemsPerPage<1?1:Math.ceil(e.totalItems/this.itemsPerPage);return Math.max(t||0,1)},this.render=function(){e.page=parseInt(i.$viewValue,10)||1},e.selectPage=function(t,a){e.page!==t&&t>0&&t<=e.totalPages&&(a&&a.target&&a.target.blur(),i.$setViewValue(t),i.$render())},e.getText=function(t){return e[t+"Text"]||n.config[t+"Text"]},e.noPrevious=function(){return 1===e.page},e.noNext=function(){return e.page===e.totalPages}}]).constant("paginationConfig",{itemsPerPage:10,boundaryLinks:!1,directionLinks:!0,firstText:"First",previousText:"Previous",nextText:"Next",lastText:"Last",rotate:!0}).directive("pagination",["$parse","paginationConfig",function(e,t){return{restrict:"EA",scope:{totalItems:"=",firstText:"@",previousText:"@",nextText:"@",lastText:"@"},require:["pagination","?ngModel"],controller:"PaginationController",templateUrl:"template/pagination/pagination.html",replace:!0,link:function(a,n,i,r){function o(e,t,a){return{number:e,text:t,active:a}}function l(e,t){var a=[],n=1,i=t,r=angular.isDefined(u)&&t>u;r&&(p?(n=Math.max(e-Math.floor(u/2),1),i=n+u-1,i>t&&(i=t,n=i-u+1)):(n=(Math.ceil(e/u)-1)*u+1,i=Math.min(n+u-1,t)));for(var l=n;i>=l;l++){var s=o(l,l,l===e);a.push(s)}if(r&&!p){if(n>1){var g=o(n-1,"...",!1);a.unshift(g)}if(t>i){var c=o(i+1,"...",!1);a.push(c)}}return a}var s=r[0],g=r[1];if(g){var u=angular.isDefined(i.maxSize)?a.$parent.$eval(i.maxSize):t.maxSize,p=angular.isDefined(i.rotate)?a.$parent.$eval(i.rotate):t.rotate;a.boundaryLinks=angular.isDefined(i.boundaryLinks)?a.$parent.$eval(i.boundaryLinks):t.boundaryLinks,a.directionLinks=angular.isDefined(i.directionLinks)?a.$parent.$eval(i.directionLinks):t.directionLinks,s.init(g,t),i.maxSize&&a.$parent.$watch(e(i.maxSize),function(e){u=parseInt(e,10),s.render()});var c=s.render;s.render=function(){c(),a.page>0&&a.page<=a.totalPages&&(a.pages=l(a.page,a.totalPages))}}}}}]).constant("pagerConfig",{itemsPerPage:10,previousText:"« Previous",nextText:"Next »",align:!0}).directive("pager",["pagerConfig",function(e){return{restrict:"EA",scope:{totalItems:"=",previousText:"@",nextText:"@"},require:["pager","?ngModel"],controller:"PaginationController",templateUrl:"template/pagination/pager.html",replace:!0,link:function(t,a,n,i){var r=i[0],o=i[1];o&&(t.align=angular.isDefined(n.align)?t.$parent.$eval(n.align):e.align,r.init(o,e))}}}]),angular.module("template/pagination/pager.html",[]).run(["$templateCache",function(e){e.put("template/pagination/pager.html",'')}]),angular.module("template/pagination/pagination.html",[]).run(["$templateCache",function(e){e.put("template/pagination/pagination.html",'')}]); -------------------------------------------------------------------------------- /src/static/js/app-react.js: -------------------------------------------------------------------------------- 1 | var ckUrl = { 2 | resetGetParams: function (params) { 3 | var url = new Url(); 4 | url.query.clear(); 5 | 6 | for (var key in params) { 7 | if (params.hasOwnProperty(key)) { 8 | url.query[key] = params[key]; 9 | } 10 | } 11 | 12 | return url.toString(); 13 | }, 14 | addGetParams: function (params) { 15 | var url = new Url(); 16 | for (var key in params) { 17 | if (params.hasOwnProperty(key)) { 18 | url.query[key] = params[key]; 19 | } 20 | } 21 | return url.toString(); 22 | } 23 | }; 24 | 25 | window.ckUrl = ckUrl; 26 | var ckAjax = { 27 | makeRequest: function (url, params, done) { 28 | $.ajax(url, { 29 | method: "POST", 30 | data: params, 31 | success: function (data) { 32 | done(data); 33 | }, 34 | error: function () { 35 | 36 | } 37 | }) 38 | }, 39 | page: { 40 | func: function (page_id, func_name, params, done) { 41 | ckAjax.makeRequest(ckUrl.resetGetParams({ 42 | page: page_id, 43 | func: func_name, 44 | action: "page_function" 45 | }), params, done); 46 | } 47 | } 48 | }; 49 | 50 | var TableHead = React.createClass({ 51 | getInitialProps: { 52 | schema: [] 53 | }, 54 | render: function () { 55 | var tableHeadRow = function (item) { 56 | return {item.name} 57 | }; 58 | return ( 59 | 60 | 61 | {this.props.schema.map(tableHeadRow)} 62 | 63 | 64 | ) 65 | } 66 | }); 67 | 68 | var TableBody = React.createClass({ 69 | getInitialProps: { 70 | schema: [], 71 | rows: [] 72 | }, 73 | render: function () { 74 | var makeCell = function (item) { 75 | return ( 76 | 77 | {item} 78 | 79 | ) 80 | }; 81 | var makeRow = function (row) { 82 | return ( 83 | 84 | {row.map(makeCell)} 85 | 86 | ) 87 | }; 88 | return ( 89 | 90 | {this.props.rows.map(makeRow)} 91 | 92 | ) 93 | } 94 | }); 95 | 96 | var SummaryTable = React.createClass({ 97 | getInitialProps: function () { 98 | return { 99 | pageId: "" 100 | } 101 | }, 102 | getInitialState: function () { 103 | return { 104 | schema: [], 105 | rows: [], 106 | currentPage: 0, 107 | loading: false, 108 | pageCount: 5, 109 | rowsPerPage: 10 110 | } 111 | }, 112 | componentWillMount: function () { 113 | var self = this; 114 | 115 | ckAjax.page.func(this.props.pageId, "get_summary_data", {}, function (data) { 116 | self.setState({ 117 | schema: data.schema, 118 | rows: data.data 119 | }); 120 | }) 121 | }, 122 | changePage: function (page) { 123 | 124 | }, 125 | render: function () { 126 | return ( 127 |
128 | 129 | 130 | 131 |
132 | 133 |
134 | ) 135 | } 136 | }); 137 | 138 | var TablePagination = React.createClass ({ 139 | getInitialProps: function () { 140 | return { 141 | loadingFlag: false, 142 | pageCount: 1, 143 | currentPage: 0 144 | } 145 | }, 146 | changePage: function (i, e) { 147 | console.log("Page is now ", i) 148 | 149 | }, 150 | render: function () { 151 | var loadingClass = this.props.loadingFlag ? 'hidden' : ''; 152 | var self = this; 153 | 154 | var generatePageNodes = function () { 155 | var items = []; 156 | for(var i = 1; i <= self.props.pageCount; i++) { 157 | var activeClass = 158 | items.push(
  • {i}
  • ); 159 | } 160 | 161 | return items; 162 | }; 163 | 164 | return ( 165 |
    166 |
    167 |
    168 | Loading... 169 |
    170 |
    171 |
    172 | 187 |
    188 |
    189 | ) 190 | } 191 | }); 192 | 193 | React.render(, document.getElementById ("dataTable")); 194 | 195 | -------------------------------------------------------------------------------- /src/static/js/app.js: -------------------------------------------------------------------------------- 1 | var ck = window.ck = { 2 | 3 | }; 4 | 5 | 6 | var ckUrl = { 7 | resetGetParams: function (params) { 8 | var url = new Url(); 9 | url.clearQuery(); 10 | 11 | for (var key in params) { 12 | if (params.hasOwnProperty(key)) { 13 | url.query[key] = params[key]; 14 | } 15 | } 16 | 17 | return url.toString(); 18 | }, 19 | addGetParams: function (params) { 20 | var url = new Url(); 21 | for (var key in params) { 22 | if (params.hasOwnProperty(key)) { 23 | url.query[key] = params[key]; 24 | } 25 | } 26 | return url.toString(); 27 | } 28 | }; 29 | 30 | var app = angular.module("ckApp", [ 31 | 'cgBusy', 32 | 'angular.filter', 33 | 'ui.bootstrap' 34 | ]); 35 | 36 | ck.flashbag = { 37 | subscribers: {}, 38 | add: function (items) { 39 | for(var category in items) { 40 | if(!items.hasOwnProperty(category)) 41 | continue; 42 | var catItems = items[category]; 43 | if(ck.flashbag.subscribers.hasOwnProperty(category)) { 44 | var subs = ck.flashbag.subscribers[category]; 45 | for(var i = 0; i < subs.length; i++) { 46 | subs[i] (catItems); 47 | } 48 | } 49 | } 50 | }, 51 | subscribe: function (category, callback) { 52 | if(!ck.flashbag.subscribers.hasOwnProperty(category)) { 53 | ck.flashbag.subscribers[category] = []; 54 | } 55 | 56 | ck.flashbag.subscribers[category].push (callback); 57 | } 58 | } 59 | 60 | var GenerateAPIFactory = function (make_call_real) { 61 | var make_call = function (url, params, urlOnly) { 62 | url += "&ajax=true"; 63 | if(urlOnly) { 64 | return url; 65 | } 66 | return make_call_real(url, params); 67 | }; 68 | 69 | var apis = { 70 | page: { 71 | func: function (page_id, func_name, params) { 72 | return make_call(ckUrl.resetGetParams({ 73 | page: page_id, 74 | func: func_name, 75 | action: "page_function" 76 | }), params); 77 | }, 78 | get_colSpec: function (page_id, params) { 79 | params = params ? params : {}; 80 | return apis.page.func(page_id, "get_colSpec", params); 81 | }, 82 | get_data: function (page_id, params) { 83 | return apis.page.func(page_id, "get_data", params); 84 | }, 85 | get_foreign: function (page_id, item_id, key, params) { 86 | params = params ? params : {}; 87 | params.foreign_key = key; 88 | params['item_id'] = item_id; 89 | return apis.page.func(page_id, "get_foreign", params); 90 | }, 91 | get_form_values: function (page_id, item_id, params) { 92 | params = params ? params : {}; 93 | params['item_id'] = item_id; 94 | 95 | return apis.page.func(page_id, "get_form_values", params); 96 | }, 97 | set_form_values: function (page_id, item_id, values, params) { 98 | params = params ? params : {}; 99 | params['item_id'] = item_id; 100 | params['values_json'] = JSON.stringify(values); 101 | 102 | return apis.page.func(page_id, "set_form_values", params); 103 | }, 104 | create_item: function (page_id, values, params) { 105 | params = params ? params : {}; 106 | params['values_json'] = JSON.stringify(values); 107 | 108 | return apis.page.func(page_id, "create_item", params); 109 | }, 110 | delete_items: function (page_id, ids, params) { 111 | params = params ? params : {}; 112 | params['delete_ids'] = JSON.stringify(ids); 113 | 114 | return apis.page.func(page_id, "delete_items", params); 115 | } 116 | } 117 | }; 118 | 119 | return apis; 120 | }; 121 | 122 | ck.fatalError = function (title, message) { 123 | BootstrapDialog.show ({ 124 | title: title, 125 | message: message, 126 | type: "type-danger", 127 | buttons: [{ 128 | icon: "fa fa-reload", 129 | label: "Reload Page", 130 | action: function () { 131 | window.location.reload() 132 | } 133 | }], 134 | closable: false 135 | }) 136 | }; 137 | 138 | ck.converters = { 139 | standard_to_js: function (schema, object) { 140 | var resultObject = {}; 141 | for(var key in schema) { 142 | if(!object.hasOwnProperty(key)) { 143 | continue; 144 | } 145 | var type = schema[key].type; 146 | var input = object[key]; 147 | var result = null; 148 | 149 | switch(type) { 150 | case "string": 151 | result = input; 152 | break; 153 | case "number": 154 | result = parseFloat(input); 155 | break; 156 | case "datetime": 157 | // Server sends a timestamp in UTC. We use that in UTC and convert to JS date 158 | result = moment.unix(parseInt(input)).tz("UTC").toDate(); 159 | break; 160 | } 161 | 162 | resultObject[key] = result; 163 | } 164 | 165 | return resultObject; 166 | }, 167 | js_to_standard: function (schema, object) { 168 | var resultObject = {}; 169 | for(var key in schema) { 170 | if(!object.hasOwnProperty(key)) { 171 | continue; 172 | } 173 | var type = schema[key].type; 174 | var input = object[key]; 175 | var result = null; 176 | 177 | switch(type) { 178 | case "string": 179 | result = input.toString(); 180 | break; 181 | case "number": 182 | result = input.toString(); 183 | break; 184 | case "datetime": 185 | result = moment(input).tz("UTC").unix(); 186 | break; 187 | } 188 | resultObject[key] = result; 189 | } 190 | 191 | return resultObject; 192 | }, 193 | standard_to_js_table: function (schema, data) { 194 | var len = data.length; 195 | var processed = []; 196 | for(var i = 0; i < len; i ++) { 197 | processed.push(ck.converters.standard_to_js(schema, data[i])); 198 | } 199 | return processed; 200 | } 201 | } 202 | 203 | app.factory ("ckAPI", function ($http, $q) { 204 | var make_call_real = function (url, params) { 205 | var deferred = $q.defer(); 206 | 207 | $http.post(url, params).error(function (data) { 208 | console.error("XHR Failed!!", data); 209 | deferred.reject($q.reject(data)); 210 | if(data.error) { 211 | ck.fatalError("Error", "

    There was an error in the server.

    " + data.error.type + "

    " + data.error.message + "

    "); 212 | } 213 | else { 214 | ck.fatalError("Error", "There was an unknown error in the server"); 215 | } 216 | }).success(function (data) { 217 | if(data.flashbag) { 218 | ck.flashbag.add(data.flashbag); 219 | } 220 | if(data.success === false) { 221 | deferred.reject("Unknown error. Conflicting success codes"); 222 | return; 223 | } 224 | deferred.resolve(data); 225 | }); 226 | return deferred.promise; 227 | }; 228 | 229 | return GenerateAPIFactory(make_call_real); 230 | }); 231 | 232 | app.controller("SummaryTableController", function ($scope, ckAPI, $q, $timeout) { 233 | var forceDataRefresh = false; // Flag to indicate whether we want to refresh the whole table data(advance filter dependency) 234 | 235 | $scope.pageId = window.pageId; 236 | $scope.perPage = window.ckValues.rowsPerPage; 237 | $scope.currentPage = 1; 238 | $scope.pageCount = 1; 239 | $scope.advancedSearchHidden = true; 240 | $scope.searchTerm = ""; 241 | $scope.advFilterItems = []; 242 | $scope.schema = []; 243 | $scope.advFilterOptions = []; 244 | $scope.allSelectedFlag = false; 245 | $scope.primaryCol = ''; 246 | $scope.selectedCount = 0; 247 | $scope.rows = []; 248 | 249 | var update_data = function (params) { 250 | params = params ? params : {}; 251 | params['pageNumber'] = $scope.currentPage; 252 | params['perPage'] = $scope.perPage; 253 | params['filters_json'] = JSON.stringify(getFilters()); 254 | $scope.loadingPromise = ckAPI.page.get_data($scope.pageId, params).then(function (data) { 255 | $scope.rows = data.rows; 256 | $scope.forceDeselect(); 257 | }); 258 | }; 259 | 260 | $scope.startAdvancedSearch = function() { 261 | if($scope.advancedSearchHidden) { 262 | $scope.addConditionButtonClicked (); 263 | $scope.advancedSearchHidden = false; 264 | } 265 | }; 266 | 267 | // First load 268 | $scope.loadingPromise = ckAPI.page.get_colSpec($scope.pageId).then(function (colSpec) { 269 | $scope.rowCount = colSpec.count; 270 | $scope.pageCount = colSpec/$scope.perPage; 271 | 272 | $scope.columns = _.map(colSpec.columns, function (val) { 273 | return _.extend(val, colSpec.schema[val.key]); 274 | }); 275 | 276 | $scope.primaryCol = _.find (colSpec.schema, 'primaryFlag', true).key; 277 | 278 | $scope.schema = colSpec.schema; 279 | $scope.advFilterOptions = _.map(colSpec.schema, function (val, key) { 280 | return _.extend(val, {id: key}); 281 | }); 282 | 283 | update_data (); 284 | }); 285 | 286 | $scope.deleteRows = function () { 287 | var selected_ids = _.chain($scope.rows).filter ('selectedFlag', true).pluck ($scope.primaryCol).map(function (val) { 288 | return parseInt(val); 289 | }).value (); 290 | return ckAPI.page.delete_items($scope.pageId, selected_ids).then (function () { 291 | update_data (); 292 | return $q.when (true); 293 | }) 294 | }; 295 | 296 | $scope.updateSelectedCount = function () { 297 | $scope.selectedCount = _.filter ($scope.rows, 'selectedFlag', true).length; 298 | }; 299 | 300 | $scope.forceDeselect = function () { 301 | $scope.allSelectedFlag = false; 302 | // Use selectAll to manually copy the allSelectedFlag 303 | $scope.selectAll (); 304 | }; 305 | 306 | $scope.selectAll = function () { 307 | _.each($scope.rows, function (val) { 308 | val.selectedFlag = $scope.allSelectedFlag; 309 | }); 310 | $scope.updateSelectedCount(); 311 | }; 312 | 313 | $scope.isWritable = function () { 314 | return !!window.ckValues.writable; 315 | } 316 | 317 | 318 | 319 | 320 | 321 | $scope.filterColumnSelected = function (item) { 322 | var id = item.id; 323 | var type = $scope.schema[id].type; 324 | 325 | var availableCmp = []; 326 | var defCmpType; // default comparison type 327 | 328 | if(type === "string") { 329 | availableCmp = [ 330 | {id:"eq", label:"Equals", inputType: "string"}, 331 | {id:"sw", label:"Starts With", inputType: "string"}, 332 | {id:"ew", label:"Ends With", inputType: "string"}, 333 | {id:"contains", label:"Contains", inputType: "string"} 334 | ]; 335 | defCmpType = availableCmp[0]; 336 | } 337 | else if(type === "number") { 338 | availableCmp = [ 339 | {id:"eq", label:"Equals", inputType: "number"}, 340 | {id:"gt", label:">", inputType: "number"}, 341 | {id:"gte", label:">=",inputType: "number"}, 342 | {id:"lt", label:"<", inputType: "number"}, 343 | {id:"lte", label:"<=", inputType: "number"} 344 | ]; 345 | defCmpType = availableCmp[0]; 346 | } 347 | 348 | item.availableCmp = availableCmp; 349 | item.cmp = defCmpType; 350 | }; 351 | 352 | var getFilters = function () { 353 | var filters = []; 354 | 355 | if($scope.searchTerm !== "") { 356 | filters.push({ 357 | id: "_ck_all_summary", 358 | type: "like", 359 | value: $scope.searchTerm 360 | }) 361 | } 362 | for(var i = 0; i < $scope.advFilterItems.length; i ++) { 363 | var filterItem = $scope.advFilterItems[i]; 364 | filters.push({ 365 | id: filterItem.id, 366 | type: typeof filterItem.cmp === 'undefined' ? 'null' : filterItem.cmp.id, 367 | value: filterItem.value 368 | }) 369 | } 370 | 371 | return filters; 372 | }; 373 | 374 | // Empty the advance filter items array 375 | var clearAdvFilterItems = function() { 376 | $scope.advFilterItems = []; 377 | }; 378 | 379 | // Initial state for the advace filter 380 | var initAdvFilterState = function() { 381 | $scope.advFilterItems.push ({ 382 | id: 'null', 383 | type: 'null', 384 | value: '' 385 | }); 386 | }; 387 | 388 | // Clear the search term input 389 | var clearSearchTerm = function() { 390 | $scope.searchTerm = ""; 391 | }; 392 | 393 | // Check if entire table refres is needed. 394 | var needsDataRefresh = function() { 395 | // Returns true if: 396 | // `advFilterItems` is the default input box and if the comparator param is `null` 397 | // OR `searchTerm` is non-empty 398 | return !($scope.advFilterItems.length === 1 && $scope.advFilterItems[0]['id'] === 'null') || $scope.searchTerm !== ''; 399 | } 400 | 401 | $scope.addConditionButtonClicked = function () { 402 | initAdvFilterState(); 403 | }; 404 | 405 | $scope.filterColumns = function () { 406 | var params = { 407 | filters_json: JSON.stringify(getFilters()) 408 | }; 409 | $scope.loadingPromise = ckAPI.page.get_colSpec($scope.pageId, params).then(function (colSpec) { 410 | $scope.rowCount = colSpec.count; 411 | $scope.pageCount = colSpec/$scope.perPage; 412 | update_data (); 413 | 414 | forceDataRefresh = needsDataRefresh(); 415 | }); 416 | }; 417 | 418 | // Disable the advanced search button 419 | // if either the filter key / condition is not selected 420 | // the value param here is not considered 421 | $scope.isAdvancedSearchBtnDisabled = function() { 422 | var filters = getFilters(); 423 | 424 | var needsToBeDisabled = function(filter) { 425 | return ( filter.id === 'null' || filter.type === 'null' ); 426 | }; 427 | 428 | for(var i=0, length=filters.length; i", {'class':"alert alert-dismissable alert-" + item.extra}); 561 | $alert.append($("", 12 | controller: function ($scope, $timeout) { 13 | $scope.busy = false; 14 | $scope.buttonClicked = function () { 15 | $scope.busy = true; 16 | $scope.originalLabel = $scope.label; 17 | $scope.label = "Working ..."; 18 | if ($scope.activate) { 19 | $scope.activate ().then (function () { 20 | $scope.busy = false; 21 | $scope.label = $scope.originalLabel; 22 | }); 23 | } 24 | } 25 | } 26 | } 27 | }); -------------------------------------------------------------------------------- /src/static/less/crudkit.less: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | 5 | } 6 | 7 | body, h1, h2, h3, h4, h5, h6, p, td, th { 8 | font-family: "Lato", 'Helvetica Neue', Arial, Helvetica, sans-serif !important; 9 | } 10 | 11 | .form-group.has-change label { 12 | color: #3c8dbc; 13 | } 14 | .form-group.has-change.form-control { 15 | border-color: #3c8dbc !important; 16 | box-shadow: none; 17 | } 18 | 19 | a[ng-click]{ 20 | cursor: pointer; 21 | } 22 | 23 | .table-checkbox { 24 | margin-left: 5px !important; 25 | margin-right: 5px !important; 26 | } -------------------------------------------------------------------------------- /src/templates/core.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ pageTitle }} | {{ title }} 5 | {% if dev %} 6 | {% include "includes/requires.twig" %} 7 | {% else %} 8 | {% include "includes/requires_prod.twig" %} 9 | {% endif %} 10 | 11 | 12 | {% block core_body %}{% endblock %} 13 | 14 | -------------------------------------------------------------------------------- /src/templates/includes/requires.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/templates/includes/requires_prod.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/templates/layout.twig: -------------------------------------------------------------------------------- 1 | {% extends "core.twig" %} 2 | {% block core_body %} 3 |
    4 | 5 |
    6 | 7 | 8 | 19 |
    20 | 34 | 35 |
    36 | {% block mainContent %}{% endblock %} 37 |
    38 |
    39 | 42 | Powered by CrudKit. 43 |
    44 |
    45 | {% endblock %} -------------------------------------------------------------------------------- /src/templates/main_page.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.twig" %} 2 | {% block mainContent %} 3 | {{ page_content|raw }} 4 | 7 | {% endblock %} -------------------------------------------------------------------------------- /src/templates/pages/base.twig: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    5 | {% block pageName %}{% endblock %} 6 |

    7 |
    8 |
    9 |
    10 | {% block actionButtons %}{% endblock %} 11 |
    12 |
    13 |
    14 |
    15 |
    16 |
    17 |
    18 |
    19 |
    20 |
    21 |
    22 |
    23 |
    24 | {% block pageContent %}{% endblock %} 25 |
    26 |
    27 |
    28 |
    29 |
    -------------------------------------------------------------------------------- /src/templates/pages/basicdata.twig: -------------------------------------------------------------------------------- 1 | {% extends "pages/base.twig" %} 2 | 3 | {% block pageName %} 4 | {{ page.getName ()}} 5 | {% endblock %} 6 | 7 | {% block actionButtons %} 8 | {% if writable %} 9 | Add New 10 | {% endif %} 11 | {% endblock %} 12 | 13 | {% block pageContent %} 14 | {% verbatim %} 15 |
    16 |
    17 |
    18 |
    19 | 20 | 21 | 22 | 23 |
    24 |
    25 |

    Advanced Search (reset)

    26 |
    27 |
    28 | 29 |
    30 |
    31 | 32 |
    33 |
    34 |
    35 |
    36 | 37 |
    38 |
    39 | 40 |
    41 |
    42 |
    43 |
    44 |
    45 | Add Condition 46 | or 47 | Start Search 48 |
    49 |
    50 |
    51 |
    52 |
    53 |

    Advanced Search

    54 |
    55 |
    56 |
    57 |
    58 |
    59 | With {{ selectedCount }} Rows: 60 |
    61 |
    62 |
    63 | 64 | 65 | 66 | 69 | 72 | 73 | 74 | 75 | 76 | 79 | 95 | 96 | 97 |
    67 | 68 | 70 | {{ col.name }} 71 |
    77 | 78 | 80 |
    81 |
    82 | {{ row[col.key] }} 83 |
    84 |
    85 | {{ row[col.key] }} 86 |
    87 |
    88 | {{ row[col.key] }} 89 |
    90 |
    91 | {{ row[col.key] }} 92 |
    93 |
    94 |
    98 | 99 | 100 | {% endverbatim %} 101 |
    102 |
    103 | 106 | {% endblock %} 107 | -------------------------------------------------------------------------------- /src/templates/pages/basicdata/edit_item.twig: -------------------------------------------------------------------------------- 1 | {% extends "pages/base.twig" %} 2 | 3 | {% block pageName %} 4 | Edit Item 5 | {% endblock %} 6 | 7 | 8 | {% block actionButtons %} 9 | {% if canDelete %} 10 | Delete 11 | {% endif %} 12 | {% endblock %} 13 | 14 | 15 | {% block pageContent %} 16 | {{ editForm | raw }} 17 | 21 | {% endblock %} -------------------------------------------------------------------------------- /src/templates/pages/basicdata/view_item.twig: -------------------------------------------------------------------------------- 1 | {% extends "pages/base.twig" %} 2 | 3 | {% block pageName %} 4 | {{ rowName }} 5 | {% endblock %} 6 | 7 | 8 | {% block actionButtons %} 9 | {% if writable %} 10 | Edit 11 | Delete 12 | {% endif %} 13 | {% endblock %} 14 | 15 | 16 | {% block pageContent %} 17 |
    18 |
    19 | {% for key, col in schema %} 20 |
    {{ col['label'] }}
    21 | {% if col['type'] == 'datetime' %} 22 |
    {{ row[key]|date('m/d/Y', "UTC")}}
    23 | {% else %} 24 |
    {{ row[key] }}
    25 | {% endif %} 26 | {% endfor %} 27 |
    28 |
    29 | 33 | {% endblock %} -------------------------------------------------------------------------------- /src/templates/pages/dummy.twig: -------------------------------------------------------------------------------- 1 | {% extends "pages/base.twig" %} 2 | 3 | {% block pageContent %} 4 |

    Page: {{ name }}

    5 | {% endblock %} -------------------------------------------------------------------------------- /src/templates/pages/html.twig: -------------------------------------------------------------------------------- 1 | {% extends "pages/base.twig" %} 2 | {% block pageName %} 3 | {{ name }} 4 | {% endblock %} 5 | 6 | {% block pageContent %} 7 | {{ content | raw }} 8 | {% endblock %} -------------------------------------------------------------------------------- /src/templates/pages/login.twig: -------------------------------------------------------------------------------- 1 | {% extends "core.twig" %} 2 | 3 | {% block core_body %} 4 | 31 | 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /src/templates/util/form.twig: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | {% for item in formItems %} 4 | {{ item.render()|raw }} 5 | {% endfor %} 6 | Save 7 | 8 |
    9 |
    10 |
    11 | -------------------------------------------------------------------------------- /src/templates/util/form_inline.twig: -------------------------------------------------------------------------------- 1 |
    2 | {% for item in formItems %} 3 | {{ item.renderInline ()|raw }} 4 | {% endfor %} 5 |
    6 | -------------------------------------------------------------------------------- /tests/ArrayDataProviderFactory.php: -------------------------------------------------------------------------------- 1 | [ 11 | 'FirstName' => [ 12 | 'label' => 'First Name', 13 | 'options' => ['required' => true] 14 | ], 15 | 'LastName' => [ 16 | 'label' => 'Last Name', 17 | 'options' => ['required' => true] 18 | ], 19 | 'City' => [ 20 | 'label' => 'City', 21 | 'options' => [] 22 | ], 23 | 'Email' => [ 24 | 'label' => 'Email', 25 | 'options' => ['required' => true] 26 | ], 27 | ], 28 | 'summary_cols' => [ 29 | 'FirstName', 30 | 'LastName' 31 | ] 32 | ]; 33 | 34 | 35 | public static function arrayDataProvider(array $schema, array $summaryCols, array $data = []) 36 | { 37 | return new ArrayDataProvider($schema, $summaryCols, $data); 38 | } 39 | 40 | public static function defaultArrayDataProviderWithEmailValidator() 41 | { 42 | $schema = static::$defaultArrayProvider['schema']; 43 | $schema['Email']['options']['validator'] = 'validate_email'; 44 | $data = static::generateDataForArrayProvider(0); 45 | return static::arrayDataProvider($schema, static::$defaultArrayProvider['summary_cols'], $data); 46 | } 47 | 48 | public static function defaultArrayDataProvider($numDataRows = 10) 49 | { 50 | $data = static::generateDataForArrayProvider($numDataRows); 51 | return static::arrayDataProvider( 52 | static::$defaultArrayProvider['schema'], 53 | static::$defaultArrayProvider['summary_cols'], 54 | $data 55 | ); 56 | } 57 | 58 | public static function generateDataForArrayProvider($numDataRows) 59 | { 60 | $data = []; 61 | foreach( range(0, ((int) $numDataRows - 1) ) as $rowNumber) { 62 | $data[] = [ 63 | 'FirstName' => 'First Name #' . $rowNumber, 64 | 'LastName' => 'Last Name #' . $rowNumber, 65 | 'City' => 'City #' . $rowNumber, 66 | 'Email' => 'email' . str_pad($rowNumber, 3, STR_PAD_LEFT) . '@example.com' 67 | ]; 68 | } 69 | return $data; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/SqlDataProviderFactory.php: -------------------------------------------------------------------------------- 1 | 'pdo_sqlite', 16 | 'path' => '/fixtures/chinook.sqlite', 17 | ]; 18 | 19 | /** 20 | * @var \Doctrine\DBAL\Connection 21 | */ 22 | private static $defaultConnection; 23 | 24 | /** 25 | * A default 26 | * 27 | * @var array 28 | */ 29 | private static $defaultSqlProvider = [ 30 | 'table' => 'Customer', 31 | 'primary_col' => 'CustomerId', 32 | 'columns' => [ 33 | 'FirstName' => [ 34 | 'label' => 'First Name', 35 | 'options' => ['required' => true] 36 | ], 37 | 'LastName' => [ 38 | 'label' => 'Last Name', 39 | 'options' => ['required' => true] 40 | ], 41 | 'City' => [ 42 | 'label' => 'City', 43 | 'options' => [] 44 | ], 45 | 'Email' => [ 46 | 'label' => 'Email', 47 | 'options' => ['required' => true] 48 | ], 49 | ], 50 | 'summary_cols' => [ 51 | 'FirstName', 52 | 'LastName', 53 | ], 54 | ]; 55 | 56 | 57 | /** 58 | * @param array $params 59 | * @return \Doctrine\DBAL\Connection 60 | * @throws \Doctrine\DBAL\DBALException 61 | */ 62 | public static function connection(array $params = []) 63 | { 64 | if(!empty($params)) { 65 | return DriverManager::getConnection($params); 66 | } 67 | 68 | $params = static::$defaultConnectionParams; 69 | $params['path'] = __DIR__ . $params['path']; 70 | if(is_null(static::$defaultConnection)) { 71 | static::$defaultConnection = DriverManager::getConnection($params); 72 | } 73 | return static::$defaultConnection; 74 | } 75 | 76 | public static function sqlDataProvider($table, $primaryCol, array $cols, array $summaryCols) 77 | { 78 | 79 | $connection = static::connection(); 80 | $provider = new SQLDataProvider($connection, $table, $primaryCol, $summaryCols); 81 | foreach($cols as $id => $column) { 82 | if(is_string($column)) { 83 | $label = $column; 84 | $options = []; 85 | } else { 86 | $label = $column['label']; 87 | $options = $column['options']; 88 | } 89 | $provider->addColumn($id, $id, $label, $options); 90 | } 91 | $provider->init(); 92 | return $provider; 93 | } 94 | 95 | public static function defaultSqlDataProvider() 96 | { 97 | $table = static::$defaultSqlProvider['table']; 98 | $primary = static::$defaultSqlProvider['primary_col']; 99 | $columns = static::$defaultSqlProvider['columns']; 100 | $summaryCols = static::$defaultSqlProvider['summary_cols']; 101 | return static::sqlDataProvider($table, $primary, $columns, $summaryCols); 102 | } 103 | 104 | public static function defaultSqlDataProviderWithEmailValidator() 105 | { 106 | $table = static::$defaultSqlProvider['table']; 107 | $primary = static::$defaultSqlProvider['primary_col']; 108 | $columns = static::$defaultSqlProvider['columns']; 109 | $summaryCols = static::$defaultSqlProvider['summary_cols']; 110 | 111 | $columns['Email']['options']['validator'] = 'validate_email'; 112 | return static::sqlDataProvider($table, $primary, $columns, $summaryCols); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tests/Unit/CrudKitTest.php: -------------------------------------------------------------------------------- 1 | getSchema(); 14 | 15 | $this->assertArrayHasKey('FirstName', $schema); 16 | $this->assertArrayHasKey('LastName', $schema); 17 | $this->assertArrayHasKey('City', $schema); 18 | $this->assertArrayHasKey('Email', $schema); 19 | } 20 | 21 | /** @test */ 22 | public function it_retrieves_its_summary_columns() 23 | { 24 | $summaryCols = ArrayDataProviderFactory::defaultArrayDataProvider()->getSummaryColumns(); 25 | 26 | $this->assertContains('FirstName', $summaryCols[0]); 27 | $this->assertContains('First Name', $summaryCols[0]); 28 | $this->assertContains('LastName', $summaryCols[1]); 29 | $this->assertContains('Last Name', $summaryCols[1]); 30 | } 31 | 32 | /** @test */ 33 | public function it_retrieves_a_row_count() 34 | { 35 | $provider = ArrayDataProviderFactory::defaultArrayDataProvider(7); 36 | $countWithoutConditions = $provider->getRowCount(); 37 | 38 | $this->assertEquals(7, $countWithoutConditions); 39 | } 40 | 41 | /** @test */ 42 | public function it_retrieves_multiple_rows_with_skip_and_take() 43 | { 44 | $rows = ArrayDataProviderFactory::defaultArrayDataProvider()->getData([ 45 | 'skip' => 3, 46 | 'take' => 5 47 | ]); 48 | 49 | $this->assertCount(5, $rows); 50 | $this->assertEquals('First Name #3', $rows[0]['FirstName']); 51 | $this->assertEquals('First Name #7', $rows[4]['FirstName']); 52 | } 53 | 54 | /** @test */ 55 | public function it_retrieves_the_order_of_columns_for_its_edit_form() 56 | { 57 | $expectedOrder = [ 58 | 'FirstName', 59 | 'LastName', 60 | 'City', 61 | 'Email', 62 | ]; 63 | $actualOrder = ArrayDataProviderFactory::defaultArrayDataProvider()->getEditFormOrder(); 64 | 65 | $this->assertEquals($expectedOrder, $actualOrder); 66 | } 67 | 68 | /** @test */ 69 | public function it_creates_an_edit_form_object() 70 | { 71 | $form = ArrayDataProviderFactory::defaultArrayDataProvider()->getEditForm(); 72 | $this->assertInstanceOf('CrudKit\Util\FormHelper', $form); 73 | } 74 | 75 | /** @test */ 76 | public function it_adds_a_row_by_id() 77 | { 78 | $expected = [ 79 | 'FirstName' => 'Foo', 80 | 'LastName' => 'Bar', 81 | 'City' => 'Baz', 82 | 'Email' => 'foo@bar.baz' 83 | ]; 84 | 85 | $provider = ArrayDataProviderFactory::defaultArrayDataProvider(); 86 | $insertedId = $provider->createItem($expected); 87 | 88 | $inserted = $provider->getRow($insertedId); 89 | 90 | $this->assertEquals($expected, $inserted); 91 | } 92 | 93 | /** 94 | * @test 95 | * @expectedException \InvalidArgumentException 96 | */ 97 | public function it_fails_when_creating_a_new_row_with_an_invalid_column() 98 | { 99 | $values = [ 100 | 'Foo' => 'Bar', 101 | 'FirstName' => 'Foo', 102 | 'LastName' => 'Bar', 103 | 'City' => 'Baz', 104 | 'Email' => 'foo@bar.baz' 105 | ]; 106 | 107 | ArrayDataProviderFactory::defaultArrayDataProvider()->createItem($values); 108 | } 109 | 110 | /** @test */ 111 | public function it_retrieves_a_row_by_index() 112 | { 113 | $actual = ArrayDataProviderFactory::defaultArrayDataProvider()->getRow(1); 114 | 115 | $this->assertArrayHasKey('FirstName', $actual); 116 | $this->assertEquals('First Name #1', $actual['FirstName']); 117 | } 118 | 119 | /** @test */ 120 | public function it_sets_a_row_by_index() 121 | { 122 | $expectedFirstName = 'Jeb ' . time(); 123 | $provider = ArrayDataProviderFactory::defaultArrayDataProvider(); 124 | $provider->setRow(1, ['FirstName' => $expectedFirstName]); 125 | $updatedRow = $provider->getRow(1); 126 | 127 | $this->assertEquals($expectedFirstName, $updatedRow['FirstName']); 128 | } 129 | 130 | /** @test */ 131 | public function it_returns_false_when_setting_an_invalid_row_by_index() 132 | { 133 | $wasSet = ArrayDataProviderFactory::defaultArrayDataProviderWithEmailValidator() 134 | ->setRow(1, ['Email' => 'not a valid email address']); 135 | 136 | $this->assertFalse($wasSet); 137 | } 138 | 139 | /** 140 | * @test 141 | * @expectedException \Exception 142 | */ 143 | public function it_fails_when_setting_a_row_with_an_invalid_column() 144 | { 145 | $provider = ArrayDataProviderFactory::defaultArrayDataProvider(); 146 | 147 | $row = $provider->getRow(1); 148 | $row['Foo'] = 'Bar'; 149 | 150 | $provider->setRow(1, $row); 151 | } 152 | 153 | /** @test */ 154 | public function it_deletes_a_row_by_index() 155 | { 156 | $provider = ArrayDataProviderFactory::defaultArrayDataProvider(); 157 | $toBeDeleted = $provider->getData(['take' => 1]); 158 | 159 | $rowsAffected = $provider->deleteItem(0); 160 | $nextItems = $provider->getData(['take' => 1]); 161 | 162 | $this->assertEquals(1, $rowsAffected); 163 | $this->assertNotEquals($toBeDeleted[0], $nextItems[0]); 164 | } 165 | 166 | /** @test */ 167 | public function it_returns_false_when_deleting_a_non_existent_index() 168 | { 169 | $wasDeleted = ArrayDataProviderFactory::defaultArrayDataProvider()->deleteItem(9999); 170 | $this->assertFalse($wasDeleted); 171 | } 172 | 173 | /** @test */ 174 | public function it_deletes_multiple_rows_by_index() 175 | { 176 | $provider = ArrayDataProviderFactory::defaultArrayDataProvider(); 177 | $idsToDelete = [0, 1, 2]; 178 | $rowsAffected = $provider->deleteMultipleItems($idsToDelete); 179 | $rowCount = $provider->getRowCount(); 180 | 181 | $this->assertEquals(3, $rowsAffected); 182 | $this->assertEquals(7, $rowCount); 183 | } 184 | 185 | /** @test */ 186 | public function it_validates_a_row_of_data_for_missing_fields() 187 | { 188 | $validData = [ 189 | 'FirstName' => 'Luís', 190 | 'LastName' => 'Gonçalves', 191 | 'City' => 'São José dos Campos', 192 | 'Email' => 'luisg@embraer.com.br', 193 | ]; 194 | $invalidData = [ 195 | 'FirstName' => 'Luís', 196 | 'City' => 'São José dos Campos', 197 | 'Email' => 'luisg@embraer.com.br', 198 | ]; 199 | 200 | $provider = ArrayDataProviderFactory::defaultArrayDataProvider(); 201 | 202 | $errorsForValidData = $provider->validateRequiredRow($validData); 203 | $errorsForInvalidData = $provider->validateRequiredRow($invalidData); 204 | 205 | $this->assertEmpty($errorsForValidData); 206 | $this->assertNotEmpty($errorsForInvalidData); 207 | $this->assertArrayHasKey('LastName', $errorsForInvalidData); 208 | } 209 | 210 | /** @test */ 211 | public function it_validates_a_row_of_data_using_a_custom_validator() 212 | { 213 | $validData = [ 214 | 'FirstName' => 'Luís', 215 | 'LastName' => 'Gonçalves', 216 | 'City' => 'São José dos Campos', 217 | 'Email' => 'luisg@embraer.com.br', 218 | ]; 219 | 220 | $invalidData = [ 221 | 'FirstName' => 'Luís', 222 | 'LastName' => 'Gonçalves', 223 | 'City' => 'São José dos Campos', 224 | 'Email' => 'I feel this particular string does not conform to the required email format', 225 | ]; 226 | 227 | $provider = ArrayDataProviderFactory::defaultArrayDataProviderWithEmailValidator(); 228 | 229 | $failuresForValidData = $provider->validateRow($validData); 230 | $failuresForInvalidData = $provider->validateRow($invalidData); 231 | 232 | $this->assertEmpty($failuresForValidData); 233 | $this->assertArrayHasKey('Email', $failuresForInvalidData); 234 | $this->assertEquals($invalidData['Email'], $failuresForInvalidData['Email']); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /tests/Unit/Data/SQLDataProviderTest.php: -------------------------------------------------------------------------------- 1 | connection = SqlDataProviderFactory::connection(); 18 | $this->connection->beginTransaction(); 19 | } 20 | 21 | public function jsonFilterData() 22 | { 23 | return [ 24 | 'summary' => [ 25 | [ 'filters_json' => '[{"id":"_ck_all_summary","type":"like","value":"Ro"}]' ], 26 | 3, 27 | 'Roberto' 28 | ], 29 | 'first name' => [ 30 | [ 'filters_json' => '[{"id":"FirstName","type":"like","value":"Ed"}]' ], 31 | 2, 32 | 'Eduardo' 33 | ], 34 | 'last name' => [ 35 | [ 'filters_json' => '[{"id":"LastName","type":"like","value":"Go"}]' ], 36 | 3, 37 | 'Luís' 38 | ] 39 | ]; 40 | } 41 | 42 | 43 | /** @test */ 44 | public function it_retrieves_a_schema_based_on_defined_columns() 45 | { 46 | $provider = SqlDataProviderFactory::defaultSqlDataProvider(); 47 | $schema = $provider->getSchema(); 48 | 49 | $this->assertArrayHasKey('CustomerId', $schema); 50 | $this->assertArrayHasKey('FirstName', $schema); 51 | $this->assertArrayHasKey('LastName', $schema); 52 | $this->assertArrayHasKey('City', $schema); 53 | } 54 | 55 | /** @test */ 56 | public function it_retrieves_its_summary_columns() 57 | { 58 | $summaryCols = SqlDataProviderFactory::defaultSqlDataProvider()->getSummaryColumns(); 59 | 60 | $this->assertContains('FirstName', $summaryCols[0]); 61 | $this->assertContains('First Name', $summaryCols[0]); 62 | $this->assertContains('LastName', $summaryCols[1]); 63 | $this->assertContains('Last Name', $summaryCols[1]); 64 | } 65 | 66 | /** @test */ 67 | public function it_retrieves_a_row_count() 68 | { 69 | $provider = SqlDataProviderFactory::defaultSqlDataProvider(); 70 | $countWithoutConditions = $provider->getRowCount(); 71 | 72 | $this->assertEquals(53, $countWithoutConditions); 73 | } 74 | 75 | /** 76 | * @test 77 | * @dataProvider jsonFilterData 78 | */ 79 | public function it_retrieves_an_accurate_row_count_with_json_filters($options, $expectedCount) 80 | { 81 | $actualCount = SqlDataProviderFactory::defaultSqlDataProvider()->getRowCount($options); 82 | $this->assertEquals($expectedCount, $actualCount); 83 | } 84 | 85 | /** @test */ 86 | public function it_retrieves_multiple_database_rows_with_skip_and_take() 87 | { 88 | $rows = SqlDataProviderFactory::defaultSqlDataProvider()->getData([ 89 | 'skip' => 3, 90 | 'take' => 5 91 | ]); 92 | 93 | $this->assertCount(5, $rows); 94 | $this->assertEquals('Eduardo', $rows[0]['FirstName']); 95 | $this->assertEquals('Mark', $rows[4]['FirstName']); 96 | } 97 | 98 | /** 99 | * @test 100 | * @dataProvider jsonFilterData 101 | */ 102 | public function it_retrieves_multiple_database_rows_with_a_json_filter($options, $expectedCount, $expectedFirstName) 103 | { 104 | $rows = SqlDataProviderFactory::defaultSqlDataProvider()->getData($options); 105 | 106 | $this->assertCount($expectedCount, $rows); 107 | $this->assertEquals($expectedFirstName, $rows[0]['FirstName']); 108 | 109 | } 110 | 111 | /** @test */ 112 | public function it_retrieves_the_order_of_columns_for_its_edit_form() 113 | { 114 | $expectedOrder = [ 115 | 'CustomerId', 116 | 'FirstName', 117 | 'LastName', 118 | 'City', 119 | 'Email', 120 | ]; 121 | $actualOrder = SqlDataProviderFactory::defaultSqlDataProvider()->getEditFormOrder(); 122 | 123 | $this->assertEquals($expectedOrder, $actualOrder); 124 | } 125 | 126 | /** @test */ 127 | public function it_creates_an_edit_form_object() 128 | { 129 | $form = SqlDataProviderFactory::defaultSqlDataProvider()->getEditForm(); 130 | $this->assertInstanceOf('CrudKit\Util\FormHelper', $form); 131 | } 132 | 133 | /** @test */ 134 | public function it_creates_a_new_database_row_by_id() 135 | { 136 | $expected = [ 137 | 'FirstName' => 'Foo', 138 | 'LastName' => 'Bar', 139 | 'City' => 'Baz', 140 | 'Email' => 'foo@bar.baz' 141 | ]; 142 | 143 | $provider = SqlDataProviderFactory::defaultSqlDataProvider(); 144 | $insertedId = $provider->createItem($expected); 145 | 146 | $inserted = $provider->getRow($insertedId); 147 | unset($inserted['CustomerId']); 148 | 149 | $this->assertEquals($expected, $inserted); 150 | } 151 | 152 | /** 153 | * @test 154 | * @expectedException \Exception 155 | */ 156 | public function it_fails_when_creating_a_new_row_with_an_invalid_column() 157 | { 158 | $values = [ 159 | 'Foo' => 'Bar', 160 | 'FirstName' => 'Foo', 161 | 'LastName' => 'Bar', 162 | 'City' => 'Baz', 163 | 'Email' => 'foo@bar.baz' 164 | ]; 165 | 166 | SqlDataProviderFactory::defaultSqlDataProvider()->createItem($values); 167 | } 168 | 169 | /** @test */ 170 | public function it_retrieves_a_database_row_by_id() 171 | { 172 | $expected = [ 173 | 'CustomerId' => 1, 174 | 'FirstName' => 'Luís', 175 | 'LastName' => 'Gonçalves', 176 | 'City' => 'São José dos Campos', 177 | 'Email' => 'luisg@embraer.com.br', 178 | ]; 179 | $actual = SqlDataProviderFactory::defaultSqlDataProvider()->getRow(1); 180 | 181 | $this->assertEquals($expected, $actual); 182 | } 183 | 184 | /** @test */ 185 | public function it_sets_a_database_row_by_id() 186 | { 187 | $expectedFirstName = 'Jeb ' . time(); 188 | $provider = SqlDataProviderFactory::defaultSqlDataProvider(); 189 | $provider->setRow(1, ['FirstName' => $expectedFirstName]); 190 | $updatedRow = $provider->getRow(1); 191 | 192 | $this->assertEquals($expectedFirstName, $updatedRow['FirstName']); 193 | } 194 | 195 | /** 196 | * @test 197 | * @expectedException \Exception 198 | */ 199 | public function it_fails_when_setting_a_database_row_with_an_invalid_column() 200 | { 201 | $provider = SqlDataProviderFactory::defaultSqlDataProvider(); 202 | 203 | $row = $provider->getRow(1); 204 | $row['Foo'] = 'Bar'; 205 | 206 | $provider->setRow(1, $row); 207 | } 208 | 209 | /** @test */ 210 | public function it_deletes_a_database_row_by_id() 211 | { 212 | $provider = SqlDataProviderFactory::defaultSqlDataProvider(); 213 | $toBeDeleted = $provider->getData(['take' => 1]); 214 | 215 | $rowsAffected = $provider->deleteItem( $toBeDeleted[0]['CustomerId'] ); 216 | $nextItems = $provider->getData(['take' => 1]); 217 | 218 | $this->assertEquals(1, $rowsAffected); 219 | $this->assertNotEquals($toBeDeleted[0], $nextItems[0]); 220 | } 221 | 222 | /** @test */ 223 | public function it_deletes_multiple_database_rows_by_id() 224 | { 225 | $provider = SqlDataProviderFactory::defaultSqlDataProvider(); 226 | $toBeDeleted = $provider->getData(['take' => 3]); 227 | 228 | $idsToDelete = array_map(function(array $item) { 229 | return $item['CustomerId']; 230 | }, $toBeDeleted); 231 | 232 | $rowsAffected = $provider->deleteMultipleItems($idsToDelete); 233 | 234 | $nextItems = $provider->getData(['take' => 3]); 235 | $nextIds = array_map(function(array $item) { 236 | return $item['CustomerId']; 237 | }, $nextItems); 238 | 239 | $this->assertEquals(3, $rowsAffected); 240 | $this->assertNotEquals($idsToDelete, $nextIds); 241 | } 242 | 243 | /** @test */ 244 | public function it_validates_a_row_of_data_for_missing_fields() 245 | { 246 | $validData = [ 247 | 'FirstName' => 'Luís', 248 | 'LastName' => 'Gonçalves', 249 | 'City' => 'São José dos Campos', 250 | 'Email' => 'luisg@embraer.com.br', 251 | ]; 252 | $invalidData = [ 253 | 'FirstName' => 'Luís', 254 | 'City' => 'São José dos Campos', 255 | 'Email' => 'luisg@embraer.com.br', 256 | ]; 257 | 258 | $provider = SqlDataProviderFactory::defaultSqlDataProvider(); 259 | 260 | $errorsForValidData = $provider->validateRequiredRow($validData); 261 | $errorsForInvalidData = $provider->validateRequiredRow($invalidData); 262 | 263 | $this->assertEmpty($errorsForValidData); 264 | $this->assertNotEmpty($errorsForInvalidData); 265 | $this->assertArrayHasKey('LastName', $errorsForInvalidData); 266 | } 267 | 268 | 269 | /** 270 | * @test 271 | * @expectedException \Exception 272 | */ 273 | public function it_throws_an_exception_if_validating_a_row_for_missing_fields_with_unknown_columns() 274 | { 275 | $unknownColumn = ['Foo' => 'Bar']; 276 | SqlDataProviderFactory::defaultSqlDataProvider()->validateRequiredRow($unknownColumn); 277 | } 278 | 279 | /** @test */ 280 | public function it_validates_a_row_of_data_using_a_custom_validator() 281 | { 282 | $validData = [ 283 | 'FirstName' => 'Luís', 284 | 'LastName' => 'Gonçalves', 285 | 'City' => 'São José dos Campos', 286 | 'Email' => 'luisg@embraer.com.br', 287 | ]; 288 | 289 | $invalidData = [ 290 | 'FirstName' => 'Luís', 291 | 'LastName' => 'Gonçalves', 292 | 'City' => 'São José dos Campos', 293 | 'Email' => 'I feel this particular string does not conform to the required email format', 294 | ]; 295 | 296 | $provider = SqlDataProviderFactory::defaultSqlDataProviderWithEmailValidator(); 297 | 298 | $failuresForValidData = $provider->validateRow($validData); 299 | $failuresForInvalidData = $provider->validateRow($invalidData); 300 | 301 | $this->assertEmpty($failuresForValidData); 302 | $this->assertArrayHasKey('Email', $failuresForInvalidData); 303 | $this->assertEquals($invalidData['Email'], $failuresForInvalidData['Email']); 304 | } 305 | 306 | /** 307 | * @test 308 | * @expectedException \Exception 309 | */ 310 | public function it_throws_an_exception_if_validating_a_row_with_unknown_columns() 311 | { 312 | $unknownColumn = ['Foo' => 'Bar']; 313 | 314 | SqlDataProviderFactory::defaultSqlDataProvider()->validateRow($unknownColumn); 315 | } 316 | 317 | public function tearDown() 318 | { 319 | parent::tearDown(); 320 | $this->connection->rollBack(); 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |