Compare commits
2 Commits
f1f29cdbe2
...
foote_spri
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46796e647a | ||
|
|
788ed2de62 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,3 @@
|
||||
*~
|
||||
__pycache__
|
||||
*.pyc
|
||||
config.py
|
||||
|
||||
6
README
6
README
@@ -1,7 +1,7 @@
|
||||
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:
|
||||
|
||||
$ 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
|
||||
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()`.
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ Daily Process
|
||||
|
||||
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
|
||||
detailed weights as it goes, and it will record data into files in the
|
||||
|
||||
@@ -1,23 +1,36 @@
|
||||
## 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
|
||||
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")
|
||||
setwd("case_grades")
|
||||
|
||||
rownames(d) <- d$unique.name
|
||||
rownames(d) <- d$discord.name
|
||||
|
||||
## show the distribution of assessments
|
||||
table(call.list$assessment)
|
||||
prop.table(table(call.list$assessment))
|
||||
table(call.list$answered)
|
||||
prop.table(table(call.list$answered))
|
||||
table(call.list.full$assessment)
|
||||
prop.table(table(call.list.full$assessment))
|
||||
table(call.list.full$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
|
||||
##########################################################
|
||||
@@ -26,47 +39,81 @@ d$part.grade <- NA
|
||||
|
||||
## print the median number of questions for (a) everybody and (b)
|
||||
## people that have been present 75% of the time
|
||||
median(d$num.calls[d$days.absent < 0.25*case.sessions])
|
||||
median(d$num.calls)
|
||||
|
||||
questions.cutoff <- median(d$num.calls)
|
||||
|
||||
## helper function to generate average grade minus number of missing
|
||||
gen.part.grade <- function (x.unique.name) {
|
||||
q.scores <- question.grades[call.list$assessment[call.list$unique.name == x.unique.name]]
|
||||
gen.part.grade <- function (x.discord.name) {
|
||||
q.scores <- question.grades[call.list$assessment[call.list$discord.name == x.discord.name]]
|
||||
base.score <- mean(q.scores, na.rm=TRUE)
|
||||
|
||||
## 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
|
||||
data.frame(unique.name=x.unique.name,
|
||||
part.grade=(base.score))
|
||||
data.frame(discord.name=x.discord.name,
|
||||
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
|
||||
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
|
||||
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
|
||||
quantile(d$num.calls, probs=(0:100*0.01))
|
||||
d.print <- merge(d, myuw[,c("StudentNo", "FirstName", "LastName", "UWNetID")],
|
||||
by.x="student.num", by.y="StudentNo")
|
||||
write.csv(d.print, file="final_participation_grades.csv")
|
||||
|
||||
## library(rmarkdown)
|
||||
library(rmarkdown)
|
||||
|
||||
## for (x.unique.name in d$unique.name) {
|
||||
## render(input="../../assessment_and_tracking/student_report_template.Rmd",
|
||||
## output_format="html_document",
|
||||
## output_file=paste("../data/case_grades/student_reports/",
|
||||
## d.print$UWNetID[d.print$unique.name == x.unique.name],
|
||||
## sep=""))
|
||||
## }
|
||||
for (x.discord.name in d$discord.name) {
|
||||
render(input="../../assessment_and_tracking/student_report_template.Rmd",
|
||||
output_format="html_document",
|
||||
output_file=paste("../data/case_grades/student_reports/",
|
||||
d.print$UWNetID[d.print$discord.name == x.discord.name],
|
||||
sep=""))
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
## 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
|
||||
## the google sheet
|
||||
|
||||
## call.list <- unlist(lapply(list.files(".", pattern="^attendance-.*tsv$"), function (x) {
|
||||
## d <- read.delim(x)
|
||||
## strsplit(d[[2]], ",")
|
||||
## })
|
||||
## )
|
||||
## present <- unique(call.list)
|
||||
## present[!present %in% gs[["Your.username.on.the.class.Discord.server"]]]
|
||||
call.list <- unlist(lapply(list.files(".", pattern="^attendance-.*tsv$"), function (x) {
|
||||
d <- read.delim(x)
|
||||
strsplit(d[[2]], ",")
|
||||
})
|
||||
)
|
||||
present <- unique(call.list)
|
||||
present[!present %in% gs[["Your.username.on.the.class.Discord.server"]]]
|
||||
|
||||
## 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]
|
||||
|
||||
|
||||
@@ -1,25 +1,109 @@
|
||||
setwd("~/online_communities/coldcallbot/data/")
|
||||
|
||||
library(ggplot2)
|
||||
library(data.table)
|
||||
|
||||
gs <- read.delim("student_information.tsv")
|
||||
d <- gs[,c(2,4)]
|
||||
colnames(d) <- c("student.num", "unique.name")
|
||||
|
||||
call.list <- do.call("rbind", lapply(list.files(".", pattern="^call_list-.*tsv$"), function (x) {read.delim(x, stringsAsFactors=FALSE)[,1:4]}))
|
||||
d <- gs[,c(2,5)]
|
||||
colnames(d) <- c("student.num", "discord.name")
|
||||
|
||||
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))
|
||||
|
||||
table(call.list$unique_name[call.list$answered])
|
||||
call.list$day <- as.Date(call.list$timestamp)
|
||||
|
||||
## drop calls where the person wasn't present
|
||||
call.list.full <- call.list
|
||||
call.list[!call.list$answered,]
|
||||
call.list <- call.list[call.list$answered,]
|
||||
|
||||
call.counts <- data.frame(table(call.list$unique.name))
|
||||
colnames(call.counts) <- c("unique.name", "num.calls")
|
||||
call.counts <- data.frame(table(call.list$discord.name))
|
||||
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()
|
||||
|
||||
|
||||
46
coldcall.py
46
coldcall.py
@@ -8,47 +8,46 @@ from csv import DictReader
|
||||
|
||||
import os.path
|
||||
import re
|
||||
import discord
|
||||
|
||||
class ColdCall():
|
||||
def __init__ (self, record_attendance=True):
|
||||
def __init__ (self, course = ''):
|
||||
self.course = course
|
||||
self.today = str(datetime.date(datetime.now()))
|
||||
# how much less likely should it be that a student is called upon?
|
||||
self.weight = 2
|
||||
self.record_attendance = record_attendance
|
||||
self.__set_filenames()
|
||||
|
||||
|
||||
def __set_filenames(self):
|
||||
# filenames
|
||||
self.__fn_studentinfo = "data/student_information.tsv"
|
||||
self.__fn_daily_calllist = f"data/call_list-{self.today}.tsv"
|
||||
self.__fn_daily_attendance = f"data/attendance-{self.today}.tsv"
|
||||
|
||||
self.preferred_names = self.__get_preferred_names()
|
||||
self.__fn_studentinfo = f"data/{self.course}/student_information.tsv"
|
||||
self.__fn_daily_calllist = f"data/{self.course}/call_list-{self.today}.tsv"
|
||||
self.__fn_daily_attendance = f"data/{self.course}/attendance-{self.today}.tsv"
|
||||
|
||||
def __load_prev_questions(self):
|
||||
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):
|
||||
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"):
|
||||
if not row["answered"] == "FALSE":
|
||||
previous_questions[row["unique_name"]] += 1
|
||||
previous_questions[row["discord_name"]] += 1
|
||||
|
||||
return previous_questions
|
||||
|
||||
def __get_preferred_names(self):
|
||||
# translate the unique name into the preferred students name,
|
||||
# if possible, otherwise return the unique name
|
||||
def __get_preferred_name(self, selected_student):
|
||||
# translate the discord name into the preferred students name,
|
||||
# if possible, otherwise return the discord name
|
||||
|
||||
preferred_names = {}
|
||||
with open(self.__fn_studentinfo, 'r') as f:
|
||||
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)
|
||||
|
||||
def __get_preferred_name(self, selected_student):
|
||||
if selected_student in self.preferred_names:
|
||||
return self.preferred_names[selected_student]
|
||||
if selected_student in preferred_names:
|
||||
return preferred_names[selected_student]
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -64,7 +63,7 @@ class ColdCall():
|
||||
weights[s] = weights[s] / self.weight
|
||||
|
||||
# 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]
|
||||
|
||||
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 not os.path.exists(self.__fn_daily_calllist):
|
||||
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
|
||||
with open(self.__fn_daily_calllist, "a") as f:
|
||||
@@ -94,7 +93,6 @@ class ColdCall():
|
||||
selected_student = self.__select_student_from_list(students_present)
|
||||
|
||||
# record the called-upon student in the right place
|
||||
if self.record_attendance:
|
||||
self.__record_attendance(students_present)
|
||||
self.__record_coldcall(selected_student)
|
||||
|
||||
@@ -105,6 +103,10 @@ class ColdCall():
|
||||
coldcall_message = f"@{selected_student}, you're up!"
|
||||
return coldcall_message
|
||||
|
||||
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"]
|
||||
|
||||
@@ -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")
|
||||
|
||||
160
coldcallbot.py
160
coldcallbot.py
@@ -3,6 +3,8 @@
|
||||
from coldcall import ColdCall
|
||||
import re
|
||||
import discord
|
||||
import config
|
||||
import random
|
||||
|
||||
## create the coldcall object
|
||||
cc = ColdCall()
|
||||
@@ -11,12 +13,15 @@ class ColdCallBot (discord.Client):
|
||||
async def on_ready(self):
|
||||
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:
|
||||
return
|
||||
|
||||
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 = []
|
||||
for member in classroom.members:
|
||||
@@ -32,6 +37,154 @@ class ColdCallBot (discord.Client):
|
||||
msg_text = cc.coldcall(present_students)
|
||||
|
||||
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
|
||||
intents = discord.Intents.default()
|
||||
@@ -39,5 +192,4 @@ intents.members = True
|
||||
intents.presences = True
|
||||
|
||||
ccb = ColdCallBot(intents=intents)
|
||||
ccb.run('CHANGEME')
|
||||
|
||||
ccb.run(config.key)
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
wget 'https://docs.google.com/spreadsheets/d/FIXME/export?gid=FIXME&format=tsv' -O 'student_information.tsv'
|
||||
|
||||
155
flask_app/app.py
155
flask_app/app.py
@@ -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])
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,,
|
||||
|
3
update_student_info.sh
Executable file
3
update_student_info.sh
Executable 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
|
||||
Reference in New Issue
Block a user