18
0

2 Commits

17 changed files with 366 additions and 456 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,3 @@
*~ *~
__pycache__ __pycache__
*.pyc config.py

6
README
View File

@@ -1,7 +1,7 @@
Setting up the Discord Bot Setting up the Discord Bot
====================================== ======================================
I run the Discord bot from my laptop. It requires the discord Python I run the Discord boy from my laptop. It requires the discord Python
module available in PyPi and installable like: module available in PyPi and installable like:
$ pip3 install discord $ pip3 install discord
@@ -18,7 +18,7 @@ the steps there, with one important exception:
need to enable both "Privileged Gateway Intents." This allows the bot need to enable both "Privileged Gateway Intents." This allows the bot
to see who is present and active in the channel. to see who is present and active in the channel.
Finally, you need to copy your bot'ss Token (also found on the "Bot" tab) Finally, you need to copy your bot's Token (also found on the "Bot" tab)
into coldcallbot.py. Pass it as the argument to `ccb.run()`. into coldcallbot.py. Pass it as the argument to `ccb.run()`.
@@ -44,7 +44,7 @@ Daily Process
You need to start the bot from the laptop each day. I do that by: You need to start the bot from the laptop each day. I do that by:
$ ./coldcallbot.py $ ./coldcallboy.py
The bot will run in the terminal, print out data as it works including The bot will run in the terminal, print out data as it works including
detailed weights as it goes, and it will record data into files in the detailed weights as it goes, and it will record data into files in the

View File

@@ -1,23 +1,36 @@
## load in the data ## load in the data
################################# #################################
myuw <- read.csv("myuw-COMMLD_570_A_spring_2021_students.csv", stringsAsFactors=FALSE) case.sessions <- 15
myuw <- read.csv("myuw-COM_482_A_autumn_2020_students.csv", stringsAsFactors=FALSE)
## class-level variables ## class-level variables
question.grades <- c("GOOD"=100, "FAIR"=100-(50/3.3), "WEAK"=100-(50/(3.3)*2)) question.grades <- c("GOOD"=100, "FAIR"=100-(50/3.3), "BAD"=100-(50/(3.3)*2))
missed.question.penalty <- (50/3.3) * 0.2 ## 1/5 of a full point on the GPA scale
source("../assessment_and_tracking/track_participation.R") source("../assessment_and_tracking/track_participation.R")
setwd("case_grades") setwd("case_grades")
rownames(d) <- d$unique.name rownames(d) <- d$discord.name
## show the distribution of assessments ## show the distribution of assessments
table(call.list$assessment) table(call.list.full$assessment)
prop.table(table(call.list$assessment)) prop.table(table(call.list.full$assessment))
table(call.list$answered) table(call.list.full$answered)
prop.table(table(call.list$answered)) prop.table(table(call.list.full$answered))
total.questions.asked <- nrow(call.list) total.questions.asked <- nrow(call.list.full)
## create new column with number of questions present
d$prop.asked <- d$num.calls / d$num.present
## generate statistics using these new variables
prop.asks.quantiles <- quantile(d$prop.asked, probs=seq(0,1, 0.01))
prop.asks.quantiles <- prop.asks.quantiles[!duplicated(prop.asks.quantiles)]
## this is generating broken stuff but it's not used for anything
d$prop.asked.quant <- cut(d$prop.asked, breaks=prop.asks.quantiles,
labels=names(prop.asks.quantiles)[1:(length(prop.asks.quantiles)-1)])
## generate grades ## generate grades
########################################################## ##########################################################
@@ -26,47 +39,81 @@ d$part.grade <- NA
## print the median number of questions for (a) everybody and (b) ## print the median number of questions for (a) everybody and (b)
## people that have been present 75% of the time ## people that have been present 75% of the time
median(d$num.calls[d$days.absent < 0.25*case.sessions])
median(d$num.calls) median(d$num.calls)
questions.cutoff <- median(d$num.calls) questions.cutoff <- median(d$num.calls)
## helper function to generate average grade minus number of missing ## helper function to generate average grade minus number of missing
gen.part.grade <- function (x.unique.name) { gen.part.grade <- function (x.discord.name) {
q.scores <- question.grades[call.list$assessment[call.list$unique.name == x.unique.name]] q.scores <- question.grades[call.list$assessment[call.list$discord.name == x.discord.name]]
base.score <- mean(q.scores, na.rm=TRUE) base.score <- mean(q.scores, na.rm=TRUE)
## number of missing days ## number of missing days
# missing.days <- nrow(missing.in.class[missing.in.class$unique.name == x.unique.name,]) missing.days <- nrow(missing.in.class[missing.in.class$discord.name == x.discord.name,])
## return the final score ## return the final score
data.frame(unique.name=x.unique.name, data.frame(discord.name=x.discord.name,
part.grade=(base.score)) part.grade=(base.score - missing.days * missed.question.penalty))
} }
tmp <- do.call("rbind", lapply(d$discord.name[d$num.calls >= questions.cutoff], gen.part.grade))
tmp <- do.call("rbind", lapply(d$unique.name, gen.part.grade)) d[as.character(tmp$discord.name), "part.grade"] <- tmp$part.grade
d[as.character(tmp$unique.name), "part.grade"] <- tmp$part.grade ## next handle the folks *under* the median
## first we handle the zeros
## step 1: first double check the people who have zeros and ensure that they didn't "just" get unlucky"
d[d$num.calls == 0,]
## set those people to 0 :(
d$part.grade[d$num.calls == 0] <- 0
## step 2 is to handle folks who got unlucky in the normal way
tmp <- do.call("rbind", lapply(d$discord.name[is.na(d$part.grade) & d$prop.asked <= median(d$prop.asked)], gen.part.grade))
d[as.character(tmp$discord.name), "part.grade"] <- tmp$part.grade
## the people who are left are lucky and still undercounted so we'll penalize them
d[is.na(d$part.grade),]
penalized.discord.names <- d$discord.name[is.na(d$part.grade)]
## generate the baseline participation grades as per the process above ## generate the baseline participation grades as per the process above
tmp <- do.call("rbind", lapply(penalized.discord.names, gen.part.grade))
d[as.character(tmp$discord.name), "part.grade"] <- tmp$part.grade
## now add "zeros" for every questions that is below the normal
d[as.character(penalized.discord.names),"part.grade"] <- ((
(questions.cutoff - d[as.character(penalized.discord.names),"num.calls"] * 0) +
(d[as.character(penalized.discord.names),"num.calls"] * d[as.character(penalized.discord.names),"part.grade"]) )
/ questions.cutoff)
d[as.character(penalized.discord.names),]
## map part grades back to 4.0 letter scale and points ## map part grades back to 4.0 letter scale and points
d$part.4point <-round((d$part.grade / (50/3.3)) - 2.6, 2) d$part.4point <-round((d$part.grade / (50/3.3)) - 2.6, 2)
d[sort.list(d$part.4point),] d[sort.list(d$prop.asked), c("discord.name", "num.calls", "num.present",
"prop.asked", "prop.asked.quant", "part.grade", "part.4point",
"days.absent")]
d[sort.list(d$part.4point), c("discord.name", "num.calls", "num.present",
"prop.asked", "prop.asked.quant", "part.grade", "part.4point",
"days.absent")]
## writing out data ## writing out data
quantile(d$num.calls, probs=(0:100*0.01))
d.print <- merge(d, myuw[,c("StudentNo", "FirstName", "LastName", "UWNetID")], d.print <- merge(d, myuw[,c("StudentNo", "FirstName", "LastName", "UWNetID")],
by.x="student.num", by.y="StudentNo") by.x="student.num", by.y="StudentNo")
write.csv(d.print, file="final_participation_grades.csv") write.csv(d.print, file="final_participation_grades.csv")
## library(rmarkdown) library(rmarkdown)
## for (x.unique.name in d$unique.name) { for (x.discord.name in d$discord.name) {
## render(input="../../assessment_and_tracking/student_report_template.Rmd", render(input="../../assessment_and_tracking/student_report_template.Rmd",
## output_format="html_document", output_format="html_document",
## output_file=paste("../data/case_grades/student_reports/", output_file=paste("../data/case_grades/student_reports/",
## d.print$UWNetID[d.print$unique.name == x.unique.name], d.print$UWNetID[d.print$discord.name == x.discord.name],
## sep="")) sep=""))
## } }

View File

@@ -1,4 +1,4 @@
myuw <- read.csv("myuw-COMMLD_570_A_spring_2021_students.csv") myuw <- read.csv("myuw-COM_482_A_autumn_2020_students.csv")
gs <- read.delim("student_information.tsv") gs <- read.delim("student_information.tsv")
## these are students who dropped the class (should be empty) ## these are students who dropped the class (should be empty)
@@ -10,14 +10,14 @@ myuw[!myuw$StudentNo %in% gs$Your.UW.student.number,]
## read all the folks who have been called and see who is missing from ## read all the folks who have been called and see who is missing from
## the google sheet ## the google sheet
## call.list <- unlist(lapply(list.files(".", pattern="^attendance-.*tsv$"), function (x) { call.list <- unlist(lapply(list.files(".", pattern="^attendance-.*tsv$"), function (x) {
## d <- read.delim(x) d <- read.delim(x)
## strsplit(d[[2]], ",") strsplit(d[[2]], ",")
## }) })
## ) )
## present <- unique(call.list) present <- unique(call.list)
## present[!present %in% gs[["Your.username.on.the.class.Discord.server"]]] present[!present %in% gs[["Your.username.on.the.class.Discord.server"]]]
## and never attended class.. ## and never attended class..
## gs[["Your.username.on.the.class.Discord.server"]][!gs[["Your.username.on.the.class.Discord.server"]] %in% present] gs[["Your.username.on.the.class.Discord.server"]][!gs[["Your.username.on.the.class.Discord.server"]] %in% present]

View File

@@ -1,25 +1,109 @@
setwd("~/online_communities/coldcallbot/data/")
library(ggplot2) library(ggplot2)
library(data.table) library(data.table)
gs <- read.delim("student_information.tsv") gs <- read.delim("student_information.tsv")
d <- gs[,c(2,4)] d <- gs[,c(2,5)]
colnames(d) <- c("student.num", "unique.name") colnames(d) <- c("student.num", "discord.name")
call.list <- do.call("rbind", lapply(list.files(".", pattern="^call_list-.*tsv$"), function (x) {read.delim(x, stringsAsFactors=FALSE)[,1:4]}))
call.list <- do.call("rbind", lapply(list.files(".", pattern="^call_list-.*tsv$"), function (x) {read.delim(x)[,1:4]}))
colnames(call.list) <- gsub("_", ".", colnames(call.list)) colnames(call.list) <- gsub("_", ".", colnames(call.list))
table(call.list$unique_name[call.list$answered]) call.list$day <- as.Date(call.list$timestamp)
## drop calls where the person wasn't present ## drop calls where the person wasn't present
call.list.full <- call.list call.list.full <- call.list
call.list[!call.list$answered,] call.list[!call.list$answered,]
call.list <- call.list[call.list$answered,] call.list <- call.list[call.list$answered,]
call.counts <- data.frame(table(call.list$unique.name)) call.counts <- data.frame(table(call.list$discord.name))
colnames(call.counts) <- c("unique.name", "num.calls") colnames(call.counts) <- c("discord.name", "num.calls")
d <- merge(d, call.counts, all.x=TRUE, all.y=TRUE, by="unique.name"); d d <- merge(d, call.counts, all.x=TRUE, all.y=TRUE, by="discord.name"); d
## set anything that's missing to zero
d$num.calls[is.na(d$num.calls)] <- 0
attendance <- unlist(lapply(list.files(".", pattern="^attendance-.*tsv$"), function (x) {d <- read.delim(x); strsplit(d[[2]], ",")}))
file.to.attendance.list <- function (x) {
tmp <- read.delim(x)
d.out <- data.frame(discord.name=unlist(strsplit(tmp[[2]], ",")))
d.out$day <- rep(as.Date(tmp[[1]][1]), nrow(d.out))
return(d.out)
}
attendance <- do.call("rbind",
lapply(list.files(".", pattern="^attendance-.*tsv$"),
file.to.attendance.list))
## create list of folks who are missing in class
missing.in.class <- call.list.full[is.na(call.list.full$answered) |
(!is.na(call.list.full$answered) & !call.list.full$answered),
c("discord.name", "day")]
missing.in.class <- unique(missing.in.class)
setDT(attendance)
setkey(attendance, discord.name, day)
setDT(missing.in.class)
setkey(missing.in.class, discord.name, day)
## drop presence for people on missing days
attendance[missing.in.class,]
attendance <- as.data.frame(attendance[!missing.in.class,])
attendance.counts <- data.frame(table(attendance$discord.name))
colnames(attendance.counts) <- c("discord.name", "num.present")
d <- merge(d, attendance.counts,
all.x=TRUE, all.y=TRUE,
by="discord.name")
days.list <- lapply(unique(attendance$day), function (day) {
day.total <- table(call.list.full$day == day)[["TRUE"]]
lapply(d$discord.name, function (discord.name) {
num.present <- nrow(attendance[attendance$day == day & attendance$discord.name == discord.name,])
if (num.present/day.total > 1) {print(day)}
data.frame(discord.name=discord.name,
days.present=(num.present/day.total))
})
})
days.tmp <- do.call("rbind", lapply(days.list, function (x) do.call("rbind", x)))
days.tbl <- tapply(days.tmp$days.present, days.tmp$discord.name, sum)
attendance.days <- data.frame(discord.name=names(days.tbl),
days.present=days.tbl,
days.absent=length(list.files(".", pattern="^attendance-.*tsv$"))-days.tbl)
d <- merge(d, attendance.days,
all.x=TRUE, all.y=TRUE, by="discord.name")
d[sort.list(d$days.absent), c("discord.name", "num.calls", "days.absent")]
## make some visualizations of whose here/not here
#######################################################
png("questions_absence_histogram_combined.png", units="px", width=800, height=600)
ggplot(d) +
aes(x=as.factor(num.calls), fill=days.absent, group=days.absent) +
geom_bar(color="black") +
scale_x_discrete("Number of questions asked") +
scale_y_continuous("Number of students") +
scale_fill_continuous("Days absent", low="red", high="blue")+
theme_bw()
dev.off()
png("questions_absenses_boxplots.png", units="px", width=800, height=600)
ggplot(data=d) +
aes(x=as.factor(num.calls), y=days.absent) +
geom_boxplot() +
scale_x_discrete("Number of questions asked") +
scale_y_continuous("Days absent")
dev.off()

View File

@@ -8,47 +8,46 @@ from csv import DictReader
import os.path import os.path
import re import re
import discord
class ColdCall(): class ColdCall():
def __init__ (self, record_attendance=True): def __init__ (self, course = ''):
self.course = course
self.today = str(datetime.date(datetime.now())) self.today = str(datetime.date(datetime.now()))
# how much less likely should it be that a student is called upon? # how much less likely should it be that a student is called upon?
self.weight = 2 self.weight = 2
self.record_attendance = record_attendance self.__set_filenames()
def __set_filenames(self):
# filenames # filenames
self.__fn_studentinfo = "data/student_information.tsv" self.__fn_studentinfo = f"data/{self.course}/student_information.tsv"
self.__fn_daily_calllist = f"data/call_list-{self.today}.tsv" self.__fn_daily_calllist = f"data/{self.course}/call_list-{self.today}.tsv"
self.__fn_daily_attendance = f"data/attendance-{self.today}.tsv" self.__fn_daily_attendance = f"data/{self.course}/attendance-{self.today}.tsv"
self.preferred_names = self.__get_preferred_names()
def __load_prev_questions(self): def __load_prev_questions(self):
previous_questions = defaultdict(int) previous_questions = defaultdict(int)
for fn in listdir("./data/"): for fn in listdir(f"./data/{self.course}/"):
if re.match("call_list-\d{4}-\d{2}-\d{2}.tsv", fn): if re.match("call_list-\d{4}-\d{2}-\d{2}.tsv", fn):
with open(f"./data/{fn}", 'r') as f: with open(f"./data/{self.course}/{fn}", 'r') as f:
for row in DictReader(f, delimiter="\t"): for row in DictReader(f, delimiter="\t"):
if not row["answered"] == "FALSE": if not row["answered"] == "FALSE":
previous_questions[row["unique_name"]] += 1 previous_questions[row["discord_name"]] += 1
return previous_questions return previous_questions
def __get_preferred_names(self): def __get_preferred_name(self, selected_student):
# translate the unique name into the preferred students name, # translate the discord name into the preferred students name,
# if possible, otherwise return the unique name # if possible, otherwise return the discord name
preferred_names = {} preferred_names = {}
with open(self.__fn_studentinfo, 'r') as f: with open(self.__fn_studentinfo, 'r') as f:
for row in DictReader(f, delimiter="\t"): for row in DictReader(f, delimiter="\t"):
preferred_names[row["Your username on the class Teams server"]] = row["Name you'd like to go by in class"] preferred_names[row["discord_name"]] = row["name"]
return(preferred_names) if selected_student in preferred_names:
return preferred_names[selected_student]
def __get_preferred_name(self, selected_student):
if selected_student in self.preferred_names:
return self.preferred_names[selected_student]
else: else:
return None return None
@@ -64,7 +63,7 @@ class ColdCall():
weights[s] = weights[s] / self.weight weights[s] = weights[s] / self.weight
# choose one student from the weighted list # choose one student from the weighted list
# print(weights) # DEBUG LINE print(weights)
return choices(list(weights.keys()), weights=list(weights.values()), k=1)[0] return choices(list(weights.keys()), weights=list(weights.values()), k=1)[0]
def __record_attendance(self, students_present): def __record_attendance(self, students_present):
@@ -83,7 +82,7 @@ class ColdCall():
# if it's the first one of the day, write it out # if it's the first one of the day, write it out
if not os.path.exists(self.__fn_daily_calllist): if not os.path.exists(self.__fn_daily_calllist):
with open(self.__fn_daily_calllist, "w") as f: with open(self.__fn_daily_calllist, "w") as f:
print("\t".join(["unique_name", "timestamp", "answered", "assessment"]), file=f) print("\t".join(["discord_name", "timestamp", "answered", "assessment"]), file=f)
# open for appending the student # open for appending the student
with open(self.__fn_daily_calllist, "a") as f: with open(self.__fn_daily_calllist, "a") as f:
@@ -94,8 +93,7 @@ class ColdCall():
selected_student = self.__select_student_from_list(students_present) selected_student = self.__select_student_from_list(students_present)
# record the called-upon student in the right place # record the called-upon student in the right place
if self.record_attendance: self.__record_attendance(students_present)
self.__record_attendance(students_present)
self.__record_coldcall(selected_student) self.__record_coldcall(selected_student)
preferred_name = self.__get_preferred_name(selected_student) preferred_name = self.__get_preferred_name(selected_student)
@@ -105,8 +103,12 @@ class ColdCall():
coldcall_message = f"@{selected_student}, you're up!" coldcall_message = f"@{selected_student}, you're up!"
return coldcall_message return coldcall_message
# cc = ColdCall() def update_course(self, course_name):
self.course = course_name
self.__set_filenames()
# cc = ColdCall()
# test_student_list = ["jordan", "Kristen Larrick", "Madison Heisterman", "Maria.Au20", "Laura (Alia) Levi", "Leona Aklipi", "anne", "emmaaitelli", "ashleylee", "allie_partridge", "Tiana_Cole", "Hamin", "Ella Qu", "Shizuka", "Ben Baird", "Kim Do", "Isaacm24", "Sam Bell", "Courtneylg"] # test_student_list = ["jordan", "Kristen Larrick", "Madison Heisterman", "Maria.Au20", "Laura (Alia) Levi", "Leona Aklipi", "anne", "emmaaitelli", "ashleylee", "allie_partridge", "Tiana_Cole", "Hamin", "Ella Qu", "Shizuka", "Ben Baird", "Kim Do", "Isaacm24", "Sam Bell", "Courtneylg"]
# print(cc.coldcall(test_student_list)) # print(cc.coldcall(test_student_list))

View File

@@ -1,15 +0,0 @@
#!/usr/bin/env python3
from coldcall import ColdCall
import re
## create the coldcall object
cc = ColdCall(record_attendance=False)
student_list = cc.preferred_names
# print out 100 students
for i in range(100):
print(f"{i + 1}. {cc.coldcall(student_list)} [ ] [ ]\n")

View File

@@ -3,6 +3,8 @@
from coldcall import ColdCall from coldcall import ColdCall
import re import re
import discord import discord
import config
import random
## create the coldcall object ## create the coldcall object
cc = ColdCall() cc = ColdCall()
@@ -11,13 +13,16 @@ class ColdCallBot (discord.Client):
async def on_ready(self): async def on_ready(self):
print(f'Logged on as {self.user}! Ready for class!') print(f'Logged on as {self.user}! Ready for class!')
async def on_message(self, message): async def on_message(self, message, voice_channel = 'Class Sessions'):
if message.author == self.user: if message.author == self.user:
return return
if message.content.startswith('$next'): if message.content.startswith('$next'):
classroom = discord.utils.get(message.guild.voice_channels, name='Classroom Voice') if message.channel.category:
if cc.course != message.channel.category:
cc.update_course(message.channel.category)
classroom = [x for x in message.guild.voice_channels if x.name == voice_channel and x.category_id == message.channel.category_id][0]
present_students = [] present_students = []
for member in classroom.members: for member in classroom.members:
if 'Students' in [r.name for r in member.roles]: if 'Students' in [r.name for r in member.roles]:
@@ -30,8 +35,156 @@ class ColdCallBot (discord.Client):
msg_text = "I don't see any students currently in the Classroom Voice channel!" msg_text = "I don't see any students currently in the Classroom Voice channel!"
else: else:
msg_text = cc.coldcall(present_students) msg_text = cc.coldcall(present_students)
await message.channel.send(msg_text) await message.channel.send(msg_text)
# TODO: Only let admin send this command
if (message.content.startswith('$network game')) and ('Teachers' in [r.name for r in message.author.roles]):
print("Starting the game")
if message.channel.category:
if cc.course != message.channel.category:
cc.update_course(message.channel.category)
classroom = [x for x in message.guild.voice_channels if x.name == voice_channel and x.category_id == message.channel.category_id][0]
present_students = []
for member in classroom.members:
if 'Students' in [r.name for r in member.roles]:
present_students.append(member)
self.assignments = get_assignments(present_students, edgelist = './network_game/test_edgelist.csv')
# Build a mapping from names to user objects so that people can refer to users
self.active_list = {x.name: x for x in self.assignments}
self.observers = [x for x in classroom.members if x not in self.assignments]
if self.assignments is not None:
for student in self.assignments:
await student.send(f"You are allowed to talk to:")
for neighbor in self.assignments[student]['neighbors']:
await student.send(f"{neighbor.mention}")
await student.send(f"You have these resources: {self.assignments[student]['has']}.")
await student.send(f"You need: {self.assignments[student]['needs']}.")
else:
for student in present_students:
await student.send("Not enough students to play")
if message.content.startswith('$send'):
try:
_, resource, u_to = message.content.split(' ')
except:
await message.author.send("Badly formed command. It has to be '$send resource @user'")
if u_to not in self.active_list:
await message.author.send(f"I can't find {u_to} in the list of users. Make sure the command is formatted as $send resource @user")
else:
u_to = self.active_list[u_to]
gave_resource = self.give_resource(resource, message.author, u_to)
if gave_resource == True:
finished = self.is_finished(u_to)
await message.author.send(f"{resource} sent to {u_to}")
await message.author.send(f"You now have {self.assignments[message.author]['has']} and you need {self.assignments[message.author]['needs']}")
if finished:
await u_to.send("You have everything you need! Well done! You can keep passing resources and talking with your 'neighbors' if you like. Just make sure to keep the resources that you need!")
else:
await u_to.send(f"You now have {self.assignments[u_to]['has']} and you need {self.assignments[u_to]['needs']}")
for o in self.observers:
await o.send(f"{message.author.name} sent {resource} to {u_to.name}")
if finished:
await o.send(f"{u_to.name} has everything they need!!!")
else:
await message.author.send(f"Resource not sent. Are you sure you have {resource}?")
def is_finished(self, u_to):
if set(self.assignments[u_to]['needs']) <= set(self.assignments[u_to]['has']):
return True
return False
def give_resource(self, resource, u_from, u_to):
if resource not in self.assignments[u_from]['has']:
return False
else:
self.assignments[u_from]['has'].remove(resource)
self.assignments[u_to]['has'].append(resource)
return True
def get_assignments(student_list,
edgelist = './network_game/edgelist.csv',
resource_prefix = './network_game/resources_'
):
def _add_connection(node1, node2):
node1 = int(node1)
node2 = int(node2)
for i in range(len(mapping[node1])):
s1 = mapping[node1][i]
s2 = mapping[node2][i]
if s1 in assignments:
assignments[s1]['neighbors'].append(s2)
else:
assignments[s1] = {'neighbors': [s2]}
def _add_resources():
fn = f"{resource_prefix}{group_size}.csv"
with open(fn, 'r') as f:
i = 1
for line in f.readlines():
resources = line.strip().split(',')
curr_students = mapping[i]
for s in curr_students:
assignments[s]['has'] = resources[:3]
assignments[s]['needs'] = resources[3:]
i += 1
assignments = {}
group_size = _get_group_size(len(student_list))
if len(student_list) < group_size:
return None
mapping = _make_mapping(student_list, group_size)
with open(edgelist, 'r') as f:
for line in f.readlines():
node1, node2 = line.strip().split(',')
if int(node2) <= group_size:
_add_connection(node1, node2)
_add_connection(node2, node1)
_add_resources()
return assignments
def _make_mapping(students, group_size):
random.shuffle(students)
n_observers = len(students) % group_size
mapping = {}
if n_observers > 0:
mapping['observers'] = students[-n_observers:]
for i, student in enumerate(students[-n_observers:]):
j = i % group_size
idx = j + 1
if idx in mapping:
mapping[idx].append(student)
else:
mapping[idx] = [student]
return mapping
def _get_group_size(n):
min_observers = None
for x in range(7,10):
observers = n % x
if min_observers is None or observers < min_observers:
best_fit = x
min_observers = observers
return best_fit
# this is necessary to get information about who is online # this is necessary to get information about who is online
intents = discord.Intents.default() intents = discord.Intents.default()
@@ -39,5 +192,4 @@ intents.members = True
intents.presences = True intents.presences = True
ccb = ColdCallBot(intents=intents) ccb = ColdCallBot(intents=intents)
ccb.run('CHANGEME') ccb.run(config.key)

View File

@@ -1,4 +0,0 @@
#!/bin/bash
wget 'https://docs.google.com/spreadsheets/d/FIXME/export?gid=FIXME&format=tsv' -O 'student_information.tsv'

View File

@@ -1,155 +0,0 @@
#!/usr/bin/env python
import pandas as pd
from random import choices, shuffle
from datetime import datetime
import csv
import os
from flask import Flask, render_template, request, abort
app = Flask(__name__)
@app.route("/")
def hello_world():
return "<p>Hello, Test!</p>"
@app.route('/response_quality', methods=['POST'])
def response_quality():
student_name = request.form['studentName']
button_value = request.form['buttonValue']
course = request.form['course']
print(button_value)
fn = f'../assessments/{course}/{course}.csv'
if button_value == 'absent':
answered = 'F'
button_value = ''
else:
answered = 'T'
if button_value != 'get_next':
write_to_file(student_name, fn,
answered=answered,
assessment=button_value)
student = coldcall_student(course)
print(f'Sending {student}')
return student
@app.route("/coldcaller/<course>", methods=['POST','GET'])
def coldcaller(course):
public = request.args.get('public')
print(public)
if request.method == "POST":
student = coldcall_student(course)
if not student:
abort(404)
else:
student = ''
return render_template('cold_caller.html', student=student, public=public)
def coldcall_student(course):
if course not in ["com_304","com_411","com_674", "amap"]:
return None
weight = 2
students = pd.read_csv(f'../assessments/{course}/{course}_students.csv').Name
out_fn = f'../assessments/{course}/{course}.csv'
caller = Caller(out_fn, students, weight)
student = caller.get_random_student()
return student
@app.route("/shuffler", methods=['POST','GET'])
def shuffler():
course = request.args.get('course')
try:
student_list = pd.read_csv(f'../assessments/{course}/{course}_students.csv').Name
except FileNotFoundError:
abort(404)
shuffle(student_list)
print(student_list)
return render_template('shuffler.html', result=student_list)
@app.route("/make_groups", methods=['POST','GET'])
def make_groups():
course = request.args.get('course')
group_size = int(request.args.get('group_size'))
print('running')
try:
student_list = pd.read_csv(f'../assessments/{course}/{course}_students.csv').Name
except FileNotFoundError:
abort(404)
shuffle(student_list)
print(student_list)
print(range(0,len(student_list)//group_size + 1, group_size))
result = []
j = 1
for i in range(0,len(student_list), group_size):
result.append((j, student_list[i:i+group_size]))
j += 1
return render_template('group_maker.html', result=result)
class Caller:
def __init__(self, out_fn, students, weight = 2):
self.weight = weight
self.fn = out_fn
self.students = students
self.last_chosen = None
self.today = datetime.now().date()
self.weights_dict = self.get_weights()
def get_weights(self):
times_called = self.get_times_called()
weights_dict = {}
for student in self.students:
try:
curr_tc = times_called[student]
except KeyError:
curr_tc = 0
student_weight = (1/self.weight) ** curr_tc
weights_dict[student] = student_weight
return weights_dict
def get_times_called(self):
try:
df = pd.read_csv(self.fn)
if len(df) > 0:
self.last_chosen = df.name.iloc[-1]
df.date = pd.to_datetime(df.date).dt.date
times_called = df[(df.answered.isin(['T','TRUE']))|(df.date==self.today)].groupby('name').size()
self.absent_today = df.loc[(df.date==self.today) & (df.answered.isin(['F', 'FALSE'])), 'name']
except FileNotFoundError or IndexError:
times_called = pd.DataFrame()
self.absent_today = pd.DataFrame()
return times_called
def update_weight(self, student):
self.weights_dict[student] /= self.weight
def get_random_student(self, can_repeat=False):
if not can_repeat:
curr_weights = {k:v for k,v in self.weights_dict.items() if k != self.last_chosen}
else:
curr_weights = self.weights_dict
for student in set(self.absent_today):
if student != self.last_chosen:
del curr_weights[student]
rand_student = choices(list(curr_weights.keys()), weights=list(curr_weights.values()), k=1)[0]
print(f"Weight of {rand_student}: {curr_weights[rand_student]}")
self.update_weight(rand_student)
return(rand_student)
def write_to_file(student, fn, answered, assessment):
if not os.path.exists(fn):
with open(fn, 'w') as f:
f.write(','.join(['name', 'date', 'answered', 'assessment']))
f.write('\n')
with open(fn, 'a') as f:
out_csv = csv.writer(f)
out_csv.writerow([student,datetime.now().date(),answered,assessment])

View File

@@ -1,25 +0,0 @@
body {
background: Linen;
margin-top: 50px;
margin-left: 100px;
margin-right: 100px;
font-family: Georgia, serif;
color: DarkSlateGray;
font-size: 1.3em;
}
p {
font-family: Georgia, serif;
font-size: 1em;
color: DarkSlateGray;
}
h1 {
font-family: Verdana, Geneva, sans-serif;
font-size: 2.5em;
color: FireBrick;
}
.rand-button {
font-size: .8em;
}

View File

@@ -1,23 +0,0 @@
$(document).ready(function() {
$('#goodButton, #badButton, #neutralButton, #absentButton, #nextButton').on('click', function() {
var studentName = $('#studentName').text();
console.log(studentName);
var buttonValue = $(this).val();
var courseCode = window.location.pathname.split('/').pop();
$.ajax({
url: '/response_quality',
type: 'POST',
data: {
studentName: studentName,
buttonValue: buttonValue,
course: courseCode
},
success: function(response) {
$('#studentName').text(response);
},
error: function(error) {
console.log(error);
}
});
});
});

View File

@@ -1,41 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Random Student Picker</title>
<link rel="stylesheet" href='/static/main.css' />
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="{{ url_for('static', filename='process_button.js') }}"></script>
</head>
<body>
<h3>
The next student is:
</h3>
<h2 id='studentName' name='studentName'>{{student}}</h2>
{% if student != '' %}
{% if public is none %}
<button class='assessment' id="goodButton" value="G">Good</button>
<button class='assessment' id="badButton" value="B">Bad</button>
<button class='assessment' id="neutralButton" value="M">Neutral</button>
<button class='assessment' id="absentButton" value="absent">Absent</button>
<button class='assessment' id="nextButton" value="get_next">Skip</button>
{% else %}
<button class='assessment' id="neutralButton" value="?">Next Student</button>
<button class='assessment' id="absentButton" value="absent">Absent</button>
{% endif %}
{% else %}
<form method="post" id="todo-form">
<button class='rand-button' type="submit">Get random student</button>
</form>
{% endif %}
</body>
</html>

View File

@@ -1,25 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Random Student Picker</title>
<link rel="stylesheet" href='/static/main.css' />
</head>
<body>
<h3>
Groups:
</h3>
{% for group in result %}
<h2>Group {{group[0]}}</h2>
<ul>
{% for member in group[1] %}
<li> {{member}} </li>
{% endfor %}
</ul>
{% endfor %}
</body>
</html>

View File

@@ -1,19 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Shuffled List</title>
<link rel="stylesheet" href='/static/main.css' />
</head>
<body>
<h3>
Shuffled List:
</h3>
<ul>
{% for member in result %}
<li> {{member}} </li>
{% endfor %}
</ul>
</body>
</html>

View File

@@ -1,71 +0,0 @@
name,date,answered,assessment
owen,2022-01-01,,
dad,2022-01-01,,
owen,2022-01-01,,
owen,2022-01-01,,
dad,2022-01-01,,
owen,2022-01-01,,
dad,2022-01-01,,
dad,2022-01-01,,
dad,2022-01-01,,
owen,2022-01-01,,
owen,2022-01-01,,
dad,2022-01-01,,
dad,2022-01-01,,
dad,2022-01-01,,
dad,2022-01-01,,
owen,2022-01-01,,
owen,2022-01-01,,
owen,2022-01-01,,
owen,2022-01-01,,
owen,2022-01-01,,
dad,2022-01-01,,
dad,2022-01-01,,
dad,2022-01-01,,
dad,2022-01-01,,
owen,2022-01-01,,
owen,2022-01-01,,
dad,2022-01-01,,
owen,2022-01-01,,
dad,2022-01-01,,
dad,2022-01-01,,
owen,2022-01-01,,
owen,2022-01-01,,
dad,2022-01-01,,
dad,2022-01-01,,
owen,2022-01-01,,
owen,2022-01-01,,
dad,2022-01-01,,
owen,2022-01-01,,
owen,2022-01-01,,
dad,2022-01-01,,
owen,2022-01-01,,
dad,2022-01-01,,
owen,2022-01-01,,
dad,2022-01-01,,
owen,2022-01-01,,
owen,2022-01-01,,
dad,2022-01-01,,
dad,2022-01-01,,
dad,2022-01-01,,
owen,2022-01-01,,
dad,2022-01-01,,
dad,2022-01-01,,
owen,2022-01-01,,
owen,2022-01-01,,
dad,2022-01-01,,
owen,2022-01-01,,
owen,2022-01-01,,
dad,2022-01-01,,
dad,2022-01-01,,
dad,2022-01-01,,
dad,2022-01-01,,
owen,2022-01-01,,
owen,2022-01-01,,
owen,2022-01-01,,
owen,2022-01-01,,
dad,2022-01-01,,
dad,2022-01-01,,
owen,2022-01-01,,
dad,2022-01-01,,
dad,2022-01-01,,
1 name date answered assessment
2 owen 2022-01-01
3 dad 2022-01-01
4 owen 2022-01-01
5 owen 2022-01-01
6 dad 2022-01-01
7 owen 2022-01-01
8 dad 2022-01-01
9 dad 2022-01-01
10 dad 2022-01-01
11 owen 2022-01-01
12 owen 2022-01-01
13 dad 2022-01-01
14 dad 2022-01-01
15 dad 2022-01-01
16 dad 2022-01-01
17 owen 2022-01-01
18 owen 2022-01-01
19 owen 2022-01-01
20 owen 2022-01-01
21 owen 2022-01-01
22 dad 2022-01-01
23 dad 2022-01-01
24 dad 2022-01-01
25 dad 2022-01-01
26 owen 2022-01-01
27 owen 2022-01-01
28 dad 2022-01-01
29 owen 2022-01-01
30 dad 2022-01-01
31 dad 2022-01-01
32 owen 2022-01-01
33 owen 2022-01-01
34 dad 2022-01-01
35 dad 2022-01-01
36 owen 2022-01-01
37 owen 2022-01-01
38 dad 2022-01-01
39 owen 2022-01-01
40 owen 2022-01-01
41 dad 2022-01-01
42 owen 2022-01-01
43 dad 2022-01-01
44 owen 2022-01-01
45 dad 2022-01-01
46 owen 2022-01-01
47 owen 2022-01-01
48 dad 2022-01-01
49 dad 2022-01-01
50 dad 2022-01-01
51 owen 2022-01-01
52 dad 2022-01-01
53 dad 2022-01-01
54 owen 2022-01-01
55 owen 2022-01-01
56 dad 2022-01-01
57 owen 2022-01-01
58 owen 2022-01-01
59 dad 2022-01-01
60 dad 2022-01-01
61 dad 2022-01-01
62 dad 2022-01-01
63 owen 2022-01-01
64 owen 2022-01-01
65 owen 2022-01-01
66 owen 2022-01-01
67 dad 2022-01-01
68 dad 2022-01-01
69 owen 2022-01-01
70 dad 2022-01-01
71 dad 2022-01-01

3
update_student_info.sh Executable file
View File

@@ -0,0 +1,3 @@
wget "https://docs.google.com/spreadsheets/d/e/2PACX-1vTBboNsATMKYdQM3WsbcIvloqRlvR9ajSgZWyHN6Bci50wfiPBjibTxaF8XcMgAJycvKNdAfR9LBHbp/pub?gid=1302288840&single=true&output=csv" -O ./data/COM\ 495\ -\ Turning\ Data\ into\ Insights\ and\ Stories/student_information.tsv
wget "https://docs.google.com/spreadsheets/d/e/2PACX-1vSPL6jKD2rXQqac6O0pMb4JRxMAOM-EdPkZw2FuebbJHiZdAl4n5Df5RCyuxoHcwOcW0VbBevnec6b-/pub?gid=1575299441&single=true&output=csv" -O ./data/COM\ 411\ -\ Communication\ and\ Social\ Networks/student_information.tsv