From 3644f5f4cd5c4c2466b369cdca4bd6da809f5bc3 Mon Sep 17 00:00:00 2001 From: sahil28297 <37302950+sahil28297@users.noreply.github.com> Date: Mon, 21 Jan 2019 16:02:40 +0530 Subject: [PATCH] feat: relative timeframe filters (#6792) * feat: relative timeframe filters * fix: resolve syntax errors * fix: Translated options --- cypress/fixtures/example.json | 5 ++ cypress/integration/relative_filters.js | 50 +++++++++++++++++++ frappe/model/db_query.py | 31 +++++++++++- .../js/frappe/ui/filters/edit_filter.html | 2 + frappe/public/js/frappe/ui/filters/filter.js | 40 ++++++++++++--- frappe/tests/test_utils.py | 26 +++++++++- frappe/utils/data.py | 4 +- 7 files changed, 148 insertions(+), 10 deletions(-) create mode 100644 cypress/fixtures/example.json create mode 100644 cypress/integration/relative_filters.js diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json new file mode 100644 index 0000000000..da18d9352a --- /dev/null +++ b/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} \ No newline at end of file diff --git a/cypress/integration/relative_filters.js b/cypress/integration/relative_filters.js new file mode 100644 index 0000000000..efc6b930b2 --- /dev/null +++ b/cypress/integration/relative_filters.js @@ -0,0 +1,50 @@ +context('Relative Timeframe', () => { + beforeEach(() => { + cy.login('Administrator', 'qwe'); + cy.visit('/desk'); + }); + before(() => { + cy.login('Administrator', 'qwe'); + cy.visit('/desk'); + cy.window().its('frappe').then(frappe => { + frappe.call("frappe.tests.test_utils.create_todo_records"); + }); + }); + it('set relative filter for Previous and check list', () => { + cy.visit('/desk#List/ToDo/List'); + cy.get('.list-row:contains("this is fourth todo")').should('exist'); + cy.get('.tag-filters-area .btn:contains("Add Filter")').click(); + cy.get('.fieldname-select-area').should('exist'); + cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 }); + cy.get('select.condition.form-control').select("Previous"); + cy.get('.filter-field select.input-with-feedback.form-control').select("1 week"); + cy.server(); + cy.route({ + method: 'POST', + url: '/' + }).as('applyFilter'); + cy.get('.filter-box .btn:contains("Apply")').click(); + cy.wait('@applyFilter'); + cy.get('.list-row-container').its('length').should('eq', 1); + cy.get('.list-row-container').should('contain', 'this is second todo'); + cy.get('.remove-filter.btn').click(); + }); + it('set relative filter for Next and check list', () => { + cy.visit('/desk#List/ToDo/List'); + cy.get('.list-row:contains("this is fourth todo")').should('exist'); + cy.get('.tag-filters-area .btn:contains("Add Filter")').click(); + cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 }); + cy.get('select.condition.form-control').select("Next"); + cy.get('.filter-field select.input-with-feedback.form-control').select("1 week"); + cy.server(); + cy.route({ + method: 'POST', + url: '/' + }).as('applyFilter'); + cy.get('.filter-box .btn:contains("Apply")').click(); + cy.wait('@applyFilter'); + cy.get('.list-row-container').its('length').should('eq', 1); + cy.get('.list-row').should('contain', 'this is first todo'); + cy.get('.remove-filter.btn').click(); + }); +}); diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 224b6bc299..ab2ee4ced5 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -16,7 +16,7 @@ import frappe, json, copy, re from frappe.model import optional_fields from frappe.client import check_parent_permission from frappe.model.utils.user_settings import get_user_settings, update_user_settings -from frappe.utils import flt, cint, get_time, make_filter_tuple, get_filter, add_to_date, cstr +from frappe.utils import flt, cint, get_time, make_filter_tuple, get_filter, add_to_date, cstr, nowdate class DatabaseQuery(object): def __init__(self, doctype, user=None): @@ -402,6 +402,35 @@ class DatabaseQuery(object): if df and df.fieldtype in ("Check", "Float", "Int", "Currency", "Percent"): can_be_null = False + if f.operator.lower() in ('previous', 'next'): + if f.operator.lower() == "previous": + if f.value == "1 week": + date_range = [add_to_date(nowdate(), days=-7), nowdate()] + elif f.value == "1 month": + date_range = [add_to_date(nowdate(), months=-1), nowdate()] + elif f.value == "3 months": + date_range = [add_to_date(nowdate(), months=-3), nowdate()] + elif f.value == "6 months": + date_range = [add_to_date(nowdate(), months=-6), nowdate()] + elif f.value == "1 year": + date_range = [add_to_date(nowdate(), years=-1), nowdate()] + elif f.operator.lower() == "next": + if f.value == "1 week": + date_range = [nowdate(), add_to_date(nowdate(), days=7)] + elif f.value == "1 month": + date_range = [nowdate(), add_to_date(nowdate(), months=1)] + elif f.value == "3 months": + date_range = [nowdate(), add_to_date(nowdate(), months=3)] + elif f.value == "6 months": + date_range = [nowdate(), add_to_date(nowdate(), months=6)] + elif f.value == "1 year": + date_range = [nowdate(), add_to_date(nowdate(), years=1)] + if df.fieldtype=="Datetime": + date_range = [frappe.db.format_datetime(date_range[0]), frappe.db.format_datetime(date_range[1])] + f.operator = "Between" + f.value = date_range + fallback = "'0001-01-01 00:00:00'" + if f.operator in ('>', '<') and (f.fieldname in ('creation', 'modified')): value = cstr(f.value) fallback = "NULL" diff --git a/frappe/public/js/frappe/ui/filters/edit_filter.html b/frappe/public/js/frappe/ui/filters/edit_filter.html index 88f3a5b20c..d77fa7e625 100644 --- a/frappe/public/js/frappe/ui/filters/edit_filter.html +++ b/frappe/public/js/frappe/ui/filters/edit_filter.html @@ -18,6 +18,8 @@ + +
diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js index 1cbbc5fc2d..84125afc1f 100644 --- a/frappe/public/js/frappe/ui/filters/filter.js +++ b/frappe/public/js/frappe/ui/filters/filter.js @@ -19,16 +19,18 @@ frappe.ui.Filter = class { ["<=", "<="], ["Between", __("Between")], ["descendants of", __("Descendants Of")], - ["ancestors of", __("Ancestors Of")] + ["ancestors of", __("Ancestors Of")], + ["Previous", __("Previous")], + ["Next", __("Next")] ]; this.invalid_condition_map = { Date: ['like', 'not like'], Datetime: ['like', 'not like'], - Data: ['Between'], - Select: ["Between", "<=", ">=", "<", ">"], - Link: ["Between"], - Currency: ["Between"], - Color: ["Between"] + Data: ['Between', 'Previous', 'Next'], + Select: ['like', 'not like'], + Link: ["Between", 'Previous', 'Next'], + Currency: ["Between", 'Previous', 'Next'], + Color: ["Between", 'Previous', 'Next'] }; this.make(); this.make_select(); @@ -182,6 +184,32 @@ frappe.ui.Filter = class { this.fieldselect.selected_doctype = doctype; this.fieldselect.selected_fieldname = fieldname; + if(["Previous", "Next"].includes(condition) && ['Date', 'Datetime', 'DateRange', 'Select'].includes(this.field.df.fieldtype)) { + df.fieldtype = 'Select'; + df.options = [ + { + label: __('1 week'), + value: '1 week' + }, + { + label: __('1 month'), + value: '1 month' + }, + { + label: __('3 months'), + value: '3 months' + }, + { + label: __('6 months'), + value: '6 months' + }, + { + label: __('1 year'), + value: '1 year' + } + ]; + } + this.make_field(df, cur.fieldtype); } diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index 8cdfe3e1a9..db66dc22a8 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -3,9 +3,10 @@ from __future__ import unicode_literals import unittest +import frappe from frappe.utils import evaluate_filters, money_in_words, scrub_urls, get_url -from frappe.utils import ceil, floor +from frappe.utils import ceil, floor, now, add_to_date class TestFilters(unittest.TestCase): def test_simple_dict(self): @@ -122,3 +123,26 @@ class TestHTMLUtils(unittest.TestCase): clean = clean_email_html(sample) self.assertTrue('

Hello

' in clean) self.assertTrue('text' in clean) + +@frappe.whitelist() +def create_todo_records(): + frappe.get_doc({ + "doctype": "ToDo", + "date": add_to_date(now(), days=3), + "description": "this is first todo" + }).insert() + frappe.get_doc({ + "doctype": "ToDo", + "date": add_to_date(now(), days=-3), + "description": "this is second todo" + }).insert() + frappe.get_doc({ + "doctype": "ToDo", + "date": add_to_date(now(), months=2), + "description": "this is third todo" + }).insert() + frappe.get_doc({ + "doctype": "ToDo", + "date": add_to_date(now(), months=-2), + "description": "this is fourth todo" + }).insert() diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 07cfd99166..5e1b7da825 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -855,7 +855,7 @@ def get_filter(doctype, f): f.operator = "=" valid_operators = ("=", "!=", ">", "<", ">=", "<=", "like", "not like", "in", "not in", - "between", "descendants of", "ancestors of", "not descendants of", "not ancestors of") + "between", "descendants of", "ancestors of", "not descendants of", "not ancestors of", "previous", "next") if f.operator.lower() not in valid_operators: frappe.throw(frappe._("Operator must be one of {0}").format(", ".join(valid_operators))) @@ -1004,4 +1004,4 @@ def get_source_value(source, key): def is_subset(list_a, list_b): '''Returns whether list_a is a subset of list_b''' - return len(list(set(list_a) & set(list_b))) == len(list_a) \ No newline at end of file + return len(list(set(list_a) & set(list_b))) == len(list_a)