this is a general can I do this and if so, how, type question. ActiveAdmin has a nifty built in ActiveAdmin::Comments class which enables a "comments" section to be dropped into an ActiveAdmin page. AA handles the creation of the active_admin_comments table in the database and it just works, with the content of the comment in the body attribute. There is also a resource_type attribute which specifies which model the comments are associated with.
We want to add this field to one of our pages, but due to SOX compliance the values entered have to be encrypted. Rails 7, which I'm using, has support for encryption at the model layer right out of the box and I'd very much like to leverage that for these comments. I've tried monkey patching encryption in like so:
module ActiveAdmin
class Comments < ApplicationRecord
encrypts :body
end
end
in a model file but it doesn't seem to be working, querying the database doesn't show the input as encrypted. Is there an easy way to add encryption, preferably limited to specific resource types, to ActiveAdmin comments?
I don't have access to an installed ActiveAdmin instance to test this solution but I can see right away that you have the wrong class name and you're inheriting from the wrong class name:
class Comments < ApplicationRecord
Make sure you look at the original class definition:
module ActiveAdmin
class Comment < ActiveRecord::Base
# ...
end
end
Model names are singular and you must inherit from ActiveRecord::Base, not your app's own customized ApplicationRecord class.
This isn't guaranteed to work for a couple of reasons:
ActiveAdmin is complex and monkey-patching it might prove difficult
ActiveRecord::Encryption supports both encrypted and unencrypted data in a single column but you must configure your application to support it first
Related
In Rails 5, is it possible to use the new attributes API with a field exposed via store_accessor on a jsonb column?
For example, I have:
class Item < ApplicationRecord
# ...
store_accessor :metadata, :publication_date
attribute :publication_date, :datetime
end
Then I'd like to call i = Item.new(publication_date: '2012-10-24'), and have metadata be a hash like: { 'publication_date' => #<DateTimeInstance> }.
However, the attribute call doesn't seem to be doing any coercion.
Hopefully I am missing something--it seems like being able to use these two features in conjunction would be very useful when working with jsonb columns. (Speaking of which, why doesn't the attributes API expose a generic array: true option? That would also be very useful for this case.)
I see that there is a project jsonb_accessor, but it seems a little heavyweight. It also seems to be designed for Rails 4 (I haven't checked whether it supports Rails 5).
You might check out a rather new (as of this writing) gem built atop the Rails 5+ Attributes API: AttrJson. I've recently started using it; some rough edges still, but the author/maintainer seems keen to improve it.
After digging in a little more, I see that the Attributes API (as it currently exists in ActiveRecord) is not really appropriate for handling jsonb data—there would be duplicate info in the attributes hash, etc.
I do think it would be nice if ActiveRecord provided typecasting/coercion for jsonb fields. I see that there is a project jsonb_accessor, but it seems a little heavyweight. It also seems to be designed for Rails 4 (I haven't checked whether it supports Rails 5).
I guess something like this might be in the works for Rails since the ActiveRecord::Type values are actually defined in ActiveModel.
For now I am using the following. I've never really loved Hashie, but this is relatively lightweight and easy to use:
class Item < ApplicationRecord
class Metadata < Hashie::Dash
include Hashie::Extensions::Dash::Coercion
include Hashie::Extensions::Dash::IndifferentAccess
property :publication_date, coerce: Time
def self.dump(obj); obj.as_json; end
def self.load(obj); new(obj); end
end
serialize :metadata, Metadata
store_accessor :metadata, :publication_date
end
We know that Rails 5 added ApplicationRecord as an abstract class which was inherited by our models (ActiveRecord).
But basically, I think every technical requirement we do with ApplicationRecord, we can also do with ActiveRecord::Base. For instance:
module MyFeatures
def do_something
puts "Doing something"
end
end
class ApplicationRecord < ActiveRecord::Base
include MyFeatures
self.abstract_class = true
end
So now every model will be attached the behaviors of MyFeatures. But we can also achieve this in Rails 4:
ActiveRecord::Base.include(MyFeatures)
So what is the benefit of using ApplicationRecord, do you think it is necessary to add ApplicationRecord?
While it may seem the same in basic Rails applications, there actually is an important difference once you begin to use rails engines, plugins / gems or direct methods from ActiveRecord::Base.
ActiveRecord::Base.include(MyFeatures) mixes in the features directly into ActiveRecord::Base and it is present there forever for all later uses of ActiveRecord::Base (it cannot be "unmixed") and there is no way to get the original ActiveRecord::Base anymore in any code after the include. This can easily lead to problems if some of the mixed in feature changed the default ActiveRecord behavior or if e.g. two engines / gems tried to include same-named methods.
On the other hand, the ApplicationRecord approach makes the features present only for the classes (models) that inherit from it, other classes, as well as direct use of ActiveRecord::Base stay pristine, uncluttered by the module features.
This is especially important when engines or rails plugins are used as it allows them to separate their own model logic from the main application's model logic which was not possible before ApplicationRecord.
All of this is also nicely described in this blog post and this github comment.
This is to expand on #BoraMa's answer, and to, hopefully, clear up some confusion around ActiveRecord::Base.abstract_class.
ActiveRecord::Base.abstract_class goes back to at least Rails 3.2.0 (http://api.rubyonrails.org/v3.2.0/classes/ActiveRecord/Inheritance/ClassMethods.html), which was released on January 20, 2012.
Rails 4.0.0 improved the documentation: http://api.rubyonrails.org/v4.0.0/classes/ActiveRecord/Inheritance/ClassMethods.html
So, to everyone who thinks ApplicationRecord is radically new, it's not. It is an improvement, not a breaking change. Nothing was added to ActiveRecord::Base to make this work.
I did the same thing on a Rails 4.2.6 project because the models used UUIDs for ids instead of integers, and this required a change to the default ORDER BY. So, instead of using copy-paste or a concern, I went with inheritance using a UuidModel class and self.abstract_class = true.
I read many blogs, and one of the themes that comes across often is that concerns (at least the way Rails defines them) are damaging to software. On balance I agree - simply including behaviour into models is violating the single responsibility principle. You end up with a god-class that does too much.
But as with many of the opinions gleaned from blogs, an alternative architecture is rarely provided.
So let's take an example app, loosely based on one I have to maintain. It's inherently a CMS, as many Rails apps tend to be.
Currently each model has a large number of concerns. Let's use a few here:
class Article < ActiveRecord::Base
include Concerns::Commentable
include Concerns::Flaggable
include Concerns::Publishable
include Concerns::Sluggable
...
end
You can imagine that 'Commentable' would require only a small amount of code added to the Article. Enough to establish relationships with comment objects and provide some utility methods to access them.
Flaggable, allowing users to flag inappropriate content, ends up adding some fields to the model: flagged, flagged_by, flagged_at for example. And some code to add functionality.
Sluggable adds a slug field for referencing in URLs. And some more code.
Publishable adds publish date and status fields, with yet more code.
Now what happens if we add a new kind of content?
class Album < ActiveRecord::Base
include Concerns::Flaggable
include Concerns::Sluggable
include Concerns::Publishable
...
end
Albums are a bit different. You can't comment on them, but you can still publish them and flag them.
Then I add some more content types: Events and Profiles, let's say.
I see a few problems with this architecture as it stands:
We have multiple database tables with exactly the same fields
(flagged_by, published_on etc.)
We can't retrieve multiple content types at once with a single SQL query.
Each model supports the duplicated field names with the included concerns, giving each class multiple responsibilities.
So what's a better way?
I've seen decorators promoted as a way to add functionality at the point where it's needed. This could help solve the issue of included code, but the database structure isn't necessarily improved. It also looks needlessly fiddly and involves adding extra loops to the code to decorate arrays of models.
So far my thinking goes like this:
Create a common 'content' model (and table):
class Content < ActiveRecord::Base
end
The associated table is probably quite small. It should probably have some kind of 'type' field, and maybe some things common to absolutely all content - like a type slug for URLs perhaps.
Then rather than adding concerns we can create an associated model for each behaviour:
class Slug < ActiveRecord::Base
belongs_to :content
...
end
class Flag < ActiveRecord::Base
belongs_to :content
...
end
class Publishing < ActiveRecord::Base
belongs_to :content
...
end
class Album < ActiveRecord::Base
belongs_to :content
...
end
...
Each of these is associated with one piece of content, so the foreign key can exist on the feature's model. All the behaviour relating to the feature can also exist solely on the feature's model, making OO purists happier.
In order to achieve the kind of behaviour that usually requires model hooks (before_create for example) I can see an observer pattern being more useful. (A slug is created once a 'content_created' event is sent, etc.)
This looks like it would clean things up no end. I can now search all content with a single query, I don't have duplicated field names in the database and I don't need to include code into the content model.
Before I merrily unleash it on my next project, has anyone tried this approach? Would it work? Or would splitting things up this much end up creating a hell of SQL queries, joins and tangled code? Can you suggest a better alternative?
Concerns are basically just a thin wrapper around the mixin pattern. It is a very useful pattern for composing pieces of software around reusable traits.
Single Table Inheritance
The issue of having the same columns across several models is often solved with Single Table Inheritance. STI however is only really suited when the models are very similar.
So lets consider your CMS example. We have several different types of content:
Page, NewsArticle, BlogPost, Gallery
Which have pretty much identical database fields:
id
title
content
timestamps
published_at
published_by
# ...
So we decide to get rid of duplication and use a common table. It would be tempting to call it contents but that is extremely ambiguous - content of the content ...
So lets copy Drupal and call our common type Node.
class Node < ActiveRecord::Base
include Concerns::Publishable
end
But we want to have different logic for each type of content. So we make subclasses for each type:
class Node < ActiveRecord::Base
self.inheritance_column = :type
include Concerns::Publishable
end
class NewsArticle < Node
has_and_belongs_to_many :agencies
end
class Gallery < Node
has_and_belongs_to_many :photos
end
# ...
This works well until the STI models start to diverge for too much from each other. Then some duplication in the database schema can be a far smaller problem than the massive complications caused by trying to shoehorn everything into the same table. Most CMS systems built on relational databases struggle with this issue. One solution is to use a schemaless non-relational database.
Composing concerns
There is nothing in that says that concerns require you to store on the models table. Lets look at several of the concerns you have listed:
Flaggable
Sluggable
Commentable
Each of these would use a table flags, slugs, comments. The key is making the relation to object that they flag, slug or comment polymorphic.
comment:
commented_type: string
commented_id: int
slugs:
slugged_type: string
slugged_id: int
flags:
flagged_type: string
flagged_id: int
# ...
class Comment
belongs_to: :commented, polymorphic: true
end
module Concerns::Commentable
# ...
has_many: :comments
end
I would suggest that you look at some of the libraries that solve these kind of common tasks such as FriendlyId, ActsAsTaggedOn etc to see how they are structured.
Conclusion
There is nothing fundamentally wrong with the idea of parallel inheritance. And the idea that you should forgo it just to placate some kind extreme OO purity ideal is ridiculous.
Traits are a part of object orientation just any other composition technique. Concerns are however not the magic-fix all that many blog posts would have you believe.
I am using the parseresource gem and it says I need to create a model.
Create a model:
class Post < ParseResource
fields :title, :author, :body
validates_presence_of :title
end
I only know how to generate a mode and it always inherits ActiveRecord::Base. What should I type into my command line to create this model?
I think what you are looking for is:
rails generate model Post title:string author:string blob:text
Then change the inherits from ActiveRecord to inherits from ParseResource in the post.rb file created.
class Post < ActiveRecord::Base
becomes
class Post < ParseResource
I don't really have enough details about the model or ParseResource for a better answer.
I do hope that helps though.
If you are new to Ruby and/or Rails I suggest running through an introduction to rails.
http://guides.rubyonrails.org/getting_started.html
or
http://ruby.railstutorial.org/ruby-on-rails-tutorial-book
Cheers!
In this case it looks like you only need to create a single file in your /app/models directory named after the model you're trying to create.
The normal model generation code that comes with Rails and it's generators don't apply here since this gem is specifically intended to only serve as a 'wrapper' to the Parse.com API.
From the project's home page:
ParseResource makes it easy to interact with Parse.com's REST API. It adheres to the ActiveRecord pattern. ParceResource is fully ActiveModel compliant, meaning you can use validations and Rails forms.
It looks like all you do is create a single file descending from ParseResource and then interact with it as if it were a normal ActiveRecord model.
Looks pretty straightforward, though I'd caution that the author says right in the docs:
ParseResource is brand new. Test coverage is decent.
This is my first gem. Be afraid.
Be careful and make sure you report any issues you find through the projects issues page in Github.
Unless the parseresource gem includes a custom generator, I think you will have to generate a standard ActiveRecord model and then edit it to inherit from ParseResource.
As another option you could just create the model from scratch by creating a post.rb file under app/models and putting your model code in there (any decent text editor will work just fine). Remember there is nothing forcing you to use a generator, they are just there to make your life easier.
rails generate model followed by your model name, then the fields with respective datatype and size.
Below is one exhaustive example.
rails generate model MyModel some_id:integer{20} some_name:string{255}
some_text:text some_int:integer{1} some_deci:decimal{10,2}
You can also have other data types such as boolean, date, time, datetime, float, binary.
What is the deal with this? I'm working with a pre-existing that I did not do myself. Everything in the database is labeled in singular form. user, security, spec, etc. I guess the right way would be users, securities, specs. At least that's what ruby on rails try's to lookup when I generate a scaffold .
How do I specifically state to use user instead of users in the sql. I don't see anywhere in my project where it is looking up the sql. I mean if my model is user you would think it would try to lookup user. Instead of users.
Thanks for any help.
You need set_table_name :name_of_the_table in your model (source).
So:
class User < ActiveRecord::Base
set_table_name :user
end
The reason they use plural for the table and singular for the model is because an instance of the model represents one user, whereas the table contains all the users. It's just to make it more readable and logical.
You can specifiy the table name:
How do I explicitly specify a Model's table-name mapping in Rails?