-
Notifications
You must be signed in to change notification settings - Fork 0
/
dash.py
374 lines (323 loc) · 14.2 KB
/
dash.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
from scraper import Scraper
import csv, fileinput, cmd, sys, json, os
import threading, time
import urllib.request
backgroundUpdate = True
loaded_config = {}
class Contestant(object):
contestantCache = {}
class MoveException(Exception):
def __init__(self, collisionUsername):
self.collidesWith = collisionUsername
def __init__(self, room, team, checkin, hackerrank, name1, name2,
completedProblems, currentBalloons):
self.room = room
self.team = team
self.checkin = checkin
self.hackerRank = hackerrank
self.partnerOne = name1
self.partnerTwo = name2
self.completedProblems = completedProblems
self.currentBalloons = currentBalloons
def get_dict(self):
return {
'Room #' : self.room,
'Team #' : self.team,
'Check-In':self.checkin,
'HackerRank': self.hackerRank,
'Partner 1 Name':self.partnerOne,
'Partner 2 Name':self.partnerTwo,
'completedProblems': self.completedProblems,
'currentBalloons': self.currentBalloons,
}
def __str__(self):
return "{}: Room {}, team {}. Completed {} problems".format(self.hackerRank,
self.room, self.team, self.completedProblems)
@classmethod
def get_contestant_count(cls):
return len(cls.contestantCache)
@classmethod
def get_fieldnames(cls):
return ['Room #', 'Team #', 'Check-In', 'HackerRank', 'Partner 1 Name',
'Partner 2 Name', 'completedProblems', 'currentBalloons']
@classmethod
def add_to_cache(cls, c):
if c.hackerRank == "":
raise ValueError('HackerRank Username cannot be blank')
cls.contestantCache[c.hackerRank] = c
@classmethod
def get_from_cache(cls, hackerrank):
return cls.contestantCache[hackerrank]
@classmethod
def get_all_contestants_iter(cls):
for hackerrankUsername in cls.contestantCache:
yield cls.contestantCache[hackerrankUsername]
@classmethod
def get_by_team_number(cls, team):
for hackerrankUsername in cls.contestantCache:
if cls.contestantCache[hackerrankUsername].team == team:
return cls.contestantCache[hackerrankUsername]
raise ValueError('Invalid team')
@classmethod
def move_team(cls, username, newTeam):
try:
currentResident = cls.get_by_team_number(newTeam)
raise cls.MoveException(currentResident.hackerRank)
except ValueError:
contestant = cls.get_from_cache(username)
contestant.team = newTeam
cls.add_to_cache(contestant)
@classmethod
def rename_team(cls, username, newUsername):
try:
currentResident = cls.get_from_cache(newUsername)
raise cls.MoveException(newUsername)
except KeyError:
try:
contestant = cls.get_from_cache(username)
contestant.hackerRank = newUsername
cls.add_to_cache(contestant)
del cls.contestantCache[username]
except KeyError:
raise KeyError from None
@classmethod
def update_completed_count(cls, username, completedCount):
contestant = cls.get_from_cache(username)
contestant.completedProblems = completedCount
cls.add_to_cache(contestant)
def performUpdate():
scr = Scraper()
try:
for competitorListChunk in scr.scrape():
for competitor in competitorListChunk:
try:
Contestant.update_completed_count(competitor.username.lower(),
competitor.completedCount)
except Exception as e:
print("ERR: Username most likely not found in spreadsheet {}. {}".format(
competitor.username, str(e)))
except Exception:
return
def save_all():
with open('dash.save', 'w+') as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=Contestant.get_fieldnames())
writer.writeheader()
for contestant in Contestant.get_all_contestants_iter():
writer.writerow(contestant.get_dict())
def send_balloon_update(contestant, missing_balloons):
global loaded_config
api_url = loaded_config['api_url']
push_url = os.path.join(api_url, 'balloonpush.php')
push_url = '{}?year=fa16&team={}&room={}&balloons={}'.format(
push_url, contestant.team, contestant.room, missing_balloons)
urllib.request.urlopen(push_url).read()
def update(parallel):
if parallel:
global backgroundUpdate
while backgroundUpdate:
performUpdate()
for competitor, missing_balloons in get_needed_balloons_and_ack():
send_balloon_update(competitor, missing_balloons)
save_all()
time.sleep(10)
else: performUpdate()
def update_last_cmd(f):
""" A decorator that makes a Cmd command write its name to the Cmd command
history """
def decorated(self,line):
rv = f(self,line)
self.lastCommand = f.__name__
return rv
decorated.__doc__ = f.__doc__
return decorated
def get_missing_balloons(contestant):
global loaded_config
balloon_list = loaded_config['balloon_on']
needed_balloons = len([req for req in balloon_list if req <= contestant.completedProblems])
if contestant.currentBalloons < needed_balloons:
return needed_balloons - contestant.currentBalloons
else:
return 0
def get_needed_balloons_and_ack(contestantList=None):
global loaded_config
contestantList = contestantList or Contestant.get_all_contestants_iter()
balloon_list = loaded_config['balloon_on']
for contestant in contestantList:
missing_balloons = get_missing_balloons(contestant)
if missing_balloons > 0:
yield (contestant, missing_balloons)
contestant.currentBalloons += missing_balloons
def query_contestants(numContestants, printQuery=True, contestantList=None, check=True):
iters = 0
iteratingItem = contestantList if not contestantList is None else Contestant.get_all_contestants_iter()
lastContestant = None
for contestant in iteratingItem:
if iters >= numContestants:
return lastContestant
missing_balloons = get_missing_balloons(contestant)
if (check and missing_balloons != 0) or not check:
if printQuery:
print("{} (Room {} Team {}) has completed {} problems. Missing {} balloons".format(
contestant.hackerRank, contestant.room, contestant.team, contestant.completedProblems,
missing_balloons))
lastContestant = contestant
iters += 1
return None
class Dashboard(cmd.Cmd):
def __init__(self, rosterfile):
cmd.Cmd.__init__(self)
self.prompt = "dash> "
self.rosterFilePath = rosterfile
self.lastCommand = ""
self.lastResult = ""
@update_last_cmd
def do_qb(self, line):
"""Balloon the competitor printed in the previous "queryone" result"""
if self.lastCommand == "do_queryone":
if not self.lastResult is None:
self.do_balloon(self.lastResult.hackerRank)
else:
print("Cannot perform qb when last command not queryone")
@update_last_cmd
def do_load(self, line):
""" Loads the provided roster file into memory. """
with open(self.rosterFilePath, 'r') as csvFile:
rosterReader = csv.DictReader(csvFile)
for row in rosterReader:
completedProblems = row['completedProblems'] if 'completedProblems' in row else 0
currentBalloons = row['currentBalloons'] if 'currentBalloons' in row else 0
contestant = Contestant(row['Room #'], row['Team #'],
row['Check-In'], row['HackerRank'].lower(),
row['Partner 1 Name'], row['Partner 2 Name'],
completedProblems, currentBalloons)
try:
Contestant.add_to_cache(contestant)
except Exception as e:
continue
@update_last_cmd
def do_list(self, line):
""" Lists all contestants if none specified. Contestants to explicitly
list can be passed via "list [contestantHackerrank] [contestantHackerrank] """
if line.split(' ')[0] == "":
for contestant in Contestant.get_all_contestants_iter():
print(contestant)
elif line.split(' ')[0] == '*' and len(line.split(' ')) > 1:
for contestant in Contestant.get_all_contestants_iter():
if contestant.completedProblems == line.split(' ')[1]:
print(contestant)
else:
for hrUser in line.split(' ')[0]:
try:
print(Contestant.get_from_cache(hrUser))
except Exception:
print('Invalid Username: {}'.format(hrUser))
@update_last_cmd
def do_move(self, line):
""" Moves the team number for the specified username to the specified number.
Example: "move <username> 20" """
try:
contestant = Contestant.get_from_cache(line.split(' ')[0])
except Exception:
print('Invalid Username: {}'.format(line.split(' ')[0]))
try:
Contestant.move_team(line.split(' ')[0], line.split(' ')[1])
except Contestant.MoveException as e:
print("Cannot move contestant - {} already sitting there".format(
e.collidesWith))
@update_last_cmd
def do_rename(self, line):
""" Renames the team from the first username to the second """
try:
contestant = Contestant.get_from_cache(line.split(' ')[0])
except Exception:
print('Invalid Username: {}'.format(line.split(' ')[0]))
return
try:
Contestant.rename_team(line.split(' ')[0], line.split(' ')[1])
except Contestant.MoveException as e:
print("Cannot rename contestant - {} already named this".format(
e.collidesWith))
except ValueError:
print("Invalid User: {}".format(line.split(' ')[0]))
except KeyError:
print("Invalid User: {}".format(line.split(' ')[0]))
@update_last_cmd
def do_update(self, line):
""" Spawns a hackerrank scraper to update all contestants """
update(False)
@update_last_cmd
def do_updateback(self,line):
""" Issues updating in the background. WARNING: Opens a new web browser
instance for every iteration """
global backgroundUpdate
backgroundUpdate = True
def update_parallel(): update(True)
s = threading.Thread(target=update_parallel)
s.start()
@update_last_cmd
def do_stopupdateback(self,line):
""" Stop any background udpating """
global backgroundUpdate
backgroundUpdate = False
@update_last_cmd
def do_query(self, line):
""" Lists the competitors who have not received a balloon for their
completed problems. Contestants to explicitly list can be passed via
"query [contestantHackerrank] [contestantHackerrank]" """
if line == "":
query_contestants(Contestant.get_contestant_count())
else:
contestantList = []
for hrUser in line.split(' '):
try:
contestant = Contestant.get_from_cache(hrUser)
contestantList.append(contestant)
except Exception:
print('Invalid Username: {}'.format(hrUser))
query_contestants(len(contestantList), contestantList = contestantList, check=False)
@update_last_cmd
def do_queryone(self, line):
""" Lists a single competitor who has not received a balloon for their
completed problems """
queried = query_contestants(1)
self.lastResult = queried
@update_last_cmd
def do_balloon(self, line):
""" Declares that the provided contestant has had their balloon status
updated """
try:
contestant = Contestant.get_from_cache(line)
contestant.currentBalloons += 1
Contestant.add_to_cache(contestant)
except Exception:
print("Invalid Username: {}".format(line))
@update_last_cmd
def do_save(self, line):
""" Saves the current information into a local file. The dash session
can then be reopened by using the local file as a commandline arg """
save_all()
@update_last_cmd
def do_EOF(self, line):
""" Quit """
return True
if __name__ == '__main__':
intro = """
############################################################################
# University of California, San Diego
# Women in Computing, Beginners' Programming Competition
# Balloon Dash
# https://github.com/ucsd-wic-bpc/balloon-dash
# 03 March 2016
############################################################################
██╗ ██╗██╗ ██████╗ ██████╗ ██████╗ ██████╗
██║ ██║██║██╔════╝ ██╔══██╗██╔══██╗██╔════╝
██║ █╗ ██║██║██║ █████╗ ██████╔╝██████╔╝██║
██║███╗██║██║██║ ╚════╝ ██╔══██╗██╔═══╝ ██║
╚███╔███╔╝██║╚██████╗ ██████╔╝██║ ╚██████╗
╚══╝╚══╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
Please remember to frequently save your data. Type "help" or "?" for info
"""
with open('config.json', 'r') as configFile:
loaded_config = json.loads(configFile.read())
if len(sys.argv) < 2: sys.argv.append("")
Dashboard(sys.argv[1]).cmdloop(intro)