Compare commits
13 Commits
foote_spri
...
f1f29cdbe2
| Author | SHA1 | Date | |
|---|---|---|---|
| f1f29cdbe2 | |||
| e756c448c0 | |||
|
|
aee2c90d1e | ||
|
|
79f9b632b1 | ||
|
|
23b91425d8 | ||
| 92e65de771 | |||
| cda1cb3b3c | |||
| 3bd4c9c2a6 | |||
| da8f47aa27 | |||
| 95a9977572 | |||
| f590bf88bc | |||
| 914f5973c3 | |||
| d8e662b5c3 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,3 @@
|
|||||||
*~
|
*~
|
||||||
__pycache__
|
__pycache__
|
||||||
config.py
|
*.pyc
|
||||||
|
|||||||
6
README
6
README
@@ -1,7 +1,7 @@
|
|||||||
Setting up the Discord Bot
|
Setting up the Discord Bot
|
||||||
======================================
|
======================================
|
||||||
|
|
||||||
I run the Discord boy from my laptop. It requires the discord Python
|
I run the Discord bot 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's Token (also found on the "Bot" tab)
|
Finally, you need to copy your bot'ss 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:
|
||||||
|
|
||||||
$ ./coldcallboy.py
|
$ ./coldcallbot.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
|
||||||
|
|||||||
@@ -1,36 +1,23 @@
|
|||||||
## load in the data
|
## load in the data
|
||||||
#################################
|
#################################
|
||||||
|
|
||||||
case.sessions <- 15
|
myuw <- read.csv("myuw-COMMLD_570_A_spring_2021_students.csv", stringsAsFactors=FALSE)
|
||||||
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), "BAD"=100-(50/(3.3)*2))
|
question.grades <- c("GOOD"=100, "FAIR"=100-(50/3.3), "WEAK"=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$discord.name
|
rownames(d) <- d$unique.name
|
||||||
|
|
||||||
## show the distribution of assessments
|
## show the distribution of assessments
|
||||||
table(call.list.full$assessment)
|
table(call.list$assessment)
|
||||||
prop.table(table(call.list.full$assessment))
|
prop.table(table(call.list$assessment))
|
||||||
table(call.list.full$answered)
|
table(call.list$answered)
|
||||||
prop.table(table(call.list.full$answered))
|
prop.table(table(call.list$answered))
|
||||||
|
|
||||||
total.questions.asked <- nrow(call.list.full)
|
total.questions.asked <- nrow(call.list)
|
||||||
|
|
||||||
## 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
|
||||||
##########################################################
|
##########################################################
|
||||||
@@ -39,81 +26,47 @@ 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.discord.name) {
|
gen.part.grade <- function (x.unique.name) {
|
||||||
q.scores <- question.grades[call.list$assessment[call.list$discord.name == x.discord.name]]
|
q.scores <- question.grades[call.list$assessment[call.list$unique.name == x.unique.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$discord.name == x.discord.name,])
|
# missing.days <- nrow(missing.in.class[missing.in.class$unique.name == x.unique.name,])
|
||||||
|
|
||||||
## return the final score
|
## return the final score
|
||||||
data.frame(discord.name=x.discord.name,
|
data.frame(unique.name=x.unique.name,
|
||||||
part.grade=(base.score - missing.days * missed.question.penalty))
|
part.grade=(base.score))
|
||||||
}
|
}
|
||||||
|
|
||||||
tmp <- do.call("rbind", lapply(d$discord.name[d$num.calls >= questions.cutoff], gen.part.grade))
|
|
||||||
|
|
||||||
d[as.character(tmp$discord.name), "part.grade"] <- tmp$part.grade
|
tmp <- do.call("rbind", lapply(d$unique.name, gen.part.grade))
|
||||||
|
|
||||||
## next handle the folks *under* the median
|
d[as.character(tmp$unique.name), "part.grade"] <- tmp$part.grade
|
||||||
|
|
||||||
## 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$prop.asked), c("discord.name", "num.calls", "num.present",
|
d[sort.list(d$part.4point),]
|
||||||
"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.discord.name in d$discord.name) {
|
## for (x.unique.name in d$unique.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$discord.name == x.discord.name],
|
## d.print$UWNetID[d.print$unique.name == x.unique.name],
|
||||||
sep=""))
|
## sep=""))
|
||||||
}
|
## }
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
myuw <- read.csv("myuw-COM_482_A_autumn_2020_students.csv")
|
myuw <- read.csv("myuw-COMMLD_570_A_spring_2021_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]
|
||||||
|
|
||||||
|
|||||||
@@ -1,109 +1,25 @@
|
|||||||
|
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,5)]
|
d <- gs[,c(2,4)]
|
||||||
colnames(d) <- c("student.num", "discord.name")
|
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]}))
|
||||||
|
|
||||||
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))
|
||||||
|
|
||||||
call.list$day <- as.Date(call.list$timestamp)
|
table(call.list$unique_name[call.list$answered])
|
||||||
|
|
||||||
## 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$discord.name))
|
call.counts <- data.frame(table(call.list$unique.name))
|
||||||
colnames(call.counts) <- c("discord.name", "num.calls")
|
colnames(call.counts) <- c("unique.name", "num.calls")
|
||||||
|
|
||||||
d <- merge(d, call.counts, all.x=TRUE, all.y=TRUE, by="discord.name"); d
|
d <- merge(d, call.counts, all.x=TRUE, all.y=TRUE, by="unique.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()
|
|
||||||
|
|
||||||
|
|||||||
52
coldcall.py
52
coldcall.py
@@ -8,46 +8,47 @@ from csv import DictReader
|
|||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
import re
|
import re
|
||||||
import discord
|
|
||||||
|
|
||||||
class ColdCall():
|
class ColdCall():
|
||||||
def __init__ (self, course = ''):
|
def __init__ (self, record_attendance=True):
|
||||||
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.__set_filenames()
|
self.record_attendance = record_attendance
|
||||||
|
|
||||||
|
|
||||||
def __set_filenames(self):
|
|
||||||
# filenames
|
# filenames
|
||||||
self.__fn_studentinfo = f"data/{self.course}/student_information.tsv"
|
self.__fn_studentinfo = "data/student_information.tsv"
|
||||||
self.__fn_daily_calllist = f"data/{self.course}/call_list-{self.today}.tsv"
|
self.__fn_daily_calllist = f"data/call_list-{self.today}.tsv"
|
||||||
self.__fn_daily_attendance = f"data/{self.course}/attendance-{self.today}.tsv"
|
self.__fn_daily_attendance = f"data/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(f"./data/{self.course}/"):
|
for fn in listdir("./data/"):
|
||||||
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/{self.course}/{fn}", 'r') as f:
|
with open(f"./data/{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["discord_name"]] += 1
|
previous_questions[row["unique_name"]] += 1
|
||||||
|
|
||||||
return previous_questions
|
return previous_questions
|
||||||
|
|
||||||
def __get_preferred_name(self, selected_student):
|
def __get_preferred_names(self):
|
||||||
# translate the discord name into the preferred students name,
|
# translate the unique name into the preferred students name,
|
||||||
# if possible, otherwise return the discord name
|
# if possible, otherwise return the unique 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["discord_name"]] = row["name"]
|
preferred_names[row["Your username on the class Teams server"]] = row["Name you'd like to go by in class"]
|
||||||
|
|
||||||
if selected_student in preferred_names:
|
return(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
|
||||||
|
|
||||||
@@ -63,7 +64,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)
|
# print(weights) # DEBUG LINE
|
||||||
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):
|
||||||
@@ -82,7 +83,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(["discord_name", "timestamp", "answered", "assessment"]), file=f)
|
print("\t".join(["unique_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:
|
||||||
@@ -93,7 +94,8 @@ 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
|
||||||
self.__record_attendance(students_present)
|
if self.record_attendance:
|
||||||
|
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)
|
||||||
@@ -103,12 +105,8 @@ class ColdCall():
|
|||||||
coldcall_message = f"@{selected_student}, you're up!"
|
coldcall_message = f"@{selected_student}, you're up!"
|
||||||
return coldcall_message
|
return coldcall_message
|
||||||
|
|
||||||
def update_course(self, course_name):
|
|
||||||
self.course = course_name
|
|
||||||
self.__set_filenames()
|
|
||||||
|
|
||||||
# cc = ColdCall()
|
# 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))
|
||||||
|
|
||||||
|
|||||||
15
coldcallbot-manual.py
Executable file
15
coldcallbot-manual.py
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/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")
|
||||||
|
|
||||||
164
coldcallbot.py
164
coldcallbot.py
@@ -3,8 +3,6 @@
|
|||||||
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()
|
||||||
@@ -13,16 +11,13 @@ 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, voice_channel = 'Class Sessions'):
|
async def on_message(self, message):
|
||||||
if message.author == self.user:
|
if message.author == self.user:
|
||||||
return
|
return
|
||||||
|
|
||||||
if message.content.startswith('$next'):
|
if message.content.startswith('$next'):
|
||||||
if message.channel.category:
|
classroom = discord.utils.get(message.guild.voice_channels, name='Classroom Voice')
|
||||||
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]:
|
||||||
@@ -35,156 +30,8 @@ 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()
|
||||||
@@ -192,4 +39,5 @@ intents.members = True
|
|||||||
intents.presences = True
|
intents.presences = True
|
||||||
|
|
||||||
ccb = ColdCallBot(intents=intents)
|
ccb = ColdCallBot(intents=intents)
|
||||||
ccb.run(config.key)
|
ccb.run('CHANGEME')
|
||||||
|
|
||||||
|
|||||||
4
data/download_data.sh
Executable file
4
data/download_data.sh
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
wget 'https://docs.google.com/spreadsheets/d/FIXME/export?gid=FIXME&format=tsv' -O 'student_information.tsv'
|
||||||
|
|
||||||
155
flask_app/app.py
Normal file
155
flask_app/app.py
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
#!/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])
|
||||||
|
|
||||||
|
|
||||||
25
flask_app/static/main.css
Normal file
25
flask_app/static/main.css
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
23
flask_app/static/process_button.js
Normal file
23
flask_app/static/process_button.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
$(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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
41
flask_app/templates/cold_caller.html
Normal file
41
flask_app/templates/cold_caller.html
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<!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>
|
||||||
25
flask_app/templates/group_maker.html
Normal file
25
flask_app/templates/group_maker.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<!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>
|
||||||
19
flask_app/templates/shuffler.html
Normal file
19
flask_app/templates/shuffler.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!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>
|
||||||
71
flask_app/test.csv
Normal file
71
flask_app/test.csv
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
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,3 +0,0 @@
|
|||||||
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