Skip to content

Commit ee2ad12

Browse files
authored
Add Progress Chart!
- Generate a chart to provide a visual representation of a scout's progress. - Renamed image files that the reports will use, if available.
1 parent a56a43c commit ee2ad12

File tree

2 files changed

+150
-57
lines changed

2 files changed

+150
-57
lines changed

README.md

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,20 @@ The tool comes in the form of a windows executable or a python script. While I e
66
See the [releases section](https://github.com/sgreasby/Scout-Progress-Report/releases/) for the most recent stable version of the tool.
77

88
# Requirements
9+
Before using the tool, log into Scoutbook and export troop advancement data.
10+
See "Optional Steps to Make Output More Visually Pleasing" section below for steps to make the output look even better.
11+
912
## Windows Executable
10-
None. Just download the .exe file.
13+
No other requirements. Just download the .exe file.
1114
## Python Script
12-
The script has been written for python 3.11 and utilizes the pandas and psutil modules.
15+
The script has been written for python 3.11 and utilizes the pandas, psutil, dominate, and matplotlib modules.
1316

1417
Before running the script for the first time, ensure python 3.11 or later is installed on your computer and install the requried modules by typing the following from the command line:
1518

16-
`pip install pandas`
17-
`pip install psutil`
18-
`pip install dominate`
19+
`pip install pandas`
20+
`pip install psutil`
21+
`pip install dominate`
22+
`pip install matplotlib`
1923

2024
# Usage
2125

@@ -29,7 +33,7 @@ To lanuch from windows drag the desired CSV file and drop it onto the progress.p
2933

3034
To launch from the command line type the following, where {} denotes optional portions and [] denotes portions to be specified by the user.
3135

32-
`python progress.py {--date=[MM/DD/YYYY]} {--cubs} [scoutbook.csv]`
36+
`python progress.py {--date=[MM/DD/YYYY]} {--cubs} {--clean} [scoutbook.csv]`
3337

3438
### Required Arguments
3539
#### [scoutbook.csv]
@@ -45,24 +49,26 @@ If omitted, a default style will be applied.
4549
#### --cubs
4650
Indicates that the progress report is generated for Cub Scouts. When this flag is set, the script will not display progress towards eagle, and will indicate Cub Scout and Webelos specific advancement and awards.
4751
If omitted, only Scouts BSA advancement and awards will be displayed.
52+
#### --clean
53+
Indicates that the old output should be cleaned (i.e. deleted) before generating new output. If this is not specified then old files will be not be deleted, however newly generated files may overwrite the old files.
4854

4955
### Optional Steps to Make Output More Visually Pleasing
5056
Defining your own CSS file will allow you to override the styles defined in the script and customize the look and layout of the progress reports.
5157

5258
If an "img" folder exists in the folder where the script/executable is located then that the img folder will be copied into the output folder and the generated reports will reference images found in that folder. The reports should still render properly even if those files are missing. Specific file names are expected. Those file names are:
53-
- unitlogo.jpg: Background image for all reports
54-
- bobcat.jpg: Bobcat rank emblem
55-
- lion.jpg: Lion rank emblem
56-
- tiger.jpg: Tiger rank emblem
57-
- wolf.jpg: Wolf rank emblem
58-
- bear.jpg: Bear rank emblem
59-
- webelos.jpg: Webelos rank emblem
60-
- arrowoflight.jpg: Arrow of Light rank emblem
61-
- scout.jpg: Scout rank emblem
62-
- tenderfoot.jpg: Tenderfoot rank emblem
63-
- secondclass.jpg: Second Class rank emblem
64-
- firstclass.jpg: First Class rank emblem
65-
- star.jpg: Star Scout rank emblem
66-
- life.jpg: Life Scout rank emblem
67-
- eagle.jpg: Eagle Scout rank emblem
59+
- **background.jpg:** Background image for all reports
60+
- **emblem_bobcat.jpg:** Bobcat rank emblem
61+
- **emblem_lion.jpg:** Lion rank emblem
62+
- **emblem_tiger.jpg:** Tiger rank emblem
63+
- **emblem_wolf.jpg:** Wolf rank emblem
64+
- **emblem_bear.jpg:** Bear rank emblem
65+
- **emblem_webelos.jpg:** Webelos rank emblem
66+
- **emblem_arrow_of_light.jpg:** Arrow of Light rank emblem
67+
- **emblem_scout.jpg:** Scout rank emblem
68+
- **emblem_tenderfoot.jpg:** Tenderfoot rank emblem
69+
- **emblem_second_class.jpg:** Second Class rank emblem
70+
- **emblem_first_class.jpg:** First Class rank emblem
71+
- **emblem_star_scout.jpg:** Star Scout rank emblem
72+
- **emblem_life_scout.jpg:** Life Scout rank emblem
73+
- **emblem_eagle.jpg:** Eagle Scout rank emblem
6874

progress.py

Lines changed: 123 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@
2929
from dominate.tags import *
3030
except:
3131
print("type \"pip install dominate\" from the command line then try again")
32+
try:
33+
import matplotlib.pyplot as plt
34+
except:
35+
print("type \"pip install matplotlib\" from the command line then try again")
36+
3237

3338
# While not needed to run the script, autopytoexe is used to build .exe releases
3439

@@ -135,7 +140,7 @@
135140
}
136141
137142
body {
138-
background-image: url('img/unitlogo.jpg');
143+
background-image: url('img/background.jpg');
139144
background-size: contain;
140145
-webkit-background-size: contain;
141146
-moz-background-size: contain;
@@ -154,10 +159,14 @@
154159
.rank {
155160
display: inline-block;
156161
}
157-
.rank_logo {
162+
.emblem {
158163
height: 30px;
159164
padding: 0px;
160165
}
166+
.rank_chart {
167+
height: 200px;
168+
padding: 0px;
169+
}
161170
hr {
162171
border: 1px solid black;
163172
margin-left: 0px;
@@ -180,20 +189,20 @@
180189
['Personal Management'],
181190
['Citizenship in Society']]
182191

183-
rankfile = {'Bobcat' : 'bobcat.jpg',
184-
'Lion' : 'lion.jpg',
185-
'Tiger' : 'tiger.jpg',
186-
'Wolf' : 'wolf.jpg',
187-
'Bear' : 'bear.jpg',
188-
'Webelos' : 'webelos.jpg',
189-
'Arrow of Light' : 'arrowoflight.jpg',
190-
'Scout' : 'scout.jpg',
191-
'Tenderfoot' : 'tenderfoot.jpg',
192-
'Second Class' : 'secondclass.jpg',
193-
'First Class' : 'firstclass.jpg',
194-
'Star Scout' : 'star.jpg',
195-
'Life Scout' : 'life.jpg',
196-
'Eagle Scout' : 'eagle.jpg'}
192+
emblemfile = {'Bobcat' : 'emblem_bobcat.jpg',
193+
'Lion' : 'emblem_lion.jpg',
194+
'Tiger' : 'emblem_tiger.jpg',
195+
'Wolf' : 'emblem_wolf.jpg',
196+
'Bear' : 'emblem_bear.jpg',
197+
'Webelos' : 'emblem_webelos.jpg',
198+
'Arrow of Light' : 'emblem_arrow_of_light.jpg',
199+
'Scout' : 'emblem_scout.jpg',
200+
'Tenderfoot' : 'emblem_tenderfoot.jpg',
201+
'Second Class' : 'emblem_second_class.jpg',
202+
'First Class' : 'emblem_first_class.jpg',
203+
'Star Scout' : 'emblem_star_scout.jpg',
204+
'Life Scout' : 'emblem_life_scout.jpg',
205+
'Eagle Scout' : 'emblem_eagle_scout.jpg'}
197206

198207
#TODO:Are these right?
199208
cub_rank_reqs={'Bobcat' :['1','2','3','4','5','6','7'],
@@ -250,6 +259,7 @@
250259
last_review=default_date
251260
cubs=False
252261
stylesheet=None
262+
clean=False
253263

254264
####################################
255265
# Functions
@@ -271,7 +281,7 @@ def usage():
271281
print("Drag CSV file on top of %s icon" %(sys.argv[0]))
272282
input("\nPress any key to continue...")
273283
else:
274-
print("Usage: %s {--date=[MM/DD/YYYY]} {--css=[style.css]} {--cubs} [scoutbook.csv]\n\n" %(sys.argv[0]))
284+
print("Usage: %s {--date=[MM/DD/YYYY]} {--css=[style.css]} {--cubs} {--clean} [scoutbook.csv]\n\n" %(sys.argv[0]))
275285
sys.exit()
276286

277287
def csv_open(csv_file):
@@ -366,6 +376,13 @@ def print_reqs(achievement,recent_reqs,previous_reqs,remaining_reqs):
366376
if response in ['y','Y']:
367377
cubs=True
368378

379+
response='unknown'
380+
while not response in ['y','Y','n','N','']:
381+
response=input("Clean up old files? y/N: ")
382+
383+
if response in ['y','Y']:
384+
clean=True
385+
369386
else:
370387
for arg in sys.argv[1:]:
371388
if arg.startswith('--date='):
@@ -376,6 +393,8 @@ def print_reqs(achievement,recent_reqs,previous_reqs,remaining_reqs):
376393
stylesheet=value
377394
elif arg == '--cubs':
378395
cubs=True
396+
elif arg == '--clean':
397+
clean=True
379398
elif not arg.startswith('--'):
380399
scoutbook,cols=csv_open(arg);
381400
else:
@@ -423,24 +442,27 @@ def print_reqs(achievement,recent_reqs,previous_reqs,remaining_reqs):
423442

424443
print("")
425444

426-
outFiles=[]
427-
import shutil
428-
if os.path.isdir('output'):
429-
try:
430-
shutil.rmtree('output')
431-
except:
432-
error("Unable to delete output folder")
433-
exit()
445+
if clean:
446+
# Delete the old output files and create a new output folder
447+
if os.path.isdir('output'):
448+
try:
449+
shutil.rmtree('output')
450+
except:
451+
error("Unable to delete output folder")
452+
exit()
453+
454+
os.mkdir('output')
434455

435-
os.mkdir('output')
456+
# Always copy new img folder to output (if one exists)
436457
if os.path.isdir('img'):
437458
shutil.copytree('img',os.path.join('output','img'))
459+
438460
os.chdir('output')
439461

440462
namesList=[]
441463
scoutFound=False
442464
for scoutID in scoutIDs:
443-
names={'last':None,'first':None,'file':None,'idle':False}
465+
names={'last':None,'first':None,'html':None,'chart':None,'idle':False}
444466
# Store all data for given scout into a new table
445467
scout_data = scoutbook[(scoutbook['BSA Member ID'] == scoutID)].copy()
446468

@@ -452,10 +474,10 @@ def print_reqs(achievement,recent_reqs,previous_reqs,remaining_reqs):
452474
rankup_date = approved_ranks['Date Completed'].max()
453475
rank = list(approved_ranks['Advancement'].loc[approved_ranks['Date Completed']==rankup_date])
454476
# Some scouts may have multiple ranks recorded on the same date
455-
# The following uses the rankfile dictionary to find the highest rank and select that one
477+
# The following uses the emblemfile dictionary to find the highest rank and select that one
456478
# If no rank was found, the rank is set to No Rank
457479
if len(rank) > 1:
458-
for key in rankfile.keys():
480+
for key in emblemfile.keys():
459481
if key in rank:
460482
rank.remove(key)
461483
break
@@ -467,7 +489,10 @@ def print_reqs(achievement,recent_reqs,previous_reqs,remaining_reqs):
467489
print("Processing %s, %s."%(last_name, first_name))
468490
names['last']=last_name
469491
names['first']=first_name
470-
names['file']="%s_%s.html" % (last_name,first_name)
492+
names['html']="%s_%s.html" % (last_name,first_name)
493+
names['chart']="%s_%s.png" % (last_name,first_name)
494+
names['chart']=os.path.join('img',names['chart'])
495+
471496
doc = dominate.document(title='%s %s Progress Report' % (first_name,last_name))
472497
with doc.head:
473498
# Include link to optional style sheet
@@ -482,9 +507,69 @@ def print_reqs(achievement,recent_reqs,previous_reqs,remaining_reqs):
482507
if not cubs and rank in cub_rank_reqs:
483508
h2("No Rank",cls='rank')
484509
else:
485-
img(src=os.path.join('img',rankfile[rank]), onerror='this.style.display=\'none\'', alt='', cls='rank_logo')
510+
img(src=os.path.join('img',emblemfile[rank]), onerror='this.style.display=\'none\'', alt='', cls='emblem')
486511
h2(" %s (%s)" %(rank,str(rankup_date.date())),cls='rank')
512+
487513
hr()
514+
515+
# Sort all the approved ranks then remove cub ranks to get a list of scouts BSA ranks
516+
bsa_ranks = approved_ranks.sort_values(by='Date Completed')
517+
for key in cub_rank_reqs.keys():
518+
bsa_ranks.drop(bsa_ranks.index[bsa_ranks['Advancement'] == key], inplace=True)
519+
520+
# Create a list of rankup dates in datetime format (now sorted)
521+
bsa_rank_dates = bsa_ranks['Date Completed'].tolist()
522+
523+
# Insert the day before each rankup into the list
524+
# Meanwhile also built up rank counts list
525+
bsa_rank_counts=[]
526+
if len(bsa_rank_dates) > 0:
527+
for i in range(len(bsa_rank_dates)-1,-1,-1):
528+
bsa_rank_dates.insert(i,bsa_rank_dates[i]-pandas.Timedelta(days=1))
529+
bsa_rank_counts=[i,i+1]+bsa_rank_counts
530+
531+
#Add entry for today
532+
bsa_rank_dates += [pandas.to_datetime("today")]
533+
bsa_rank_counts += [bsa_rank_counts[-1]]
534+
else:
535+
#Add entry for today
536+
bsa_rank_dates = [pandas.to_datetime("today")]
537+
bsa_rank_counts = [0]
538+
539+
#Add entry for Jan 1 of the year when the scout earned their first mb or rank
540+
merit_badges = scout_data[(scout_data['Advancement Type'] == 'Merit Badge')]
541+
if merit_badges.shape[0] > 0:
542+
first_mb = merit_badges['Date Completed'].min()
543+
first_adv = min(first_mb,bsa_rank_dates[0])
544+
else:
545+
first_adv = bsa_rank_dates[0]
546+
bsa_rank_dates = [pandas.to_datetime("1/1/%d"%first_adv.year)]+bsa_rank_dates
547+
bsa_rank_counts = [0] + bsa_rank_counts
548+
549+
# Reassmble into a pandas dataframe for plotting
550+
rankups = pandas.DataFrame()
551+
rankups['value'] = bsa_rank_counts
552+
rankups['datetime'] = bsa_rank_dates
553+
rankups.set_index('datetime',inplace=True)
554+
555+
# Plot It!
556+
plt.clf()
557+
ax = plt.axes()
558+
plt.plot(rankups)
559+
ax.yaxis.set_ticks([0,1,2,3,4,5,6,7])
560+
ax.set_yticklabels(['No Rank', 'Scout','Tenderfoot','Second Class','First Class','Star Scout','Life Scout','Eagle Scout'])
561+
plt.xlim(bsa_rank_dates[0],pandas.to_datetime("1/1/%d"%(bsa_rank_dates[0].year+8)))
562+
plt.ylim(0,7)
563+
plt.grid(True)
564+
plt.plot(rankups, color="blue")
565+
plt.gcf().autofmt_xdate()
566+
# Save to file
567+
plt.savefig(names['chart'], bbox_inches='tight', transparent=True)
568+
569+
# Add to html file
570+
img(src=names['chart'], onerror='this.style.display=\'none\'', alt='', cls='rank_chart')
571+
572+
488573
# Remove old requirement for completed ranks
489574
ranks = approved_ranks['Advancement'].unique()
490575
for rank in ranks:
@@ -604,6 +689,8 @@ def print_reqs(achievement,recent_reqs,previous_reqs,remaining_reqs):
604689
p("Eagle MBs = %s/%s (%s in progess)" %(mb_eagle_cnt['Approved'],mb_eagle_cnt['Required'],mb_eagle_cnt['Complete']+mb_eagle_cnt['In Progress']))
605690
p("Elective MBs = %s/%s (%s in progress)" %(mb_elective_cnt['Approved'],mb_elective_cnt['Required'],mb_elective_cnt['Complete']+mb_elective_cnt['In Progress']))
606691

692+
693+
607694
# Search scout record for entried completed since last review
608695
recent_entries = len(scout_data['Date Completed'].loc[scout_data['Date Completed'] > last_review])
609696
if recent_entries == 0:
@@ -808,7 +895,7 @@ def print_reqs(achievement,recent_reqs,previous_reqs,remaining_reqs):
808895
# Print completed reqs
809896
print_reqs(award,new_reqs,prev_reqs,None)
810897

811-
with open(names['file'], 'w') as file:
898+
with open(names['html'], 'w') as file:
812899
file.write(doc.render())
813900
namesList.append(names)
814901

@@ -825,18 +912,18 @@ def print_reqs(achievement,recent_reqs,previous_reqs,remaining_reqs):
825912
with div(cls='toc').add(ol()):
826913
for names in namesList:
827914
if not names['idle']:
828-
li(a("%s %s"%(names['first'],names['last']), href=names['file']))
915+
li(a("%s %s"%(names['first'],names['last']), href=names['html']))
829916
h2("Idle Scouts")
830917
with div(cls='toc').add(ol()):
831918
for names in namesList:
832919
if names['idle']:
833-
li(a("%s %s"%(names['first'],names['last']), href=names['file']))
920+
li(a("%s %s"%(names['first'],names['last']), href=names['html']))
834921

835922
with open('index.html', 'w') as file:
836923
file.write(doc.render())
837924

838925
print("Opening output in browser")
839926
webbrowser.open('file://'+os.path.realpath('index.html'))
840927

841-
#if windows:
842-
# input("\nPress any key to continue...")
928+
if windows:
929+
input("\nPress any key to continue...")

0 commit comments

Comments
 (0)