Single Table Inheritance(STI) Problems - associations

I'm new to rails and I'm having some problems specifying a couple of entities and relationships in my database.
Let's assume the following entities.
Racer Entity
Where the migration file looks like this:
class CreateRacers < ActiveRecord::Migration
def self.up
create_table :racers, options: "ENGINE=InnoDB" do |t|
t.string :email, limit: 60, null: false
end
add_index :racers, :email, unique: true
execute "ALTER TABLE racers MODIFY id INT UNSIGNED AUTO_INCREMENT;"
end
def self.down
drop_table :racers
end
end
The model file looks like this:
class Racer < ActiveRecord::Base
attr_accessible :email
validates_uniqueness_of :email
before_save { |racer| racer.email = email.downcase }
# Email validation
VALID_EMAIL_REGEX = /([\w+.]+)#[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: {maximum: 60}, format: { with: VALID_EMAIL_REGEX }
has_many :tracks, dependent: :delete_all
end
Track Entity
The migration file looks like this:
class CreateTracks < ActiveRecord::Migration
def self.up
create_table :tracks, options: "ENGINE=InnoDB" do |t|
t.column :user_id, 'integer unsigned', null: false
t.string :description, limit: 250, null: false
t.string :image, null: true
end
execute "ALTER TABLE tracks MODIFY id INT UNSIGNED AUTO_INCREMENT;"
add_foreign_key :tracks, :racers
end
def self.down
remove_foreign_key :tracks, :racers
drop_table :tracks
end
end
And the model file looks like this:
class Track < ActiveRecord::Base
attr_accessible :description, :image
validates :description, presence: true, length: {maximum: 250}
belongs_to :racer
validates_presence_of :racer_id
has_many :stops, dependent: :delete_all
end
Stop Entity
The migration file looks like this:
class CreateStops < ActiveRecord::Migration
def self.up
create_table :stops, options: "ENGINE=InnoDB" do |t|
t.column :track_id, 'integer unsigned', null: false
t.column :coordinates, :point, null: false
t.string :name, limit: 30, null: true
t.column :sequence_order, 'integer unsigned', null: false
end
execute "ALTER TABLE stops MODIFY id INT UNSIGNED AUTO_INCREMENT;"
add_foreign_key :stops, :tracks
end
def self.down
remove_foreign_key :stops, :tracks
drop_table :stops
end
end
And the model file looks like this:
class Stop < ActiveRecord::Base
attr_accessible :coordinates, :name, :sequence_order
validates :name, presence: true, length: {maximum: 30}
validates :coordinates, presence: true
validates :spot_sequence_order, presence: true
belongs_to :track
validates_presence_of :track_id
has_one :challenge, dependent: :delete_all
end
Challenge, Puzzle, Quiz, QuizOption entities (where the problem is)
Has seen above the Stop entity has_one challenge and I want that Challenge to be, among other things, a Quiz or a Puzzle. The Challenge belongs_to a Stop. So far I have the following migrations:
class CreatePuzzles < ActiveRecord::Migration
def self.up
create_table :puzzles, options: "ENGINE=InnoDB" do |t|
t.string :image_path, null: false
t.int :ver_split, null: false, default: 4
t.int :hor_split, null: false, default: 4
end
execute "ALTER TABLE puzzlies MODIFY id INT UNSIGNED AUTO_INCREMENT;"
end
def self.down
drop_table :quizzes
end
end
class CreateQuizzes < ActiveRecord::Migration
def self.up
create_table :quizzes, options: "ENGINE=InnoDB" do |t|
t.string :question, null: false
end
execute "ALTER TABLE quizzes MODIFY id INT UNSIGNED AUTO_INCREMENT;"
end
def self.down
drop_table :quizzes
end
end
And the following models
class Puzzle < ActiveRecord::Base
attr_accessor :image_path, :ver_split, hor_split
validates :image_path, presence: true, allow_blank: false
validates :ver_split, allow_blank: false
validates :hor_split, allow_blank: false
belongs_to :stop
end
class Quiz < ActiveRecord::Base
attr_accessor :question
validates :question, presence: true, length: { maximum: 255 }, allow_blank: false
belongs_to :spot
has_many :quiz_options
end
The Quiz has several answers, where one or more are correct.
class CreateQuizOptions < ActiveRecord::Migration
def self.up
create_table :quiz_options do |t|
t.column :quiz_id, 'integer unsigned', null: false
t.string :option, null: false
t.boolean :is_correct, null: false, default: false
end
add_foreign_key :quiz_options, :quizzes
execute "ALTER TABLE quiz_options MODIFY id INT UNSIGNED AUTO_INCREMENT;"
end
def self.down
remove_foreign_key :quiz_options, :quizzes
drop_table :quiz_options
end
end
class QuizOption < ActiveRecord::Base
attr_accessor :option, :is_correct
validates :option, presence: true, length: { maximum: 255 }
validates_inclusion_of :is_correct, in: [true,false]
belongs_to :quiz
validates_presence_of :quiz_id
end
The Question
How should I specify my migrations,models and controllers to accomplish this goal?
I have found a couple of examples of STI and Polymorphic-Associations but I don't know which one to apply and how to apply them to this case.
First I have tried to use STI and declare all the necessary fields in the Challenge table and then the Quiz and Puzzle model inherit from Challenge model. The problem is that I don't know where to put the has_many :quiz_options.
Then I have tried to use Polymorphic-Associations explained here and here but I honestly can't understand how to adapt it to this specific case.
Thanks in advance.
Edit: I forgot to say that I'm using MySQL. I also have some gems to manage Spatial data types(rgeo, activerecord-mysql2spatial-adapter) and foreign_keys(foreigner).

Ok, I think I solved my own problem by applying Single Table Inheritance.
The result is not pretty but it works and due to the lack of proper information on other solutions I will stick to it by now.
So let's get back to my example.
The Racer, Track and Stop entity stay as they are, no changes there.
The Challenge migration will look like this:
class CreateChallenges < ActiveRecord::Migration
def self.up
create_table :challenges, options: "ENGINE=InnoDB" do |t|
t.column :stop_id, 'integer unsigned', null: false
t.string :type
# Common attributes to all challenges
t.string :description, null: false
# Puzzle attributes
t.string :image_path
t.int :vert_split
t.int :hor_split
# Quiz attributes
t.string :question
end
add_foreign_key :challenges, :stops
execute "ALTER TABLE quizzes MODIFY id INT UNSIGNED AUTO_INCREMENT;"
end
def self.down
remove_foreign_key :challenges, :stops
drop_table :quizzes
end
end
In this table I just added all the necessary fields to specify both Quizzes and Puzzles.
Now the models will look like this:
class Challenge < ActiveRecord::Base
validates_presence_of :spot_id, :description
attr_accessible :description, :image_path
# Validate description attribute
validates :description, length: { maximum: 255 }, allow_blank: false
# Validates image type
VALID_IMAGE_TYPE_REGEX = /^[\/]?([\w]+[\/])*([\w]+\.(png|jpg))$/
validates :image_path, length: { maximum: 255 }, allow_blank: true, format: { with: VALID_IMAGE_TYPE_REGEX }
# Validates type attribute
validates_inclusion_of :type, in: %w( Quiz Puzzle )
# Associations
belongs_to :stop
has_many :quiz_options
end
class Puzzle < Challenge
validates_presence_of :image_path
attr_accessor :image_path, :ver_split, hor_split
validates :image_path, allow_blank: false
validates :ver_split, allow_blank: false
validates :hor_split, allow_blank: false
end
class Quiz < Challenge
validates_presence_of :question
attr_accessor :question
validates :question, length: { maximum: 255 }, allow_blank: false
end
Now to finish everything, the QuizOption migration and model will look like this:
class CreateQuizOptions < ActiveRecord::Migration
def self.up
create_table :quiz_options do |t|
t.column :challenge_id, 'integer unsigned', null: false
t.string :option, null: false
t.boolean :is_correct, null: false, default: false
end
add_foreign_key :quiz_options, :challenges
execute "ALTER TABLE quiz_options MODIFY id INT UNSIGNED AUTO_INCREMENT;"
end
def self.down
remove_foreign_key :quiz_options, :challenges
drop_table :quiz_options
end
end
class QuizOption < ActiveRecord::Base
validates_presence_of :challenge_id, :option
attr_accessor :option, :is_correct
validates :option, length: { maximum: 255 }
validates_inclusion_of :is_correct, in: [true,false]
belongs_to :challenge
end
This way everything works as I wanted.

Related

Extending Solidus to have file upload attached to an order

app/models/upload/graphic.rb
class Upload::Graphic < ApplicationRecord
belongs_to :order, class_name: "Spree::Order", optional: true
has_one_attached :image
def order
Spree::Order.find(spree_order_id)
end
end
migration
class CreateUploadGraphics < ActiveRecord::Migration[6.1]
def change
create_table :upload_graphics do |t|
t.references :spree_order, foreign_key: true
t.timestamps
end
end
end
This part works great i can do .order on an Upload::Graphic object and it returns the order.
Question: how do i do the inverse? Start with a Spree::Order and find the Upload::Graphic

How do i do an type OR Type. String OR Int?

I would like to be able to allow a string or an integer in a field. How do I do this?
This is my current schema:
'minSize': {'type': 'any'},
I'm quoting the docs:
A list of types can be used to allow different values
>>> v.schema = {'quotes': {'type': ['string', 'list']}}
>>> v.validate({'quotes': 'Hello world!'})
True
>>> v.validate({'quotes': ['Do not disturb my circles!', 'Heureka!']})
True

Ecto association to more than one schemes

Let's say I have these schemas:
defmodule Sample.Post do
use Ecto.Schema
schema "post" do
field :title
has_many :comments, Sample.Comment
end
end
defmodule Sample.User do
use Ecto.Schema
schema "user" do
field :name
has_many :comments, Sample.Comment
end
end
defmodule Sample.Comment do
use Ecto.Schema
schema "comment" do
field :text
belongs_to :post, Sample.Post
belongs_to :user, Sample.User
end
end
My questions is how can I use Ecto.build_assoc to save a comment?
iex> post = Repo.get(Post, 13)
%Post{id: 13, title: "Foo"}
iex> comment = Ecto.build_assoc(post, :comments)
%Comment{id: nil, post_id: 13, user_id: nil}
So far it's ok, all I need to do is use the same function to set the user_id in my Comment struct, however since the return value of build_assoc is Comment struct, I can not use the same function
iex> user = Repo.get(User, 1)
%User{id: 1, name: "Bar"}
iex> Ecto.build_assoc(user, :comment, comment)
** (UndefinedFunctionError) undefined function: Sample.Comment.delete/2
...
I have two options but neither of them looks good to me:
First one is to set user_id manually!
iex> comment = %{comment| user_id: user.id}
%Comment{id: nil, post_id: 13, user_id: 1}
Second one is to convert the struct to map and ... I don't even want to go there
Any suggestion?
Why don't you want convert struct to map? It is really easy.
build_assoc expects map of attributes as last value. Internally it tries to delete key :__meta__. Structs have compile time guarantees, that they will contain all defined fields, so you are getting:
** (UndefinedFunctionError) undefined function: Sample.Comment.delete/2
But you can just write:
comment = Ecto.build_assoc(user, :comment, Map.from_struct comment)
and everything will work fine.
Just pass it along with build_assoc
iex> comment = Ecto.build_assoc(post, :comments, user_id: 1)
%Comment{id: nil, post_id: 13, user_id: 1}
Check here for more details.

Sinatra + SQLite + ActiveRecord (strings can not be saved)

I have a problem with saving strings in database. Integers are going well. I have a model Paymentwith one field :command(String). This is from console:
p = Payment.new
p.command = 'test'
=> "test"
p.save
=> D, [2014-10-20T15:30:43.383504 #2229] DEBUG -- : (0.2ms) begin transaction
=> D, [2014-10-20T15:30:43.386985 #2229] DEBUG -- : (0.1ms) commit transaction
=> true
Payment.last
=> #<Payment id: 1, command: nil>
My app file
require 'sinatra'
require 'activerecord'
require 'sinatra/activerecord'
set :database, 'sqlite3:qiwi_exline_development.sqlite3'
get '/' do
#payments = Payment.all
haml :index
end
class Payment < ActiveRecord::Base
include ActiveModel::Model
attr_accessor :command
end
My schema
ActiveRecord::Schema.define(version: 20141020092721) do
create_table "payments", force: true do |t|
t.string "command"
end
end
Interesting moment: If i change string to integer in schema -> integer values are saved without any problems.
Remove attr_accessor from model. In the case of ActiveRecord models, getters and setters are already generated by ActiveRecord for your data columns. attr_accessor is not needed. attr_accessor — is a deprecated method of controlling mass assignment within ActiveRecord models.

the return value of QAbstractItemModel::setData()

when reimplement QAbstractItemModel::setData ( const QModelIndex & index, const QVariant & value, int role = Qt::EditRole )
as for the roles that we want to ignore ,what should we do ?
return false or return true ?or anything else ?
examples in the Qt documentation are even self-contradictory
here the return value is false
http://qt-project.org/doc/qt-4.8/model-view-programming.html#making-the-model-editable
but here the return value is true
http://qt-project.org/doc/qt-4.8/modelview.html#2-5-the-minimal-editing-example
You can return false if you consider that is an error to set data for this role and you want to avoid that.
You can return true if you want to ignore this data role but it isn't critical to try (but you should print an warning).
In this way, you don't "break" the setData() behavior (false: there is something wrong, true: it's OK, you can continue).
You should not return anything else to avoid weird behavior because user will wait a boolean. For example, if you test the return value that should be a boolean:
if setData( "test", Qt.DisplayRole) == False:
foo()
else:
bar()
bar() will be called even if setData() doesn't return True.

Resources