jenkins-bot has submitted this change. ( 
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/655303 )

Change subject: [FEAT] Allow querying alldeletedrevisions
......................................................................

[FEAT] Allow querying alldeletedrevisions

* Implement APISite.alldeletedrevisions()
* Implement User.deleted_contributions()

Change-Id: I669a7bcfde7e8dda6803b3b8858ef14de541439d
---
M pywikibot/page/__init__.py
M pywikibot/site/__init__.py
M tests/site_tests.py
3 files changed, 297 insertions(+), 13 deletions(-)

Approvals:
  Xqt: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/pywikibot/page/__init__.py b/pywikibot/page/__init__.py
index b4d259f..c6c8711 100644
--- a/pywikibot/page/__init__.py
+++ b/pywikibot/page/__init__.py
@@ -31,7 +31,7 @@

 import pywikibot

-from pywikibot.backports import cache, Dict, List
+from pywikibot.backports import cache, Dict, Iterable, List, Tuple
 from pywikibot import config, i18n, textlib
 from pywikibot.comms import http
 from pywikibot.data.api import APIError
@@ -3255,6 +3255,23 @@
         """
         return next(self.contributions(total=1), None)

+    def deleted_contributions(
+        self, *, total: int = 500, **kwargs
+    ) -> Iterable[Tuple[Page, Revision]]:
+        """Yield tuples describing this user's deleted edits.
+
+        @param: total: Limit results to this number of pages
+        @keyword start: Iterate contributions starting at this Timestamp
+        @keyword end: Iterate contributions ending at this Timestamp
+        @keyword reverse: Iterate oldest contributions first (default: newest)
+        @keyword namespaces: Only iterate pages in these namespaces
+        """
+        for data in self.site.alldeletedrevisions(user=self.username,
+                                                  total=total, **kwargs):
+            page = Page(self.site, data['title'], data['ns'])
+            for contrib in data['revisions']:
+                yield page, Revision(**contrib)
+
     @deprecate_arg('number', 'total')
     def uploadedImages(self, total=10):
         """
diff --git a/pywikibot/site/__init__.py b/pywikibot/site/__init__.py
index 348f34d..60a7ab5 100644
--- a/pywikibot/site/__init__.py
+++ b/pywikibot/site/__init__.py
@@ -28,13 +28,13 @@
 from itertools import zip_longest
 from pywikibot.login import LoginStatus as _LoginStatus
 from textwrap import fill
-from typing import Optional, Union
+from typing import Any, Optional, Union
 from warnings import warn

 import pywikibot
 import pywikibot.family

-from pywikibot.backports import List
+from pywikibot.backports import Dict, List
 from pywikibot.comms.http import get_authentication
 from pywikibot.data import api
 from pywikibot.echo import Notification
@@ -62,6 +62,7 @@
     SiteDefinitionError,
     SpamblacklistError,
     TitleblacklistError,
+    UserRightsError,
     UnknownExtension,
 )
 from pywikibot.site._basesite import BaseSite, PageInUse, RemovedSite
@@ -3136,6 +3137,24 @@
                                                 filters)
         return wlgen

+    def _check_view_deleted(self, msg_prefix: str, prop: List[str]) -> None:
+        """Check if the user can view deleted comments and content.
+
+        @param msg_prefix: The calling method name
+        @param prop: Requested props to check
+        @raises UserRightsError: user cannot view a requested prop
+        """
+        err = '{}: User:{} not authorized to view '.format(msg_prefix,
+                                                           self.user())
+        if not self.has_right('deletedhistory'):
+            if self.mw_version < '1.34':
+                raise UserRightsError(err + 'deleted revisions.')
+            if 'comment' in prop or 'parsedcomment' in prop:
+                raise UserRightsError(err + 'comments of deleted revisions.')
+        if ('content' in prop and not (self.has_right('deletedtext')
+                                       or self.has_right('undelete'))):
+            raise UserRightsError(err + 'deleted content.')
+
     @deprecated_args(step=None, get_text='content', page='titles',
                      limit='total')
     def deletedrevs(self, titles=None, start=None, end=None,
@@ -3199,16 +3218,7 @@
         if start and end:
             self.assert_valid_iter_params('deletedrevs', start, end, reverse)

-        err = ('deletedrevs: User:{} not authorized to '
-               .format(self.user()))
-        if not self.has_right('deletedhistory'):
-            if self.mw_version < '1.34':
-                raise Error(err + 'access deleted revisions.')
-            if 'comment' in prop or 'parsedcomment' in prop:
-                raise Error(err + 'access comments of deleted revisions.')
-        if ('content' in prop and not (self.has_right('deletedtext')
-                                       or self.has_right('undelete'))):
-            raise Error(err + 'view deleted content.')
+        self._check_view_deleted('deletedrevs', prop)

         revids = kwargs.pop('revids', None)
         if not (bool(titles) ^ (revids is not None)):
@@ -3253,6 +3263,63 @@
                     data['revisions'] = data.pop('deletedrevisions')
                     yield data

+    @need_version('1.25')
+    def alldeletedrevisions(
+        self,
+        *,
+        namespaces=None,
+        reverse: bool = False,
+        content: bool = False,
+        total: Optional[int] = None,
+        **kwargs
+    ) -> typing.Iterable[Dict[str, Any]]:
+        """
+        Iterate all deleted revisions.
+
+        @see: U{https://www.mediawiki.org/wiki/API:Alldeletedrevisions}
+
+        @param namespaces: Only iterate pages in these namespaces
+        @type namespaces: iterable of str or Namespace key,
+            or a single instance of those types. May be a '|' separated
+            list of namespace identifiers.
+        @param reverse: Iterate oldest revisions first (default: newest)
+        @param content: If True, retrieve the content of each revision
+        @param total: Number of revisions to retrieve
+        @keyword from: Start listing at this title
+        @keyword to: Stop listing at this title
+        @keyword prefix: Search for all page titles that begin with this value
+        @keyword excludeuser: Exclude revisions by this user
+        @keyword tag: Only list revisions tagged with this tag
+        @keyword user: List revisions by this user
+        @keyword start: Iterate revisions starting at this Timestamp
+        @keyword end: Iterate revisions ending at this Timestamp
+        @keyword prop: Which properties to get. Defaults are ids, timestamp,
+            flags, user, and comment (if you have the right to view).
+        @type prop: List[str]
+        """
+        if 'start' in kwargs and 'end' in kwargs:
+            self.assert_valid_iter_params('alldeletedrevisions',
+                                          kwargs['start'],
+                                          kwargs['end'],
+                                          reverse)
+        prop = kwargs.pop('prop') or []
+        parameters = {'adr' + k: v for k, v in kwargs.items()}
+        if not prop:
+            prop = ['ids', 'timestamp', 'flags', 'user']
+            if self.has_right('deletedhistory'):
+                prop.append('comment')
+        if content:
+            prop.append('content')
+        self._check_view_deleted('alldeletedrevisions', prop)
+        parameters['adrprop'] = prop
+        if reverse:
+            parameters['adrdir'] = 'newer'
+        yield from self._generator(api.ListGenerator,
+                                   type_arg='alldeletedrevisions',
+                                   namespaces=namespaces,
+                                   total=total,
+                                   parameters=parameters)
+
     def users(self, usernames):
         """Iterate info about a list of users by name or IP.

diff --git a/tests/site_tests.py b/tests/site_tests.py
index e3469e5..0f082bf 100644
--- a/tests/site_tests.py
+++ b/tests/site_tests.py
@@ -1624,6 +1624,144 @@
                           end='2008-10-03T00:00:01Z', reverse=True, total=5)


+class TestAlldeletedrevisionsAsUser(DefaultSiteTestCase):
+
+    """Test site method site.alldeletedrevisions() with bot user."""
+
+    user = True
+
+    def test_basic(self):
+        """Test the site.alldeletedrevisions() method."""
+        mysite = self.get_site()
+        drev = list(mysite.alldeletedrevisions(user=mysite.user(), total=10))
+        self.assertTrue(all(isinstance(data, dict)
+                            for data in drev))
+        self.assertTrue(all('revisions' in data
+                            and isinstance(data['revisions'], dict)
+                            for data in drev))
+        self.assertTrue(all('user' in rev
+                            and rev['user'] == mysite.user()
+                            for data in drev
+                            for rev in data))
+
+    def test_namespaces(self):
+        """Test the site.alldeletedrevisions() method using namespaces."""
+        mysite = self.get_site()
+        for data in mysite.alldeletedrevisions(namespaces=14, total=5):
+            self.assertIsInstance(data, dict)
+            self.assertIn('title', data)
+            self.assertTrue(data['title'].startswith(mysite.namespace(14)))
+
+        for data in mysite.alldeletedrevisions(user=mysite.user(),
+                                               namespaces=[10, 11],
+                                               total=5):
+            self.assertIsInstance(data, dict)
+            self.assertIn('title', data)
+            self.assertIn(data['ns'], (10, 11))
+
+    def test_excludeuser(self):
+        """Test the site.alldeletedrevisions() method using excludeuser."""
+        mysite = self.get_site()
+        for data in mysite.alldeletedrevisions(excludeuser=mysite.user(),
+                                               total=5):
+            self.assertIsInstance(data, dict)
+            for drev in data:
+                self.assertIsInstance(drev, dict)
+                self.assertIn('user', data)
+                self.assertNotEqual('user', mysite.user())
+
+    def test_user_range(self):
+        """Test the site.alldeletedrevisions() method with range."""
+        mysite = self.get_site()
+        start = '2008-10-06T01:02:03Z'
+        for data in mysite.alldeletedrevisions(
+                user=mysite.user(),
+                start=pywikibot.Timestamp.fromISOformat(start),
+                total=5):
+            for drev in data:
+                self.assertLessEqual(drev['timestamp'], start)
+
+        end = '2008-10-07T02:03:04Z'
+        for data in mysite.alldeletedrevisions(
+                user=mysite.user(),
+                end=pywikibot.Timestamp.fromISOformat(end),
+                total=5):
+            for drev in data:
+                self.assertGreaterEqual(drev['timestamp'], end)
+
+        start = '2008-10-10T11:59:59Z'
+        end = '2008-10-10T00:00:01Z'
+        for data in mysite.alldeletedrevisions(
+                user=mysite.user(),
+                start=pywikibot.Timestamp.fromISOformat(start),
+                end=pywikibot.Timestamp.fromISOformat(end),
+                total=5):
+            for drev in data:
+                self.assertTrue(end <= drev['timestamp'] <= start)
+
+    def test_user_range_reverse(self):
+        """Test the site.alldeletedrevisions() method with range reversed."""
+        mysite = self.get_site()
+        start = '2008-10-08T03:05:07Z'
+        for data in mysite.alldeletedrevisions(
+                user=mysite.user(),
+                start=pywikibot.Timestamp.fromISOformat(start),
+                total=5, reverse=True):
+            for drev in data:
+                self.assertGreaterEqual(drev['timestamp'], start)
+
+        for data in mysite.alldeletedrevisions(
+                user=mysite.user(),
+                end=pywikibot.Timestamp.fromISOformat('2008-10-09T04:06:08Z'),
+                total=5, reverse=True):
+            for drev in data:
+                self.assertLessEqual(drev['timestamp'], '2008-10-09T04:06:08Z')
+
+        start = '2008-10-11T06:00:01Z'
+        end = '2008-10-11T23:59:59Z'
+        for data in mysite.alldeletedrevisions(
+                user=mysite.user(),
+                start=pywikibot.Timestamp.fromISOformat(start),
+                end=pywikibot.Timestamp.fromISOformat(end),
+                reverse=True, total=5):
+            for drev in data:
+                self.assertTrue(start <= drev['timestamp'] <= end)
+
+    def test_invalid_range(self):
+        """Test site.alldeletedrevisions() method with invalid range."""
+        mysite = self.get_site()
+        # start earlier than end
+        self.assertRaises(AssertionError, mysite.alldeletedrevisions,
+                          user=mysite.user(),
+                          start='2008-10-03T00:00:01Z',
+                          end='2008-10-03T23:59:59Z', total=5)
+        # reverse: end earlier than start
+        self.assertRaises(AssertionError, mysite.alldeletedrevisions,
+                          user=mysite.user(),
+                          start='2008-10-03T23:59:59Z',
+                          end='2008-10-03T00:00:01Z', reverse=True, total=5)
+
+
+class TestAlldeletedrevisionsWithoutUser(DefaultSiteTestCase):
+
+    """Test site method site.alldeletedrevisions() without bot user."""
+
+    def test_prefix(self):
+        """Test the site.alldeletedrevisions() method with prefix."""
+        mysite = self.get_site()
+        for data in mysite.alldeletedrevisions(prefix='John', total=5):
+            self.assertIsInstance(data, dict)
+            for key in ('title', 'ns'):
+                self.assertIn(key, data)
+            title = data['title']
+            if data['ns'] > 0:
+                *_, title = title.partition(':')
+            self.assertTrue(title.startswith('John'))
+            for drev in data['revisions']:
+                for key in ('parentid', 'revid', 'timestamp', 'user'):
+                    self.assertIn(key, drev)
+
+
 class SiteWatchlistRevsTestCase(DefaultSiteTestCase):

     """Test site method watchlist_revs()."""
@@ -1817,6 +1955,68 @@
                                          reverse=True)
                 next(gen)

+    def test_alldeletedrevisions(self):
+        """Test the site.alldeletedrevisions() method."""
+        mysite = self.get_site()
+        myuser = mysite.user()
+        if not mysite.has_right('deletedhistory'):
+            self.skipTest(
+                "You don't have permission to view the deleted revisions "
+                'on {0}.'.format(mysite))
+        prop = ['ids', 'timestamp', 'flags', 'user', 'comment']
+        gen = mysite.alldeletedrevisions(total=10, prop=prop)
+
+        for item in gen:
+            break
+        else:
+            self.skipTest('{0} does not have deleted edits.'.format(myuser))
+        self.assertIn('revisions', item)
+        for drev in item['revisions']:
+            for key in ('parentid', 'revid', 'timestamp', 'user', 'comment'):
+                self.assertIn(key, drev)
+
+        with self.subTest(start='2008-10-11T01:02:03Z', reverse=False,
+                          prop=prop):
+            for item in mysite.alldeletedrevisions(
+                start='2008-10-11T01:02:03Z',
+                total=5
+            ):
+                for drev in item['revisions']:
+                    self.assertIsInstance(drev, dict)
+                    self.assertLessEqual(drev['timestamp'],
+                                         '2008-10-11T01:02:03Z')
+
+        with self.subTest(start='2008-10-11T01:02:03Z', reverse=True,
+                          prop=prop):
+            for item in mysite.alldeletedrevisions(
+                start='2008-10-11T01:02:03Z',
+                total=5
+            ):
+                for drev in item['revisions']:
+                    self.assertIsInstance(drev, dict)
+                    self.assertGreaterEqual(drev['timestamp'],
+                                            '2008-10-11T01:02:03Z')
+
+        # start earlier than end
+        with self.subTest(start='2008-09-03T00:00:01Z',
+                          end='2008-09-03T23:59:59Z',
+                          reverse=False, prop=prop):
+            with self.assertRaises(AssertionError):
+                gen = mysite.alldeletedrevisions(start='2008-09-03T00:00:01Z',
+                                                 end='2008-09-03T23:59:59Z',
+                                                 total=5)
+                next(gen)
+
+        # reverse: end earlier than start
+        with self.subTest(start='2008-09-03T23:59:59Z',
+                          end='2008-09-03T00:00:01Z',
+                          reverse=True, prop=prop):
+            with self.assertRaises(AssertionError):
+                gen = mysite.alldeletedrevisions(start='2008-09-03T23:59:59Z',
+                                                 end='2008-09-03T00:00:01Z',
+                                                 total=5, reverse=True)
+                next(gen)
+

 class TestSiteSysopWrite(TestCase):


--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/core/+/655303
To unsubscribe, or for help writing mail filters, visit 
https://gerrit.wikimedia.org/r/settings

Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Change-Id: I669a7bcfde7e8dda6803b3b8858ef14de541439d
Gerrit-Change-Number: 655303
Gerrit-PatchSet: 10
Gerrit-Owner: JJMC89 <[email protected]>
Gerrit-Reviewer: Xqt <[email protected]>
Gerrit-Reviewer: jenkins-bot
Gerrit-CC: DannyS712 <[email protected]>
Gerrit-MessageType: merged
_______________________________________________
Pywikibot-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/pywikibot-commits

Reply via email to