← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~finnrg/launchpad:feat/LP-2652-searchQuestion-datecreated into launchpad:master

 

Finn Gärtner has proposed merging ~finnrg/launchpad:feat/LP-2652-searchQuestion-datecreated into launchpad:master.

Commit message:
feat: Extend searchQuestions API with new parameters created_before and created_since

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #2112473 in Launchpad itself: "Add date filtering to `searchQuestions` API call"
  https://bugs.launchpad.net/launchpad/+bug/2112473

For more details, see:
https://code.launchpad.net/~finnrg/launchpad/+git/launchpad/+merge/492040
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~finnrg/launchpad:feat/LP-2652-searchQuestion-datecreated into launchpad:master.
diff --git a/lib/lp/answers/interfaces/questioncollection.py b/lib/lp/answers/interfaces/questioncollection.py
index e6d370b..83e778e 100644
--- a/lib/lp/answers/interfaces/questioncollection.py
+++ b/lib/lp/answers/interfaces/questioncollection.py
@@ -21,7 +21,7 @@ from lazr.restful.declarations import (
 )
 from lazr.restful.fields import ReferenceChoice
 from zope.interface import Attribute, Interface
-from zope.schema import Choice, Int, List, TextLine
+from zope.schema import Choice, Datetime, Int, List, TextLine
 
 from lp import _
 from lp.answers.enums import (
@@ -48,6 +48,20 @@ class IQuestionCollection(Interface):
             value_type=ReferenceChoice(vocabulary="Language"),
         ),
         sort=Choice(title=_("Sort"), required=False, vocabulary=QuestionSort),
+        created_before=Datetime(
+            title=_(
+                "Search for questions that were created"
+                "before the given date."
+            ),
+            required=False,
+        ),
+        created_since=Datetime(
+            title=_(
+                "Search for questions that were created"
+                "since the given date."
+            ),
+            required=False,
+        ),
     )
     @operation_returns_collection_of(Interface)  # IQuestion.
     @export_read_operation()
@@ -57,6 +71,8 @@ class IQuestionCollection(Interface):
         status=list(QUESTION_STATUS_DEFAULT_SEARCH),
         language=None,
         sort=None,
+        created_before=None,
+        created_since=None,
     ):
         """Return the questions from the collection matching search criteria.
 
@@ -74,6 +90,12 @@ class IQuestionCollection(Interface):
         :param sort: An attribute of QuestionSort. If None, a default value is
             used. When there is a search_text value, the default is to sort by
             RELEVANCY, otherwise results are sorted NEWEST_FIRST.
+
+        :param created_since: Only return results whose `datecreated` property
+            is greater than or equal to this date.
+
+        :param created_before: Only return results whose `datecreated` property
+            is smaller than this date.
         """
 
     def getQuestionLanguages():
@@ -118,6 +140,8 @@ class ISearchableByQuestionOwner(IQuestionCollection):
         sort=None,
         owner=None,
         needs_attention_from=None,
+        created_before=None,
+        created_since=None,
     ):
         """Return the questions from the collection matching search criteria.
 
@@ -139,6 +163,10 @@ class ISearchableByQuestionOwner(IQuestionCollection):
         :param sort: An attribute of QuestionSort. If None, a default value is
             used. When there is a search_text value, the default is to sort by
             RELEVANCY, otherwise results are sorted NEWEST_FIRST.
+        :param created_since: Only return results whose `datecreated` property
+            is greater than or equal to this date.
+        :param created_before: Only return results whose `datecreated` property
+            is smaller than this date.
         """
 
 
diff --git a/lib/lp/answers/interfaces/questionsperson.py b/lib/lp/answers/interfaces/questionsperson.py
index 2561a74..c4ece40 100644
--- a/lib/lp/answers/interfaces/questionsperson.py
+++ b/lib/lp/answers/interfaces/questionsperson.py
@@ -13,7 +13,7 @@ from lazr.restful.declarations import (
 )
 from lazr.restful.fields import ReferenceChoice
 from zope.interface import Interface
-from zope.schema import Bool, Choice, List, TextLine
+from zope.schema import Bool, Choice, Datetime, List, TextLine
 
 from lp import _
 from lp.answers.enums import (
@@ -67,6 +67,20 @@ class IQuestionsPerson(IQuestionCollection):
             title=_("Needs attentions from"), default=False, required=False
         ),
         sort=Choice(title=_("Sort"), required=False, vocabulary=QuestionSort),
+        created_before=Datetime(
+            title=_(
+                "Search for questions that were created"
+                "before the given date."
+            ),
+            required=False,
+        ),
+        created_since=Datetime(
+            title=_(
+                "Search for questions that were created"
+                "since the given date."
+            ),
+            required=False,
+        ),
     )
     @operation_returns_collection_of(Interface)  # IQuestion.
     @export_read_operation()
@@ -80,6 +94,8 @@ class IQuestionsPerson(IQuestionCollection):
         sort=None,
         participation=None,
         needs_attention=None,
+        created_before=None,
+        created_since=None,
     ):
         """Search the person's questions.
 
@@ -104,4 +120,8 @@ class IQuestionsPerson(IQuestionCollection):
         :param sort: An attribute of QuestionSort. If None, a default value is
             used. When there is a search_text value, the default is to sort by
             RELEVANCY, otherwise results are sorted NEWEST_FIRST.
+        :param created_since: Only return results whose `datecreated` property
+            is greater than or equal to this date.
+        :param created_before: Only return results whose `datecreated` property
+            is smaller than this date.
         """
diff --git a/lib/lp/answers/model/question.py b/lib/lp/answers/model/question.py
index bd760b9..853fe2d 100644
--- a/lib/lp/answers/model/question.py
+++ b/lib/lp/answers/model/question.py
@@ -887,6 +887,8 @@ class QuestionSet:
         language=None,
         status=QUESTION_STATUS_DEFAULT_SEARCH,
         sort=None,
+        created_before=None,
+        created_since=None,
     ):
         """See `IQuestionSet`"""
         return QuestionSearch(
@@ -894,6 +896,8 @@ class QuestionSet:
             status=status,
             language=language,
             sort=sort,
+            created_before=created_before,
+            created_since=created_since,
         ).getResults()
 
     def getQuestionLanguages(self):
@@ -1073,6 +1077,8 @@ class QuestionSearch:
         distribution=None,
         sourcepackagename=None,
         projectgroup=None,
+        created_before=None,
+        created_since=None,
     ):
         self.search_text = search_text
         self.nl_phrase_used = False
@@ -1098,6 +1104,8 @@ class QuestionSearch:
         self.distribution = distribution
         self.sourcepackagename = sourcepackagename
         self.projectgroup = projectgroup
+        self.created_before = created_before
+        self.created_since = created_since
 
     def getTargetConstraints(self):
         """Return the constraints related to the IQuestionTarget context."""
@@ -1221,6 +1229,12 @@ class QuestionSearch:
                 )
             )
 
+        if self.created_before:
+            constraints.append(Question.datecreated < self.created_before)
+
+        if self.created_since:
+            constraints.append(Question.datecreated >= self.created_since)
+
         return constraints
 
     def getPrejoins(self):
@@ -1348,6 +1362,8 @@ class QuestionTargetSearch(QuestionSearch):
         product=None,
         distribution=None,
         sourcepackagename=None,
+        created_before=None,
+        created_since=None,
     ):
         assert (
             product is not None
@@ -1365,6 +1381,8 @@ class QuestionTargetSearch(QuestionSearch):
             product=product,
             distribution=distribution,
             sourcepackagename=sourcepackagename,
+            created_before=created_before,
+            created_since=created_since,
         )
 
         if owner:
@@ -1450,6 +1468,8 @@ class QuestionPersonSearch(QuestionSearch):
         sort=None,
         participation=None,
         needs_attention=False,
+        created_before=None,
+        created_since=None,
     ):
         if needs_attention:
             needs_attention_from = person
@@ -1463,6 +1483,8 @@ class QuestionPersonSearch(QuestionSearch):
             language=language,
             needs_attention_from=needs_attention_from,
             sort=sort,
+            created_before=created_before,
+            created_since=created_since,
         )
 
         assert IPerson.providedBy(person), "expected IPerson, got %r" % person
diff --git a/lib/lp/answers/model/questionsperson.py b/lib/lp/answers/model/questionsperson.py
index 964c050..3c907cb 100644
--- a/lib/lp/answers/model/questionsperson.py
+++ b/lib/lp/answers/model/questionsperson.py
@@ -28,6 +28,8 @@ class QuestionsPersonMixin:
         sort=None,
         participation=None,
         needs_attention=None,
+        created_before=None,
+        created_since=None,
     ):
         """See `IQuestionsPerson`."""
         return QuestionPersonSearch(
@@ -38,6 +40,8 @@ class QuestionsPersonMixin:
             sort=sort,
             participation=participation,
             needs_attention=needs_attention,
+            created_before=created_before,
+            created_since=created_since,
         ).getResults()
 
     def getQuestionLanguages(self):
diff --git a/lib/lp/answers/tests/test_question.py b/lib/lp/answers/tests/test_question.py
index 29adfe5..f9599b3 100644
--- a/lib/lp/answers/tests/test_question.py
+++ b/lib/lp/answers/tests/test_question.py
@@ -1,6 +1,8 @@
 # Copyright 2013-2017 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
+from datetime import datetime, timedelta, timezone
+
 from testtools.testcase import ExpectedException
 from zope.component import getUtility
 from zope.security.interfaces import Unauthorized
@@ -72,3 +74,47 @@ class TestQuestionSearch(TestCaseWithFactory):
         questions = list(getUtility(IQuestionSet).searchQuestions())
         self.assertIn(active_question, questions)
         self.assertNotIn(inactive_question, questions)
+
+    def test_created_before(self):
+        today = datetime.now(timezone.utc)
+        nine_days_ago = today - timedelta(days=9)
+
+        q_nine_days_ago = self.factory.makeQuestion(
+            datecreated=today - timedelta(days=9)
+        )
+        q_ten_days_ago = self.factory.makeQuestion(
+            datecreated=today - timedelta(days=10)
+        )
+
+        questions = list(
+            getUtility(IQuestionSet).searchQuestions(
+                created_before=nine_days_ago
+            )
+        )
+
+        # Requires using assertIn/assertNotIn instead of assertEqual
+        # because database already contains multiple questions
+        self.assertIn(q_ten_days_ago, questions)
+        self.assertNotIn(q_nine_days_ago, questions)
+
+    def test_created_since(self):
+        today = datetime.now(timezone.utc)
+        nine_days_ago = today - timedelta(days=9)
+
+        q_nine_days_ago = self.factory.makeQuestion(
+            datecreated=today - timedelta(days=9)
+        )
+        q_ten_days_ago = self.factory.makeQuestion(
+            datecreated=today - timedelta(days=10)
+        )
+
+        questions = list(
+            getUtility(IQuestionSet).searchQuestions(
+                created_since=nine_days_ago
+            )
+        )
+
+        # Requires using assertIn/assertNotIn instead of assertEqual
+        # because database already contains multiple questions
+        self.assertIn(q_nine_days_ago, questions)
+        self.assertNotIn(q_ten_days_ago, questions)
diff --git a/lib/lp/registry/model/distribution.py b/lib/lp/registry/model/distribution.py
index 646475d..d34e4e0 100644
--- a/lib/lp/registry/model/distribution.py
+++ b/lib/lp/registry/model/distribution.py
@@ -1443,6 +1443,8 @@ class Distribution(
         owner=None,
         needs_attention_from=None,
         unsupported=False,
+        created_before=None,
+        created_since=None,
     ):
         """See `IQuestionCollection`."""
         if unsupported:
@@ -1459,6 +1461,8 @@ class Distribution(
             owner=owner,
             needs_attention_from=needs_attention_from,
             unsupported_target=unsupported_target,
+            created_before=created_before,
+            created_since=created_since,
         ).getResults()
 
     def getTargetTypes(self):
diff --git a/lib/lp/registry/model/projectgroup.py b/lib/lp/registry/model/projectgroup.py
index 1ad7db5..31df8f9 100644
--- a/lib/lp/registry/model/projectgroup.py
+++ b/lib/lp/registry/model/projectgroup.py
@@ -376,6 +376,8 @@ class ProjectGroup(
         owner=None,
         needs_attention_from=None,
         unsupported=False,
+        created_before=None,
+        created_since=None,
     ):
         """See `IQuestionCollection`."""
         if unsupported:
@@ -392,6 +394,8 @@ class ProjectGroup(
             owner=owner,
             needs_attention_from=needs_attention_from,
             unsupported_target=unsupported_target,
+            created_before=created_before,
+            created_since=created_since,
         ).getResults()
 
     def getQuestionLanguages(self):
diff --git a/lib/lp/registry/model/sourcepackage.py b/lib/lp/registry/model/sourcepackage.py
index 9197ca9..e3a5954 100644
--- a/lib/lp/registry/model/sourcepackage.py
+++ b/lib/lp/registry/model/sourcepackage.py
@@ -106,6 +106,8 @@ class SourcePackageQuestionTargetMixin(QuestionTargetMixin):
         owner=None,
         needs_attention_from=None,
         unsupported=False,
+        created_before=None,
+        created_since=None,
     ):
         """See `IQuestionCollection`."""
         if unsupported:
@@ -123,6 +125,8 @@ class SourcePackageQuestionTargetMixin(QuestionTargetMixin):
             owner=owner,
             needs_attention_from=needs_attention_from,
             unsupported_target=unsupported_target,
+            created_before=created_before,
+            created_since=created_since,
         ).getResults()
 
     def getAnswerContactsForLanguage(self, language):

Follow ups