├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gemfile ├── MIT-LICENSE ├── README.md ├── Rakefile ├── app ├── assets │ ├── images │ │ └── kms_models │ │ │ └── .keep │ ├── javascripts │ │ ├── kms_models │ │ │ ├── application.js │ │ │ └── application │ │ │ │ ├── controllers │ │ │ │ ├── entries_controller.coffee.erb │ │ │ │ ├── fields_controller.coffee.erb │ │ │ │ └── models_controller.coffee.erb │ │ │ │ └── routes.coffee.erb │ │ └── templates │ │ │ ├── entries │ │ │ ├── edit.html.slim │ │ │ ├── form.html.slim │ │ │ ├── index.html.slim │ │ │ └── new.html.slim │ │ │ ├── fields │ │ │ ├── belongs_to_field.html.slim │ │ │ ├── checkbox_field.html.slim │ │ │ ├── date_field.html.slim │ │ │ ├── file_field.html.slim │ │ │ ├── has_many_field.html.slim │ │ │ ├── string_field.html.slim │ │ │ └── text_field.html.slim │ │ │ ├── help │ │ │ ├── models_endpoints.html.slim │ │ │ └── models_variables.html.slim │ │ │ └── models │ │ │ ├── edit.html.slim │ │ │ ├── fields.html.slim │ │ │ ├── form.html.slim │ │ │ ├── index.html.slim │ │ │ └── new.html.slim │ └── stylesheets │ │ └── kms_models │ │ └── application.css ├── controllers │ └── kms │ │ ├── models │ │ ├── entries_controller.rb │ │ ├── fields_controller.rb │ │ └── models_controller.rb │ │ └── public │ │ └── entries_controller.rb ├── helpers │ └── kms_models │ │ └── application_helper.rb ├── models │ └── kms │ │ ├── belongs_to_field.rb │ │ ├── checkbox_field.rb │ │ ├── date_field.rb │ │ ├── entry.rb │ │ ├── field.rb │ │ ├── file_field.rb │ │ ├── has_many_field.rb │ │ ├── model.rb │ │ ├── models_wrapper.rb │ │ ├── page_decorator.rb │ │ ├── string_field.rb │ │ └── text_field.rb ├── serializers │ └── kms │ │ ├── entry_serializer.rb │ │ ├── field_serializer.rb │ │ ├── model_serializer.rb │ │ └── simple_model_serializer.rb ├── uploaders │ └── entry_file_uploader.rb └── views │ └── layouts │ └── kms_models │ └── application.html.erb ├── bin └── rails ├── config.ru ├── config ├── initializers │ ├── ability.rb │ ├── externals.rb │ ├── help.rb │ └── resources.rb ├── locales │ ├── en.yml │ └── ru.yml └── routes.rb ├── db └── migrate │ ├── 20150409124420_create_kms_models.rb │ ├── 20150409125056_create_kms_fields.rb │ ├── 20150413143711_create_kms_entries.rb │ ├── 20150820080436_add_label_field_to_kms_models.rb │ ├── 20150820132142_add_slug_to_kms_entries.rb │ ├── 20150821201250_fix_models_column_name.rb │ ├── 20150901115303_add_class_name_to_kms_fields.rb │ ├── 20150910081440_add_position_to_kms_entries.rb │ ├── 20170209125819_add_allow_creation_using_form_to_models.rb │ ├── 20170802063046_change_values_column_to_jsonb.rb │ ├── 20170802085121_add_position_to_kms_fields.rb │ └── 20180122135245_add_description_to_kms_models.rb ├── kms_models.gemspec ├── lib ├── drops │ └── kms │ │ ├── entry_drop.rb │ │ └── models_wrapper_drop.rb ├── generators │ └── kms_models │ │ └── install │ │ └── install_generator.rb ├── kms │ └── models │ │ ├── engine.rb │ │ └── version.rb ├── kms_models.rb └── tasks │ └── kms_models_tasks.rake └── spec ├── controllers └── kms │ ├── models │ ├── fields_controller_spec.rb │ └── models_controller_spec.rb │ └── public │ └── entries_controller_spec.rb ├── factories ├── fields.rb ├── models.rb └── users.rb ├── internal ├── Rakefile ├── config │ ├── database.yml.travis │ ├── initializers │ │ └── devise.rb │ └── routes.rb ├── db │ └── schema.rb ├── log │ └── .gitignore └── public │ └── favicon.ico ├── models └── kms │ ├── field_spec.rb │ ├── has_many_field_spec.rb │ └── model_spec.rb ├── spec_helper.rb └── support ├── controller_macros.rb └── request_helpers.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | log/*.log 3 | pkg/ 4 | spec/internal/config/database.yml 5 | spec/internal/log/*.log 6 | spec/internal/tmp/ 7 | spec/internal/.sass-cache 8 | *.swp 9 | *.swo 10 | *~ 11 | .byebug_history 12 | Gemfile.lock 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.3.0 4 | services: 5 | - postgresql 6 | addons: 7 | postgresql: "9.4" 8 | script: 9 | - bundle exec rspec 10 | before_script: 11 | - psql -c 'create database kms_test;' -U postgres 12 | - cp spec/internal/config/database.yml.travis spec/internal/config/database.yml 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Version 1.1.0 2018-02-09 2 | 3 | * [added] add Date Field - closes #5 4 | * [added] add description to Models - closes #6 5 | * [added] add small UI element - model iteration hint 6 | * [changed] better error messages + hint for Label Field 7 | * [changed] use FontAwesome icons for identifying different field types 8 | * [fixed] use current locale for CKEditor - closes #17 9 | * [fixed] correct processing for CheckboxFields - closes #11 10 | 11 | ## Version 1.0.1 2017-08-09 12 | 13 | * [fixed] fix "uninitialized constant Rails::Generators::Base" 14 | 15 | ## Version 1.0.0 2017-08-02 16 | 17 | * [fixed] find_by and find_all_by now can find results not only by dynamic fields but also by id, slug, position and etc. 18 | * [added] add slug attribute to entry drop 19 | * [added] support Amazon S3 storage - closes #14 20 | * [added] add blank state screens for models and entries 21 | * [added] automatic Model's collection_name and Fields liquor_name generating - closes #16 22 | * [changed] replace JSON column with JSONB - closes #15 23 | * [added] Sorting (by dran'n'drop) Model's fields - closes #4 24 | * [fixed] fetch item only in scope of model - page's templatable type 25 | 26 | ## Version 0.8.0 2017-03-23 27 | 28 | * [fixed] fix incorrect url for file field when no value was stored 29 | * [changed] preserve order of has_many field values - closes #9 30 | * [added] Rspec setup 31 | * [added] add feature allowing model elements creation using website forms 32 | 33 | ## Version 0.7.0 2016-12-05 34 | 35 | * [fixed] fix bug when running kms_models install generator 36 | * [changed] Rails 5 support 37 | 38 | ## Version 0.6.0 2016-10-01 39 | 40 | * [changed] allow to edit Entry slug, but autogenerate on creation 41 | * [added] English translations 42 | 43 | ## Version 0.5.0 2016-02-09 44 | 45 | * [fixed] fix saving unchecked checkbox 46 | * [fixed] element updating not working when some field are empty 47 | * [changed] allow to pass File object when storing values for File Fields 48 | * [changed] allow more file types for file fields - doc, xls, pdf, mp4, webm (previously only images allowed) 49 | * [changed] alert about errors using Alertify 50 | * [added] add ability to order model entries - using `order` method 51 | * [added] register model in ModelDrop after creation - no need in app restart 52 | * [added] Collection name method added - `item.model_collection_name` 53 | * [added] expose entry id field in liquor 54 | * [added] add confirmation before Model deleting 55 | * [added] added BelongsTo field for Models (similar to Rails belongs_to) 56 | * [added] support searching/filtering model entries with 'find_all_by', 'find_by' 57 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | This is really easy: 2 | 3 | - [x] Fork it 4 | - [x] Code it 5 | - [x] Submit Pull Request 6 | - [x] Get it merged 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Declare your gem's dependencies in kms_models.gemspec. 4 | # Bundler will treat runtime dependencies like base dependencies, and 5 | # development dependencies will be added by default to the :development group. 6 | gemspec 7 | 8 | # Declare any dependencies that are still in development here instead of in 9 | # your gemspec. These might include edge Rails or gems from your path or 10 | # Git. Remember to move these dependencies to your gemspec before releasing 11 | # your gem to rubygems.org. 12 | gem 'kms', github: 'webgradus/kms' 13 | 14 | # To use a debugger 15 | gem 'byebug', group: [:development, :test] 16 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Igor Petrov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## KMS Models 2 | 3 | [](https://travis-ci.org/apiqcms/kms_models) 4 | [](https://codeclimate.com/github/apiqcms/kms_models) 5 | 6 | This extension adds "Models" section in [KMS](https://github.com/apiqcms/kms) and allows to define custom models on-the-fly. Supported fields for definition in Model: String, Text, Checkbox, File, HasMany, BelongsTo. Note that this extension requires at least PostgreSQL 9.2 because of JSON column type. 7 | 8 | ## Installation 9 | 10 | 1. Add to Gemfile 11 | 12 | gem "kms_models" 13 | # or for edge version: 14 | gem "kms_models", github: "webgradus/kms_models" 15 | 16 | 2. Run generator: 17 | 18 | rails g kms_models:install 19 | 20 | 3. Copy migrations: 21 | 22 | rails kms_models:install:migrations 23 | 24 | 4. Migrate: 25 | 26 | bundle exec rails db:migrate 27 | 28 | 5. Recompile assets: 29 | 30 | bundle exec rails assets:precompile 31 | 32 | 6. Restart KMS instance 33 | 34 | ## Getting started 35 | Please watch this video to start using KMS Models: 36 | 37 | [](https://youtu.be/_INzPDZimsA "Getting started with KMS Models extension") 38 | 39 | ## Contributing 40 | 41 | Please follow [CONTRIBUTING.md](CONTRIBUTING.md). 42 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'bundler/setup' 3 | rescue LoadError 4 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 5 | end 6 | 7 | require 'rdoc/task' 8 | 9 | RDoc::Task.new(:rdoc) do |rdoc| 10 | rdoc.rdoc_dir = 'rdoc' 11 | rdoc.title = 'KmsModels' 12 | rdoc.options << '--line-numbers' 13 | rdoc.rdoc_files.include('README.rdoc') 14 | rdoc.rdoc_files.include('lib/**/*.rb') 15 | end 16 | 17 | APP_RAKEFILE = File.expand_path("../spec/internal/Rakefile", __FILE__) 18 | load 'rails/tasks/engine.rake' 19 | 20 | 21 | load 'rails/tasks/statistics.rake' 22 | 23 | 24 | 25 | Bundler::GemHelper.install_tasks 26 | 27 | require 'rake/testtask' 28 | 29 | Rake::TestTask.new(:test) do |t| 30 | t.libs << 'lib' 31 | t.libs << 'test' 32 | t.pattern = 'test/**/*_test.rb' 33 | t.verbose = false 34 | end 35 | 36 | 37 | task default: :test 38 | -------------------------------------------------------------------------------- /app/assets/images/kms_models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apiqcms/kms_models/6278f32b0550d57a8401c4fc0543b5953d06d11e/app/assets/images/kms_models/.keep -------------------------------------------------------------------------------- /app/assets/javascripts/kms_models/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require "kms_models/application/routes" 14 | //= require_tree "../templates" 15 | //= require_tree "./application/controllers" 16 | -------------------------------------------------------------------------------- /app/assets/javascripts/kms_models/application/controllers/entries_controller.coffee.erb: -------------------------------------------------------------------------------- 1 | EntriesController = ($scope, $state, Restangular, $stateParams, Alertify, ErrorsService) -> 2 | $scope.modelStore = Restangular.all('models') 3 | $scope.store = Restangular.one("models", $stateParams.modelId).all("entries") 4 | $scope.editorOptions = 5 | filebrowserUploadUrl: '/kms/assets/ckeditor' 6 | language: '<%= I18n.locale.to_s %>' 7 | 8 | #Restangular.all('users').customGET('kms_user').then (current_user) -> 9 | #$scope.currentUser = current_user 10 | #$scope.currentUser.admin = $scope.currentUser.role == 'admin' 11 | 12 | $scope.entriesSortableOptions = 13 | orderChanged: (event)-> 14 | for entry, index in event.dest.sortableScope.modelValue 15 | entry_copy = 16 | id: entry.id 17 | position: index 18 | Restangular.restangularizeElement($scope.model, entry_copy, 'entries').put() 19 | 20 | $scope.modelStore.get($stateParams.modelId).then (model)-> 21 | $scope.model = model 22 | fields = $scope.getAssociationFields(model) 23 | _.each fields, (field)-> 24 | $scope[field.liquor_name] = [] # pre-init 25 | Restangular.one("models", field.class_name).all("entries").getList().then (entries)-> 26 | $scope[field.liquor_name] = entries 27 | $scope.initAssociationField(field) 28 | 29 | $scope.initAssociationField = (field)-> 30 | if field.type == 'Kms::HasManyField' 31 | fieldEntries = _.map $scope.entry.values[field.liquor_name], (entryId)-> 32 | _.find $scope[field.liquor_name], { 'id': parseInt(entryId) } 33 | $scope.entry.values[field.liquor_name] = _.compact fieldEntries 34 | else 35 | $scope.entry.values[field.liquor_name] = _.find $scope[field.liquor_name], (element)-> 36 | $scope.entry.values[field.liquor_name] == element.id.toString() 37 | 38 | $scope.store.getList().then (entries)-> 39 | $scope.entries = entries 40 | 41 | if $stateParams.id 42 | $scope.store.get($stateParams.id).then (entry)-> 43 | $scope.entry = entry 44 | else 45 | $scope.entry = {values: {}} 46 | 47 | $scope.getAssociationFields = (model)-> 48 | _.filter model.fields_attributes, (field) -> field.type == 'Kms::HasManyField' or field.type == 'Kms::BelongsToField' 49 | 50 | $scope.getFieldTemplateName = (field)-> 51 | typeSplitted = field.type.split '::' 52 | _.snakeCase(typeSplitted[typeSplitted.length - 1]) 53 | 54 | $scope.create = -> 55 | fd = new FormData 56 | if $scope.entry.slug 57 | fd.append("entry[slug]", $scope.entry.slug) 58 | for key, value of $scope.entry.values 59 | fd.append("entry[values][#{key}]", value || '') 60 | $scope.store.withHttpConfig({ transformRequest: angular.identity }).post(fd, null, {"Content-Type": undefined}).then -> 61 | $state.go('models.entries', modelId: $scope.model.id) 62 | ,(response)-> 63 | Alertify.error(ErrorsService.prepareErrorsString(response.data.errors)) 64 | 65 | $scope.update = -> 66 | fd = new FormData 67 | if $scope.entry.slug 68 | fd.append("entry[slug]", $scope.entry.slug) 69 | for key, value of $scope.entry.values 70 | # continue if value == undefined 71 | unless _.isEmpty(value) 72 | if value.constructor.name == 'Array' 73 | for element in value 74 | id = if element.constructor.name == 'Object' then element.id else element 75 | fd.append("entry[values][#{key}][]", id) 76 | else if value.constructor.name != 'Object' 77 | fd.append("entry[values][#{key}]", value || '') 78 | else 79 | fd.append("entry[values][#{key}]", if value? then value else '') 80 | $scope.entry.withHttpConfig({ transformRequest: angular.identity }).post('', fd, '', {"Content-Type": undefined}).then -> 81 | $state.go('models.entries', modelId: $scope.model.id) 82 | ,(response)-> 83 | Alertify.error(ErrorsService.prepareErrorsString(response.data.errors)) 84 | 85 | $scope.destroy = (entry)-> 86 | if(confirm('<%= I18n.t(:are_you_sure) %>')) 87 | entry.remove().then -> 88 | $scope.entries = _.without($scope.entries, entry) 89 | 90 | 91 | 92 | angular.module('KMS') 93 | .controller('EntriesController', ['$scope', '$state', 'Restangular', '$stateParams', 'Alertify', 'ErrorsService', EntriesController]) 94 | -------------------------------------------------------------------------------- /app/assets/javascripts/kms_models/application/controllers/fields_controller.coffee.erb: -------------------------------------------------------------------------------- 1 | FieldsController = ($scope, $state, Restangular, $stateParams, TransliterationService) -> 2 | $scope.types = [ 3 | { id: 'Kms::StringField', name: "<%= I18n.t("field_types.string") %>", icon: 'fa-font'}, 4 | { id: 'Kms::TextField', name: "<%= I18n.t("field_types.text") %>", icon: 'fa-file-text-o'}, 5 | { id: 'Kms::CheckboxField', name: "<%= I18n.t("field_types.checkbox") %>", icon: 'fa-check-square-o'}, 6 | { id: 'Kms::DateField', name: "<%= I18n.t("field_types.date") %>", icon: 'fa-calendar'}, 7 | { id: 'Kms::FileField', name: "<%= I18n.t("field_types.file") %>", icon: 'fa-image'}, 8 | { id: 'Kms::HasManyField', name: "<%= I18n.t("field_types.has_many") %>", icon: 'fa-sitemap'}, 9 | { id: 'Kms::BelongsToField', name: "<%= I18n.t("field_types.belongs_to") %>", icon: 'fa-leaf'}, 10 | ] 11 | 12 | $scope.fieldsSortableOptions = 13 | orderChanged: (event)-> 14 | for field, index in event.dest.sortableScope.modelValue 15 | field_copy = 16 | id: field.id 17 | position: index 18 | Restangular.restangularizeElement($scope.model, field_copy, 'fields').put() 19 | 20 | Restangular.all('resources').getList().then (templatable_types)-> 21 | $scope.templatable_types = templatable_types 22 | 23 | Restangular.all('users').customGET('kms_user').then (current_user) -> 24 | $scope.currentUser = current_user 25 | $scope.currentUser.admin = $scope.currentUser.role == 'admin' 26 | 27 | $scope.field = {} 28 | 29 | $scope.$watch 'field.name', (newValue, oldValue) -> 30 | if newValue? and !$scope.field.id 31 | $scope.field.liquor_name = _.snakeCase TransliterationService.translit(newValue, 5).replace(/`/g, '') 32 | 33 | $scope.findTypeByField = (field) -> 34 | _.find $scope.types, (type) -> type.id == field.type 35 | 36 | $scope.formatType = (field) -> 37 | fieldType = $scope.findTypeByField(field) 38 | if $scope.isAssociationField(field) then "#{fieldType.name} (#{$scope.getDisplayableTemplatableType(field)})" else fieldType.name 39 | 40 | $scope.getDisplayableTemplatableType = (field)-> 41 | templatable_type = _.find $scope.templatable_types, (templatable_type) -> templatable_type.type == field.class_name 42 | templatable_type.title 43 | 44 | $scope.isAssociationField = (field)-> 45 | field.type == 'Kms::HasManyField' or field.type == 'Kms::BelongsToField' 46 | 47 | $scope.isValidField = -> 48 | $scope.field.name and $scope.field.liquor_name and $scope.field.type 49 | 50 | $scope.addField = -> 51 | if $scope.isValidField() 52 | $scope.model.fields_attributes.push($scope.field) 53 | $scope.field = {} 54 | 55 | $scope.removeField = (field)-> 56 | field['_destroy'] = '1' # for rails deletion 57 | #$scope.model.fields = _.without($scope.model.fields, field) 58 | 59 | 60 | angular.module('KMS') 61 | .controller('FieldsController', ['$scope', '$state', 'Restangular', '$stateParams', 'TransliterationService', FieldsController]) 62 | -------------------------------------------------------------------------------- /app/assets/javascripts/kms_models/application/controllers/models_controller.coffee.erb: -------------------------------------------------------------------------------- 1 | ModelsController = ($scope, $state, Restangular, $stateParams, Alertify, ErrorsService, TransliterationService) -> 2 | $scope.store = Restangular.all('models') 3 | 4 | Restangular.all('users').customGET('kms_user').then (current_user) -> 5 | $scope.currentUser = current_user 6 | $scope.currentUser.admin = $scope.currentUser.role == 'admin' 7 | 8 | $scope.store.getList().then (models)-> 9 | $scope.models = models 10 | 11 | if $stateParams.id 12 | $scope.store.get($stateParams.id).then (model)-> 13 | $scope.model = model 14 | else 15 | $scope.model = {fields_attributes: []} 16 | 17 | $scope.$watchCollection 'model.fields_attributes', (newFields, oldFields) -> 18 | if newFields and newFields.length > 0 and oldFields and oldFields.length == 0 19 | $scope.model.label_field =newFields[0].liquor_name 20 | 21 | $scope.$watch 'model.kms_model_name', (newValue, oldValue) -> 22 | if newValue? and !$scope.model.id 23 | $scope.model.collection_name = _.snakeCase TransliterationService.translit(newValue, 5).replace(/`/g, '') 24 | 25 | $scope.create = -> 26 | $scope.store.post($scope.model).then -> 27 | # for adding to Menu - better to render resources via js 28 | window.location.reload() 29 | #$state.go('models') 30 | ,(response)-> 31 | Alertify.error(ErrorsService.prepareErrorsString(response.data.errors)) 32 | 33 | $scope.update = -> 34 | $scope.model.put().then -> 35 | $state.go('models') 36 | ,(response)-> 37 | Alertify.error(ErrorsService.prepareErrorsString(response.data.errors)) 38 | 39 | $scope.destroy = (model)-> 40 | if confirm('<%= I18n.t(:are_you_sure) %>') 41 | model.remove().then -> 42 | window.location.reload() 43 | 44 | 45 | angular.module('KMS') 46 | .controller('ModelsController', ['$scope', '$state', 'Restangular', '$stateParams', 'Alertify', 'ErrorsService', 'TransliterationService', ModelsController]) 47 | -------------------------------------------------------------------------------- /app/assets/javascripts/kms_models/application/routes.coffee.erb: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | angular.module('KMS').config ['$stateProvider', '$urlRouterProvider', ($stateProvider, $urlRouterProvider) -> 4 | 5 | # Application routes 6 | $stateProvider 7 | .state('models', { 8 | url: '/kms/models', 9 | views: 10 | "header": 11 | template: "<%= Kms::Model.model_name.human(count: 1.1) %>" 12 | "@": 13 | controller: 'ModelsController', 14 | controllerAs: 'models', 15 | templateUrl: 'models/index.html', 16 | }) 17 | .state('models.new', { 18 | url: '/new', 19 | views: 20 | "header@": 21 | template: "<%= I18n.t(:new_model) %>" 22 | "@": 23 | controller: 'ModelsController', 24 | controllerAs: 'models', 25 | templateUrl: 'models/new.html', 26 | }) 27 | .state('models.edit', { 28 | url: '/:id/edit', 29 | views: 30 | "header@": 31 | template: "<%= I18n.t(:edit_model) %>" 32 | "@": 33 | controller: 'ModelsController', 34 | controllerAs: 'models', 35 | templateUrl: 'models/edit.html', 36 | }) 37 | .state('models.entries', { 38 | url: '/:modelId/entries', 39 | views: 40 | "header@": 41 | template: "<%= Kms::Entry.model_name.human(count: 1.1) %>" 42 | "@": 43 | controller: 'EntriesController', 44 | controllerAs: 'entries', 45 | templateUrl: 'entries/index.html', 46 | }) 47 | .state('models.entries.new', { 48 | url: '/new', 49 | views: 50 | "header@": 51 | template: "<%= I18n.t(:new_entry) %>" 52 | "@": 53 | controller: 'EntriesController', 54 | controllerAs: 'entries', 55 | templateUrl: 'entries/new.html' 56 | }) 57 | .state('models.entries.edit', { 58 | url: '/:id/edit', 59 | views: 60 | "header@": 61 | template: "<%= I18n.t(:edit_entry) %>" 62 | "@": 63 | controller: 'EntriesController', 64 | controllerAs: 'entries', 65 | templateUrl: 'entries/edit.html' 66 | }) 67 | ] 68 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/entries/edit.html.slim: -------------------------------------------------------------------------------- 1 | .row 2 | .col-lg-12 3 | form role="form" ng-submit="update()" novalidate="" 4 | ng-include src="'entries/form.html'" 5 | .form-group 6 | label for="slug" 7 | = Kms::Entry.human_attribute_name(:slug) 8 | input.form-control type="text" ng-model="entry.slug" id="entry[slug]" 9 | button.btn.btn-default type="submit" = I18n.t(:update_entry) 10 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/entries/form.html.slim: -------------------------------------------------------------------------------- 1 | .form-group ng-repeat="field in model.fields_attributes" 2 | ng-include src="'fields/' + getFieldTemplateName(field) + '.html'" 3 | /input.form-control type="text" ng-model="entry.values[field.liquor_name]" ng-attr-id="{{field.liquor_name}}" 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/entries/index.html.slim: -------------------------------------------------------------------------------- 1 | .row ng-show="entries.length" 2 | .col-lg-12 3 | .widget 4 | .widget-header 5 | i.fa.fa-list 6 | | {{ model.description || model.kms_model_name }} 7 | a.btn.btn-sm.btn-primary.pull-right ui-sref="models.entries.new({modelId: model.id})" 8 | = I18n.t("add_entry") 9 | .widget-body.no-padding 10 | .table-responsive 11 | table.table 12 | tbody as-sortable="entriesSortableOptions" ng-model="entries" 13 | tr ng-repeat="entry in entries" as-sortable-item="" 14 | td style="width: 80%" 15 | i.fa.fa-bars as-sortable-item-handle="" 16 | a ui-sref="models.entries.edit({modelId: model.id, id: entry.id})" 17 | | {{ entry.values[model.label_field] || entry.id }} 18 | td 19 | .btn-group.pull-right 20 | a.btn.btn-sm.btn-danger ng-click="destroy(entry)" 21 | i.fa.fa-times 22 | .row ng-show="!entries.length" 23 | .center-block 24 | .jumbotron.vertical-center.text-center 25 | .container 26 | h1 27 | span.fa-stack.fa-lg 28 | i.fa.fa-circle.fa-stack-2x 29 | i.fa.fa-list.fa-stack-1x 30 | p = I18n.t(:entries_description) 31 | p 32 | a.btn.btn-primary.btn-lg ui-sref="models.entries.new({modelId: model.id})" role="button" = I18n.t(:create_first_entry) 33 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/entries/new.html.slim: -------------------------------------------------------------------------------- 1 | .row 2 | .col-lg-12 3 | form role="form" ng-submit="create()" novalidate="" 4 | ng-include src="'entries/form.html'" 5 | button.btn.btn-default type="submit" = I18n.t(:add_entry) 6 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/fields/belongs_to_field.html.slim: -------------------------------------------------------------------------------- 1 | label for="{{ field.liquor_name }}" 2 | | {{ field.name }} 3 | ui-select ng-model="entry.values[field.liquor_name]" theme="bootstrap" 4 | ui-select-match placeholder=I18n.t(:belongs_to_field_placeholder) 5 | | {{ $select.selected.values[$select.selected.model.label_field] || $select.selected.id }} 6 | ui-select-choices repeat="relatedEntry.id as relatedEntry in {{ field.liquor_name }}" 7 | div ng-bind-html="relatedEntry.values[relatedEntry.model.label_field] | highlight: $select.search" 8 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/fields/checkbox_field.html.slim: -------------------------------------------------------------------------------- 1 | .checkbox 2 | label 3 | input type="checkbox" ng-model="entry.values[field.liquor_name]" ng-attr-id="{{field.liquor_name}}" 4 | | {{ field.name }} 5 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/fields/date_field.html.slim: -------------------------------------------------------------------------------- 1 | label for="{{ field.liquor_name }}" 2 | | {{ field.name }} 3 | input.form-control datepicker-popup="" is-open="field.datepickerOpened" show-button-bar="false" ng-click="field.datepickerOpened = true" type="text" ng-model="entry.values[field.liquor_name]" ng-attr-id="{{field.liquor_name}}" 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/fields/file_field.html.slim: -------------------------------------------------------------------------------- 1 | label for="{{ field.liquor_name }}" 2 | | {{ field.name }} 3 | div flow-init="{singleFile: true, headers: setHeaders, fileParameterName: 'entry.values[{{field.liquor_name}}]'}" flow-files-submitted="entry.values.{{field.liquor_name}} = $flow.files[0].file" flow-file-added="!!{png:1,gif:1,jpg:1,jpeg:1,doc:1,xls:1,xlsx:1,pdf:1,docx:1,mp4:1,webm:1}[$file.getExtension()]" flow-file-success="$file.msg = $message" 4 | input type="file" flow-btn="" ng-model="entry.values[field.liquor_name]" 5 | div class="thumbnail" ng-show="!$flow.files.length" 6 | img ng-src="{{entry.values[field.liquor_name].url}}" 7 | div class="thumbnail" ng-show="$flow.files.length" 8 | img flow-img="$flow.files[0]" 9 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/fields/has_many_field.html.slim: -------------------------------------------------------------------------------- 1 | label for="{{ field.liquor_name }}" 2 | | {{ field.name }} 3 | ui-select multiple="" ng-model="entry.values[field.liquor_name]" theme="bootstrap" on-select="addObject($item, field)" on-remove='removeObject($item, field)' 4 | ui-select-match placeholder=I18n.t(:has_many_field_placeholder) class="ui-select-match" 5 | | {{ $item.values[$item.model.label_field] || $item.id }} 6 | ui-select-choices repeat="childEntry.id as childEntry in {{ field.liquor_name }}" 7 | div ng-bind-html="childEntry.values[childEntry.model.label_field] | highlight: $select.search" 8 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/fields/string_field.html.slim: -------------------------------------------------------------------------------- 1 | label for="{{ field.liquor_name }}" 2 | | {{ field.name }} 3 | input.form-control type="text" ng-model="entry.values[field.liquor_name]" ng-attr-id="{{field.liquor_name}}" 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/fields/text_field.html.slim: -------------------------------------------------------------------------------- 1 | label for="{{ field.liquor_name }}" 2 | | {{ field.name }} 3 | textarea.form-control ng-model="entry.values[field.liquor_name]" rows="15" ckeditor='editorOptions' ng-attr-id="{{field.liquor_name}}" 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/help/models_endpoints.html.slim: -------------------------------------------------------------------------------- 1 | h4 = I18n.t("liquor_help.endpoints_title") 2 | p 3 | ul 4 | li 5 | var POST /entries/:collection_name 6 | p 7 | span = I18n.t('liquor_help.endpoints.entries.post.main_description') 8 | p 9 | code ng-non-bindable='' 10 | | <form action="/entries/:collection_name" method="post"> 11 | br 12 | | <input type="hidden" name='authenticity_token' value='{{ request.form_authenticity_token }}'> 13 | br 14 | | <input type="text" name="entry[name]"> 15 | br 16 | | <input type="submit" value="Send"> 17 | br 18 | | </form> 19 | p 20 | table.table 21 | tr 22 | th #{ I18n.t('liquor_help.parameter') } /entries/:collection_name 23 | th = I18n.t('liquor_help.description') 24 | tr 25 | td entry[:field_liquor_name] 26 | td = I18n.t('liquor_help.endpoints.entries.post.parameters.entry') 27 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/help/models_variables.html.slim: -------------------------------------------------------------------------------- 1 | h4 = I18n.t("liquor_help.variables_title") 2 | p 3 | ul 4 | li 5 | var models 6 | p 7 | span = I18n.t('liquor_help.variables.models.main_description') 8 | p 9 | code 10 | | {% for service in: models.services do: %} 11 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/models/edit.html.slim: -------------------------------------------------------------------------------- 1 | .row 2 | .col-lg-12 3 | form role="form" ng-submit="update()" novalidate="" 4 | ng-include src="'models/form.html'" 5 | ng-include src="'models/fields.html'" 6 | button.btn.btn-default type="submit" = I18n.t(:update_model) 7 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/models/fields.html.slim: -------------------------------------------------------------------------------- 1 | .form-group ng-controller="FieldsController" 2 | table.table 3 | thead 4 | tr 5 | th colspan="4" 6 | h4 = Kms::Model.human_attribute_name(:fields) 7 | tbody as-sortable="fieldsSortableOptions" ng-model="model.fields_attributes" 8 | tr ng-repeat="field in model.fields_attributes" ng-hide="field._destroy" as-sortable-item="" 9 | td 10 | i.fa.fa-bars as-sortable-item-handle="" 11 | | {{ field.name }} 12 | td 13 | | {{ field.liquor_name }} 14 | td 15 | i.fa ng-class="findTypeByField(field).icon" 16 | | {{ formatType(field) }} 17 | td 18 | | {{ field.required }} 19 | td 20 | .btn-group.pull-right 21 | a.btn.btn-sm.btn-danger ng-click="removeField(field)" 22 | i.fa.fa-times 23 | tr ng-hide="model.fields_attributes.length > 0" 24 | td 25 | i = I18n.t(:no_fields_yet) 26 | .row 27 | .col-lg-3 28 | input.form-control type="text" required="" placeholder=Kms::Field.human_attribute_name(:name) ng-model="field.name" 29 | .col-lg-3 30 | input.form-control type="text" required="" placeholder=Kms::Field.human_attribute_name(:liquor_name) ng-model="field.liquor_name" 31 | .col-lg-2 32 | ui-select ng-model="field.type" theme="bootstrap" 33 | ui-select-match placeholder=I18n.t(:select_field_type) 34 | i.fa ng-class="$select.selected.icon" 35 | | {{ $select.selected.name }} 36 | ui-select-choices repeat="type.id as type in types" 37 | i.fa ng-class="type.icon" 38 | | {{ type.name | highlight: $select.search }} 39 | .col-lg-2 ng-show="isAssociationField(field)" 40 | select#field_class_name.form-control ng-model="field.class_name" ng-options="templatable_type.type as templatable_type.title for templatable_type in templatable_types" required="" placeholder=Kms::Field.human_attribute_name(:class_name) 41 | option value="" disabled="" selected="" = I18n.t(:select_model) 42 | .col-lg-2 43 | a.btn.btn-small.btn-primary ng-click="addField()" 44 | i.fa.fa-plus 45 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/models/form.html.slim: -------------------------------------------------------------------------------- 1 | .row 2 | .col-md-6.col-sm-12 3 | .form-group 4 | label for="kms_model_name" = Kms::Model.human_attribute_name(:kms_model_name) 5 | input#kms_model_name.form-control type="text" ng-model="model.kms_model_name" 6 | .form-group 7 | label for="collection_name" = Kms::Model.human_attribute_name(:collection_name) 8 | input#collection_name.form-control type="text" ng-model="model.collection_name" 9 | small.help-block = I18n.t(:collection_name_field_hint) 10 | .form-group 11 | label for="description" = Kms::Model.human_attribute_name(:description) 12 | textarea#description.form-control ng-model="model.description" 13 | small.help-block = I18n.t(:description_field_hint) 14 | .form-group 15 | label for="label_field" = Kms::Model.human_attribute_name(:label_field) 16 | select#label_field.form-control ng-model="model.label_field" ng-options="field.liquor_name as field.name for field in model.fields_attributes" 17 | small.help-block = I18n.t(:label_field_hint) 18 | .form-group 19 | label for="allow_creation_using_form" style="margin-right:10px;" 20 | = Kms::Model.human_attribute_name(:allow_creation_using_form) 21 | toggle-switch ng-model="model.allow_creation_using_form" on-label=I18n.t(:yes_word) off-label=I18n.t(:no_word) 22 | small.help-block = I18n.t(:allow_creation_using_form_field_hint) 23 | .col-md-6.col-sm-12 24 | .widget.model-iteration-code-snippet 25 | .widget-body 26 | .widget-content 27 | .widget-icon.pull-left 28 | i class="fa fa-clipboard" 29 | .title Model iteration hint 30 | .comment Use sample code below for iterating your model entries in Template, Page or Snippet: 31 | br 32 | pre 33 | | {% for element in: models.{{ model.collection_name }} do: %} 34 | br 35 | code ng-non-bindable="" 36 | | <p>{{ 37 | | element.{{ model.label_field }} 38 | code ng-non-bindable="" 39 | | }}</p> 40 | br 41 | | {% end for %} 42 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/models/index.html.slim: -------------------------------------------------------------------------------- 1 | .row ng-show="models.length" 2 | .col-lg-12 3 | .widget 4 | .widget-header 5 | i.fa.fa-tasks 6 | = Kms::Model.model_name.human(count: 1.1) 7 | a.btn.btn-sm.btn-primary.pull-right ui-sref="models.new" 8 | = I18n.t("add_model") 9 | .widget-body.no-padding 10 | .table-responsive 11 | table.table 12 | tbody 13 | tr ng-repeat="model in models" 14 | td style="width: 80%" 15 | a ui-sref="models.edit({id: model.id})" 16 | | {{ model.kms_model_name }} 17 | td 18 | .btn-group.pull-right 19 | a.btn.btn-sm.btn-danger ng-click="destroy(model)" ng-show="currentUser.admin" 20 | i.fa.fa-times 21 | .row ng-show="!models.length" 22 | .center-block 23 | .jumbotron.vertical-center.text-center 24 | .container 25 | h1 26 | span.fa-stack.fa-lg 27 | i.fa.fa-circle.fa-stack-2x 28 | i.fa.fa-tasks.fa-stack-1x 29 | p = I18n.t(:models_description) 30 | p 31 | a.btn.btn-primary.btn-lg ui-sref="models.new" role="button" = I18n.t(:create_first_model) 32 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/models/new.html.slim: -------------------------------------------------------------------------------- 1 | .row 2 | .col-lg-12 3 | form role="form" ng-submit="create()" novalidate="" 4 | ng-include src="'models/form.html'" 5 | ng-include src="'models/fields.html'" 6 | button.btn.btn-default type="submit" = I18n.t(:add_model) 7 | -------------------------------------------------------------------------------- /app/assets/stylesheets/kms_models/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any styles 10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new 11 | * file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | .as-sortable-dragging td { 17 | padding: 0 10px; 18 | } 19 | .widget.model-iteration-code-snippet { 20 | margin-top: 23px; 21 | } 22 | .widget.model-iteration-code-snippet .widget-icon { 23 | background: #ff5274; 24 | } 25 | -------------------------------------------------------------------------------- /app/controllers/kms/models/entries_controller.rb: -------------------------------------------------------------------------------- 1 | module Kms 2 | module Models 3 | class EntriesController < ApplicationController 4 | def index 5 | model = Model.find(params[:model_id]) 6 | @entries = model.entries.order('position') 7 | render json: @entries, root: false 8 | end 9 | 10 | def show 11 | model = Model.find(params[:model_id]) 12 | @entry = model.entries.find(params[:id]) 13 | render json: @entry, root: false 14 | end 15 | 16 | def create 17 | model = Model.find(params[:model_id]) 18 | @entry = model.entries.new(entry_params) 19 | if @entry.save 20 | render json: @entry, root: false 21 | else 22 | render json: { errors: @entry.errors.full_messages }.to_json, status: :unprocessable_entity 23 | end 24 | end 25 | 26 | def update 27 | model = Model.find(params[:model_id]) 28 | @entry = model.entries.find(params[:id]) 29 | if @entry.update_attributes(entry_params) 30 | render json: @entry, root: false 31 | else 32 | render json: { errors: @entry.errors.full_messages }.to_json, status: :unprocessable_entity 33 | end 34 | end 35 | 36 | def destroy 37 | @entry = Entry.find(params[:id]) 38 | @entry.destroy 39 | render json: @entry, root: false 40 | end 41 | 42 | protected 43 | 44 | def entry_params 45 | params.require(:entry).permit! 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /app/controllers/kms/models/fields_controller.rb: -------------------------------------------------------------------------------- 1 | module Kms 2 | module Models 3 | class FieldsController < ApplicationController 4 | wrap_parameters :field, include: [:position] 5 | 6 | def update 7 | model = Model.find(params[:model_id]) 8 | @field = model.fields.find(params[:id]) 9 | if @field.update(field_params) 10 | head :no_content 11 | else 12 | render json: @field.to_json(methods: :errors), status: :unprocessable_entity 13 | end 14 | end 15 | 16 | protected 17 | 18 | def field_params 19 | params.require(:field).permit(:position) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/controllers/kms/models/models_controller.rb: -------------------------------------------------------------------------------- 1 | module Kms 2 | module Models 3 | class ModelsController < ApplicationController 4 | wrap_parameters :model, include: [:kms_model_name, :collection_name, :description, :label_field, :fields_attributes, :allow_creation_using_form] 5 | 6 | def index 7 | render json: Model.all, root: false 8 | end 9 | 10 | def show 11 | @model = Model.find(params[:id]) 12 | render json: @model, root: false 13 | end 14 | 15 | def create 16 | @model = Model.new(model_params) 17 | if @model.save 18 | Kms::ResourceService.register(:models, @model, "fa-tasks") 19 | Kms::ModelsWrapperDrop.register_model @model.collection_name 20 | else 21 | render json: { errors: @model.errors.full_messages }.to_json, status: :unprocessable_entity 22 | end 23 | end 24 | 25 | def update 26 | @model = Model.find(params[:id]) 27 | unless @model.update_attributes(model_params) 28 | render json: { errors: @model.errors.full_messages }.to_json, status: :unprocessable_entity 29 | end 30 | end 31 | 32 | def destroy 33 | @model = Model.find(params[:id]) 34 | @model.destroy 35 | Kms::ResourceService.unregister(:models, @model) 36 | end 37 | 38 | protected 39 | 40 | def model_params 41 | params.require(:model).permit(:kms_model_name, :collection_name, :description, :label_field, :allow_creation_using_form, fields_attributes: [:id, :name, :liquor_name, :type, :class_name, :_destroy]) 42 | end 43 | 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /app/controllers/kms/public/entries_controller.rb: -------------------------------------------------------------------------------- 1 | module Kms 2 | module Public 3 | class EntriesController < ActionController::Base 4 | protect_from_forgery with: :exception 5 | before_action :find_model 6 | 7 | def create 8 | entry = @model.entries.new(values: entry_params) 9 | unless @model.allow_creation_using_form? && entry.save 10 | render json: {errors: entry.errors}.to_json, status: :unprocessable_entity 11 | end 12 | end 13 | 14 | protected 15 | 16 | def find_model 17 | @model = Model.find_by!(collection_name: params[:collection_name]) 18 | end 19 | 20 | def entry_params 21 | params.require(:entry).permit(@model.fields.pluck(:liquor_name)) 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/helpers/kms_models/application_helper.rb: -------------------------------------------------------------------------------- 1 | module KmsModels 2 | module ApplicationHelper 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/models/kms/belongs_to_field.rb: -------------------------------------------------------------------------------- 1 | module Kms 2 | class BelongsToField < Field 3 | def get_value(entry) 4 | entry_id = entry.values[liquor_name] 5 | association_record = Kms::Entry.find_by(id: entry_id) 6 | Liquor::DropDelegation.wrap_element(association_record) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/models/kms/checkbox_field.rb: -------------------------------------------------------------------------------- 1 | module Kms 2 | class CheckboxField < Field 3 | def get_value(entry) 4 | entry.values[liquor_name] == 'true' 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/models/kms/date_field.rb: -------------------------------------------------------------------------------- 1 | module Kms 2 | class DateField < Field 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/models/kms/entry.rb: -------------------------------------------------------------------------------- 1 | module Kms 2 | class Entry < ActiveRecord::Base 3 | include Liquor::Dropable 4 | extend ::FriendlyId 5 | include CompileTemplates 6 | 7 | friendly_id :slug_candidates, use: :slugged 8 | belongs_to :model 9 | after_save :store_files, if: :cache_names_present? 10 | 11 | validates :slug, uniqueness: { scope: :model_id } 12 | 13 | attr_reader :cache_names 14 | 15 | def slug_candidates 16 | [values[model.label_field]] 17 | end 18 | 19 | def permalink 20 | templatable_page = Kms::Page.where(templatable_type: model.name).first 21 | return nil unless templatable_page 22 | Pathname.new(templatable_page.parent.fullpath).join(slug.to_s).to_s 23 | end 24 | 25 | def values 26 | read_attribute(:values) || {} 27 | end 28 | 29 | def values=(new_values) 30 | files_params = new_values.select { |_, v| v.is_a?(ActionDispatch::Http::UploadedFile) || v.is_a?(File) } 31 | @cache_names ||= {} 32 | files_params.each do |k, v| 33 | uploader = EntryFileUploader.new(OpenStruct.new(model: self, field_name: k)) 34 | uploader.cache!(v) 35 | @cache_names[k] = uploader.cache_name 36 | files_params[k] = v.original_filename 37 | end 38 | super values.merge new_values.merge(files_params) 39 | end 40 | 41 | def method_missing(name, *args, &block) 42 | model.fields.where(liquor_name: name.to_s).exists? ? values[name.to_s] : super 43 | end 44 | 45 | def respond_to_missing?(method_name, include_private = false) 46 | model.fields.where(liquor_name: method_name.to_s).exists? || super 47 | end 48 | 49 | protected 50 | 51 | def cache_names_present? 52 | cache_names.present? 53 | end 54 | 55 | def store_files 56 | values.each do |k, v| 57 | next unless cache_names.keys.include? k 58 | uploader = EntryFileUploader.new(OpenStruct.new(model: self, field_name: k)) 59 | uploader.retrieve_from_cache!(cache_names[k]) 60 | uploader.store! 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /app/models/kms/field.rb: -------------------------------------------------------------------------------- 1 | module Kms 2 | class Field < ActiveRecord::Base 3 | belongs_to :model 4 | scope :file_fields, -> { where(type: Kms::FileField.name) } 5 | scope :checkbox_fields, -> { where(type: Kms::CheckboxField.name) } 6 | scope :date_fields, -> { where(type: Kms::DateField.name) } 7 | 8 | def get_value(entry) 9 | # OVERRIDE in subclasses if needed 10 | entry.values[liquor_name] 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/models/kms/file_field.rb: -------------------------------------------------------------------------------- 1 | module Kms 2 | class FileField < Field 3 | def get_value(entry) 4 | value = entry.values[liquor_name] 5 | uploader = EntryFileUploader.new(OpenStruct.new(model: entry, field_name: liquor_name)) 6 | uploader.retrieve_from_store! value 7 | uploader.file.exists? ? uploader.url : nil 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/models/kms/has_many_field.rb: -------------------------------------------------------------------------------- 1 | module Kms 2 | class HasManyField < Field 3 | def get_value(entry) 4 | entry_ids = entry.values[liquor_name] 5 | association_records = Kms::Entry.where(id: entry_ids) 6 | if entry_ids.present? 7 | # this one allows ORDER BY the IN value list like this example: 8 | # SELECT * FROM "comments" WHERE ("comments"."id" IN (1,3,2,4)) 9 | # ORDER BY id=1 DESC, id=3 DESC, id=2 DESC, id=4 DESC 10 | order_sql = entry_ids.map {|entry_id| "id=#{entry_id} DESC"} 11 | association_records = association_records.order(order_sql.join(',')) 12 | end 13 | Liquor::DropDelegation.wrap_scope(association_records) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/models/kms/model.rb: -------------------------------------------------------------------------------- 1 | module Kms 2 | class Model < ActiveRecord::Base 3 | has_many :fields, class_name: 'Kms::Field', dependent: :destroy 4 | has_many :entries, class_name: 'Kms::Entry', dependent: :destroy 5 | accepts_nested_attributes_for :fields, allow_destroy: true 6 | 7 | validates :kms_model_name, :collection_name, presence: true 8 | 9 | def name 10 | id.to_s 11 | end 12 | 13 | # Hacking hack 14 | def model_name 15 | value = kms_model_name 16 | value.instance_eval do 17 | def human(options={}) 18 | self 19 | end 20 | end 21 | value 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/models/kms/models_wrapper.rb: -------------------------------------------------------------------------------- 1 | module Kms 2 | class ModelsWrapper 3 | include Liquor::Dropable 4 | 5 | def method_missing(name, *args, &block) 6 | model = Kms::Model.find_by(collection_name: name.to_s) 7 | model ? model.entries.order('position') : super 8 | end 9 | 10 | def respond_to_missing?(method_name, include_private = false) 11 | Kms::Model.where(collection_name: method_name.to_s).exists? || super 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/models/kms/page_decorator.rb: -------------------------------------------------------------------------------- 1 | Kms::Page.class_eval do 2 | # fetch item by slug 3 | def fetch_item(slug) 4 | return nil unless templatable? 5 | templatable_type.constantize.find_by_slug!(slug) 6 | rescue NameError 7 | model = Kms::Model.find(templatable_type.to_i) 8 | model.entries.find_by_slug(slug) 9 | # Kms::Entry.find_by_slug(slug) 10 | end 11 | 12 | # fetch items for templatable page 13 | def fetch_items 14 | templatable_type.constantize.all 15 | rescue NameError 16 | # in templatable_type we store id of Kms::Model object in this case 17 | model = Kms::Model.find(templatable_type.to_i) 18 | model.entries 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/models/kms/string_field.rb: -------------------------------------------------------------------------------- 1 | module Kms 2 | class StringField < Field 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/models/kms/text_field.rb: -------------------------------------------------------------------------------- 1 | module Kms 2 | class TextField < Field 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/serializers/kms/entry_serializer.rb: -------------------------------------------------------------------------------- 1 | module Kms 2 | class EntrySerializer < ActiveModel::Serializer 3 | attributes :id, :values, :position, :slug 4 | 5 | has_one :model, serializer: Kms::SimpleModelSerializer 6 | 7 | # OPTIMIZE 8 | def values 9 | values_with_urls = object.values.dup 10 | # prepare file fields 11 | object.model.fields.file_fields.each do |file_field| 12 | uploader = EntryFileUploader.new(OpenStruct.new(model: object, field_name: file_field.liquor_name)) 13 | uploader.retrieve_from_store! values_with_urls[file_field.liquor_name] 14 | values_with_urls[file_field.liquor_name] = { url: uploader.file.exists? ? uploader.url : nil } 15 | end 16 | # prepare checkbox fields - cause PostgreSQL json stored as strings 17 | object.model.fields.checkbox_fields.each do |checkbox_field| 18 | values_with_urls[checkbox_field.liquor_name] = values_with_urls[checkbox_field.liquor_name] == 'true' 19 | end 20 | # prepare checkbox fields - cause PostgreSQL json stored as strings 21 | object.model.fields.date_fields.each do |date_field| 22 | values_with_urls[date_field.liquor_name] = Date.parse(values_with_urls[date_field.liquor_name]) 23 | end 24 | values_with_urls 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/serializers/kms/field_serializer.rb: -------------------------------------------------------------------------------- 1 | module Kms 2 | class FieldSerializer < ActiveModel::Serializer 3 | attributes :id, :name, :liquor_name, :type, :class_name 4 | end 5 | 6 | end 7 | -------------------------------------------------------------------------------- /app/serializers/kms/model_serializer.rb: -------------------------------------------------------------------------------- 1 | module Kms 2 | class ModelSerializer < ActiveModel::Serializer 3 | attributes :id, :kms_model_name, :collection_name, :description, :label_field, :allow_creation_using_form, :fields_attributes 4 | 5 | has_many :fields_attributes, serializer: Kms::FieldSerializer 6 | 7 | def fields_attributes 8 | object.fields.order(:position) 9 | end 10 | 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/serializers/kms/simple_model_serializer.rb: -------------------------------------------------------------------------------- 1 | module Kms 2 | class SimpleModelSerializer < ActiveModel::Serializer 3 | attributes :id, :label_field 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/uploaders/entry_file_uploader.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | class EntryFileUploader < CarrierWave::Uploader::Base 4 | 5 | # Include RMagick or MiniMagick support: 6 | # include CarrierWave::RMagick 7 | # include CarrierWave::MiniMagick 8 | 9 | # Choose what kind of storage to use for this uploader: 10 | storage (ENV['KMS_ASSETS_STORAGE'] && ENV['KMS_ASSETS_STORAGE'].to_sym) || :file 11 | 12 | # Override the directory where uploaded files will be stored. 13 | # This is a sensible default for uploaders that are meant to be mounted: 14 | def store_dir 15 | "uploads/#{model.model.class.to_s.underscore}/#{model.model.id}/#{model.field_name}" 16 | end 17 | 18 | # Provide a default URL as a default if there hasn't been a file uploaded: 19 | # def default_url 20 | # # For Rails 3.1+ asset pipeline compatibility: 21 | # # ActionController::Base.helpers.asset_path("fallback/" + [version_name, "default.png"].compact.join('_')) 22 | # 23 | # "/images/fallback/" + [version_name, "default.png"].compact.join('_') 24 | # end 25 | 26 | # Process files as they are uploaded: 27 | # process :scale => [200, 300] 28 | # 29 | # def scale(width, height) 30 | # # do something 31 | # end 32 | 33 | # Create different versions of your uploaded files: 34 | # version :thumb do 35 | # process :resize_to_fit => [50, 50] 36 | # end 37 | 38 | # Add a white list of extensions which are allowed to be uploaded. 39 | # For images you might use something like this: 40 | # def extension_white_list 41 | # %w(jpg jpeg gif png) 42 | # end 43 | 44 | # Override the filename of the uploaded files: 45 | # Avoid using model.id or version_name here, see uploader/store.rb for details. 46 | # def filename 47 | # "something.jpg" if original_filename 48 | # end 49 | 50 | end 51 | -------------------------------------------------------------------------------- /app/views/layouts/kms_models/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |