Skip to content

Instantly share code, notes, and snippets.

@happy5214
Last active December 9, 2017 00:36
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save happy5214/dff10388cb9684e3a1ff1acaa360c841 to your computer and use it in GitHub Desktop.
Save happy5214/dff10388cb9684e3a1ff1acaa360c841 to your computer and use it in GitHub Desktop.
GSoC 2017 work (Pywikibot/Thanks)
class Post(FlowObject):
# [...] (New code below)
def thank(self):
"""Thank the user who made this post."""
self.site.thank_post(self)
# -*- coding: utf-8 -*-
"""Tests for thanks-related code."""
#
# (C) Pywikibot team, 2016-17
#
# Distributed under the terms of the MIT license.
#
from __future__ import absolute_import, unicode_literals
from pywikibot.flow import Topic
from tests.aspects import TestCase
NO_THANKABLE_POSTS = 'There is no recent post which can be test thanked.'
class TestThankFlowPost(TestCase):
"""Test thanks for Flow posts."""
family = 'test'
code = 'test'
write = True
@classmethod
def setUpClass(cls):
"""Set up class."""
super(TestThankFlowPost, cls).setUpClass()
cls._topic_title = 'Topic:Tvkityksg1ukyrrw'
def test_thank_post(self):
"""Test thanks for Flow posts."""
found_log = False
site = self.get_site()
topic = Topic(site, self._topic_title)
for post in reversed(topic.replies()):
user = post.creator
if site.user() == user.username:
continue
if user.is_thankable:
break
else:
self.skipTest(NO_THANKABLE_POSTS)
before_time = site.getcurrenttimestamp()
post.thank()
log_entries = site.logevents(logtype='thanks', total=5, page=user,
start=before_time, reverse=True)
for __ in log_entries:
found_log = True
break
self.assertTrue(found_log)
def test_self_thank(self):
"""Test that thanking one's own Flow post causes an error."""
site = self.get_site()
topic = Topic(site, self._topic_title)
my_reply = topic.reply('My attempt to thank myself.')
self.assertAPIError('invalidrecipient', None, my_reply.thank)
# Main goals
- https://gerrit.wikimedia.org/r/362324
- https://gerrit.wikimedia.org/r/361017
- https://gerrit.wikimedia.org/r/359366
- https://gerrit.wikimedia.org/r/357957
- https://gerrit.wikimedia.org/r/294901 (written last year and heavily modified by me)
# Stretch goals
- https://gerrit.wikimedia.org/r/253385 (Flow revision patch under active review)
- https://gerrit.wikimedia.org/r/372493 (Thanks notification logging code waiting for eyes)
# New code only.
class UserTargetLogEntry(LogEntry):
"""A log entry whose target is a user page."""
def page(self):
"""Return the target user.
This returns a User object instead of the Page object returned by the
superclass method.
@return: target user
@rtype: pywikibot.User
"""
if not hasattr(self, '_page'):
self._page = pywikibot.User(super(UserTargetLogEntry, self).page())
return self._page
class ThanksEntry(UserTargetLogEntry):
"""Thanks log entry."""
_expectedType = 'thanks'
# New code only.
class TestLogentriesMeta(MetaTestCaseClass):
"""Test meta class for TestLogentries."""
def __new__(cls, name, bases, dct):
"""Create the new class."""
def test_method(logtype):
def test_logevent(self, key):
"""Test a single logtype entry."""
# Next two lines are new code.
if key == 'old' and logtype == 'thanks':
self.skipTest('Thanks extension not on old.')
self._test_logevent(logtype)
return test_logevent
# create test methods for the support logtype classes
for logtype in LogEntryFactory.logtypes:
cls.add_method(dct, 'test_%sEntry' % logtype.title(),
test_method(logtype))
return super(TestLogentriesMeta, cls).__new__(cls, name, bases, dct)
class TestLogentryParams(TestLogentriesBase):
# [...] (New code below)
def test_thanks_page(self, key):
"""Test Thanks page method return type."""
if not self.site.has_extension('Thanks'):
self.skipTest('Thanks extension not available.')
logentry = self._get_logentry('thanks')
self.assertIsInstance(logentry.page(), pywikibot.User)
class User(Page):
# [...] (New code below)
@property
def is_thankable(self):
"""
Determine if the user has thanks notifications enabled.
NOTE: This doesn't accurately determine if thanks is enabled for user.
Privacy of thanks preferences is under discussion, please see
https://phabricator.wikimedia.org/T57401#2216861, and
https://phabricator.wikimedia.org/T120753#1863894
@rtype: bool
"""
if self.isAnonymous():
return False
if 'bot' in self.groups():
return False
return True
# [...]
class Revision(DotReadableDict):
# [...] (New code below)
@staticmethod
def _thank(revid, site, source='pywikibot'):
"""Thank a user for this revision.
@param site: The Site object for this revision.
@type site: Site
@param source: An optional source to pass to the API.
@type source: str
"""
site.thank_revision(revid, source)
# New code only.
class APISite(BaseSite):
# [...] (New code below)
# Thanks API calls
@need_extension('Thanks')
def thank_revision(self, revid, source=None):
"""Corresponding method to the 'action=thank' API action.
@param revid: Revision ID for the revision to be thanked.
@type revid: int
@param source: A source for the thanking operation.
@type source: str
@raise APIError: On thanking oneself or other API errors.
@return: The API response.
"""
token = self.tokens['csrf']
req = self._simple_request(action='thank', rev=revid, token=token,
source=source)
data = req.submit()
if data['result']['success'] != 1:
raise api.APIError('Thanking unsuccessful')
return data
@need_extension('Flow')
@need_extension('Thanks')
def thank_post(self, post):
"""Corresponding method to the 'action=flowthank' API action.
@param post: The post to be thanked for.
@type post: Post
@raise APIError: On thanking oneself or other API errors.
@return: The API response.
"""
post_id = post.uuid
token = self.tokens['csrf']
req = self._simple_request(action='flowthank',
postid=post_id, token=token)
data = req.submit()
if data['result']['success'] != 1:
raise api.APIError('Thanking unsuccessful')
return data

Implement Thanks support in Pywikibot (Google Summer of Code 2017)

By: Alexander Jones

Summary

My project was to add support for the Thanks MediaWiki extension to Pywikibot, a bot framework designed to work with MediaWiki wikis. This involved adding code to handle API calls, wrapper code to serve as the bot-writing API, thorough tests, and a script to generate tables of thankers and thankees on particular wikis. Server-side was to be done as time allowed.

Results

Pywikibot bots can now thank normal revisions and Flow posts, and the script generating the reports is available on a personal GitHub repo. I also wrote a patch for the server-side Thanks extension. Overall, all five points of https://phabricator.wikimedia.org/T129049 were at least mostly addressed. I think the work accomplished meets realistic goals for this project.

Status

A patch dating back to 2015 which would add support for Flow revisions in PWB is almost done, but it needs more tests. I have a pending patch on the server-side extension as a proof-of-concept to log Thanks notification changes, though it's not quite ready for merging.

#!/usr/bin/python
# -*- coding: utf-8 -*-
"""Create reports for thankers and thankees by wiki and month.
These are the specific command line parameters for this script:
&params;
-minimum The minimum number of thanks actions needed for inclusion.
-year The year for which to generate the report.
-month The month for which to generate the report (as a number).
"""
#
# (C) Alexander Jones, 2017
#
# Distributed under the terms of the MIT license.
#
from __future__ import absolute_import, print_function, unicode_literals
from collections import Counter
import datetime
import pywikibot
from tabulate import tabulate
class ThanksReportBot(object):
"""Bot class used to implement reports for thankers and thankees."""
def __init__(self, minimum_actions, year, month):
"""Constructor.
@param minimum_actions: The minimum numbers of actions (sending or
receiving) needed for inclusion in the report.
@type minimum_actions: int
"""
self.site = pywikibot.Site()
self.minimum_actions = minimum_actions
self.year = year
self.month = month
def run(self):
"""Run the bot."""
data = self.parse(*(self.gather()))
month = datetime.date(self.year, self.month, 1)
month_str = month.strftime('%B %Y')
print("Thank givers for {}".format(month_str))
print(self.format(data[0]))
print()
print("Thank recipients for {}".format(month_str))
print(self.format(data[1]))
def gather(self):
"""Gather and parse the log data."""
thankers = []
thankees = []
start_time = datetime.datetime(self.year, self.month, 1)
end_time = datetime.datetime(self.year, self.month + 1, 1)
for entry in self.site.logevents(logtype='thanks', start=end_time,
end=start_time):
thankers.append(entry.user())
thankees.append(entry.page())
thankers_count = Counter(thankers)
thankees_count = Counter(thankees)
return (thankers_count, thankees_count)
def parse(self, thankers_count, thankees_count):
included_thankers = []
included_thankees = []
for k,v in thankers_count.items():
if v >= self.minimum_actions:
included_thankers.append((-v,k))
for k,v in thankees_count.items():
if v >= self.minimum_actions:
included_thankees.append((-v,k))
included_thankers.sort()
included_thankees.sort()
return (included_thankers, included_thankees)
def format(self, data):
"""Format the parsed data."""
index = 1
same = 0
last_count = data[0][0] + 1
rows = []
for entry in data:
count = entry[0]
if count != last_count:
index += same
same = 1
else:
same += 1
user = entry[1]
if isinstance(user, pywikibot.User):
user = user.username
user_link = '[[Special:CentralAuth/{}]]'.format(user)
rows.append([index, user_link, -count])
last_count = count
return tabulate(rows, ['#', 'User', 'Thanks'], tablefmt='mediawiki')
def main(*args):
"""
Process command line arguments and invoke bot.
If args is an empty list, sys.argv is used.
@param args: command line arguments
@type args: list of unicode
"""
local_args = pywikibot.handle_args(args)
minimum_actions = 1
today = datetime.date.today()
year = today.year
month = today.month
# Parse command line arguments
for arg in local_args:
option, sep, value = arg.partition(':')
if option == '-minimum':
minimum_actions = int(value)
elif option == '-year':
year = int(value)
elif option == '-month':
month = int(value)
else:
pywikibot.warning(
u'argument "%s" not understood; ignoring.' % arg)
bot = ThanksReportBot(minimum_actions, year, month)
bot.run()
if __name__ == "__main__":
main()
# -*- coding: utf-8 -*-
"""Tests for thanks-related code."""
#
# (C) Pywikibot team, 2016-17
#
# Distributed under the terms of the MIT license.
#
from __future__ import absolute_import, unicode_literals
from pywikibot.page import Page, Revision, User
from tests.aspects import TestCase
NO_THANKABLE_REVS = 'There is no recent change which can be test thanked.'
class TestThankRevision(TestCase):
"""Test thanks for revisions."""
family = 'test'
code = 'test'
write = True
def test_thank_revision(self):
"""Test thanks for normal revisions.
NOTE: This test relies on activity in recentchanges, and
there must make edits made before reruns of this test.
Please see https://phabricator.wikimedia.org/T137836.
"""
found_log = False
site = self.get_site()
data = site.recentchanges(total=20)
for rev in data:
revid = rev['revid']
username = rev['user']
user = User(site, username)
if user.is_thankable:
break
else:
self.skipTest(NO_THANKABLE_REVS)
before_time = site.getcurrenttimestamp()
Revision._thank(revid, site, source='pywikibot test')
log_entries = site.logevents(logtype='thanks', total=5, page=user,
start=before_time, reverse=True)
for __ in log_entries:
found_log = True
break
self.assertTrue(found_log)
def test_self_thank(self):
"""Test that thanking oneself causes an error.
This test is not in TestThankRevisionErrors because it may require
making a successful edit in order to test the API call thanking the user
running the test.
"""
site = self.get_site()
my_name = self.get_userpage().username
data = site.usercontribs(user=my_name, total=1)
for rev in data:
revid = rev['revid']
break
else:
test_page = Page(site, 'Pywikibot Thanks test')
test_page.text += '* ~~~~\n'
test_page.save('Pywikibot Thanks test')
revid = test_page.latest_revision_id
self.assertAPIError('invalidrecipient', None, Revision._thank,
revid, site, source='pywikibot test')
class TestThankRevisionErrors(TestCase):
"""Test errors when thanking revisions."""
family = 'test'
code = 'test'
write = -1
def test_bad_recipient(self):
"""Test that thanking a bad recipient causes an error."""
site = self.get_site()
data = site.recentchanges(total=20)
for rev in data:
revid = rev['revid']
username = rev['user']
user = User(site, username)
if not user.is_thankable:
break
else:
self.skipTest(NO_THANKABLE_REVS)
self.assertAPIError('invalidrecipient', None, Revision._thank,
revid, site, source='pywikibot test')
def test_invalid_revision(self):
"""Test that passing an invalid revision ID causes an error."""
site = self.get_site()
invalid_revids = (0, -1, 0.99, 'zero, minus one, and point nine nine',
(0, -1, 0.99), [0, -1, 0.99])
for invalid_revid in invalid_revids:
self.assertAPIError('invalidrevision', None, Revision._thank,
invalid_revid, site, source='pywikibot test')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment