├── .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 | [![Build Status](https://travis-ci.org/apiqcms/kms_models.svg?branch=master)](https://travis-ci.org/apiqcms/kms_models) 4 | [![Code Climate](https://codeclimate.com/github/apiqcms/kms_models/badges/gpa.svg)](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 | [![Getting started with KMS Models extension](http://img.youtube.com/vi/WPZoWyd-thE/0.jpg)](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 | KmsModels 5 | <%= stylesheet_link_tag "kms_models/application", media: "all" %> 6 | <%= javascript_include_tag "kms_models/application" %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application. 3 | 4 | ENGINE_ROOT = File.expand_path('../..', __FILE__) 5 | ENGINE_PATH = File.expand_path('../../lib/kms_models/engine', __FILE__) 6 | 7 | # Set up gems listed in the Gemfile. 8 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 9 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 10 | 11 | require 'rails/all' 12 | require 'rails/engine/commands' 13 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | 4 | Bundler.require :default, :development 5 | 6 | Combustion.initialize! :all 7 | run Combustion::Application 8 | -------------------------------------------------------------------------------- /config/initializers/ability.rb: -------------------------------------------------------------------------------- 1 | if Kms::Model.table_exists? 2 | Kms::Model.all.each do |model| 3 | Kms::AbilityService.register do 4 | can :manage, model 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /config/initializers/externals.rb: -------------------------------------------------------------------------------- 1 | Kms::ExternalsRegistry.register(:models) {|_,_| Kms::ModelsWrapper.new.to_drop } 2 | -------------------------------------------------------------------------------- /config/initializers/help.rb: -------------------------------------------------------------------------------- 1 | Kms::HelpService.register_templates Kms::Models::Engine, 'help/models_variables.html', 'help/models_endpoints.html' 2 | -------------------------------------------------------------------------------- /config/initializers/resources.rb: -------------------------------------------------------------------------------- 1 | Kms::ResourceService.register(:models, Kms::Model, "fa-tasks") 2 | if Kms::Model.table_exists? 3 | Kms::Model.all.each do |model| 4 | Kms::ResourceService.register(:models, model, "fa-tasks") 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | models: "Models" 3 | add_model: "Add Model" 4 | new_model: "New Model" 5 | edit_model: "Edit Model" 6 | update_model: "Update Model" 7 | add_entry: "Add Entry" 8 | new_entry: "New Entry" 9 | edit_entry: "Edit Entry" 10 | update_entry: "Update Entry" 11 | no_fields_yet: "No fields yet. Add first field using form below." 12 | select_field_type: "Select field type" 13 | select_model: "Select Model" 14 | collection_name_field_hint: "You can access model collection like this: models.your_collection_name (ex., models.services)" 15 | description_field_hint: "Optional field. Just write some description so everyone would be aware what's the purpose of this Model" 16 | label_field_hint: "Add at least one Field below. And then you could choose one that would be used for item permalink generation and entries list displaying" 17 | allow_creation_using_form_field_hint: "On website you can place a form allowing to create model entries" 18 | has_many_field_placeholder: "Select related objects..." 19 | belongs_to_field_placeholder: "Select related object..." 20 | models_description: "Models are where you organize your content (basically dynamic) like 'News', 'Blog', 'Services' and etc." 21 | create_first_model: "Create first model" 22 | entries_description: "Here you would create content for corresponding Model" 23 | create_first_entry: "Create first entry" 24 | field_types: 25 | string: "String" 26 | text: "Text" 27 | checkbox: "Checkbox" 28 | date: "Date" 29 | file: "File" 30 | has_many: "Has many" 31 | belongs_to: "Belongs to" 32 | liquor_help: 33 | variables: 34 | models: 35 | main_description: 'Variable "models" gives access to models collections of entries. You can access collection using model collection name - "Collection name (for Liquor)" field. For example, if you created some Model with "services" collection name, then you could iterate its collection using "for" tag like this:' 36 | endpoints: 37 | entries: 38 | post: 39 | main_description: This endpoint allows you to setup form submitting Model's elements. For example, if you have a model with collection name "posts" (and field with "title" Liquor name), you could create a form with action="/entries/posts" and an input with name="entry[title]" 40 | parameters: 41 | entry: Value of each parameter will be saved to corresponding Model's entry field 42 | 43 | activerecord: 44 | models: 45 | kms/model: 46 | one: "Model" 47 | few: "Models" 48 | many: "Models" 49 | other: "Models" 50 | kms/entry: 51 | one: "Entry" 52 | few: "Entries" 53 | many: "Entries" 54 | other: "Entries" 55 | attributes: 56 | kms/model: 57 | kms_model_name: "Name" 58 | collection_name: "Collection name (for Liquor)" 59 | description: "Description" 60 | fields: "Fields" 61 | label_field: "Label field (used for URL/slug generating)" 62 | allow_creation_using_form: "Allow creation using form" 63 | kms/field: 64 | name: "Name" 65 | liquor_name: "Name for Liquor" 66 | type: "Type" 67 | required: "Required" 68 | kms/entry: 69 | slug: "Slug" 70 | -------------------------------------------------------------------------------- /config/locales/ru.yml: -------------------------------------------------------------------------------- 1 | ru: 2 | models: "Модели" 3 | add_model: "Добавить модель" 4 | new_model: "Новая Модель" 5 | edit_model: "Редактирование модели" 6 | update_model: "Обновить модель" 7 | add_entry: "Добавить элемент" 8 | new_entry: "Новый Элемент" 9 | edit_entry: "Редактирование Элемента" 10 | update_entry: "Обновить элемент" 11 | no_fields_yet: "Пока нет ни одного свойства. Добавьте первое свойство с помощью формы ниже." 12 | select_field_type: "Выберите тип свойства" 13 | select_model: "Выберите модель" 14 | collection_name_field_hint: "К коллекции можно будет обратиться так: models.your_collection_name (напр., models.services)" 15 | description_field_hint: "Необязательное поле. Напишите небольшое описание, чтобы было понятно назначение данной Модели" 16 | label_field_hint: "Добавьте хотя бы одно Свойство ниже. И затем вы можете выбрать свойство для генерации ссылки на элемент, а так же для отображения элементов в списке" 17 | allow_creation_using_form_field_hint: "На сайте можно будет разместить форму для создания элементов модели" 18 | has_many_field_placeholder: "Выберите связанные объекты..." 19 | belongs_to_field_placeholder: "Выберите связанный объект..." 20 | models_description: "Модели - это ваш динамический контент. Например, 'Новости', 'Блог', 'Услуги' и тд." 21 | create_first_model: "Создать первую модель" 22 | entries_description: "Здесь вы можете создавать элементы соответствующей Модели" 23 | create_first_entry: "Создать первый элемент" 24 | field_types: 25 | string: "Строка" 26 | text: "Текст" 27 | checkbox: "Чекбокс" 28 | date: "Дата" 29 | file: "Файл" 30 | has_many: "Имеет много" 31 | belongs_to: "Связано с" 32 | liquor_help: 33 | variables: 34 | models: 35 | main_description: 'Переменная models предоставляет доступ к коллекциям моделей, определённых пользователем на вкладке "Модели". Доступ осуществляется через название коллекции модели - поле "Название коллекции (для Liquor)" при создании модели. Например, если создана модель с названием коллекции services, то можно использовать for для итерации по элементам коллекции следующим образом:' 36 | endpoints: 37 | entries: 38 | post: 39 | main_description: Этот запрос позволяет создавать формы для отправки элементов модели. Например, если есть модель с названием коллекции "posts" (и поле с названием для Liquor - "title"), то можно создать форму с action="/entries/posts" и input с name="entry[title]" 40 | parameters: 41 | entry: Значение каждого параметра такого вида будет сохранено в соответствующее поле элемента модели 42 | activerecord: 43 | models: 44 | kms/model: 45 | one: "Модель" 46 | few: "Модели" 47 | many: "Моделей" 48 | other: "Модели" 49 | kms/entry: 50 | one: "Элемент" 51 | few: "Элемента" 52 | many: "Элементов" 53 | other: "Элементы" 54 | attributes: 55 | kms/model: 56 | kms_model_name: "Название" 57 | collection_name: "Название коллекции (для Liquor)" 58 | description: "Описание" 59 | fields: "Свойства" 60 | label_field: "Поле, используемое для генерации ссылок на объекты" 61 | allow_creation_using_form: "Разрешить создание элементов с помощью форм" 62 | kms/field: 63 | name: "Название" 64 | liquor_name: "Название для Liquor" 65 | type: "Тип" 66 | required: "Обязательное" 67 | kms/entry: 68 | slug: "Ссылка/URL" 69 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Kms::Models::Engine.routes.draw do 2 | constraints(format: "json") do 3 | resources :models, format: true do 4 | resources :entries, format: true do 5 | member do 6 | post '' => 'entries#update' 7 | end 8 | end 9 | resources :fields, only: :update, format: true 10 | end 11 | end 12 | end 13 | Rails.application.routes.draw do 14 | post '/entries/:collection_name' => 'kms/public/entries#create' 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20150409124420_create_kms_models.rb: -------------------------------------------------------------------------------- 1 | class CreateKmsModels < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :kms_models do |t| 4 | t.string :model_name 5 | t.string :collection_name 6 | 7 | t.timestamps null: false 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20150409125056_create_kms_fields.rb: -------------------------------------------------------------------------------- 1 | class CreateKmsFields < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :kms_fields do |t| 4 | t.string :name 5 | t.string :liquor_name 6 | t.string :type 7 | t.boolean :required 8 | t.belongs_to :model 9 | 10 | t.timestamps null: false 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20150413143711_create_kms_entries.rb: -------------------------------------------------------------------------------- 1 | class CreateKmsEntries < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :kms_entries do |t| 4 | t.belongs_to :model, index: true 5 | t.json :values 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20150820080436_add_label_field_to_kms_models.rb: -------------------------------------------------------------------------------- 1 | class AddLabelFieldToKmsModels < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :kms_models, :label_field, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150820132142_add_slug_to_kms_entries.rb: -------------------------------------------------------------------------------- 1 | class AddSlugToKmsEntries < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :kms_entries, :slug, :string 4 | add_index :kms_entries, :slug, unique: true 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20150821201250_fix_models_column_name.rb: -------------------------------------------------------------------------------- 1 | class FixModelsColumnName < ActiveRecord::Migration[4.2] 2 | def change 3 | rename_column :kms_models, :model_name, :kms_model_name 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150901115303_add_class_name_to_kms_fields.rb: -------------------------------------------------------------------------------- 1 | class AddClassNameToKmsFields < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :kms_fields, :class_name, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150910081440_add_position_to_kms_entries.rb: -------------------------------------------------------------------------------- 1 | class AddPositionToKmsEntries < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :kms_entries, :position, :integer, default: 0, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170209125819_add_allow_creation_using_form_to_models.rb: -------------------------------------------------------------------------------- 1 | class AddAllowCreationUsingFormToModels < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :kms_models, :allow_creation_using_form, :boolean, default: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170802063046_change_values_column_to_jsonb.rb: -------------------------------------------------------------------------------- 1 | class ChangeValuesColumnToJsonb < ActiveRecord::Migration[5.1] 2 | def change 3 | execute "ALTER TABLE kms_entries ALTER COLUMN values SET DATA TYPE jsonb USING values::jsonb" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170802085121_add_position_to_kms_fields.rb: -------------------------------------------------------------------------------- 1 | class AddPositionToKmsFields < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :kms_fields, :position, :integer, default: 0, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180122135245_add_description_to_kms_models.rb: -------------------------------------------------------------------------------- 1 | class AddDescriptionToKmsModels < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :kms_models, :description, :text 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /kms_models.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | 3 | # Maintain your gem's version: 4 | require "kms/models/version" 5 | 6 | # Describe your gem and declare its dependencies: 7 | Gem::Specification.new do |s| 8 | s.name = "kms_models" 9 | s.version = Kms::Models::VERSION 10 | s.authors = ["Igor Petrov"] 11 | s.email = ["garik.piton@gmail.com"] 12 | s.homepage = "https://github.com/webgradus/kms_models" 13 | s.summary = "Extension for KMS" 14 | s.description = "KMS Models allows to define custom models on-the-fly." 15 | s.license = "MIT" 16 | 17 | s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.rdoc"] 18 | s.test_files = Dir["test/**/*"] 19 | 20 | s.add_dependency 'friendly_id', '~> 5.0.0' 21 | s.add_dependency 'kms', ">= 1.0.0" 22 | 23 | s.add_development_dependency 'combustion', '~> 0.5' 24 | s.add_development_dependency 'factory_girl_rails', '~> 4.8' 25 | s.add_development_dependency 'rspec-rails', '~> 3.5', '>= 3.5.0' 26 | s.add_development_dependency 'shoulda-matchers', '~> 3.1' 27 | end 28 | -------------------------------------------------------------------------------- /lib/drops/kms/entry_drop.rb: -------------------------------------------------------------------------------- 1 | module Kms 2 | class EntryDrop < Liquor::Drop 3 | 4 | attributes :id, :created_at, :slug, :permalink, :model_collection_name 5 | scopes :order 6 | 7 | # overriding methods cause we work with 'json' column 8 | class Scope 9 | def find_by(_, fields={}) 10 | fields, = Liquor::Drop.unwrap_scope_arguments([ fields ]) 11 | 12 | plain_fields, json_fields = fields_partition(fields) 13 | result = @source.where(fields_query(fields), *(json_fields.values + plain_fields.values).map(&:to_s)).first 14 | Liquor::DropDelegation.wrap_element result if result 15 | end 16 | 17 | def find_all_by(_, fields={}) 18 | fields, = Liquor::Drop.unwrap_scope_arguments([ fields ]) 19 | 20 | plain_fields, json_fields = fields_partition(fields) 21 | result = @source.where(fields_query(fields), *(json_fields.values + plain_fields.values).map(&:to_s)) 22 | Liquor::DropDelegation.wrap_scope(result) 23 | end 24 | 25 | def order(*args) 26 | args = Liquor::Drop.unwrap_scope_arguments(args) 27 | parsed_args = args.map do |arg| 28 | order_clause = arg.split(' ') 29 | if order_clause[0].in? Kms::Entry.column_names - ['values'] 30 | arg 31 | else 32 | ["values ->> '#{order_clause[0]}'", order_clause[1].to_s].join(' ') 33 | end 34 | end 35 | # we use reorder because by default we order by position 36 | Liquor::DropDelegation.wrap_scope @source.reorder(*parsed_args) 37 | end 38 | 39 | private 40 | 41 | def fields_partition(fields) 42 | fields.partition {|name, _| (Kms::Entry.column_names - ['values']).include? name.to_s}.map(&:to_h) 43 | end 44 | 45 | def fields_query(fields) 46 | plain_fields, json_fields = fields_partition(fields) 47 | json_fields_query = json_fields.map {|name, _| "values ->> '#{name}' = ?" }.join(" AND ") 48 | plain_fields_query = plain_fields.map {|name, _| "#{name} = ?"}.join(" AND ") 49 | [json_fields_query, plain_fields_query].reject(&:empty?).join(' OR ') 50 | end 51 | 52 | end 53 | 54 | def initialize(source) 55 | self.class.instance_eval do 56 | source.model.fields.pluck(:liquor_name).each do |field_name| 57 | export field_name.to_sym 58 | end 59 | end 60 | super(source) 61 | end 62 | 63 | def method_missing(name, *args, &block) 64 | field = source.model.fields.find_by(liquor_name: name.to_s) 65 | field ? field.get_value(source) : super 66 | end 67 | 68 | def respond_to_missing?(method_name, include_private = false) 69 | source.model.fields.where(liquor_name: method_name.to_s).exists? || super 70 | end 71 | 72 | def created_at 73 | source.created_at.to_s 74 | end 75 | 76 | def model_collection_name 77 | source.model.collection_name 78 | end 79 | 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/drops/kms/models_wrapper_drop.rb: -------------------------------------------------------------------------------- 1 | module Kms 2 | class ModelsWrapperDrop < Liquor::Drop 3 | def self.register_model(collection_name) 4 | has_many collection_name.to_sym 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/generators/kms_models/install/install_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators' 2 | module KmsModels 3 | class InstallGenerator < Rails::Generators::Base 4 | 5 | source_root File.expand_path('../../../../../', __FILE__) 6 | 7 | def insert_engine_routes 8 | route %( 9 | mount Kms::Models::Engine => '/kms' 10 | ) 11 | end 12 | 13 | def insert_javascript 14 | append_file "app/assets/javascripts/application.js", "//= require kms_models/application\n" 15 | end 16 | 17 | def insert_stylesheet 18 | gsub_file "app/assets/stylesheets/application.css", '*/', "*= require kms_models/application\n*/" 19 | end 20 | 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/kms/models/engine.rb: -------------------------------------------------------------------------------- 1 | require "friendly_id" 2 | module Kms 3 | module Models 4 | class Engine < ::Rails::Engine 5 | engine_name 'kms_models' 6 | isolate_namespace Kms::Models 7 | config.eager_load_paths += Dir["#{config.root}/lib/**/"] 8 | config.to_prepare do 9 | Dir.glob(File.join(File.dirname(__FILE__), "../../../app/**/*_decorator*.rb")) do |c| 10 | require_dependency(c) 11 | end 12 | end 13 | 14 | initializer "kms_models.register_models_collections" do |app| 15 | app.config.after_initialize do 16 | Kms::Model.pluck(:collection_name).each do |collection_name| 17 | Kms::ModelsWrapperDrop.register_model collection_name 18 | end if Kms::Model.table_exists? 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/kms/models/version.rb: -------------------------------------------------------------------------------- 1 | module Kms 2 | module Models 3 | VERSION = '1.1.0' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/kms_models.rb: -------------------------------------------------------------------------------- 1 | require "kms/models/engine" 2 | 3 | module Kms 4 | module Models 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/tasks/kms_models_tasks.rake: -------------------------------------------------------------------------------- 1 | # desc "Explaining what the task does" 2 | # task :kms_models do 3 | # # Task goes here 4 | # end 5 | -------------------------------------------------------------------------------- /spec/controllers/kms/models/fields_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Kms 4 | module Models 5 | describe FieldsController, type: :controller do 6 | routes { Kms::Models::Engine.routes } 7 | 8 | login_user 9 | describe '#update' do 10 | it "responds with 204 status" do 11 | model = FactoryGirl.create(:model_with_string_field) 12 | put :update, params: { model_id: model.id, id: model.fields.first.id, field: { position: 1 } }, format: :json 13 | expect(response).to be_success 14 | end 15 | it "updates field's values" do 16 | model = FactoryGirl.create(:model_with_string_field) 17 | first_field = model.fields.first # position 0 here 18 | put :update, params: { model_id: model.id, id: first_field.id, field: { position: 1 } }, format: :json 19 | expect(first_field.reload.position).to be_eql(1) 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/controllers/kms/models/models_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Kms 4 | module Models 5 | describe ModelsController, type: :controller do 6 | routes { Kms::Models::Engine.routes } 7 | 8 | login_user 9 | describe '#index' do 10 | it 'returns models json representation' do 11 | model = FactoryGirl.create(:model) 12 | get :index, format: :json 13 | 14 | expect(response).to be_success 15 | expect(json.length).to eq(1) 16 | expect(json[0].keys).to include('id', 'collection_name', 'kms_model_name', 'label_field', 'allow_creation_using_form', 'fields_attributes') 17 | end 18 | end 19 | describe '#show' do 20 | it 'returns model json representation' do 21 | model = FactoryGirl.create(:model) 22 | get :show, params: { id: model.id }, format: :json 23 | expect(response).to be_success 24 | expect(json.keys).to include('id', 'collection_name', 'kms_model_name', 'label_field', 'allow_creation_using_form', 'fields_attributes') 25 | end 26 | end 27 | describe '#create' do 28 | context 'when validation failed' do 29 | it "returns errors" do 30 | post :create, params: { model: { kms_model_name: '' } }, format: :json 31 | expect(response.unprocessable?).to be true 32 | expect(json['errors']).to_not be nil 33 | end 34 | end 35 | context 'when valid' do 36 | it "returns no content" do 37 | attributes = { kms_model_name: 'Posts', collection_name: 'posts', description: 'Posts', allow_creation_using_form: true } 38 | post :create, params: { model: attributes }, format: :json 39 | expect(response).to have_http_status(204) 40 | expect(Kms::Model.last.attributes.symbolize_keys).to include attributes 41 | end 42 | end 43 | end 44 | describe '#update' do 45 | context 'when validation failed' do 46 | it "returns errors" do 47 | model = FactoryGirl.create(:model) 48 | put :update, params: { id: model.id, model: { kms_model_name: '' } }, format: :json 49 | expect(response.unprocessable?).to be true 50 | expect(json['errors']).to_not be nil 51 | end 52 | end 53 | context 'when valid' do 54 | it "returns no content" do 55 | model = FactoryGirl.create(:model) 56 | new_attributes = { kms_model_name: 'Comments', collection_name: 'comments', allow_creation_using_form: true } 57 | put :update, params: { id: model.id, model: new_attributes }, format: :json 58 | expect(response).to have_http_status(204) 59 | expect(Kms::Model.last.attributes.symbolize_keys).to include new_attributes 60 | end 61 | end 62 | end 63 | describe '#destroy' do 64 | it 'destroys model and returns no content' do 65 | model = FactoryGirl.create(:model) 66 | expect{delete :destroy, params: { id: model.id }, format: :json}.to change{Kms::Model.count}.by(-1) 67 | expect(response).to have_http_status(204) 68 | end 69 | end 70 | 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/controllers/kms/public/entries_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Kms 4 | module Public 5 | describe EntriesController, type: :controller do 6 | describe '#create' do 7 | let(:model) { FactoryGirl.create(:model) } 8 | context 'when no model with provided collection name' do 9 | it 'returns 404' do 10 | expect { post :create, params: { collection_name: 'oops' }, format: :json }.to raise_exception(ActiveRecord::RecordNotFound) 11 | end 12 | end 13 | context 'when creation using forms allowed' do 14 | let(:model) { FactoryGirl.create(:model_allowing_creation) } 15 | it 'returns 204 status' do 16 | post :create, params: { collection_name: model.collection_name, entry: { name: 'Test' } }, format: :json 17 | expect(response).to have_http_status(204) 18 | expect(Kms::Entry.last.values.symbolize_keys).to include({ name: 'Test' }) 19 | end 20 | end 21 | context 'when creation using forms not allowed' do 22 | it 'returns 422 status' do 23 | post :create, params: { collection_name: model.collection_name, entry: { name: 'Test' } }, format: :json 24 | expect(response.unprocessable?).to be true 25 | end 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/factories/fields.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :field, class: 'Kms::Field' do 3 | name 'Name' 4 | liquor_name 'name' 5 | type Kms::StringField.name 6 | factory :has_many_field do 7 | type Kms::HasManyField.name 8 | name 'Comments' 9 | liquor_name 'comments' 10 | class_name { FactoryGirl.create(:associated_model).id } 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/factories/models.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :model, class: 'Kms::Model' do 3 | kms_model_name 'Posts' 4 | collection_name 'posts' 5 | label_field 'name' 6 | factory :model_with_string_field do 7 | after(:create) do |model, evaluator| 8 | create_list(:field, 1, model: model) 9 | end 10 | factory :model_allowing_creation do 11 | allow_creation_using_form true 12 | end 13 | 14 | factory :associated_model do 15 | kms_model_name 'Comments' 16 | collection_name 'comments' 17 | end 18 | end 19 | factory :model_with_has_many_field do 20 | after(:create) do |model, evaluator| 21 | create_list(:has_many_field, 1, model: model) 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/factories/users.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :user, class: Kms::User do 3 | email "admin@example.com" 4 | password "password" 5 | password_confirmation "password" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/internal/Rakefile: -------------------------------------------------------------------------------- 1 | require 'combustion' 2 | Combustion::Application.load_tasks 3 | -------------------------------------------------------------------------------- /spec/internal/config/database.yml.travis: -------------------------------------------------------------------------------- 1 | test: 2 | adapter: postgresql 3 | database: kms_test 4 | username: postgres 5 | password: 6 | encoding: utf-8 7 | host: localhost 8 | -------------------------------------------------------------------------------- /spec/internal/config/initializers/devise.rb: -------------------------------------------------------------------------------- 1 | # Use this hook to configure devise mailer, warden hooks and so forth. 2 | # Many of these configuration options can be set straight in your model. 3 | Devise.setup do |config| 4 | # The secret key used by Devise. Devise uses this key to generate 5 | # random tokens. Changing this key will render invalid all existing 6 | # confirmation, reset password and unlock tokens in the database. 7 | config.secret_key = '57e00a5d56d16d1341110df1455911bf2890d652efc6a2cf52ea3f341405770878d6a471530601ad1a7058a2e073537315eb3852e34b8384a93c108e05097725' 8 | 9 | # ==> Mailer Configuration 10 | # Configure the e-mail address which will be shown in Devise::Mailer, 11 | # note that it will be overwritten if you use your own mailer class 12 | # with default "from" parameter. 13 | config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' 14 | 15 | # Configure the class responsible to send e-mails. 16 | # config.mailer = 'Devise::Mailer' 17 | 18 | # ==> ORM configuration 19 | # Load and configure the ORM. Supports :active_record (default) and 20 | # :mongoid (bson_ext recommended) by default. Other ORMs may be 21 | # available as additional gems. 22 | require 'devise/orm/active_record' 23 | 24 | # ==> Configuration for any authentication mechanism 25 | # Configure which keys are used when authenticating a user. The default is 26 | # just :email. You can configure it to use [:username, :subdomain], so for 27 | # authenticating a user, both parameters are required. Remember that those 28 | # parameters are used only when authenticating and not when retrieving from 29 | # session. If you need permissions, you should implement that in a before filter. 30 | # You can also supply a hash where the value is a boolean determining whether 31 | # or not authentication should be aborted when the value is not present. 32 | # config.authentication_keys = [ :email ] 33 | 34 | # Configure parameters from the request object used for authentication. Each entry 35 | # given should be a request method and it will automatically be passed to the 36 | # find_for_authentication method and considered in your model lookup. For instance, 37 | # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. 38 | # The same considerations mentioned for authentication_keys also apply to request_keys. 39 | # config.request_keys = [] 40 | 41 | # Configure which authentication keys should be case-insensitive. 42 | # These keys will be downcased upon creating or modifying a user and when used 43 | # to authenticate or find a user. Default is :email. 44 | config.case_insensitive_keys = [ :email ] 45 | 46 | # Configure which authentication keys should have whitespace stripped. 47 | # These keys will have whitespace before and after removed upon creating or 48 | # modifying a user and when used to authenticate or find a user. Default is :email. 49 | config.strip_whitespace_keys = [ :email ] 50 | 51 | # Tell if authentication through request.params is enabled. True by default. 52 | # It can be set to an array that will enable params authentication only for the 53 | # given strategies, for example, `config.params_authenticatable = [:database]` will 54 | # enable it only for database (email + password) authentication. 55 | # config.params_authenticatable = true 56 | 57 | # Tell if authentication through HTTP Auth is enabled. False by default. 58 | # It can be set to an array that will enable http authentication only for the 59 | # given strategies, for example, `config.http_authenticatable = [:database]` will 60 | # enable it only for database authentication. The supported strategies are: 61 | # :database = Support basic authentication with authentication key + password 62 | # config.http_authenticatable = false 63 | 64 | # If http headers should be returned for AJAX requests. True by default. 65 | # config.http_authenticatable_on_xhr = true 66 | 67 | # The realm used in Http Basic Authentication. 'Application' by default. 68 | # config.http_authentication_realm = 'Application' 69 | 70 | # It will change confirmation, password recovery and other workflows 71 | # to behave the same regardless if the e-mail provided was right or wrong. 72 | # Does not affect registerable. 73 | # config.paranoid = true 74 | 75 | # By default Devise will store the user in session. You can skip storage for 76 | # particular strategies by setting this option. 77 | # Notice that if you are skipping storage for all authentication paths, you 78 | # may want to disable generating routes to Devise's sessions controller by 79 | # passing skip: :sessions to `devise_for` in your config/routes.rb 80 | config.skip_session_storage = [:http_auth] 81 | 82 | # By default, Devise cleans up the CSRF token on authentication to 83 | # avoid CSRF token fixation attacks. This means that, when using AJAX 84 | # requests for sign in and sign up, you need to get a new CSRF token 85 | # from the server. You can disable this option at your own risk. 86 | # config.clean_up_csrf_token_on_authentication = true 87 | 88 | # ==> Configuration for :database_authenticatable 89 | # For bcrypt, this is the cost for hashing the password and defaults to 10. If 90 | # using other encryptors, it sets how many times you want the password re-encrypted. 91 | # 92 | # Limiting the stretches to just one in testing will increase the performance of 93 | # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use 94 | # a value less than 10 in other environments. Note that, for bcrypt (the default 95 | # encryptor), the cost increases exponentially with the number of stretches (e.g. 96 | # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). 97 | config.stretches = Rails.env.test? ? 1 : 10 98 | 99 | # Setup a pepper to generate the encrypted password. 100 | # config.pepper = '941376f1bd6d84642f36af53a6ab6e4cde564019db133c446f53ee1fb2999536d72793983406f515b7bcb9fdb5e2cf174e9bdfab23edf0427a80b6a6977ee48e' 101 | 102 | # ==> Configuration for :confirmable 103 | # A period that the user is allowed to access the website even without 104 | # confirming their account. For instance, if set to 2.days, the user will be 105 | # able to access the website for two days without confirming their account, 106 | # access will be blocked just in the third day. Default is 0.days, meaning 107 | # the user cannot access the website without confirming their account. 108 | # config.allow_unconfirmed_access_for = 2.days 109 | 110 | # A period that the user is allowed to confirm their account before their 111 | # token becomes invalid. For example, if set to 3.days, the user can confirm 112 | # their account within 3 days after the mail was sent, but on the fourth day 113 | # their account can't be confirmed with the token any more. 114 | # Default is nil, meaning there is no restriction on how long a user can take 115 | # before confirming their account. 116 | # config.confirm_within = 3.days 117 | 118 | # If true, requires any email changes to be confirmed (exactly the same way as 119 | # initial account confirmation) to be applied. Requires additional unconfirmed_email 120 | # db field (see migrations). Until confirmed, new email is stored in 121 | # unconfirmed_email column, and copied to email column on successful confirmation. 122 | config.reconfirmable = true 123 | 124 | # Defines which key will be used when confirming an account 125 | # config.confirmation_keys = [ :email ] 126 | 127 | # ==> Configuration for :rememberable 128 | # The time the user will be remembered without asking for credentials again. 129 | # config.remember_for = 2.weeks 130 | 131 | # If true, extends the user's remember period when remembered via cookie. 132 | # config.extend_remember_period = false 133 | 134 | # Options to be passed to the created cookie. For instance, you can set 135 | # secure: true in order to force SSL only cookies. 136 | # config.rememberable_options = {} 137 | 138 | # ==> Configuration for :validatable 139 | # Range for password length. 140 | config.password_length = 8..128 141 | 142 | # Email regex used to validate email formats. It simply asserts that 143 | # one (and only one) @ exists in the given string. This is mainly 144 | # to give user feedback and not to assert the e-mail validity. 145 | # config.email_regexp = /\A[^@]+@[^@]+\z/ 146 | 147 | # ==> Configuration for :timeoutable 148 | # The time you want to timeout the user session without activity. After this 149 | # time the user will be asked for credentials again. Default is 30 minutes. 150 | # config.timeout_in = 30.minutes 151 | 152 | # If true, expires auth token on session timeout. 153 | # config.expire_auth_token_on_timeout = false 154 | 155 | # ==> Configuration for :lockable 156 | # Defines which strategy will be used to lock an account. 157 | # :failed_attempts = Locks an account after a number of failed attempts to sign in. 158 | # :none = No lock strategy. You should handle locking by yourself. 159 | # config.lock_strategy = :failed_attempts 160 | 161 | # Defines which key will be used when locking and unlocking an account 162 | # config.unlock_keys = [ :email ] 163 | 164 | # Defines which strategy will be used to unlock an account. 165 | # :email = Sends an unlock link to the user email 166 | # :time = Re-enables login after a certain amount of time (see :unlock_in below) 167 | # :both = Enables both strategies 168 | # :none = No unlock strategy. You should handle unlocking by yourself. 169 | # config.unlock_strategy = :both 170 | 171 | # Number of authentication tries before locking an account if lock_strategy 172 | # is failed attempts. 173 | # config.maximum_attempts = 20 174 | 175 | # Time interval to unlock the account if :time is enabled as unlock_strategy. 176 | # config.unlock_in = 1.hour 177 | 178 | # Warn on the last attempt before the account is locked. 179 | # config.last_attempt_warning = false 180 | 181 | # ==> Configuration for :recoverable 182 | # 183 | # Defines which key will be used when recovering the password for an account 184 | # config.reset_password_keys = [ :email ] 185 | 186 | # Time interval you can reset your password with a reset password key. 187 | # Don't put a too small interval or your users won't have the time to 188 | # change their passwords. 189 | config.reset_password_within = 6.hours 190 | 191 | # ==> Configuration for :encryptable 192 | # Allow you to use another encryption algorithm besides bcrypt (default). You can use 193 | # :sha1, :sha512 or encryptors from others authentication tools as :clearance_sha1, 194 | # :authlogic_sha512 (then you should set stretches above to 20 for default behavior) 195 | # and :restful_authentication_sha1 (then you should set stretches to 10, and copy 196 | # REST_AUTH_SITE_KEY to pepper). 197 | # 198 | # Require the `devise-encryptable` gem when using anything other than bcrypt 199 | # config.encryptor = :sha512 200 | 201 | # ==> Scopes configuration 202 | # Turn scoped views on. Before rendering "sessions/new", it will first check for 203 | # "users/sessions/new". It's turned off by default because it's slower if you 204 | # are using only default views. 205 | # config.scoped_views = false 206 | 207 | # Configure the default scope given to Warden. By default it's the first 208 | # devise role declared in your routes (usually :user). 209 | # config.default_scope = :user 210 | 211 | # Set this configuration to false if you want /users/sign_out to sign out 212 | # only the current scope. By default, Devise signs out all scopes. 213 | # config.sign_out_all_scopes = true 214 | 215 | # ==> Navigation configuration 216 | # Lists the formats that should be treated as navigational. Formats like 217 | # :html, should redirect to the sign in page when the user does not have 218 | # access, but formats like :xml or :json, should return 401. 219 | # 220 | # If you have any extra navigational formats, like :iphone or :mobile, you 221 | # should add them to the navigational formats lists. 222 | # 223 | # The "*/*" below is required to match Internet Explorer requests. 224 | # config.navigational_formats = ['*/*', :html] 225 | 226 | # The default HTTP method used to sign out a resource. Default is :delete. 227 | config.sign_out_via = :get 228 | 229 | # ==> OmniAuth 230 | # Add a new OmniAuth provider. Check the wiki for more information on setting 231 | # up on your models and hooks. 232 | # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' 233 | 234 | # ==> Warden configuration 235 | # If you want to use other strategies, that are not supported by Devise, or 236 | # change the failure app, you can configure them inside the config.warden block. 237 | # 238 | # config.warden do |manager| 239 | # manager.intercept_401 = false 240 | # manager.default_strategies(scope: :user).unshift :some_external_strategy 241 | # end 242 | 243 | # ==> Mountable engine configurations 244 | # When using Devise inside an engine, let's call it `MyEngine`, and this engine 245 | # is mountable, there are some extra configurations to be taken into account. 246 | # The following options are available, assuming the engine is mounted as: 247 | # 248 | # mount MyEngine, at: '/my_engine' 249 | # 250 | # The router that invoked `devise_for`, in the example above, would be: 251 | config.router_name = :kms 252 | config.parent_controller = 'Kms::ApplicationController' 253 | # 254 | # When using omniauth, Devise cannot automatically set Omniauth path, 255 | # so you need to do it manually. For the users scope, it would be: 256 | # config.omniauth_path_prefix = '/my_engine/users/auth' 257 | end 258 | -------------------------------------------------------------------------------- /spec/internal/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | mount Kms::Models::Engine => '/kms' 3 | end 4 | -------------------------------------------------------------------------------- /spec/internal/db/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define do 2 | create_table "kms_users", force: :cascade do |t| 3 | t.string "email", default: "", null: false 4 | t.string "encrypted_password", default: "", null: false 5 | t.string "reset_password_token" 6 | t.datetime "reset_password_sent_at" 7 | t.datetime "remember_created_at" 8 | t.datetime "created_at" 9 | t.datetime "updated_at" 10 | t.string "role" 11 | t.boolean "alert", default: false, null: false 12 | t.index ["email"], name: "index_kms_users_on_email", unique: true, using: :btree 13 | t.index ["reset_password_token"], name: "index_kms_users_on_reset_password_token", unique: true, using: :btree 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/internal/log/.gitignore: -------------------------------------------------------------------------------- 1 | *.log -------------------------------------------------------------------------------- /spec/internal/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apiqcms/kms_models/6278f32b0550d57a8401c4fc0543b5953d06d11e/spec/internal/public/favicon.ico -------------------------------------------------------------------------------- /spec/models/kms/field_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Kms 4 | describe Field, type: :model do 5 | it { should belong_to(:model) } 6 | describe '#get_value' do 7 | it 'returns value stored in entry' do 8 | model = FactoryGirl.create(:model_with_string_field) 9 | field = model.fields.first 10 | entry = model.entries.create(values: {name: 'Test'}) 11 | expect(field.get_value(entry)).to be_eql('Test') 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/models/kms/has_many_field_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Kms 4 | describe HasManyField, type: :model do 5 | describe '#get_value' do 6 | it 'returns drop scope containing association records' do 7 | model = FactoryGirl.create(:model_with_has_many_field) 8 | field = model.fields.first 9 | associated_model = Kms::Model.find(field.class_name) 10 | association_entry = associated_model.entries.create(values: {name: 'Test'}) 11 | entry = model.entries.create(values: { comments: [association_entry.id] }) 12 | expect(field.get_value(entry)).to be_instance_of(Kms::EntryDrop::Scope) 13 | expect(field.get_value(entry).source).to include(association_entry) 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/models/kms/model_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Kms 4 | describe Model, type: :model do 5 | it { should have_many(:fields) } 6 | it { should have_many(:entries) } 7 | it { should have_db_column(:allow_creation_using_form) } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | 4 | require 'combustion' 5 | require 'shoulda/matchers' 6 | 7 | Combustion.initialize! :all 8 | 9 | require 'rspec/rails' 10 | require 'factory_girl_rails' 11 | require 'devise' 12 | 13 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 14 | 15 | RSpec.configure do |config| 16 | config.include Devise::Test::ControllerHelpers, type: :controller 17 | config.extend ControllerMacros, type: :controller 18 | config.include Requests::JsonHelpers, type: :controller 19 | config.color = true 20 | config.mock_with :rspec 21 | config.use_transactional_fixtures = true 22 | config.infer_base_class_for_anonymous_controllers = false 23 | end 24 | Shoulda::Matchers.configure do |config| 25 | config.integrate do |with| 26 | # Choose a test framework: 27 | with.test_framework :rspec 28 | 29 | # Or, choose the following (which implies all of the above): 30 | with.library :rails 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/support/controller_macros.rb: -------------------------------------------------------------------------------- 1 | module ControllerMacros 2 | 3 | def login_user 4 | before(:each) do 5 | @request.env["devise.mapping"] = Devise.mappings[:kms_user] 6 | user = FactoryGirl.create(:user) 7 | # user.confirm! # or set a confirmed_at inside the factory. Only necessary if you are using the "confirmable" module 8 | sign_in user 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/support/request_helpers.rb: -------------------------------------------------------------------------------- 1 | module Requests 2 | module JsonHelpers 3 | def json 4 | JSON.parse(response.body) 5 | end 6 | end 7 | end 8 | --------------------------------------------------------------------------------