I am using the Ancestry gem. Each node has a numerical value associated with it (:value). Each time I CRUD a node, I want to recalculate the whole tree so that each node ends up with the sum of the values of its children. (Leaf nodes stay unchanged.)
My controller actions contain this code to trigger the recalculation:
def create
...
#tree = Tree.new(tree_params)
if #tree.save
Tree.roots.each do |r|
recalc_tree(r)
end
redirect_to trees_url, notice: "Node was successfully created. " + note.to_s
else
render :new, notice: "There was an error saving this node. Please try again."
end
and in the private section at the bottom of the controller I have this custom helper method:
def recalc_tree(r)
total = 0
if r.has_children?
r.children.each do |c|
total = total + recalc_tree(c)
end
r.value = total
else
r.value
end
end
I don't get any errors in the browser (the node saves correctly) but neither does this update any of the values of the ancestor nodes.
I tried running the helper in the console but got an "undefined method" error:
>> helper.recalc_tree(Tree.roots.first)
Tree Load (0.0ms) SELECT "trees".* FROM "trees" WHERE "trees"."ancestry" IS NULL ORDER BY "trees"."id" ASC LIMIT 1
NoMethodError: undefined method `recalc_tree' for #<ActionView::Base:0x5fc82c0>
What am I doing wrongly?
The more rails-style way to do this is to use ActiveRecord's callback (http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html). Also since when a node is updated, only the updated node's ancestors will need to be updated, so we can save some CPU cycles by updating only ancestors along the path to the root.
Example:
class Tree < ActiveRecord::Base
after_save :update_ancestors
...
...
private
def update_ancestors
self.ancestors.each do |ancestor|
ancestor.value = ancestor.descendants.pluck(:value).sum
ancestor.save
end
end
end
I did not actually ran this code on my machine, but this is pretty much the direction to this problem!
Related
I have two entities.
First is my Parent entity, that has property called productsCount.
This entity has some another entity linked to it called Store and store have linked Room. Each room can have multiple products in it.
When I edit products that are assigned to room, I want to update the Parent productsCount to store count of all products in all rooms in all stores.
I have a SQL query that does calculate the count. I need to do this each time I update the Room with new products. This is done using preFlush hook using EntityListener on the Room entity.
The preFlush hook gets triggered properly, but then it will timeout for some reason.
Here is the preFlush code example
public function preFlush(Room $room, PreFlushEventArgs $args)
{
$em = $args->getEntityManager();
$parent = $room->getStore()->getParent();
$rsm = new ResultSetMapping();
$rsm->addScalarResult('COUNT(product_id)', 'count');
$query = $em->createNativeQuery(
'select COUNT(product_id)
from room_product where `room_id` in
(select id from room where store_id in
(select id from store where parent_id = :parentId))', $rsm);
$query->setParameter('parentId', $parent->getId());
$result = $query->getOneOrNullResult();
$parent->setNumberOfServices($result['count']);
$em->persist($parent);
$em->flush();
}
The query should be working fine, so I think it has something to do with flushing and persisting the parent entity.
Any ideas?
So, I was able to find a solution after few hours of fighting.
Turns out I dont have to persist, nor flush the changes. The solution is to use Unit of Work to recompute changes on the $parent and then doctrine will flush them by its own. I also had to change the way I count the products, as in the preFlush stage the change is not yet in the database, so the query will not work properly. Thats why I count them manually by traversing the tree of relations.
Here is a code sample.
public function preFlush(Room $room, PreFlushEventArgs $args)
{
$em = $args->getEntityManager();
$numOfProducts = 0;
$parent = $room->getStore()->getParent();
foreach($parent->getStores() as $store) {
foreach($store->getRooms() as $room) {
$numOfServices += count($room->getProducts());
}
}
$parent->setNumberOfProducts($numOfProducts);
$classMetadata = $em->getClassMetadata(get_class($parent));
$em->getUnitOfWork()->computeChangeSet($classMetadata, $parent);
}
The reason is recursion: you're calling $em->flush(); from the subscriber, EntityManager enters flush, fires preFlush event which calls your handler which again calls $em->flush() and so on.
IIRC preFlush is called before change set calculation so just updating your entity with new value should be enough for Doctrine to detect said change.
I am getting an error in my SpecFlow unit test:
System.Collections.Generic.KeyNotFoundException : The given key was not present in the dictionary.
at TechTalk.SpecFlow.SpecFlowContext.Get[T](String key)
at F*****.Live.PS.*******.Steps.*******.EnterStagingDateAndSaveSteps.ThenUserSeesInNEXTRE_ENROLMENTWINDOWTextFieldInPENSIONASSESSMENTDATESPageInPENSIONModule(String p0) in c:\Git\LivePeopleSystemTests\Fourth.Live.PS.AutomationTests\F*****.Live.PS.*******.Steps*******\EnterStagingDateAndSaveSteps.cs:line 175
Here is the referenced method in EnterStagingDateAndSaveSteps.cs
[Then(#"user sees ""(.*)"" in NEXT RE-ENROLMENT WINDOW Text Field in PENSION ASSESSMENT DATES page in PENSION module")]
public void ThenUserSeesInNEXTRE_ENROLMENTWINDOWTextFieldInPENSIONASSESSMENTDATESPageInPENSIONModule(string p0)
{
// GetNextReEnrollmentWindow
string validator =
ScenarioContext.Current.Get<PensionAssessmentDates>("_nextReEnrollmentWindow")
.GetNextReEnrollmentWindow();
Assert.IsTrue(validator == p0);
}
(I also tried return Driver.WaitAndGetText(_nextReEnrollmentWindow); in there)
so it looks like the key _nextReEnrollmentWindow doesn't exist, but here it is defined in PensionAssessmentDates.cs:
private readonly By _nextReEnrollmentWindow = By.Id("NextReEnrollmentWindow");
The PensionAssessmentSteps is set into the ScenarioContext.Current like this:
[Then(#"user sees ""(.*)"" in NEXT RE-ENROLMENT WINDOW Text Field in PENSION ASSESSMENT DATES page in PENSION module")]
public void ThenUserSeesInNEXTRE_ENROLMENTWINDOWTextFieldInPENSIONASSESSMENTDATESPageInPENSIONModule(string p0)
{
// GetNextReEnrollmentWindow
string validator =
ScenarioContext.Current.Get<PensionAssessmentDates>("_nextReEnrollmentWindow")
.GetNextReEnrollmentWindow();
Assert.IsTrue(validator == p0);
}
and here is the actual web-page I am trying to test showing the element exists:
I'd be grateful for any pointers or advice on what I have missed which is causing my unit test to stop as below:
EnterStagingDateAndSaveSteps.WhenUserClicksSAVEButtonUnderAUTOMATICENROLMENTATSTAGINGDATEFrameInPENSIONASSESSMENTDATESPageInPENSIONModule() (0.7s)
Then user sees "02 Oct 2019 to 02 Apr 2020" in NEXT RE-ENROLMENT WINDOW Text Field in PENSION ASSESSMENT DATES page in PENSION module
-> error: The given key was not present in the dictionary.
The ScenarioContext.Current.Get and Set methods simply allow you to add an object to the dictionary of values attached to the current scenario using a key.
Once you have added an element to the dictionary in one step, you can retrieve it in another step. The exception you are getting implies that you have not added anything to the dictionary using the key _nextReEnrollmentWindow.
Do you call ScenarioContext.Current.Set("_nextReEnrollmentWindow", something); anywhere in your code? I suspect not.
Given the way you have asked the question I'm suspecting that you expect the ScenarioContext.Current.Get<PensionAssessmentDates>("_nextReEnrollmentWindow") call to get you the current instance of the page object of type PensionAssessmentDates and then get the element using the selector _nextReEnrollmentWindow. This is not how it works.
you want to do one of two things I believe. Either add your page object PensionAssessmentDates to the ScenarioContext.Current and then get the page object out and call the method which uses the private field _nextReEnrollmentWindow.
Or (much better in my opinion) ditch your use of the ScenarioContext.Current altogether and instead create objects which hold your page objects and let Specflows internal DI framework provide those to your step classes using context injection.
I have been losing my mind over the last couple of weeks due to some very strange behaviour from ProMotion / RedPotion.
I have narrowed down the strange behaviour to a seemingly out of order execution of ProMotion's lifecycle methods and an API call.
I have a Screen that will get info from my API and display an image based on the image url returned from the API. I have oversimplified my project to a Screen, a Model and a Stylesheet as follows:
TestScreen:
class TestScreen < PM::Screen
title "Your title here"
stylesheet TestScreenStylesheet
def on_load
mp "ran on_load"
#face_image = append!(UIImageView, :face_image)
mp "getting data from API"
Face.get(1) do |response, face|
if response.success?
mp "face returned from API:"
mp face.inspect
#face = face
else
#face = [{attributes: {name: "No faces found"}}]
end
end
mp "should have printed data obtained from API"
end
def will_appear
mp "ran on_will_appear"
mp "face in will_appear:"
if #face
rmq(:face_image).attr(remote_image: #face.picture)
else
mp "#face is nil!!!"
end
end
end
Stylesheet:
class TestScreenStylesheet < ApplicationStylesheet
def setup
end
def root_view(st)
st.background_color = color.white
end
def face_image(st)
st.frame = {l: 30, t: 140, w: 250, h: 250}
st.placeholder_image = image.resource("placeholder_image.png")
end
end
Model:
class Face
attr_accessor :id, :name, :description, :local_description, :picture, :bio_picture, :star_ranking, :status, :facetype_id, :answers
def initialize(response)
#id = response[:data][0][:id]
#name = response[:data][0][:attributes][:name]
#description = response[:data][0][:attributes][:description]
#local_description = response[:data][0][:attributes][:local_description]
#picture = response[:data][0][:attributes][:picture]
#bio_picture = response[:data][0][:attributes][:bio_picture]
#star_ranking = response[:data][0][:attributes][:star_ranking]
#status = response[:data][0][:attributes][:status]
#facetype_id = response[:data][0][:attributes][:facetype_id]
#answers = response[:data][0][:attributes][:answers]
end
def self.get(category_id,&callback)
ApiClient.client.get "random_face?mycategory=#{category_id}" do |response|
model = nil
if response.success?
model = self.new(response.object)
end
callback.call(response, model)
end
end
end
I have placed printout commands (mp) so that I can figure out what is being executed when, and as you can see from the results below, everything is out of order:
"ran on_load"
"getting data from API"
"should have printed data obtained from API"
"ran on_will_appear"
"face in will_appear:"
"#face is nil!!!"
"face returned from API:"
"#<Face:0x113c37c90 #id=\"1\" #name=\"Leonel Messi\" #description=\"Leonel Messi es un jugador portugués de fútbol, 4 veces ganador del Balón de Oro\" #local_description=\"translation missing: en.Leonel Messi_description\" #picture=\"default_url\" #bio_picture=\"default_url\" #star_ranking=1 #status=\"active\" #facetype_id=nil #answers=\"[\\\"Kun Aguero\\\", \\\"Nicolas Shevchenko\\\", \\\"Leonel Messi\\\", \\\"Clarence Seedorf\\\"]\">"
The on_load method fires first, as expected, but the API call, RIGHT IN THE MIDDLE OF THE on_load method, fires last. Therefore the image attrib I am trying to set in the view_did_load method is nil and fails.
I come from a Rails background and I am new to RubyMotion / ProMotion / RedPotion so I may be going about this all wrong, but it certainly seems something is very wrong here.
As andrewhavens pointed out here:
https://github.com/infinitered/redpotion/issues/164
the reason this happens is that my API call is asynchronous, so it will not finish executing and set the instance variable in time for the will_appear method to use it (it will still be nil at the time will_appear runs).
In order to set the attributes properly, they must be set AFTER the API call completes, for example in the callback itself like this:
...
if response.success?
mp "face returned from API:"
mp face.inspect
#face = face
rmq(:face_image).attr(remote_image: #face.picture)
else
...
then it will work just fine.
Hope this saves someone hours of troubleshooting.
I am trying to create a new record using BS server script.
Since the process is taking place inside the BS, the context of Parent is not present, hence I am unable to get Parent Row_Id which I need to explicitly stamp against the child record being created for visibility.
Initially I tried to pass the Parent Row_Id from applet as a profile, but this fails when there are no records in the child applet, ie this.BusComp().ParentBusComp().GetFieldValue returns "This operation is invalid when there are no records present" as the "this" context is unavailable.
Any suggestions?
I was able to achieve the desired with the below code
sId = TheApplication().ActiveBusObject().GetBusComp("Q").ParentBusComp().GetFieldValue("Id");
if(this.BusComp().CountRecords() > 0)
{
sA = TheApplication().ActiveBusObject().GetBusComp("Q").GetFieldValue("A");
sB = TheApplication().ActiveBusObject().GetBusComp("Q").GetFieldValue("B");
}
sEntity = TheApplication().ActiveBusObject().GetBusComp("Q").Name();
It is for these reasons that Siebel provides Pre-Default settings at the Business Component Field level. If you wish to do this entirely through scripting, you will have to find the Active context, you have to know which BC is the parent.
Lets say you know that the Parent BC has to be Account. So
ActiveBusObject().GetBusComp("Account").GetFieldValue("Id") will give you the row id of the currently selected Account BC record. But do make sure that this script fires only in this context. So check the ActiveViewName to check this.
if(TheApplication().GetProfileAttr("ActiveViewName")=="Custom View")
{
//put the scripting here.
}
In Grails 1.3.7, I have a code that filters objects by ID and I need to test that
The domain class has a sequence
static mapping = {
id generator: 'sequence', params[sequence: 'seq_shipping_service']
}
In the test, the object is created several times and I need the identifier to be 11 in all the tests and even though, it deletes the whole database between every test, it doesn't reset the sequence. So I would get a superior ID
foo = createFoo()
foo.id = 11l
foo.save () //This gets error
My ideas are
1) Reset somehow the id sequence so it's everytime the same number between tests
2) Set somehow the id
I don't know if I make myself clear
Personally, if I need to do something like this I:
create an object mother to spawn all required objects (i.e. foo.id == 11)
use Groovy SQL to insert data into database. In your case:
public class BlahBlahTest extends Specification {
DataSource dataSource
def setup () {
new Sql(dataSource).exec ("insert into blablah (11, ...)")
}
...
Obviously you could do anything else there - for example recreate the sequence to start with 11.