Extending Solidus to have file upload attached to an order - ruby-on-rails-6

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

Related

Rails 6 convention for declaring namespaced classes? zeitwerk autoloader

Curious what the preferred namespaced code should look like in rails 6 which uses zeitwerk for autoloading.
Previously I used:
# app/controllers/api/users_controller.rb
module Api
class UsersController
def index
render json: {}
end
end
end
With zeitwerk should we now use: ???
# app/controllers/api/users_controller.rb
class Api::UsersController
def index
render json: {}
end
end
Based on example in https://weblog.rubyonrails.org/2019/2/22/zeitwerk-integration-in-rails-6-beta-2/ it appears the 2nd style is being used.
By default rubocop will raise Style/ClassAndModuleChildren error with 2nd style and there are slight behavior differences:
module Foo
class Bar
def fud
end
end
end
module Foo
class Woo
def woo_woo
Bar.new.fud
end
end
end
class Foo::Bar
def fud
end
end
class Foo::Woo
def woo_woo
# NameError: uninitialized constant Foo::Woo::Bar
Bar.new.fud
# no error
Foo::Bar.new.fud
end
end
I don't think Zeitwerk itself cares about that either way. At the end of the day, controllers/api/users_controller.rb still defines Api::UsersController and Zeitwerk is able to find it in either case.
As a general rule,
module Api
class UsersController
end
end
is the preferred style, so you should probably stick with that.
See https://github.com/fxn/zeitwerk/issues/57

How to dynamically shape a DAG in Airflow

I have a custom DAG (meant to be subclassed), let's name it MyDAG. In the __enter__ method I want to add (or not) an operator based on the subclassing DAG. I'm not interested in using the BranchPythonOperator.
class MyDAG(DAG):
def __enter__(self, context):
start = DummyOperator(taks_id=start)
end = DummyOperator(task_id=end)
op = self.get_additional_operator()
if op:
start >> op
else:
start >> end
retrun self
def get_additional_operator(self):
# None if the subclass doesn't add any operator. A reference to another operator otherwise
if get_additional_operator is returning a reference, I'm obtaining this shape (two branches):
* start --> op
* end
otherwise, if it's returning None, I'm obtaining this (one branch):
* start --> end
What I want is not having end at all in the subclass inherting from MyDAG if get_additional_operator doesn't return None, something like this:
* start --> op
Instead of the two branches I'm obtaining above.
Airflow is somehow parsing every operator declared in the __enter__ method of a subclass of MyDAG. From that assumption, in order not to have an operator it only suffices to declare the operator in the right place. code below:
class MyDAG(DAG):
def __enter__(self, context):
start = DummyOperator(taks_id=start)
op = self.get_additional_operator()
if op:
start >> op
else:
end = DummyOperator(task_id=end)
start >> end
retrun self
def get_additional_operator(self):
# None if the subclass doesn't add any operator. A reference to another operator otherwise
The declaration of the end operator is made in the else section. I think it's only parsed when the else is evaluated to 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.

Single Table Inheritance(STI) Problems

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.

Resources