add updated_at/created_at to query

pull/7407/head
ulferts 6 years ago
parent 45037e9476
commit 6465f3f54a
No known key found for this signature in database
GPG Key ID: A205708DE1284017
  1. 13
      app/models/queries/queries.rb
  2. 35
      app/models/queries/queries/filters/id_filter.rb
  3. 35
      app/models/queries/queries/filters/updated_at_filter.rb
  4. 8
      db/migrate/20190619143049_add_timestamps_to_query.rb
  5. 40
      docs/api/apiv3/endpoints/queries.apib
  6. 1
      docs/api/apiv3/endpoints/versions.apib
  7. 45
      lib/api/v3/queries/query_representer.rb
  8. 10
      lib/api/v3/queries/schemas/query_schema_representer.rb
  9. 15
      modules/boards/spec/features/support/board_page.rb
  10. 10
      spec/lib/api/v3/queries/query_representer_generation_spec.rb
  11. 26
      spec/lib/api/v3/queries/schemas/query_schema_representer_spec.rb
  12. 50
      spec/models/queries/queries/filters/updated_at_filter_spec.rb
  13. 17
      spec/models/queries/queries/query_query_spec.rb
  14. 74
      spec/requests/api/v3/queries/query_resource_spec.rb

@ -30,8 +30,15 @@
# Configures a Query on the Query model. This allows to
# e.g get all queries that belong to a specific project or
# all projects that are global
module Queries::Queries
Queries::Register.filter Queries::Queries::QueryQuery, Queries::Queries::Filters::ProjectFilter
Queries::Register.filter Queries::Queries::QueryQuery, Queries::Queries::Filters::ProjectIdentifierFilter
Queries::Register.filter Queries::Queries::QueryQuery, Queries::Queries::Filters::HiddenFilter
filters_ns = Queries::Queries::Filters
query_ns = Queries::Queries::QueryQuery
register = Queries::Register
register.filter query_ns, filters_ns::ProjectFilter
register.filter query_ns, filters_ns::ProjectIdentifierFilter
register.filter query_ns, filters_ns::HiddenFilter
register.filter query_ns, filters_ns::UpdatedAtFilter
register.filter query_ns, filters_ns::IdFilter
end

@ -0,0 +1,35 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
class Queries::Queries::Filters::IdFilter < Queries::Queries::Filters::QueryFilter
def type
:integer
end
end

@ -0,0 +1,35 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
class Queries::Queries::Filters::UpdatedAtFilter < Queries::Queries::Filters::QueryFilter
def type
:datetime_past
end
end

@ -0,0 +1,8 @@
class AddTimestampsToQuery < ActiveRecord::Migration[5.2]
def change
add_column :queries, :created_at, :datetime
add_column :queries, :updated_at, :datetime
add_index(:queries, :updated_at)
end
end

@ -43,6 +43,8 @@ Please note, that all the properties listed above will also be embedded when ind
| hidden | Should the query be hidden from the query list? | Boolean | | READ |
| public | Can users besides the owner see the query? | Boolean | | READ |
| starred | Should the query be highlighted to the user? | Boolean | | READ |
| createdAt | Time of creation | DateTime | not null | READ |
| updatedAt | Time of the most recent change to the query | DateTime | not null | READ |
A query that is not assigned to a project (`"project": null`) is called a global query. Global queries filter work packages regardless of the project they are assigned to. As such, a different set of filters exists for those queries.
@ -115,6 +117,8 @@ If the values are nonprimitive (e.g. User, Project), they will be listed as obje
"_type": "Query",
"id": 9,
"name": "fdsfdsfdsf",
"createdAt": "2015-03-20T12:56:56Z",
"updatedAt": "2015-05-20T18:16:53Z",
"filters": [
{
"_type": "StatusQueryFilter",
@ -768,6 +772,8 @@ Same as [viewing an existing, persisted Query](#queries-query-get) in its respon
"_type": "Query",
"id": 9,
"name": "fdsfdsfdsf",
"createdAt": "2015-03-20T12:56:56Z",
"updatedAt": "2015-05-20T18:16:53Z",
"filters": [
{
"_type": "StatusQueryFilter",
@ -967,6 +973,8 @@ Same as [viewing an existing, persisted Query](#queries-query-get) in its respon
"_type": "Query",
"id": 9,
"name": "fdsfdsfdsf",
"createdAt": "2015-03-20T12:56:56Z",
"updatedAt": "2015-05-20T18:16:53Z",
"filters": [
{
"_type": "StatusQueryFilter",
@ -1180,6 +1188,8 @@ Same as [viewing an existing, persisted Query](#queries-query-get) in its respon
"_type": "Query",
"id": 9,
"name": "fdsfdsfdsf",
"createdAt": "2015-03-20T12:56:56Z",
"updatedAt": "2015-05-20T18:16:53Z",
"filters": [
{
"_type": "StatusQueryFilter",
@ -1329,6 +1339,8 @@ Returns a collection of queries. The collection can be filtered via query parame
Accepts the same format as returned by the [queries](#queries) endpoint.
Currently supported filters are:
+ project: filters queries by the project they are assigned to. If the project filter is passed with the `!*` (not any) operator, global queries are returned.
+ id: filters queries based on their id
+ updated_at: filters queries based on the last time they where updated
+ Response 200 (application/hal+json)
@ -1429,6 +1441,20 @@ For more details and all possible responses see the general specification of [Fo
"minLength": 1,
"maxLength": 255
},
"createdAt": {
"type": "DateTime",
"name": "Created on",
"required": true,
"hasDefault": false,
"writable": false
},
"updatedAt": {
"type": "DateTime",
"name": "Updated on",
"required": true,
"hasDefault": false,
"writable": false
},
"user": {
"type": "User",
"name": "User",
@ -1729,6 +1755,20 @@ Retrieve the schema for global queries, those, that are not assigned to a projec
"minLength": 1,
"maxLength": 255
},
"createdAt": {
"type": "DateTime",
"name": "Created on",
"required": true,
"hasDefault": false,
"writable": false
},
"updatedAt": {
"type": "DateTime",
"name": "Updated on",
"required": true,
"hasDefault": false,
"writable": false
},
"user": {
"type": "User",
"name": "User",

@ -95,6 +95,7 @@ Depending on custom fields defined for versions, additional properties might exi
Updates the given version by applying the attributes provided in the body. Please note that while there is a fixed set of attributes, custom fields can extend a version's attributes and are accepted by the endpoint.
+ Parameters
+ id (required, integer, `1`) ... Version id

@ -38,16 +38,17 @@ module API
self_link
include API::Decorators::LinkedResource
include API::Decorators::DateProperty
associated_resource :project,
setter: ->(fragment:, **) {
id = id_from_href "projects", fragment['href']
id = if id.to_i.nonzero?
id # return numerical ID
else
Project.where(identifier: id).pluck(:id).first # lookup Project by identifier
end
id # return numerical ID
else
Project.where(identifier: id).pluck(:id).first # lookup Project by identifier
end
represented.project_id = id if id
},
@ -60,10 +61,10 @@ module API
link :results do
path = if represented.project
api_v3_paths.work_packages_by_project(represented.project.id)
else
api_v3_paths.work_packages
end
api_v3_paths.work_packages_by_project(represented.project.id)
else
api_v3_paths.work_packages
end
url_query = ::API::V3::Queries::QueryParamsRepresenter
.new(represented)
@ -93,10 +94,10 @@ module API
link :schema do
href = if represented.project
api_v3_paths.query_project_schema(represented.project.identifier)
else
api_v3_paths.query_schema
end
api_v3_paths.query_project_schema(represented.project.identifier)
else
api_v3_paths.query_schema
end
{
href: href
}
@ -104,10 +105,10 @@ module API
link :update do
href = if represented.new_record?
api_v3_paths.create_query_form
else
api_v3_paths.query_form(represented.id)
end
api_v3_paths.create_query_form
else
api_v3_paths.query_form(represented.id)
end
{
href: href,
@ -117,7 +118,8 @@ module API
link :updateImmediately do
next unless represented.new_record? && allowed_to?(:create) ||
represented.persisted? && allowed_to?(:update)
represented.persisted? && allowed_to?(:update)
{
href: api_v3_paths.query(represented.id),
method: :patch
@ -126,7 +128,7 @@ module API
link :updateOrderedWorkPackages do
next unless represented.new_record? && allowed_to?(:create) ||
represented.persisted? && allowed_to?(:reorder_work_packages)
represented.persisted? && allowed_to?(:reorder_work_packages)
{
href: api_v3_paths.query(represented.id),
@ -136,7 +138,7 @@ module API
link :delete do
next if represented.new_record? ||
!allowed_to?(:destroy)
!allowed_to?(:destroy)
{
href: api_v3_paths.query(represented.id),
@ -277,6 +279,11 @@ module API
property :id,
writeable: false
property :name
date_time_property :created_at
date_time_property :updated_at
property :filters,
exec_context: :decorator

@ -71,6 +71,14 @@ module API
max_length: 255,
visibility: false
schema :created_at,
type: 'DateTime',
visibility: false
schema :updated_at,
type: 'DateTime',
visibility: false
schema :user,
type: 'User',
has_default: true,
@ -257,7 +265,7 @@ module API
end
def filters_schemas
# TODO: The RelatableFilter is not supported by the schema depdendencies yet
# TODO: The RelatableFilter is not supported by the schema dependencies yet
filters = represented
.available_filters
.reject { |f| f.is_a?(::Queries::WorkPackages::Filter::RelatableFilter) }

@ -117,6 +117,20 @@ module Pages
end
end
##
# Expect the given work packages (or their subjects) to be listed in that exact order in the list.
# No non mentioned cards are allowed to be in the list.
def expect_cards_in_order(list_name, *card_titles)
within_list(list_name) do
found = all('.wp-card .wp-card--subject')
.map(&:text)
expected = card_titles.map { |title| title.is_a?(WorkPackage) ? title.subject : title.to_s }
expect(found)
.to match expected
end
end
def move_card(index, from:, to:)
source = page.all("#{list_selector(from)} .wp-card")[index]
target = page.find list_selector(to)
@ -276,7 +290,6 @@ module Pages
end
end
expect_and_dismiss_notification message: I18n.t('js.notice_successful_update')
page.within('.board--header-container') do

@ -563,6 +563,16 @@ describe ::API::V3::Queries::QueryRepresenter do
end
end
it_behaves_like 'has UTC ISO 8601 date and time' do
let(:date) { query.created_at }
let(:json_path) { 'createdAt' }
end
it_behaves_like 'has UTC ISO 8601 date and time' do
let(:date) { query.updated_at }
let(:json_path) { 'updatedAt' }
end
describe 'highlighting' do
context 'with EE', with_ee: %i[conditional_highlighting] do
it 'renders when the value is set' do

@ -134,6 +134,32 @@ describe ::API::V3::Queries::Schemas::QuerySchemaRepresenter do
it_behaves_like 'has no visibility property'
end
describe 'createdAt' do
let(:path) { 'createdAt' }
it_behaves_like 'has basic schema properties' do
let(:type) { 'DateTime' }
let(:name) { Query.human_attribute_name('created_at') }
let(:required) { true }
let(:writable) { false }
end
it_behaves_like 'has no visibility property'
end
describe 'updatedAt' do
let(:path) { 'updatedAt' }
it_behaves_like 'has basic schema properties' do
let(:type) { 'DateTime' }
let(:name) { Query.human_attribute_name('updated_at') }
let(:required) { true }
let(:writable) { false }
end
it_behaves_like 'has no visibility property'
end
describe 'user' do
let(:path) { 'user' }

@ -0,0 +1,50 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe Queries::Queries::Filters::UpdatedAtFilter, type: :model do
it_behaves_like 'basic query filter' do
let(:type) { :datetime_past }
let(:class_key) { :updated_at }
describe '#available?' do
it 'is true' do
expect(instance).to be_available
end
end
describe '#allowed_values' do
it 'is nil' do
expect(instance.allowed_values).to be_nil
end
end
it_behaves_like 'non ar filter'
end
end

@ -41,6 +41,20 @@ describe Queries::Queries::QueryQuery, type: :model do
end
end
context 'with an updated_at filter' do
before do
instance.where('updated_at', '<>d', ['2018-03-22 20:00:00'])
end
describe '#results' do
it 'is the same as handwriting the query' do
expected = base_scope.merge(Query.where("queries.updated_at >= '2018-03-22 20:00:00'"))
expect(instance.results.to_sql).to eql expected.to_sql
end
end
end
context 'with a project filter' do
before do
instance.where('project_id', '=', ['1', '2'])
@ -51,8 +65,7 @@ describe Queries::Queries::QueryQuery, type: :model do
# apparently, strings are accepted to be compared to
# integers in the dbs (mysql, postgresql)
expected = base_scope
.merge(Query
.where("queries.project_id IN ('1','2')"))
.merge(Query.where("queries.project_id IN ('1','2')"))
expect(instance.results.to_sql).to eql expected.to_sql
end

@ -93,9 +93,9 @@ describe 'API v3 Query resource', type: :request, content_type: :json do
context 'filtering for project' do
let(:path) do
filter = [project: { operator: "=", values: [project.id.to_s] }].to_json
filter = [project: { operator: "=", values: [project.id.to_s] }]
"#{api_v3_paths.queries}?filters=#{URI::escape(filter)}"
api_v3_paths.path_for(:queries, filters: filter)
end
let(:prepare) do
@ -104,9 +104,9 @@ describe 'API v3 Query resource', type: :request, content_type: :json do
other_query
FactoryBot.create(:member,
roles: [role],
project: other_query.project,
user: current_user)
roles: [role],
project: other_query.project,
user: current_user)
end
it 'includes only queries from the specified project' do
@ -124,9 +124,9 @@ describe 'API v3 Query resource', type: :request, content_type: :json do
context 'filtering for global query' do
let(:path) do
filter = [project: { operator: "!*", values: [] }].to_json
filter = [project: { operator: "!*", values: [] }]
"#{api_v3_paths.queries}?filters=#{URI::escape(filter)}"
api_v3_paths.path_for(:queries, filters: filter)
end
let(:prepare) do
@ -135,12 +135,64 @@ describe 'API v3 Query resource', type: :request, content_type: :json do
other_query
FactoryBot.create(:member,
roles: [role],
project: other_query.project,
user: current_user)
roles: [role],
project: other_query.project,
user: current_user)
end
it 'includes only queries from the specified project' do
it 'includes only queries not belonging to a project' do
expect(last_response.body)
.to be_json_eql(1)
.at_path("count")
expect(last_response.body)
.to be_json_eql(1)
.at_path("total")
expect(last_response.body)
.to be_json_eql(global_query.name.to_json)
.at_path("_embedded/elements/0/name")
end
end
context 'filtering by updated_at' do
let(:old_query) { FactoryBot.create(:public_query, project: project) }
let(:prepare) do
query
old_query.update_column(:updated_at, DateTime.current - 4.hours)
end
let(:path) do
filter = [updated_at: { operator: "<>d", values: [(DateTime.current - 3.hour).to_s] }]
api_v3_paths.path_for(:queries, filters: filter)
end
it 'includes only queries updated after the value' do
expect(last_response.body)
.to be_json_eql(1)
.at_path("count")
expect(last_response.body)
.to be_json_eql(1)
.at_path("total")
expect(last_response.body)
.to be_json_eql(query.name.to_json)
.at_path("_embedded/elements/0/name")
end
end
context 'filtering by id' do
let(:prepare) do
query
global_query
end
let(:path) do
filter = [id: { operator: "=", values: [global_query.id.to_s] }]
api_v3_paths.path_for(:queries, filters: filter)
end
it 'includes only queries with that id' do
expect(last_response.body)
.to be_json_eql(1)
.at_path("count")

Loading…
Cancel
Save