Groovy way of doing it in Grails - grails

Article {
String categoryName
static hasMany = [
tags: Tag
]
}
Tag {
String name
}
Now I want to find the list of all related articles. Related meaning, all articles that have the same category name as myArticle or any of the same tags as myArtcle.
With only matching categoryName, here is how I would get the relatedArticles using closures.
def relatedArticles = Article.list().find {it.categoryName == myArticle.categoryName }
Anyone wants to give a shot to find all articles by CategoryName or Tag Name (in a groovy way)?
Any solutions using Criteria or custom queries is appreciated too.

This would work:
def myArticle = // the article you want to match
def relatedArticles = Article.list().findAll {
(it.categoryName == myArticle.categoryName ||
it.tags.name.intersect(myArticle.tags.name)) &&
it.id != myArticle.id)
}
However, if you have a reasonably large number of articles and only a small number of them are expected to match it would be horribly inefficient, as this will load all articles then iterate through them all looking for matches.
A better way would be to simply write a query that only loads matching articles (instead of calling Article.list())

I think that you need to use a separate query per article tag:
// Use a LinkedHashSet to retain the result order if that is important
def results = new LinkedHashSet()
results.addAll(Article.findAll("from Article as article \
where article.categoryName = :categoryName \
and article.id != :id",
[
categoryName:myArticle.categoryName,
id:myArticle.id,
])
myArticle.tags.each {
results.addAll(Article.executeQuery(
"select distinct article from Article as article, \
Tag as tag \
where tag.name = :tag \
and tag in elements(article.tags) \
and article.id != :id",
[
tag:it.name,
id:myArticle.id,
]))
}
def relatedArticles = results as List
This is obviously worthwhile doing when you have a lot of content in the system and wish to avoid loading the entire database for a single page request. Other improvements include specifying max and offset parameters to the queries.

Ideally, you'd use Criteria Query, bug since you said you are not concerned about performance, something like this should work:
def category =
def tagName
def relatedArticles = Article.list().findAll {
(it.categoryName == myArticle.categoryName) || ( it.tags.contains(tagName) )
}

Related

Mongoid Aggregate result into an instance of a rails model

Introduction
Correcting a legacy code, there is an index of object LandingPage where most columns are supposed to be sortable, but aren't. This was mostly corrected, but few columns keep posing me trouble.
Theses columns are the one needing an aggregation, because based on a count of other documents. To simplify the explanation of the problem, I will speak only about one of them which is called Visit, as the rest of the code will just be duplication.
The code fetch sorted and paginate data, then modify each object using LandingPage methods before sending the json back. It was already like this and I can't modify it.
Because of that, I need to do an aggregation (to sort LandingPage by Visit counts), then get the object as LandingPage instance to let the legacy code work on them.
The problem is the incapacity to transform Mongoid::Document to a LandingPage instance
Here is the error I got:
Mongoid::Errors::UnknownAttribute:
Message:
unknown_attribute : message
Summary:
unknown_attribute : summary
Resolution:
unknown_attribute : resolution
Here is my code:
def controller_function
landing_pages = fetch_landing_page
landing_page_hash[:data] = landing_pages.map do |landing_page|
landing_page.do_something
# Do other things
end
render json: landing_page_hash
end
def fetch_landing_page
criteria = LandingPage.where(archived: false)
columns_name = params[:columns_name]
column_direction = params[:column_direction]
case order_column_name
when 'visit'
order_by_visits(criteria, column_direction)
else
criteria.order_by(columns_name => column_direction).paginate(
per_page: params[:length],
page: (params[:start].to_i / params[:length].to_i) + 1
)
end
def order_by_visit(criteria, order_direction)
def order_by_visits(landing_pages, column_direction)
LandingPage.collection.aggregate([
{ '$match': landing_pages.selector },
{ '$lookup': {
from: 'visits',
localField: '_id',
foreignField: 'landing_page_id',
as: 'visits'
}},
{ '$addFields': { 'visits_count': { '$size': '$visits' }}},
{ '$sort': { 'visits_count': column_direction == 'asc' ? 1 : -1 }},
{ '$unset': ['visits', 'visits_count'] },
{ '$skip': params[:start].to_i },
{ '$limit': params[:length].to_i }
]).map { |attrs| LandingPage.new(attrs) { |o| o.new_record = false } }
end
end
What I have tried
Copy and past the hash in console to LandingPage.new(attributes), and the instance was created and valid.
Change the attributes key from string to symbole, and it still didn't work.
Using is_a?(hash) on any element of the returned array returns true.
Put it to json and then back to a hash. Still got a Mongoid::Document.
How can I make the return of the Aggregate be a valid instance of LandingPage ?
Aggregation pipeline is implemented by the Ruby MongoDB driver, not by Mongoid, and as such does not return Mongoid model instances.
An example of how one might obtain Mongoid model instances is given in documentation.

How to extract given array of string with numbers from string in groovy

I'm trying to check if commit-msg from git contains particular ticket number with project key of Jira using groovy in Jenkins pipeline
def string_array = ['CO', 'DEVOPSDESK', 'SEC', 'SRE', 'SRE00IN', 'SRE00EU', 'SRE00US', 'REL']
def string_msg = 'CO-10389, CO-10302 new commit'
To extract numbers I am using below logic.
findAll( /\d+/ )*.toInteger()
Not sure how to extract exact ticket number with project key.
Thanks in advance.
You could use Groovy's find operator - =~, combined with a findAll() method to extract all matching elements. For that, you could create a pattern that matches CO-\d+ OR DEOPSDESK-\d+ OR ..., and so on. You could keep project IDs in a list and then dynamically create a regex pattern.
Consider the following example:
def projectKeys = ['CO', 'DEVOPSDESK', 'SEC', 'SRE', 'SRE00IN', 'SRE00EU', 'SRE00US', 'REL']
def commitMessage = 'CO-10389, CO-10302 new commit'
// Generate a pattern "CO-\d+|DEVOPSDEKS-\d+|SEC-\d+|...
def pattern = projectKeys.collect { /${it}-\d+/ }.join("|")
// Uses =~ (find) operator and extracts matching elements
def jiraIds = (commitMessage =~ pattern).findAll()
assert jiraIds == ["CO-10389","CO-10302"]
// Another example
assert ("SEC-1,REL-2001 some text here" =~ pattern).findAll() == ["SEC-1","REL-2001"]
The regex can be assembled a bit simpler:
def projectKeys = ['CO', 'DEVOPSDESK', 'SEC', 'SRE', 'SRE00IN', 'SRE00EU', 'SRE00US', 'REL']
def commitMessage = 'CO-10389, REL-10302 new commit'
String regex = /(${projectKeys.join('|')})-\d+/
assert ['CO-10389', 'REL-10302'] == (commitMessage =~ regex).findAll()*.first()
You can have also another option with finer contol over matching:
def res = []
commitMessage.eachMatch( regex ){ res << it[ 0 ] }
assert ['CO-10389', 'REL-10302'] == res

how to get the key value from the nested hash inside the array?

I have a array which is inside a hash. I want know the result of the student (pass/fail) using the following array. First I have to match them with particular standard and compare their marks with the hash pass and fails. And I want to get the key pass or fail based on their mark. How to achieve this using Ruby?
array = [
{
:standard =>1
:pass=>{:tamil=>30,:eng=>25,:math=>35},
:fail=>{:tamil=>10,:eng=>15,:maths=>20}
},
{
:standard =>2,
:pass=>{:tamil=>40,:eng=>35,:math=>45},
:fail=>{:tamil=>20,:eng=>25,:maths=>30}
}
]
#student is assumed to be defined
standard = array.select {|standard| standard[:standard] == #student.standard}
eng_pass = #student.eng_mark >= standard[:pass][:eng]
eng_fail = #student.eng_mark <= standard[:fail][:eng]
return [eng_pass, eng_fail, whatever_else_you_want]
So on and forth for various topics.
The syntax in reading values from this structure is something like:
array[0][:pass][:eng]
and accordingly you can do the comparison as usual in batch:
for i in 0..#students_array.length
num = # student's score
standard = # something like array[0][:pass][:eng]
if num > standard
# something like 'put "You passed!"'
end
end

Prevent criteria filter inside conjunctive statement

An example of what I'm trying to do is:
def authorName = "John Smith"
def books = Book.createCriteria().list() {
eq('genre', 'fiction')
eq('publishDate', '2007')
if(authorName != null){
Author author = Author.findWhere(name: authorName)
if( author == null ) //what do I do here?
else { eq('authorId', author.id }
}
}
If there is no author for the given id, then the author doesn't exist (assuming it wasn't removed) and thus there are no books written by the author. The evaluation should stop there and not return any results. What can I use to accomplish this?
I am not really 100% what you are trying to do. If you only want to execute Book query if the author exists, you could so something like this...
def authorName = "John Smith"
Author author = Author.findWhere(name: authorName)
def books
if(author) {
books = Book.withCriteria {
eq('genre', 'fiction')
eq('publishDate', '2007')
// I can't tell if this is the right thing because
// I don't know what your model looks like, but I will
// assume this part is valid because it is what you had
// in your example.
eq 'authorId', author.id
}
}
Depending on what your model looks like, you could also just make the authorName part of the criteria so now you don't have to execute 2 queries...
def authorName = "John Smith"
def books = Book.withCriteria {
eq('genre', 'fiction')
eq('publishDate', '2007')
// this assumes that Book has a property named
// "author" which points to the Author
author {
eq 'name', authorName
}
}

search models tagged_with OR title like (with acts_as_taggable_on)

I'm doing a search on a model using a scope. This is being accessed by a search form with the search parameter q. Currently I have the code below which works fine for searches on tags associated with the model. But I would also like to search the title field. If I add to this scope then I will get all results where there is a tag and title matching the search term.
However, I need to return results that match the company_id and category_id, and either/or matching title or tag. I'm stuck with how to add an OR clause to this scope.
def self.get_all_products(company, category = nil, subcategory = nil, q = nil)
scope = scoped{}
scope = scope.where "company_id = ?", company
scope = scope.where "category_id = ?", category unless category.blank?
scope = scope.tagged_with(q) unless q.blank?
scope
end
I'm using Rails 3.
Ever consider Arel? You can do something like this
t = TableName.arel_table
scope = scope.where(t[:title].eq("BLAH BLAH").or(t[:tag].eq("blah blah")))
or else you can do
scope = scope.where("title = ? OR tag = ", title_value, tag_value)
I could be wrong, but I don't think scopes can help you to construct an or condition. You'll have to hand-write the code to buid your where clause instead. Maybe something like this...
clause = "company_id=?"
qparams = [company]
unless category.blank?
clause += " or category_id=?"
qparams <= category
end
scope.where clause, *qparams

Resources