Merge lp:~marco-gallotta/ibid/contest into lp:~ibid-core/ibid/old-trunk-1.6

Proposed by marcog
Status: Superseded
Proposed branch: lp:~marco-gallotta/ibid/contest
Merge into: lp:~ibid-core/ibid/old-trunk-1.6
Diff against target: 276 lines (+262/-0)
2 files modified
ibid/plugins/contest.py (+259/-0)
scripts/ibid-plugin (+3/-0)
To merge this branch: bzr merge lp:~marco-gallotta/ibid/contest
Reviewer Review Type Date Requested Status
Stefano Rivera Needs Fixing
Review via email: mp+17469@code.launchpad.net

This proposal has been superseded by a proposal from 2010-01-19.

To post a comment you must log in.
Revision history for this message
marcog (marco-gallotta) wrote :

First iteration of the contest plugin, with USACO scriping pretty much done.

Revision history for this message
Stefano Rivera (stefanor) wrote :

Run pyflakes. Otherwise you are simply asking me to do it for you:

ibid/plugins/contest.py:1: 'codecs' imported but unused
ibid/plugins/contest.py:4: 'ElementTree' imported but unused
ibid/plugins/contest.py:8: 'ibid' imported but unused
ibid/plugins/contest.py:10: 'eagerload' imported but unused
ibid/plugins/contest.py:70: local variable 'etree' is assigned to but never used

The name "contest" is too board. It should be called programming_contest, if anything.
Also, it's really esoteric, so it should have autoload = False on all the Processors.

> priority = -20
why? Put a comment in explaining please.

> urlencode({u'NAME': user, u'PASSWORD': password})
URLencode doesn't encode to utf-8, so utf-8 encode the parameters yourself, and give it bytestrings.

Applies to all your urlencodes()

> if font.text and font.text.find('Please try again') != -1:
More pythonic:
if font.text and u'Please try again' in font.text:

This crops up again in: division = [b.text for b in etree.getiterator(u'b') if b.text and b.text.find(usaco_user) != -1][0]

> class UsacoException(Exception):
> pass

I believe that if you are creating your own exception to carry an exception message it should do so and not rely on Exception, as Python 3's Exception doesn't have a message. We don't support Python 3 yet, but it's good practice.
> .filter(and_(
> Identity.identity == user,
> Identity.source == event.source)) \

You can save a line by doing:
 .filter(Identity.identity == user) \
 .filter(Identity.source == event.source) \

> usaco_account = [attr.value for attr in account.attributes if attr.name == 'usaco_account']

If you are going to do that, you do want to eager load attributes.

> __redacted__
I'd prefer [redacted]. It looks nicer :)

> def setup(self):
> pass

Be aware that if lp:~stefanor/ibid/ibid-plugin-507489 gets in first, it'll clash.

review: Needs Fixing
lp:~marco-gallotta/ibid/contest updated
851. By marcog

Remove unused imports

852. By marcog

Handle usaco user not found

853. By marcog

Collection of minor changes: autoload=False, utf-8 encode params and other less important changes

854. By marcog

Make UsacoException have a msg so that it works in Python3

855. By marcog

Eagerload attributes and shave a line with filters

856. By marcog

__redacted__ -> [redacted], to make stefano happy :)

Revision history for this message
marcog (marco-gallotta) wrote :

> The name "contest" is too board. It should be called programming_contest, if
> anything.

We still need to agree on a name. I agree that contest is too general. I think programming_contest is too long (20 characters and the longest current name is 12); if we do go with this, I think we should drop the _ to follow the style of other plugins (gameservers, buildbot). Vhata suggested progcomp, but as he said that's ugly. There's also the possibility that maths contests are added: what then?

> Run pyflakes. Otherwise you are simply asking me to do it for you:
>
> ibid/plugins/contest.py:1: 'codecs' imported but unused
> ibid/plugins/contest.py:4: 'ElementTree' imported but unused
> ibid/plugins/contest.py:8: 'ibid' imported but unused
> ibid/plugins/contest.py:10: 'eagerload' imported but unused
> ibid/plugins/contest.py:70: local variable 'etree' is assigned to but never
> used
>
> Also, it's really esoteric, so it should have autoload = False on all the
> Processors.
>
> > priority = -20
> why? Put a comment in explaining please.
>
> > urlencode({u'NAME': user, u'PASSWORD': password})
> URLencode doesn't encode to utf-8, so utf-8 encode the parameters yourself,
> and give it bytestrings.
>
> Applies to all your urlencodes()
>
> > if font.text and font.text.find('Please try again') != -1:
> More pythonic:
> if font.text and u'Please try again' in font.text:
>
> This crops up again in: division = [b.text for b in etree.getiterator(u'b') if
> b.text and b.text.find(usaco_user) != -1][0]
>
> > class UsacoException(Exception):
> > pass
>
> I believe that if you are creating your own exception to carry an exception
> message it should do so and not rely on Exception, as Python 3's Exception
> doesn't have a message. We don't support Python 3 yet, but it's good practice.
> > .filter(and_(
> > Identity.identity == user,
> > Identity.source == event.source)) \
>
> You can save a line by doing:
> .filter(Identity.identity == user) \
> .filter(Identity.source == event.source) \
>
> > usaco_account = [attr.value for attr in account.attributes if attr.name ==
> 'usaco_account']
>
> If you are going to do that, you do want to eager load attributes.
>
> > __redacted__
> I'd prefer [redacted]. It looks nicer :)

Done

> > def setup(self):
> > pass
>
> Be aware that if lp:~stefanor/ibid/ibid-plugin-507489 gets in first, it'll
> clash.

Hopefully someone notices :)

lp:~marco-gallotta/ibid/contest updated
857. By marcog

Fix some unicode stuff

858. By marcog

Saving another and_

859. By marcog

Fix some minor bugs

860. By marcog

Put autoload=False back

Revision history for this message
Stefano Rivera (stefanor) wrote :

ibid/plugins/contest.py:7: 'and_' imported but unused

And the name needs to be sorted. But otherwise I approve

review: Approve
lp:~marco-gallotta/ibid/contest updated
861. By marcog

Handle updates to usaco account

Revision history for this message
Stefano Rivera (stefanor) wrote :

Can you add a copyright header to the top (see any other file in current trunk)

review: Needs Fixing
Revision history for this message
marcog (marco-gallotta) wrote :

> Can you add a copyright header to the top (see any other file in current
> trunk)

Done

lp:~marco-gallotta/ibid/contest updated
862. By marcog

Copyright header

863. By marcog

Search for usaco results also by name and usaco account when not linked

864. By marcog

Add timeout for fetching USACO results and suggest that the site might be down

865. By marcog

Forbid giving passwords in public

866. By marcog

Allow admins to link usaco accounts without a usaco password

867. By marcog

Tell the user when usaco site appears dead

868. By marcog

Change from looking up Identities to Accounts table to find users not on the current source

869. By marcog

Improve the 'you need an account' msg

870. By marcog

Put division inline if only one person is listed in usaco results

871. By marcog

Correct the test for 'not a teacher account'

872. By marcog

Rename contest->codecontest

873. By marcog

Small tidy-up

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'ibid/plugins/contest.py'
--- ibid/plugins/contest.py 1970-01-01 00:00:00 +0000
+++ ibid/plugins/contest.py 2010-01-19 13:45:24 +0000
@@ -0,0 +1,259 @@
1# Copyright (c) 2010, Marco Gallotta
2# Released under terms of the MIT/X/Expat Licence. See COPYING for details.
3
4import re
5from urllib2 import HTTPError
6
7from urllib import urlencode
8
9from ibid.config import Option
10from ibid.db import eagerload
11from ibid.db.models import Account, Attribute, Identity
12from ibid.plugins import Processor, match
13from ibid.utils import cacheable_download
14from ibid.utils.html import get_html_parse_tree
15
16help = {u'usaco': u'Query USACO sections, divisions and more. Since this info is private, users are required to provide their USACO password when linking their USACO account to their ibid account and only linked accounts can be queried. Your password is used only to confirm that the account is yours and is discarded immediately.'}
17
18class UsacoException(Exception):
19 def __init__(self, msg):
20 self.msg = msg
21
22 def __unicode__(self):
23 return unicode(self.msg)
24
25class Usaco(Processor):
26 """usaco <section|division> for <user>
27 usaco <contest> results [for <user>]
28 i am <usaco_username> on usaco password <usaco_password>"""
29
30 admin_user = Option('admin_user', 'Admin user on USACO', None)
31 admin_password = Option('admin_password', 'Admin password on USACO', None)
32
33 feature = 'usaco'
34 # Clashes with identity, so lower our priority since if we match, then
35 # this is the better match
36 priority = -20
37 autoload = False
38
39 def _login(self, user, password):
40 params = urlencode({'NAME': user.encode('utf-8'), 'PASSWORD': password.encode('utf-8')})
41 etree = get_html_parse_tree(u'http://ace.delos.com/usacogate', data=params, treetype=u'etree')
42 for font in etree.getiterator(u'font'):
43 if font.text and u'Please try again' in font.text:
44 return None
45 return etree
46
47 def _check_login(self, user, password):
48 return self._login(user, password) is not None
49
50 def _get_section(self, monitor_url, usaco_user, user):
51 etree = get_html_parse_tree(monitor_url, treetype=u'etree')
52 usaco_user = usaco_user.lower()
53 header = True
54 for tr in etree.getiterator(u'tr'):
55 if header:
56 header = False
57 continue
58 tds = [t.text for t in tr.getiterator(u'td')]
59 section = u'is on section %s' % tds[5]
60 if tds[5] == u'DONE':
61 section = u'has completed USACO training'
62 if tds[0] and tds[0].lower() == usaco_user:
63 return u'%(user)s (%(usaco_user)s on USACO) %(section)s and last logged in %(days)s ago' % {
64 'user': user,
65 'usaco_user': usaco_user,
66 'days': tds[3],
67 'section': section,
68 }
69
70 return None
71
72 def _add_user(self, monitor_url, user):
73 matches = re.search(r'a=(.+)&', monitor_url)
74 auth = matches.group(1)
75 params = urlencode({'STUDENTID': user.encode('utf-8'), 'ADD': 'ADD STUDENT',
76 'a': auth.encode('utf-8'), 'monitor': '1'})
77 etree = get_html_parse_tree(monitor_url, treetype=u'etree', data=params)
78 for font in etree.getiterator(u'font'):
79 if font.text and u'No STATUS file for' in font.text:
80 raise UsacoException(u'Sorry, user %s not found' % user)
81
82 def _get_monitor_url(self):
83 if self.admin_user is None or self.admin_password is None:
84 raise UsacoException(u'Sorry, you need to configure a USACO admin account')
85 return
86 etree = self._login(self.admin_user, self.admin_password)
87 if etree is None:
88 raise UsacoException(u'Sorry, the configured USACO admin account is invalid')
89
90 urls = [a.get(u'href') for a in etree.getiterator(u'a')]
91 monitor_url = [url for url in urls if u'monitor' in url][0]
92 if len(monitor_url) == 0:
93 raise UsacoException(u'USACO admin account does not have teacher status')
94
95 return monitor_url
96
97 def _get_usaco_user(self, event, user):
98 account = event.session.query(Account) \
99 .options(eagerload('attributes')) \
100 .filter(Account.username == user) \
101 .first()
102 if account is None:
103 account = event.session.query(Account) \
104 .options(eagerload('attributes')) \
105 .join('identities') \
106 .filter(Identity.identity == user) \
107 .filter(Identity.source == event.source) \
108 .first()
109 if account is None:
110 raise UsacoException(u'Sorry, %s has not been linked to a USACO account yet' % user)
111
112 usaco_account = [attr.value for attr in account.attributes if attr.name == 'usaco_account']
113 if len(usaco_account) == 0:
114 raise UsacoException(u'Sorry, %s has not been linked to a USACO account yet' % user)
115 return usaco_account[0]
116
117 def _get_usaco_users(self, event):
118 accounts = event.session.query(Identity) \
119 .join(['account', 'attributes']) \
120 .add_entity(Attribute) \
121 .filter(Attribute.name == u'usaco_account') \
122 .filter(Identity.source == event.source) \
123 .all()
124
125 users = {}
126 for a in accounts:
127 users[a[1].value] = a[0].identity
128 return users
129
130 @match(r'^usaco\s+section\s+(?:for\s+)?(.+)$')
131 def get_section(self, event, user):
132 try:
133 usaco_user = self._get_usaco_user(event, user)
134 monitor_url = self._get_monitor_url()
135 except UsacoException, e:
136 event.addresponse(e)
137 return
138
139 section = self._get_section(monitor_url, usaco_user, user)
140 if section:
141 event.addresponse(section)
142 return
143
144 try:
145 self._add_user(monitor_url, user)
146 except UsacoException, e:
147 event.addresponse(e)
148 return
149
150 event.addresponse(self._get_section(monitor_url, usaco_user, user))
151
152 @match(r'^usaco\s+division\s+(?:for\s+)?(.+)$')
153 def get_division(self, event, user):
154 try:
155 usaco_user = self._get_usaco_user(event, user)
156 except UsacoException, e:
157 event.addresponse(e)
158 return
159
160 params = urlencode({'id': usaco_user.encode('utf-8'), 'search': 'SEARCH'})
161 etree = get_html_parse_tree(u'http://ace.delos.com/showdiv', data=params, treetype=u'etree')
162 division = [b.text for b in etree.getiterator(u'b') if b.text and usaco_user in b.text][0]
163 if division.find(u'would compete') != -1:
164 event.addresponse(u'%(user)s (%(usaco_user)s on USACO) has not competed in a USACO before',
165 {u'user': user, u'usaco_user': usaco_user})
166 matches = re.search(r'(\w+) Division', division)
167 division = matches.group(1).lower()
168 event.addresponse(u'%(user)s (%(usaco_user)s on USACO) is in the %(division)s division',
169 {u'user': user, u'usaco_user': usaco_user, u'division': division})
170
171 def _redact(self, event, term):
172 for type in ['raw', 'deaddressed', 'clean', 'stripped']:
173 event['message'][type] = re.sub(r'(.*)(%s)' % re.escape(term), r'\1[redacted]', event['message'][type])
174
175 @match(r'^i\s+am\s+(\S+)\s+on\s+usaco\s+password\s+(\S+)$')
176 def usaco_account(self, event, user, password):
177 self._redact(event, password)
178
179 if not self._check_login(user, password):
180 event.addresponse(u'Sorry, that account is invalid')
181 return
182 if not event.account:
183 event.addresponse(u'Sorry, you need to create an account first')
184 return
185
186 try:
187 monitor_url = self._get_monitor_url()
188 except UsacoException, e:
189 event.addresponse(e)
190 return
191
192 self._add_user(monitor_url, user)
193
194 account = event.session.query(Account).get(event.account)
195 usaco_account = [attr for attr in account.attributes if attr.name == u'usaco_account']
196 if usaco_account:
197 usaco_account[0].value = user
198 else:
199 account.attributes.append(Attribute('usaco_account', user))
200 event.session.save_or_update(account)
201 event.session.commit()
202
203 event.addresponse(u'Done')
204
205 @match(r'^usaco\s+(\S+)\s+results(?:\s+for\s+(\S+))?$')
206 def usaco_results(self, event, contest, user):
207 if user is not None:
208 try:
209 usaco_user = self._get_usaco_user(event, user)
210 except UsacoException, e:
211 event.addresponse(e)
212 return
213
214 url = u'http://ace.delos.com/%sresults' % contest.upper()
215 try:
216 filename = cacheable_download(url, u'usaco/results_%s' % contest.upper())
217 except HTTPError:
218 event.addresponse(u"Sorry, the results for %s aren't released yet", contest)
219
220 if user is not None:
221 users = {usaco_user: user}
222 else:
223 users = self._get_usaco_users(event)
224
225 text = open(filename, 'r').read().decode('ISO-8859-2')
226 divisions = [u'gold', u'silver', u'bronze']
227 results = [[], [], []]
228 division = None
229 for line in text.splitlines():
230 for index, d in enumerate(divisions):
231 if d in line.lower():
232 division = index
233 # Example results line:
234 # 2010 POL Jakub Pachocki meret1 ***** ***** 270 ***** ***** * 396 ***** ***** ** 324 1000
235 matches = re.match(r'^\s*(\d{4})\s+([A-Z]{3})\s+(.+?)\s+(\S+\d)\s+([\*xts\.e0-9 ]+?)\s+(\d+)\s*$', line)
236 if matches:
237 year = matches.group(1)
238 country = matches.group(2)
239 name = matches.group(3)
240 usaco_user = matches.group(4)
241 scores = matches.group(5)
242 total = matches.group(6)
243 if usaco_user in users.keys():
244 results[division].append((year, country, name, usaco_user, scores, total))
245
246 response = []
247 for i, division in enumerate(divisions):
248 if results[i]:
249 response.append(u'%s division results:' % division.title())
250 for result in results[i]:
251 response.append(u'%(user)s (%(usaco_user)s on USACO) scored %(total)s (%(scores)s)' % {
252 u'user': users[result[3]],
253 u'usaco_user': result[3],
254 u'total': result[5],
255 u'scores': result[4],
256 })
257 event.addresponse(u'\n'.join(response), conflate=False)
258
259# vi: set et sta sw=4 ts=4:
0260
=== modified file 'scripts/ibid-plugin'
--- scripts/ibid-plugin 2010-01-18 22:13:23 +0000
+++ scripts/ibid-plugin 2010-01-19 13:45:25 +0000
@@ -65,6 +65,9 @@
65 permissions = []65 permissions = []
66 supports = ('action', 'multiline', 'notice')66 supports = ('action', 'multiline', 'notice')
6767
68 def setup(self):
69 pass
70
68 def logging_name(self, name):71 def logging_name(self, name):
69 return name72 return name
7073

Subscribers

People subscribed via source and target branches