Merge lp:~marco-gallotta/ibid/uva into lp:~ibid-core/ibid/old-trunk-1.6
- uva
- Merge into old-trunk-1.6
Status: | Superseded | ||||
---|---|---|---|---|---|
Proposed branch: | lp:~marco-gallotta/ibid/uva | ||||
Merge into: | lp:~ibid-core/ibid/old-trunk-1.6 | ||||
Diff against target: |
853 lines (+666/-36) 3 files modified
ibid/db/__init__.py (+2/-1) ibid/event.py (+1/-1) ibid/plugins/codecontest.py (+663/-34) |
||||
To merge this branch: | bzr merge lp:~marco-gallotta/ibid/uva | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Stefano Rivera | Approve | ||
Review via email: mp+18342@code.launchpad.net |
This proposal supersedes a proposal from 2010-01-26.
This proposal has been superseded by a proposal from 2010-02-13.
Commit message
Description of the change
marcog (marco-gallotta) wrote : Posted in a previous version of this proposal | # |
Max Rabkin (max-rabkin) wrote : Posted in a previous version of this proposal | # |
I'm not sure how I feel about setting the current contest via config. Config is for, well, configuration.
I'd prefer "monitor uva <id>" -- and if there's only one running contest (this usually seems to be the case on UVa) then one shouldn't need to specify.
Keegan Carruthers-Smith (keegan-csmith) wrote : Posted in a previous version of this proposal | # |
> I'm not sure how I feel about setting the current contest via config. Config
> is for, well, configuration.
>
> I'd prefer "monitor uva <id>" -- and if there's only one running contest (this
> usually seems to be the case on UVa) then one shouldn't need to specify.
I agree with Max. Config feels dirty. I would enforce only one contest at a time.
marcog (marco-gallotta) wrote : Posted in a previous version of this proposal | # |
> I'm not sure how I feel about setting the current contest via config. Config
> is for, well, configuration.
>
> I'd prefer "monitor uva <id>" -- and if there's only one running contest (this
> usually seems to be the case on UVa) then one shouldn't need to specify.
I originally had it like you suggest, but there were a few issues with that -- maybe we can get past them though. The "uva countdown" feature works before the contest starts -- perhaps it can be made to countdown to the next contest and take an optional <id> arg. The "uva scoreboard" feature only could do the same -- except, it uses the current/previous contest if an <id> is not provided.
Usage would then become
14:37 < mibid> marcog: You can use uva in the following ways:
14:37 < mibid> uva countdown <contest_id>
14:37 < mibid> uva scoreboard <contest_id>
14:37 < mibid> monitor uva <contest_id>
14:37 < mibid> stop monitoring uva
Sound good?
Keegan Carruthers-Smith (keegan-csmith) wrote : Posted in a previous version of this proposal | # |
Maybe add a "uva set contest <contest_id>" then we can use that for
the contest_id default.
Stefano Rivera (stefanor) wrote : Posted in a previous version of this proposal | # |
Serious stuff:
--------------
What's with the modification to identity? I thought we needed to use a string.
Query: create acm team foo
./ibid/
event.
Response: Created ACM team foo
Query: acm team foo
ERROR:scripts.
Traceback (most recent call last):
File "scripts/
processor.
File "./ibid/
method(event, *match.groups())
File "./ibid/
u'uva_teams': uva_teams,
File "./ibid/event.py", line 53, in addresponse
response = response % params
ValueError: incomplete format
WARNING:
Response: I'm not feeling too well
Stylistic section of the review:
-------
> event.addrespon
All of those kind of things should be replaced with either a more descriptive answer ("OK, Team created"), or simply addresponse(True).
> member = event.session.
> .filter(
> .filter_
It'd read better if the two filter lines where the other way around, or you used filter() for both instead of one filter(), one filter_by().
> human_join(
You can use a generator expression (i.e. remove the square brackets)
> uva_teams = ', and is known as %s on UVa' % human_join(
> else:
> uva_teams = ''
Unicode
> u'%(team_name)s has member%(plural)s %(team_
> ...
> u'plural': plural(
Bad usage of plural. It should be plural(
> hours = time_penalty / 3600
> time_penalty -= hours * 3600
> minutes = time_penalty / 60
Use the integer division operator //, it makes intent clearer.
> class ACMProblemScore:
That should probably be a new-style class. Applies to the others, too.
Not that it matters :)
> def __str__(self):
Should probably be __unicode__, as it returns unicode. Also applies to the other class.
> def setup(self):
> super(Uva, self).setup()
Redundant.
> u"Sorry, I'm not monitoring the scoreboard anyway"
That needs to be reworded, it doesn't parse.
marcog (marco-gallotta) wrote : Posted in a previous version of this proposal | # |
> Maybe add a "uva set contest <contest_id>" then we can use that for
> the contest_id default.
When does the bot decide to drop this default? Moving on when once the contest is over makes sense for countdown, but not necessarily for scoreboard.
Stefano Rivera (stefanor) wrote : Posted in a previous version of this proposal | # |
I agree re contest_id in config.
> 14:37 < mibid> marcog: You can use uva in the following ways:
> 14:37 < mibid> uva countdown <contest_id>
> 14:37 < mibid> uva scoreboard <contest_id>
> 14:37 < mibid> monitor uva <contest_id>
> 14:37 < mibid> stop monitoring uva
Marco: optional parameters need square brackets. i.e. uva countdown [for <contest_id>]
Keegan Carruthers-Smith (keegan-csmith) wrote : Posted in a previous version of this proposal | # |
>> Maybe add a "uva set contest <contest_id>" then we can use that for
>> the contest_id default.
>
> When does the bot decide to drop this default? Moving on when once the contest is over makes sense for countdown, but not necessarily for scoreboard.
The contest_id is optional. So if you want the scoreboard for the
non-current contest you will have to specify it.
Also is countdown the correct name for that function? It implies it
will be doing some... counting down. "time left" is a better name.
marcog (marco-gallotta) wrote : Posted in a previous version of this proposal | # |
> Serious stuff:
> --------------
>
> What's with the modification to identity? I thought we needed to use a string.
Oops, that slipped through the cleanup I did. r885
> Query: create acm team foo
> ./ibid/
> event.session.
> Response: Created ACM team foo
> Query: acm team foo
> ERROR:scripts.
> codecontest plugin
> Traceback (most recent call last):
> File "scripts/
> processor.
> File "./ibid/
> method(event, *match.groups())
> File "./ibid/
> u'uva_teams': uva_teams,
> File "./ibid/event.py", line 53, in addresponse
> response = response % params
> ValueError: incomplete format
> WARNING:
> Response: I'm not feeling too well
r886
> Stylistic section of the review:
> -------
>
> > event.addrespon
> All of those kind of things should be replaced with either a more descriptive
> answer ("OK, Team created"), or simply addresponse(True).
r887
> > member = event.session.
> > .filter(
> > .filter_
> It'd read better if the two filter lines where the other way around, or you
> used filter() for both instead of one filter(), one filter_by().
r888
> > human_join(
> You can use a generator expression (i.e. remove the square brackets)
r889
> > uva_teams = ', and is known as %s on UVa' %
> human_join(
> > else:
> > uva_teams = ''
> Unicode
r890
> > u'%(team_name)s has member%(plural)s %(team_
> > ...
> > u'plural': plural(
> Bad usage of plural. It should be plural(
> 'members'). There's another one further down.
r891
> > hours = time_penalty / 3600
> > time_penalty -= hours * 3600
> > minutes = time_penalty / 60
> Use the integer division operator //, it makes intent clearer.
r892
> > class ACMProblemScore:
> That should probably be a new-style class. Applies to the others, too.
> Not that it matters :)
r893
> > def __str__(self):
> Should probably be __unicode__, as it returns unicode. Also applies to the
> other class.
r984
> > def setup(self):
> > super(Uva, self).setup()
> Redundant.
r895
> > u"Sorry, I'm not monitoring the scoreboard anyway"
> That needs to be reworded, it doesn't parse.
r896 - 'I never even started monitoring the scoreboard' better?
marcog (marco-gallotta) wrote : Posted in a previous version of this proposal | # |
> >> Maybe add a "uva set contest <contest_id>" then we can use that for
> >> the contest_id default.
> >
> > When does the bot decide to drop this default? Moving on when once the
> contest is over makes sense for countdown, but not necessarily for scoreboard.
>
> The contest_id is optional. So if you want the scoreboard for the
> non-current contest you will have to specify it.
We seem to be thinking along different paths. I'm thinking that the bot should determine the default contest_id as the current contest (if one is running), or the next upcoming contest. I thought your "set contest" option would be to temporarily override this default (e.g. if we know we're going to do this contest in 2 weeks time and don't care about the ones before then). My question was: if we do this, when do we discard the "set contest" default provided?
Example:
me: uva countdown
bot: <countdown for contest 42>
me: uva scoreboard
bot: <scoreboard for contest 41>
(contest 42 begins)
me: uva countdown
bot: <countdown for contest 42>
me: uva scoreboard
bot: <scoreboard for contest 42>
(contest 42 ends)
me: uva countdown
bot: <countdown for contest 43>
me: uva scoreboard
bot: <scoreboard for contest 42>
then...
me: uva set contest 45
me: uva countdown
bot: <countdown for contest 45>
me: uva scoreboard
bot: <scoreboard for contest 45 -- error>
(contest 45 begins)
me: uva countdown
bot: <countdown for contest 45>
me: uva scoreboard
bot: <scoreboard for contest 45 -- error>
(contest 45 ends)
me: uva countdown
bot: <countdown for contest 45> <-- this makes no sense!
me: uva scoreboard
bot: <scoreboard for contest 45> <-- but this does
or we could do:
(contest 45 ends)
me: uva countdown
bot: <countdown for contest 45>
me: uva scoreboard
bot: <scoreboard for contest 46> <-- the distinction is unexpected though
> Also is countdown the correct name for that function? It implies it
> will be doing some... counting down. "time left" is a better name.
It has aliases "remaining time" and "time remaining".
Stefano Rivera (stefanor) wrote : Posted in a previous version of this proposal | # |
Query: create acm team foo
./ibid/
event.
Response: Created ACM team foo
Query: create acm team foo
ERROR:scripts.
Traceback (most recent call last):
File "scripts/
processor.
File "./ibid/
method(event, *match.groups())
File "./ibid/
event.
File "/usr/lib/
self.
File "/usr/lib/
self.
File "/usr/lib/
self.
File "/usr/lib/
self.
File "/usr/lib/
flush_
File "/usr/lib/
UOWExecutor
File "/usr/lib/
self.
File "/usr/lib/
self.
File "/usr/lib/
task.
File "/usr/lib/
c = connection.
File "/usr/lib/
return Connection.
File "/usr/lib/
return self.__
File "/usr/lib/
self.
File "/usr/lib/
self.
File "/usr/lib/
raise exc.DBAPIError.
IntegrityError: (IntegrityError) column name is not unique u'INSERT INTO acmteams (name) VALUES (?)' [u'foo']
WARNING:
Response: That didn't seem to agree with me
And another:
Query: foo is uva team bar
./ibid/
event.
Response: Righto
Query: acm team foo
Response: foo has no team members, and is known as bar o...
marcog (marco-gallotta) wrote : Posted in a previous version of this proposal | # |
> Query: create acm team foo
> ./ibid/
> event.session.
> Response: Created ACM team foo
> Query: create acm team foo
> ERROR:scripts.
> codecontest plugin
r899
> Query: foo is uva team bar
> ./ibid/
> event.session.
> Response: Righto
> Query: acm team foo
> Response: foo has no team members, and is known as bar on UVa
> Query: delete acm team foo
> ERROR:scripts.
> codecontest plugin
r900
Stefano Rivera (stefanor) wrote : Posted in a previous version of this proposal | # |
Do we want this behaviour?
Query: joe is on acm team foo
./ibid/
event.
Response: Added joe to team foo
Query: joe is on acm team foo
Response: Added joe to team foo
Query: acm team foo
Response: foo has members joe and joe, and is known as bar and baz on UVa
Query: delete joe from acm team foo
Response: Sorry, I don't know that person
Query: joe is on acm team qux
Response: Added joe to team qux
My unconfigured UVa also does this:
Query: uva countdown
ERROR:scripts.
Traceback (most recent call last):
File "scripts/
processor.
File "./ibid/
method(event, *match.groups())
File "./ibid/
time_elapsed = [h for h in etree.findall(
IndexError: list index out of range
WARNING:
Response: That didn't go down very well. Burp.
Query: uva scoreboard
ERROR:scripts.
Traceback (most recent call last):
File "scripts/
processor.
File "./ibid/
method(event, *match.groups())
File "./ibid/
problems, scores = self._get_
File "./ibid/
return (problems, scores)
UnboundLocalError: local variable 'problems' referenced before assignment
WARNING:
Response: That didn't go down very well. Burp.
marcog (marco-gallotta) wrote : Posted in a previous version of this proposal | # |
> Do we want this behaviour?
>
> Query: joe is on acm team foo
> ./ibid/
> event.session.
> Response: Added joe to team foo
> Query: joe is on acm team foo
> Response: Added joe to team foo
> Query: acm team foo
> Response: foo has members joe and joe, and is known as bar and baz on UVa
> Query: delete joe from acm team foo
> Response: Sorry, I don't know that person
> Query: joe is on acm team qux
> Response: Added joe to team qux
r902. It makes sense to have multiple Robert's on the same team, but I've fixed deletion.
> My unconfigured UVa also does this:
Planning to rework this based on Max/Keegan's comments anyway.
marcog (marco-gallotta) wrote : Posted in a previous version of this proposal | # |
Alrighty, the selection of contest id is now handled automatically unless a contest id is specified on a per-command basis. Monitoring checks for the running contest only (I assume no parallel contests -- note that this isn't always true, but very rare). It's still not very robust to poor user commands (e.g. an unknown contest id will result in an unhandled exception).
Stefano Rivera (stefanor) wrote : | # |
First pass
I'm glad to see you've shortened long lines :)
However, be careful with string types. If you do u'a' 'b', you are concatenating different types of string. That's ok, if ugly, and the result will be unicode, which is what you want. However: r'\a' '\r'. Only the first one is a raw string the second one is a literal CR. I'm talking about diff lines 21x.
== Major ==
62 - autoload = False
63 + #autoload = False
I don't think I need to say anything. And there's more than one.
== Minor ==
44 +from sqlalchemy import Table, Column, ForeignKey
45 +from sqlalchemy.orm import relation
46 +from sqlalchemy.
I think you can get those from ibid.db
83 + return u'%(user)s (%(usaco_user)s on USACO) %(section)s and ' \
84 + 'last logged in %(days)s ago' % {
Inside brackets (of any kind), you don't need a continuation character (\). There are lots of those.
500 +class Contest:
Newstyle, please.
533 + u'start': self.start.
There's a helper function for formatting times in a bot-wide configurable format.
538 + return mktime(
I think datetimes can be subtracted from each other (giving a timedelta). But obviously this works.
marcog (marco-gallotta) wrote : | # |
> However, be careful with string types. If you do u'a' 'b', you are
r910
> 62 - autoload = False
> 63 + #autoload = False
r911
> 44 +from sqlalchemy import Table, Column, ForeignKey
> 45 +from sqlalchemy.orm import relation
> 46 +from sqlalchemy.
>
> I think you can get those from ibid.db
r912. InvalidRequestError was not there, but I added it. That ok?
> Inside brackets (of any kind), you don't need a continuation character (\).
> There are lots of those.
r913
> 500 +class Contest:
>
> Newstyle, please.
r914
> 533 + u'start': self.start.
>
> There's a helper function for formatting times in a bot-wide configurable
> format.
r915. Nice, it even converts to local tz which I was planning on adding later. :)
> 538 + return mktime(
> mktime(
>
> I think datetimes can be subtracted from each other (giving a timedelta). But
> obviously this works.
In [1]: datetime.now() - datetime.now()
Out[1]: datetime.
I don't see a way to get the sign from that that will always work, besides adding days+hours+
- 910. By marcog
-
Multi-line strings need a u/r before each line
- 911. By marcog
-
autoload=False, again
- 912. By marcog
-
Import from ibid.db rather than sqlalchemy directly
- 913. By marcog
-
Remvoe unneccessary \ characters
- 914. By marcog
-
Newstyle class for Contest
- 915. By marcog
-
Use helper function for time formatting
Stefano Rivera (stefanor) wrote : | # |
r'\s+([\*xts\.e0-9 ]+?)'
I don't think * or . must be escaped inside square brackets.
296 + def __repr(self):
It returns unicode, so it should be a __unicode__ (besides the obvious fact that it's currently incorrectly named). There are a few __reper__s.
I'm running out of things to say. Now I just need to test it.
marcog (marco-gallotta) wrote : | # |
> r'\s+([\*xts\.e0-9 ]+?)'
> I don't think * or . must be escaped inside square brackets.
r917
> It returns unicode, so it should be a __unicode__ (besides the obvious fact
r916
Stefano Rivera (stefanor) wrote : | # |
Somewhere around line 725, reformat badly formatted times (zero-padding)
marcog (marco-gallotta) wrote : | # |
> Somewhere around line 725, reformat badly formatted times (zero-padding)
r918
Stefano Rivera (stefanor) wrote : | # |
Can you rename acmteams to acmteam.
Query: acm teams
Response: ACM teams:
Query: uva contests
/usr/lib/
str._
Response: UVa contests: A Canadian Contest (248) starts 2010-01-09 10:00:00 SAST and lasts 5 hours, CUET Inter-university Programming Contest (247) starts 2010-01-09 17:00:00 SAST and lasts 5 hours, A Bangladeshi Contest (246) starts 2009-12-19 10:00:00 SAST and lasts 5 hours, Alkhawarizmi Programming Contest 2009 (245) starts 2009-12-05 11:00:00 SAST and lasts 5 hours, World Finals Warmup II (244) starts 2010-01-23 16:00:00 SAST and lasts 5 hours, World Finals Warmup I (243) starts 2010-01-16 11:00:00 SAST and lasts 5 hours 15 minutes, Wuhan Regional Semilive (242) starts 2009-11-02 11:00:00 SAST and lasts 5 hours, IIUPC 2009 (241) starts 2009-10-21 11:00:00 SAST and lasts 5 hours, Another Regional Contest (240) starts 2009-11-14 09:00:00 SAST and lasts 5 hours, Nordic Collegiate Programming Contest NCPC 2009 (239) starts 2009-10-03 11:00:00 SAST and lasts 5 hours, Waterloo ACM Programming Contest Fall 2 (238) starts 2009-10-04 19:00:00 SAST and lasts 3 hours, Waterloo ACM Programming Contest Fall 1 (237) starts 2009-09-27 19:00:00 SAST and lasts 3 hours, Dhaka regional Semi-live (236) starts 2009-10-24 13:00:00 SAST and lasts 5 hours, Once Again! A Bangladeshi Contest (235) starts 2009-08-22 11:00:00 SAST and lasts 5 hours 45 minutes, Regional Warmup 1 (234) starts 2009-09-13 15:00:00 SAST and lasts 5 hours, ULM Local Contest (233) starts 2009-07-18 11:00:00 SAST and lasts 5 hours, XXIII Colombian Programming Contest (232) starts 2009-09-05 21:00:00 SAST and lasts 5 hours, Yet another Bangladeshi Contest (231) starts 2009-08-01 11:00:00 SAST and lasts 5 hours, One more ICPC Regional Contest (230) starts 2009-11-07 11:00:00 SAST and lasts 5 hours, ACM ICPC South America Regional Contest (229) starts 2009-10-24 22:00:00 SAST and lasts 5 hours, CUPCAM 2009 (228) starts 2009-10-17 10:00:00 SAST and lasts 5 hours, Brazilian National Contest 2009 (227) starts 2009-09-19 22:00:00 SAST and lasts 5 hours, Waterloo Local Spring 2009 (226) starts 2009-06-13 20:00:00 SAST and lasts 4 hours, The first contest of the new season (225) starts 2009-05-09 11:00:00 SAST and lasts 5 hours, VII Programming Olympiads in Murcia (224) starts 2009-05-23 09:30:00 SAST and lasts 4 hours, Another high quality contest (223) starts 2009-02-21 11:00:00 SAST and lasts 5 hours, Pre-warmup contest (222) starts 2009-02-14 11:00:00 SAST and lasts 3 hours, World Finals Warmup II (221) starts 2009-04-05 10:00:00 SAST and lasts 1 day, World Finals Warmup I (220) starts 2009-03-29 16:00:00 SAST and lasts 1 day and Contest of Newbies V (219) starts 2008-12-27 14:00:00 SAST and lasts 4 hours
That looks a little long for IRC, you may need some paging. But quite honestly I wouldn't worry about it for now.
marcog (marco-gallotta) wrote : | # |
> Can you rename acmteams to acmteam.
r919
> That looks a little long for IRC, you may need some paging. But quite honestly
> I wouldn't worry about it for now.
It gets truncated and we usually don't care about the ones way in the past.
Stefano Rivera (stefanor) wrote : | # |
I'd still like something to deal with the null-length acm team list response, but I approve
marcog (marco-gallotta) wrote : | # |
> I'd still like something to deal with the null-length acm team list response,
> but I approve
Oh, missed that in your last comment. r920
Unmerged revisions
- 922. By marcog
-
Merge trunk
- 921. By marcog
-
spelling
- 920. By marcog
-
Handle case of no acm teams
- 919. By marcog
-
Rename acmteams to acmteam
- 918. By marcog
-
Pad countdown times with 0's
- 917. By marcog
-
Fix usaco results regex (don't escape . and * inside [])
- 916. By marcog
-
__repr_
_->__unicode_ _ - 915. By marcog
-
Use helper function for time formatting
- 914. By marcog
-
Newstyle class for Contest
- 913. By marcog
-
Remvoe unneccessary \ characters
Preview Diff
1 | === modified file 'ibid/db/__init__.py' |
2 | --- ibid/db/__init__.py 2010-02-05 15:58:06 +0000 |
3 | +++ ibid/db/__init__.py 2010-02-13 21:24:14 +0000 |
4 | @@ -11,7 +11,8 @@ |
5 | from sqlalchemy.sql import func |
6 | from sqlalchemy.ext.declarative import declarative_base as _declarative_base |
7 | |
8 | -from sqlalchemy.exceptions import IntegrityError, SADeprecationWarning |
9 | +from sqlalchemy.exceptions import IntegrityError, SADeprecationWarning, \ |
10 | + InvalidRequestError |
11 | |
12 | metadata = _MetaData() |
13 | Base = _declarative_base(metadata=metadata) |
14 | |
15 | === modified file 'ibid/event.py' |
16 | --- ibid/event.py 2010-01-17 20:17:23 +0000 |
17 | +++ ibid/event.py 2010-02-13 21:24:14 +0000 |
18 | @@ -49,7 +49,7 @@ |
19 | u'difficulties with translation later.', |
20 | SyntaxWarning, stacklevel=2) |
21 | |
22 | - if isinstance(response, basestring) and params: |
23 | + if isinstance(response, basestring) and (params or params in (u'', '')): |
24 | response = response % params |
25 | |
26 | if isinstance(response, str): |
27 | |
28 | === modified file 'ibid/plugins/codecontest.py' |
29 | --- ibid/plugins/codecontest.py 2010-01-25 18:31:04 +0000 |
30 | +++ ibid/plugins/codecontest.py 2010-02-13 21:24:14 +0000 |
31 | @@ -1,20 +1,31 @@ |
32 | # Copyright (c) 2010, Marco Gallotta |
33 | # Released under terms of the MIT/X/Expat Licence. See COPYING for details. |
34 | |
35 | +from dateutil.relativedelta import relativedelta |
36 | import re |
37 | +from time import mktime |
38 | from urllib2 import HTTPError, URLError |
39 | |
40 | +from dateutil.parser import parse |
41 | from urllib import urlencode |
42 | |
43 | -from ibid.config import Option |
44 | -from ibid.db import eagerload |
45 | +from ibid.config import Option, IntOption |
46 | +from ibid.db import Base, eagerload |
47 | from ibid.db.models import Account, Attribute, Identity |
48 | -from ibid.plugins import Processor, match, auth_responses |
49 | -from ibid.utils import cacheable_download |
50 | +from ibid.db.types import IbidUnicode, Integer |
51 | +from ibid.db.versioned_schema import VersionedSchema |
52 | +from ibid.plugins import Processor, match, periodic, auth_responses |
53 | +from ibid.utils import cacheable_download, plural, human_join, format_date |
54 | from ibid.utils.html import get_html_parse_tree |
55 | - |
56 | -help = {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.'} |
57 | - |
58 | +from ibid.db import Table, Column, ForeignKey, relation, InvalidRequestError |
59 | + |
60 | +help = {} |
61 | + |
62 | +help[u'usaco'] = u'Query USACO sections, divisions and more. Since this info ' \ |
63 | + u'is private, users are required to provide their USACO password when ' \ |
64 | + u'linking their USACO account to their ibid account and only linked ' \ |
65 | + u'accounts can be queried. Your password is used only to confirm ' \ |
66 | + u'that the account is yours and is discarded immediately.' |
67 | class UsacoException(Exception): |
68 | def __init__(self, msg): |
69 | self.msg = msg |
70 | @@ -37,9 +48,13 @@ |
71 | autoload = False |
72 | |
73 | def _login(self, user, password): |
74 | - params = urlencode({'NAME': user.encode('utf-8'), 'PASSWORD': password.encode('utf-8')}) |
75 | + params = urlencode({ |
76 | + 'NAME': user.encode('utf-8'), |
77 | + 'PASSWORD': password.encode('utf-8') |
78 | + }) |
79 | try: |
80 | - etree = get_html_parse_tree(u'http://ace.delos.com/usacogate', data=params, treetype=u'etree') |
81 | + etree = get_html_parse_tree(u'http://ace.delos.com/usacogate', |
82 | + data=params, treetype=u'etree') |
83 | except URLError: |
84 | raise UsacoException(u'Sorry, USACO (or my connection?) is down') |
85 | for font in etree.getiterator(u'font'): |
86 | @@ -66,7 +81,8 @@ |
87 | if tds[5] == u'DONE': |
88 | section = u'has completed USACO training' |
89 | if tds[0] and tds[0].lower() == usaco_user: |
90 | - return u'%(user)s (%(usaco_user)s on USACO) %(section)s and last logged in %(days)s ago' % { |
91 | + return u'%(user)s (%(usaco_user)s on USACO) %(section)s and ' \ |
92 | + u'last logged in %(days)s ago' % { |
93 | 'user': user, |
94 | 'usaco_user': usaco_user, |
95 | 'days': tds[3], |
96 | @@ -119,11 +135,14 @@ |
97 | .filter(Identity.source == event.source) \ |
98 | .first() |
99 | if account is None: |
100 | - raise UsacoException(u'Sorry, %s has not been linked to a USACO account yet' % user) |
101 | + raise UsacoException(u'Sorry, %s has not been linked to a ' |
102 | + u'USACO account yet' % user) |
103 | |
104 | - usaco_account = [attr.value for attr in account.attributes if attr.name == 'usaco_account'] |
105 | + usaco_account = [attr.value for attr in account.attributes |
106 | + if attr.name == 'usaco_account'] |
107 | if len(usaco_account) == 0: |
108 | - raise UsacoException(u'Sorry, %s has not been linked to a USACO account yet' % user) |
109 | + raise UsacoException(u'Sorry, %s has not been linked to a USACO ' |
110 | + u'account yet' % user) |
111 | return usaco_account[0] |
112 | |
113 | def _get_usaco_users(self, event): |
114 | @@ -169,21 +188,26 @@ |
115 | |
116 | params = urlencode({'id': usaco_user.encode('utf-8'), 'search': 'SEARCH'}) |
117 | try: |
118 | - etree = get_html_parse_tree(u'http://ace.delos.com/showdiv', data=params, treetype=u'etree') |
119 | + etree = get_html_parse_tree(u'http://ace.delos.com/showdiv', |
120 | + data=params, treetype=u'etree') |
121 | except URLError: |
122 | event.addresponse(u'Sorry, USACO (or my connection?) is down') |
123 | - division = [b.text for b in etree.getiterator(u'b') if b.text and usaco_user in b.text][0] |
124 | + division = [b.text for b in etree.getiterator(u'b') |
125 | + if b.text and usaco_user in b.text][0] |
126 | if division.find(u'would compete') != -1: |
127 | - event.addresponse(u'%(user)s (%(usaco_user)s on USACO) has not competed in a USACO before', |
128 | + event.addresponse(u'%(user)s (%(usaco_user)s on USACO) has not ' |
129 | + u'competed in a USACO before', |
130 | {u'user': user, u'usaco_user': usaco_user}) |
131 | matches = re.search(r'(\w+) Division', division) |
132 | division = matches.group(1).lower() |
133 | - event.addresponse(u'%(user)s (%(usaco_user)s on USACO) is in the %(division)s division', |
134 | + event.addresponse(u'%(user)s (%(usaco_user)s on USACO) is in ' |
135 | + u'the %(division)s division', |
136 | {u'user': user, u'usaco_user': usaco_user, u'division': division}) |
137 | |
138 | def _redact(self, event, term): |
139 | for type in ['raw', 'deaddressed', 'clean', 'stripped']: |
140 | - event['message'][type] = re.sub(r'(.*)(%s)' % re.escape(term), r'\1[redacted]', event['message'][type]) |
141 | + event['message'][type] = re.sub(r'(.*)(%s)' % re.escape(term), |
142 | + r'\1[redacted]', event['message'][type]) |
143 | |
144 | @match(r'^(\S+)\s+(?:is|am)\s+(\S+)\s+on\s+usaco(?:\s+password\s+(\S+))?$') |
145 | def usaco_account(self, event, user, usaco_user, password): |
146 | @@ -191,16 +215,19 @@ |
147 | self._redact(event, password) |
148 | |
149 | if event.public and password: |
150 | - event.addresponse(u"Giving your password in public is bad! Please tell me that again in a private message.") |
151 | + event.addresponse(u'Giving your password in public is bad! ' |
152 | + u'Please tell me that again in a private message.') |
153 | return |
154 | |
155 | if not event.account: |
156 | - event.addresponse(u'Sorry, you need to create an account with me first (type "usage accounts" to see how)') |
157 | + event.addresponse(u'Sorry, you need to create an account with ' |
158 | + u'me first (type "usage accounts" to see how)') |
159 | return |
160 | admin = auth_responses(event, u'usacoadmin') |
161 | if user.lower() == 'i': |
162 | if password is None and not admin: |
163 | - event.addresponse(u'Sorry, I need your USACO password to verify your account') |
164 | + event.addresponse(u'Sorry, I need your USACO password to ' |
165 | + u'verify your account') |
166 | if password and not self._check_login(user, password): |
167 | event.addresponse(u'Sorry, that account is invalid') |
168 | return |
169 | @@ -226,7 +253,8 @@ |
170 | event.addresponse(e) |
171 | return |
172 | |
173 | - usaco_account = [attr for attr in account.attributes if attr.name == u'usaco_account'] |
174 | + usaco_account = [attr for attr in account.attributes |
175 | + if attr.name == u'usaco_account'] |
176 | if usaco_account: |
177 | usaco_account[0].value = usaco_user |
178 | else: |
179 | @@ -234,7 +262,7 @@ |
180 | event.session.save_or_update(account) |
181 | event.session.commit() |
182 | |
183 | - event.addresponse(u'Done') |
184 | + event.addresponse(True) |
185 | |
186 | @match(r'^usaco\s+(\S+)\s+results(?:\s+for\s+(.+))?$') |
187 | def usaco_results(self, event, contest, user): |
188 | @@ -249,18 +277,19 @@ |
189 | |
190 | url = u'http://ace.delos.com/%sresults' % contest.upper() |
191 | try: |
192 | - filename = cacheable_download(url, u'usaco/results_%s' % contest.upper(), timeout=30) |
193 | + filename = cacheable_download(url, |
194 | + u'usaco/results_%s' % contest.upper(), timeout=30) |
195 | except HTTPError: |
196 | event.addresponse(u"Sorry, the results for %s aren't released yet", contest) |
197 | except URLError: |
198 | - event.addresponse(u"Sorry, I couldn't fetch the USACO results. Maybe USACO is down?") |
199 | + event.addresponse(u"Sorry, I couldn't fetch the USACO results. " |
200 | + u"Maybe USACO is down?") |
201 | |
202 | if user is not None: |
203 | users = {usaco_user: user.lower()} |
204 | else: |
205 | try: |
206 | users = self._get_usaco_users(event) |
207 | - print users |
208 | except UsacoException, e: |
209 | event.addresponse(e) |
210 | return |
211 | @@ -275,8 +304,16 @@ |
212 | if d in line.lower(): |
213 | division = index |
214 | # Example results line: |
215 | - # 2010 POL Jakub Pachocki meret1 ***** ***** 270 ***** ***** * 396 ***** ***** ** 324 1000 |
216 | - matches = re.match(r'^\s*(\d{4})\s+([A-Z]{3})\s+(.+?)\s+(\S+\d)\s+([\*xts\.e0-9 ]+?)\s+(\d+)\s*$', line) |
217 | + # 2010 POL Jakub Pachocki meret1 \ |
218 | + # ***** ***** 270 ***** ***** * 396 ***** ***** ** 324 1000 |
219 | + matches = re.match( |
220 | + r'^\s*(\d{4})' # Year |
221 | + r'\s+([A-Z]{3})' # Country code |
222 | + r'\s+(.+?)' # Name |
223 | + r'\s+(\S+\d)' # Account name |
224 | + r'\s+([*xts.e0-9 ]+?)' # Scores |
225 | + r'\s+(\d+)' # Total score |
226 | + r'\s*$', line) |
227 | if matches: |
228 | year = matches.group(1) |
229 | country = matches.group(2) |
230 | @@ -291,7 +328,8 @@ |
231 | match = True |
232 | users[usaco_user] = user |
233 | if match: |
234 | - results[division].append((year, country, name, usaco_user, scores, total)) |
235 | + results[division].append((year, country, name, |
236 | + usaco_user, scores, total)) |
237 | count += 1 |
238 | |
239 | response = [] |
240 | @@ -309,7 +347,8 @@ |
241 | division_string = u' in the %s division' % division.title() |
242 | else: |
243 | division_string = u'' |
244 | - response.append(u'%(user)s scored %(total)s%(division)s (%(scores)s)' % { |
245 | + response.append(u'%(user)s scored %(total)s%(division)s ' |
246 | + u'(%(scores)s)' % { |
247 | u'user': user_string, |
248 | u'total': result[5], |
249 | u'scores': result[4], |
250 | @@ -323,9 +362,599 @@ |
251 | u'contest': contest, |
252 | }) |
253 | else: |
254 | - event.addresponse(u"Sorry, I don't know anyone that entered %s", contest) |
255 | - return |
256 | - |
257 | - event.addresponse(u'\n'.join(response), conflate=False) |
258 | + event.addresponse(u"Sorry, I don't know anyone " |
259 | + u"that entered %s", contest) |
260 | + return |
261 | + |
262 | + event.addresponse(u'\n'.join(response), conflate=False) |
263 | + |
264 | + |
265 | +class UVATeam(Base): |
266 | + __table__ = Table('uvateams', Base.metadata, |
267 | + Column('id', Integer, primary_key=True), |
268 | + Column('acm_id', Integer, ForeignKey('acmteams.id'), nullable=False), |
269 | + Column('name', IbidUnicode(64, case_insensitive=True), unique=True, |
270 | + nullable=False, index=True), |
271 | + useexisting=True) |
272 | + |
273 | + __table__.versioned_schema = VersionedSchema(__table__, 1) |
274 | + |
275 | + def __init__(self, acm_id, name): |
276 | + self.acm_id = acm_id |
277 | + self.name = name |
278 | + |
279 | + def __unicode__(self): |
280 | + return u'<UVATeam %s is ACMTeam %d>' % (self.name, self.acm_id) |
281 | + |
282 | + |
283 | +class ACMTeamMember(Base): |
284 | + __table__ = Table('acmteammembers', Base.metadata, |
285 | + Column('id', Integer, primary_key=True), |
286 | + Column('team_id', Integer, ForeignKey('acmteams.id'), nullable=False, |
287 | + index=True), |
288 | + Column('name', IbidUnicode(64, case_insensitive=True), nullable=False), |
289 | + useexisting=True) |
290 | + |
291 | + __table__.versioned_schema = VersionedSchema(__table__, 1) |
292 | + |
293 | + def __init__(self, team_id, name): |
294 | + self.team_id = team_id |
295 | + self.name = name |
296 | + |
297 | + def __unicode__(self): |
298 | + return u'<ACMTeamMember %s in %s>' % (self.name, self.team_id) |
299 | + |
300 | + |
301 | +class ACMTeam(Base): |
302 | + __table__ = Table('acmteams', Base.metadata, |
303 | + Column('id', Integer, primary_key=True), |
304 | + Column('name', IbidUnicode(64, case_insensitive=True), unique=True, |
305 | + index=True), useexisting=True) |
306 | + |
307 | + __table__.versioned_schema = VersionedSchema(__table__, 1) |
308 | + |
309 | + uvateams = relation(UVATeam, backref='team', cascade='all,delete') |
310 | + members = relation(ACMTeamMember, backref='team', cascade='all,delete') |
311 | + |
312 | + def __init__(self, name=None): |
313 | + self.name = name |
314 | + |
315 | + def __unicode__(self): |
316 | + return u'<ACMTeam %s>' % self.name |
317 | + |
318 | + |
319 | +help[u'acmteam'] = u'Manage ACM teams by and their members and link them ' \ |
320 | + u'to accounts on various judge sites (currently only UVa is supported).' |
321 | +class ACMTeamManagement(Processor): |
322 | + """create acm team <team_name> |
323 | + delete acm team <team_name> |
324 | + <team_name> is uva team <uva_name> |
325 | + <member_name> is on acm team <team_name> |
326 | + delete <member_name> from acm team <team_name> |
327 | + acm teams |
328 | + acm team <team_name>""" |
329 | + |
330 | + feature = 'acmteam' |
331 | + priority = -10 # Alternate syntax 'remove foo' clashes with auth |
332 | + autoload = False |
333 | + |
334 | + @match(r'^create\s+acm\s+team\s+(.+)$') |
335 | + def create_acmteam(self, event, name): |
336 | + team = event.session.query(ACMTeam) \ |
337 | + .filter_by(name=name) \ |
338 | + .first() |
339 | + if team: |
340 | + event.addresponse(u'I already know about a team called %s', name) |
341 | + return |
342 | + team = ACMTeam(name) |
343 | + event.session.save_or_update(team) |
344 | + event.session.commit() |
345 | + event.addresponse(u'Created ACM team %s', name) |
346 | + |
347 | + @match('^(?:delete|remove)\s+acm\s+team\s+(.+)$') |
348 | + def delete_acmteam(self, event, name): |
349 | + try: |
350 | + team = event.session.query(ACMTeam) \ |
351 | + .filter_by(name=name) \ |
352 | + .one() |
353 | + except InvalidRequestError: |
354 | + event.addresponse(u"Sorry, I don't know the team %s", name) |
355 | + return |
356 | + event.session.delete(team) |
357 | + event.session.commit() |
358 | + event.addresponse(True) |
359 | + |
360 | + @match(r'^(.+)\s+is\s+uva\s+team\s+(.+)$') |
361 | + def link_uvateam(self, event, team_name, uva_name): |
362 | + try: |
363 | + team = event.session.query(ACMTeam) \ |
364 | + .filter_by(name=team_name) \ |
365 | + .one() |
366 | + except InvalidRequestError: |
367 | + event.addresponse(u"Sorry, I don't know the team %s", team_name) |
368 | + return |
369 | + uvateam = event.session.query(UVATeam) \ |
370 | + .filter_by(name=uva_name) \ |
371 | + .first() |
372 | + if uvateam: |
373 | + event.addresponse(u'UVa team %s is already linke to an ACM team', |
374 | + uva_name) |
375 | + return |
376 | + uva = UVATeam(team.id, uva_name) |
377 | + event.session.save_or_update(uva) |
378 | + event.session.commit() |
379 | + event.addresponse(True) |
380 | + |
381 | + @match(r'^(.+)\s+is\s+[io]n\s+acm\s+team\s+(.+)$') |
382 | + def add_acmmember(self, event, member_name, team_name): |
383 | + try: |
384 | + team = event.session.query(ACMTeam) \ |
385 | + .filter_by(name=team_name) \ |
386 | + .one() |
387 | + except InvalidRequestError: |
388 | + event.addresponse(u"Sorry, I don't know the team %s", team_name) |
389 | + return |
390 | + member = ACMTeamMember(team.id, member_name) |
391 | + event.session.save_or_update(member) |
392 | + event.session.commit() |
393 | + event.addresponse(u'Added %(member)s to team %(team)s', { |
394 | + u'member': member_name, |
395 | + u'team': team_name, |
396 | + }) |
397 | + |
398 | + @match(r'^(?:delete|remove)\s+(.+)\s+from\s+acm\s+team\s+(.+)$') |
399 | + def remove_acmmember(self, event, member_name, team_name): |
400 | + member = event.session.query(ACMTeamMember) \ |
401 | + .filter_by(name=member_name) \ |
402 | + .filter(ACMTeam.name==team_name) \ |
403 | + .first() |
404 | + if not member: |
405 | + event.addresponse(u"Sorry, I don't know that person") |
406 | + return |
407 | + event.session.delete(member) |
408 | + event.session.commit() |
409 | + event.addresponse(u'Removed %(member)s from team %(team)s', { |
410 | + u'member': member_name, |
411 | + u'team': team_name, |
412 | + }) |
413 | + |
414 | + @match(r'^(?:list\s+)?acm\s+teams$') |
415 | + def list_acmteams(self, event): |
416 | + teams = event.session.query(ACMTeam).all() |
417 | + if teams: |
418 | + event.addresponse(u'ACM teams: %s', |
419 | + human_join(sorted(team.name for team in teams))) |
420 | + else: |
421 | + event.addresponse(u'No ACM teams registered with me') |
422 | + |
423 | + @match(r'^acm\s+team\s+(.+)$') |
424 | + def acmteam_info(self, event, name): |
425 | + try: |
426 | + team = event.session.query(ACMTeam) \ |
427 | + .filter_by(name=name) \ |
428 | + .one() |
429 | + except InvalidRequestError: |
430 | + event.addresponse(u"Sorry, I don't know the team %s", name) |
431 | + return |
432 | + uva_teams = [t.name for t in team.uvateams] |
433 | + if uva_teams: |
434 | + uva_teams = u', and is known as %s on UVa' % human_join(uva_teams) |
435 | + else: |
436 | + uva_teams = u'' |
437 | + members = [m.name for m in team.members] |
438 | + if members: |
439 | + event.addresponse(u'%(team_name)s has %(plural)s ' |
440 | + '%(team_members)s%(uva_teams)s', { |
441 | + u'team_name': name, |
442 | + u'team_members': human_join(sorted(m.name for m in team.members)), |
443 | + u'uva_teams': uva_teams, |
444 | + u'plural': plural(len(team.members), 'member', 'members') |
445 | + }) |
446 | + else: |
447 | + event.addresponse(u'%(team_name)s has no team members%(uva_teams)s', { |
448 | + u'team_name': name, |
449 | + u'uva_teams': uva_teams, |
450 | + }) |
451 | + |
452 | + |
453 | +def time_penalty_str(time_penalty): |
454 | + hours = time_penalty // 3600 |
455 | + time_penalty -= hours * 3600 |
456 | + minutes = time_penalty // 60 |
457 | + time_penalty -= minutes * 60 |
458 | + seconds = time_penalty |
459 | + return u'%02d:%02d:%02d' % (hours, minutes, seconds) |
460 | + |
461 | + |
462 | +class ACMProblemScore(object): |
463 | + def __init__(self, correct, submissions, time): |
464 | + self.correct = correct |
465 | + self.submissions = submissions |
466 | + self.time = time |
467 | + |
468 | + def points(self): |
469 | + return int(self.correct) |
470 | + |
471 | + def time_penalty(self): |
472 | + if self.correct: |
473 | + return self.time + 20*60*(self.submissions-1) |
474 | + return 0 |
475 | + |
476 | + def __unicode__(self): |
477 | + if self.correct: |
478 | + time_penalty = time_penalty_str(self.time_penalty()) |
479 | + else: |
480 | + time_penalty = u'--' |
481 | + return u'(%(submissions)d) %(time_penalty)s' % { |
482 | + u'submissions': self.submissions, |
483 | + u'time_penalty': time_penalty |
484 | + } |
485 | + |
486 | + |
487 | +class ACMScore(object): |
488 | + def __init__(self, problem_scores): |
489 | + self.problem_scores = problem_scores |
490 | + |
491 | + def points(self): |
492 | + return sum(s.points() for s in self.problem_scores.itervalues()) |
493 | + |
494 | + def time_penalty(self): |
495 | + return sum(s.time_penalty() for s in self.problem_scores.itervalues()) |
496 | + |
497 | + def __cmp__(self, b): |
498 | + if self.points() != b.points(): |
499 | + return -(self.points() - b.points()) |
500 | + return self.time_penalty() - b.time_penalty() |
501 | + |
502 | + def __unicode__(self): |
503 | + return u'%(problem_scores)s %(points)d %(time_penalty)s' % { |
504 | + u'problem_scores': u' '.join(str(s) for p, s in |
505 | + sorted(self.problem_scores.iteritems())), |
506 | + u'points': self.points(), |
507 | + u'time_penalty': time_penalty_str(self.time_penalty()), |
508 | + } |
509 | + |
510 | + |
511 | +class Contest(object): |
512 | + def __init__(self, id, name, start, end, dayfirst=False, yearfirst=False): |
513 | + self.id = int(id) |
514 | + self.name = name |
515 | + self.start = parse(start) |
516 | + self.end = parse(end) |
517 | + |
518 | + def duration(self): |
519 | + time = relativedelta(self.end, self.start) |
520 | + |
521 | + if time.days == 0: |
522 | + days = u'' |
523 | + else: |
524 | + days = u'%d %s ' % (time.days, plural(time.days, u'day', u'days')) |
525 | + |
526 | + if time.hours == 0: |
527 | + hours = u'' |
528 | + else: |
529 | + hours = u'%d %s ' % (time.hours, |
530 | + plural(time.hours, u'hour', u'hours')) |
531 | + |
532 | + if time.minutes == 0: |
533 | + minutes = u'' |
534 | + else: |
535 | + minutes = u'%d %s ' % (time.minutes, |
536 | + plural(time.minutes, u'minute', u'minutes')) |
537 | + |
538 | + return (u'%s%s%s' % (days, hours, minutes)).strip() |
539 | + |
540 | + def __unicode__(self): |
541 | + return u'%(name)s (%(id)d) starts %(start)s and lasts %(duration)s' % { |
542 | + u'name': self.name, |
543 | + u'id': self.id, |
544 | + u'start': format_date(self.start), |
545 | + u'duration': self.duration(), |
546 | + } |
547 | + |
548 | + def __cmp__(self, other): |
549 | + return mktime(self.start.timetuple()) - mktime(other.start.timetuple()) |
550 | + |
551 | + |
552 | +def ranking(scores, team): |
553 | + for position, (t, score) in enumerate(sorted( |
554 | + scores.iteritems(), key=lambda (k,v): (v,k))): |
555 | + if t == team: |
556 | + return position + 1 |
557 | + |
558 | + |
559 | +def human_ranking(ranking): |
560 | + if ranking % 10 == 1 and ranking % 100 != 11: |
561 | + return u'%dst' % ranking |
562 | + if ranking % 10 == 2 and ranking % 100 != 12: |
563 | + return u'%dnd' % ranking |
564 | + if ranking % 10 == 3 and ranking % 100 != 13: |
565 | + return u'%drd' % ranking |
566 | + return u'%dth' % ranking |
567 | + |
568 | + |
569 | +help[u'uva'] = u'Monitor the scoreboard of an ACM contest hosted on the UVa ' \ |
570 | + u'online judge. To add teams to monitor, first use the "acmteam" ' \ |
571 | + u'feature to create the teams and link them to UVa accounts. The ' \ |
572 | + u'<contest_id> option is the contest=? parameter in the UVa contest ' \ |
573 | + u'you would like to monitor.' |
574 | +class Uva(Processor): |
575 | + """[past|running|coming|all] uva contests |
576 | + [previous|next] uva contest |
577 | + uva countdown [<contest_id>] |
578 | + uva scoreboard [<contest_id>] |
579 | + monitor uva |
580 | + stop monitoring uva""" |
581 | + |
582 | + feature = u'uva' |
583 | + autoload = False |
584 | + |
585 | + scores_url = u'http://uva.onlinejudge.org/index2.php?option=' \ |
586 | + u'com_onlinejudge&Itemid=13&page=show_contest_standings&contest=%s&fullscreen=1' |
587 | + uva_monitor_period = IntOption(u'uva_monitor_period', |
588 | + u'Delay period between monitor calls', 60) |
589 | + |
590 | + teams = {} |
591 | + monitors = [] |
592 | + |
593 | + def _get_contests(self, type='all'): |
594 | + if type == 'all': |
595 | + contests = [] |
596 | + for t in ('past', 'running', 'coming'): |
597 | + contests.extend(self._get_contests(t)) |
598 | + return contests |
599 | + elif type == 'past': |
600 | + item_id = 13 |
601 | + elif type == 'running': |
602 | + item_id = 12 |
603 | + elif type == 'coming': |
604 | + item_id = 11 |
605 | + else: |
606 | + raise Exception(u'Unexpected contest type: %s' % type) |
607 | + |
608 | + url = u'http://uva.onlinejudge.org/index.php?option=' \ |
609 | + u'com_onlinejudge&Itemid=%s' % item_id |
610 | + etree = get_html_parse_tree(url, treetype=u'etree') |
611 | + |
612 | + contests = [] |
613 | + for tr in etree.findall(u'body/div/div/div/div/div/div/table/tbody/tr'): |
614 | + if tr.get(u'class') in (u'sectiontableentry1', u'sectiontableentry2'): |
615 | + tds = [td for td in tr.getiterator(u'td')] |
616 | + id = tds[0].text |
617 | + name = tds[2].find(u'a').text |
618 | + start = tds[3].text |
619 | + end = tds[4].text |
620 | + contest = Contest(id, name, start, end, dayfirst=False, yearfirst=True) |
621 | + contests.append(contest) |
622 | + |
623 | + return contests |
624 | + |
625 | + def _get_recent_contest(self): |
626 | + contests = sorted(self._get_contests(u'past')) |
627 | + if contests: |
628 | + return contests[-1] |
629 | + return None |
630 | + |
631 | + def _get_running_contest(self): |
632 | + contests = sorted(self._get_contests(u'running')) |
633 | + if contests: |
634 | + return contests[0] |
635 | + return None |
636 | + |
637 | + def _get_next_contest(self): |
638 | + contests = sorted(self._get_contests(u'coming')) |
639 | + if contests: |
640 | + return contests[-1] |
641 | + return None |
642 | + |
643 | + def _get_running_or_next_or_recent_contest(self): |
644 | + return self._get_running_contest() or self._get_next_contest() \ |
645 | + or self._get_recent_contest() |
646 | + |
647 | + def _get_running_or_recent_contest(self): |
648 | + return self._get_running_contest() or self._get_recent_contest() |
649 | + |
650 | + @match(r'^(?:(past|running|coming|all|previous|next)\s+)?uva\s+contests?$') |
651 | + def get_contests(self, event, type): |
652 | + if not type: |
653 | + type = u'all' |
654 | + if type == u'previous': |
655 | + contests = [self._get_recent_contest()] |
656 | + elif type == u'next': |
657 | + contests = [self._get_next_contest()] |
658 | + else: |
659 | + contests = self._get_contests(type) |
660 | + if type == u'all': |
661 | + type = u'' |
662 | + else: |
663 | + type = '%s ' % type |
664 | + if contests and contests[0]: |
665 | + event.addresponse(u'%(type)sUVa %(contest_s)s: %(contests)s', { |
666 | + u'type': type.title(), |
667 | + u'contest_s': plural(len(contests), u'contest', u'contests'), |
668 | + u'contests': human_join(map(unicode, contests)), |
669 | + }) |
670 | + else: |
671 | + event.addresponse(u'No %(type)sUVa %(contest_s)s', { |
672 | + u'type': type, |
673 | + u'contest_s': plural((type not in (u'previous', 'next')) * 2, |
674 | + u'contest', u'contests'), |
675 | + }) |
676 | + |
677 | + def _get_scores(self, event, contest_id): |
678 | + url = self.scores_url % contest_id |
679 | + etree = get_html_parse_tree(url, treetype=u'etree') |
680 | + |
681 | + teams = {} |
682 | + for team in event.session.query(UVATeam).all(): |
683 | + teams[team.name] = u'%(name)s (%(members)s)' % { |
684 | + u'name': team.team.name, |
685 | + u'members': human_join(m.name for m in team.team.members), |
686 | + } |
687 | + |
688 | + scores = {} |
689 | + header = 0 |
690 | + for tr in etree.getiterator(u'tr'): |
691 | + tds = [t for t in tr.getiterator(u'td')] |
692 | + if header == 0: |
693 | + header += 1 |
694 | + continue |
695 | + if header == 1: |
696 | + problems = [t.text.strip() for t in tds[2:-2]] |
697 | + header += 1 |
698 | + continue |
699 | + team = tds[1].text |
700 | + if team in teams.keys(): |
701 | + team_scores = {} |
702 | + for i, problem in enumerate(problems): |
703 | + matches = re.search(r'(\d+):(\d+):(\d+)\s*\((\d+)\)', tds[i+2].text) |
704 | + penalty_time = (int(matches.group(1)) * 60 + \ |
705 | + int(matches.group(2))) * 60 + int(matches.group(3)) |
706 | + team_scores[problem] = ACMProblemScore(penalty_time > 0, |
707 | + int(matches.group(4)) + int(penalty_time > 0), penalty_time) |
708 | + scores[teams[team]] = ACMScore(team_scores) |
709 | + return (problems, scores) |
710 | + |
711 | + @match(r'^uva\s+(remaining\s+time|time\s+remaining|countdown)(?:\s+(\d+))?$') |
712 | + def get_remaining_time(self, event, phrase, contest_id): |
713 | + if contest_id is None: |
714 | + if u'remaining' in phrase: |
715 | + contest_id = self._get_running_or_recent_contest().id |
716 | + else: |
717 | + contest_id = self._get_running_or_next_or_recent_contest().id |
718 | + url = u'http://uva.onlinejudge.org/index2.php?option=com_onlinejudge&' \ |
719 | + u'Itemid=12&page=show_contest_statistics&contest=%s&fullscreen=1' \ |
720 | + % contest_id |
721 | + etree = get_html_parse_tree(url, treetype='etree') |
722 | + time_elapsed = [h for h in etree.findall( |
723 | + u'body/table/tbody/tr/td/div/h1')][0].text.strip() |
724 | + time_remaining = [h for h in etree.findall( |
725 | + u'body/table/tbody/tr/td/div/h1')][2].text.strip() |
726 | + time_remaining = re.sub(r'\b(\d)\b', r'0\1', time_remaining) |
727 | + if '-' in time_elapsed: |
728 | + event.addresponse(u'%s until contest begins', |
729 | + time_elapsed.replace(u'-', u'')) |
730 | + elif time_remaining == '00:00:00': |
731 | + event.addresponse(u'The contest is over') |
732 | + else: |
733 | + event.addresponse(u'%s time remaining', time_remaining) |
734 | + |
735 | + @match(r'^uva\s+scoreboard(?:\s+(\d+))?$') |
736 | + def get_scoreboard(self, event, contest_id): |
737 | + if contest_id is None: |
738 | + contest_id = self._get_running_or_recent_contest().id |
739 | + problems, scores = self._get_scores(event, contest_id) |
740 | + response = [] |
741 | + |
742 | + for position, (team, score) in enumerate(sorted( |
743 | + scores.iteritems(), key=lambda (k,v): (v,k))): |
744 | + response.append(u'%(position)d. %(team)s: %(score)s' % { |
745 | + u'position': position+1, |
746 | + u'team': team, |
747 | + u'score': str(score), |
748 | + }) |
749 | + |
750 | + response.append(u'Full scoreboard: %s' % (self.scores_url % contest_id)) |
751 | + event.addresponse(u'\n'.join(response), conflate=False) |
752 | + |
753 | + @periodic(config_key='uva_monitor_period', initial_delay=5) |
754 | + def monitor(self, event): |
755 | + for source, channel in self.monitors: |
756 | + contest = self._get_running_contest() |
757 | + if contest is None: |
758 | + self.scores = {} |
759 | + continue |
760 | + problems, scores = self._get_scores(event, contest.id) |
761 | + response = [] |
762 | + for team in scores.keys(): |
763 | + if not self.scores.has_key(team): |
764 | + score = {} |
765 | + for id, problem in len(scores[team][0]): |
766 | + score[problem] = ACMProblemScore(False, 0, 0) |
767 | + self.scores[team] = ACMScore(score) |
768 | + for problem in sorted(problems): |
769 | + if scores[team].problem_scores[problem].correct and \ |
770 | + not self.scores[team].problem_scores[problem].correct: |
771 | + prev_ranking = ranking(self.scores, team) |
772 | + new_ranking = ranking(scores, team) |
773 | + if prev_ranking == new_ranking: |
774 | + ranking_change = u'remaining in %s' % \ |
775 | + human_ranking(new_ranking) |
776 | + else: |
777 | + ranking_change = u'moving from %(prev)s to %(new)s' % { |
778 | + u'prev': human_ranking(prev_ranking), |
779 | + u'new': human_ranking(new_ranking), |
780 | + } |
781 | + response.append(u'%(team)s has solved problem %(problem)s, ' |
782 | + u'%(ranking_change)s with %(points)d problems in ' |
783 | + u'%(time_penalty)s' % { |
784 | + u'team': team, |
785 | + u'problem': problem, |
786 | + u'ranking_change': ranking_change, |
787 | + u'points': scores[team].points(), |
788 | + u'time_penalty': time_penalty_str(scores[team].time_penalty()), |
789 | + }) |
790 | + elif not scores[team].problem_scores[problem].correct and \ |
791 | + self.scores[team].problem_scores[problem].correct: |
792 | + response.append(u"%(team)s's previously correct submission " |
793 | + u"for problem %(problem)s has been rejudged as incorrect!" % { |
794 | + u'team': team, |
795 | + u'problem': problem, |
796 | + }) |
797 | + elif scores[team].problem_scores[problem].submissions > \ |
798 | + self.scores[team].problem_scores[problem].submissions: |
799 | + num = scores[team].problem_scores[problem].submissions - \ |
800 | + self.scores[team].problem_scores[problem].submissions |
801 | + total_wrong = scores[team].problem_scores[problem].submissions |
802 | + if num > 1: |
803 | + number = str(num) |
804 | + else: |
805 | + number = u'a' |
806 | + if total_wrong == num: |
807 | + overall = u'their first incorrect %s for the problem' % \ |
808 | + plural(num, u'submission', u'submissions') |
809 | + else: |
810 | + overall = u'bringing them to %(total_wrong)d incorrect ' \ |
811 | + '%(plural)s for the problem' % { |
812 | + u'total_wrong': total_wrong, |
813 | + u'plural': plural(total_wrong, u'submission', u'submissions'), |
814 | + } |
815 | + response.append(u'%(team)s has submitted %(number)s new incorrect ' |
816 | + u'%(plural)s for problem %(problem)s, %(overall)s' % { |
817 | + u'team': team, |
818 | + u'problem': problem, |
819 | + u'number': number, |
820 | + u'plural': plural(num, u'submission', u'submissions'), |
821 | + u'overall': overall, |
822 | + }) |
823 | + self.scores = scores |
824 | + if response: |
825 | + event.addresponse(u'\n'.join(response), conflate=False, |
826 | + source=source, target=channel) |
827 | + |
828 | + @match(r'^(?:monitor\s+uva|uva\s+monitor)$') |
829 | + def start_monitor(self, event): |
830 | + contest = self._get_running_contest() |
831 | + if contest is None: |
832 | + self.scores = {} |
833 | + else: |
834 | + problems, self.scores = self._get_scores(event, contest.id) |
835 | + if (event.source, event.channel) in self.monitors: |
836 | + event.addresponse(u"Sorry, I'm already monitoring the scoreboard") |
837 | + return |
838 | + self.monitors.append((event.source, event.channel)) |
839 | + event.addresponse(u"Okay, I'll monitor the scoreboard for you") |
840 | + if self.monitor.lock.acquire(0): |
841 | + self.monitor(event) |
842 | + self.monitor.lock.release() |
843 | + |
844 | + @match(r'^(?:stop\s+monitoring\s+uva|uva\s+stop\s+monitor)$') |
845 | + def stop_monitor(self, event): |
846 | + if (event.source, event.channel) not in self.monitors: |
847 | + event.addresponse(u'I never even started monitoring the scoreboard') |
848 | + return |
849 | + self.monitors.remove((event.source, event.channel)) |
850 | + event.addresponse(u'Okay, no longer monitoring the scoreboard') |
851 | + |
852 | |
853 | # vi: set et sta sw=4 ts=4: |
Add UVa support to codecontest plugin. UVa run ACM ICPC-style programming contests. I hope to support ACM ICPC-style judges in the future, so I've tried to separate out some of the non-UVa-specific bits for now.