]> code.communitydata.science - coldcallbot-discord.git/commitdiff
Merge branch 'master' of code.communitydata.science:coldcallbot-discord master
authorBenjamin Mako Hill <mako@atdot.cc>
Wed, 5 Jan 2022 04:52:27 +0000 (13:52 +0900)
committerBenjamin Mako Hill <mako@atdot.cc>
Wed, 5 Jan 2022 04:52:27 +0000 (13:52 +0900)
README
assessment_and_tracking/compute_final_case_grades.R
assessment_and_tracking/track_enrolled.R
assessment_and_tracking/track_participation.R
coldcall.py
coldcallbot-manual.py [new file with mode: 0755]
data/download_data.sh [new file with mode: 0755]

diff --git a/README b/README
index da52a4dbf64a7fe32a8bd33164c48fc315a784d1..733a2e8c1ad42012471a70bf8e5bb9735ffb69d2 100644 (file)
--- a/README
+++ b/README
@@ -1,14 +1,27 @@
 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:
 
     $ pip3 install discord
 
 module available in PyPi and installable like:
 
     $ pip3 install discord
 
-I don't have details on how I set up my own Discord bot and/or invited
-it to my server but I hope you'll add to this file as you do this and
-figure out what needs to happen.
+Setting up the Bot
+=====================================
+
+The documentation for the `discord` python package
+(https://discordpy.readthedocs.io/en/latest/discord.html) does a good
+job explaining how to set up a Discord bot with your server. Follow
+the steps there, with one important exception:
+
+1. On the "Bot" tab in the discord application configuration page you
+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)
+into coldcallbot.py. Pass it as the argument to `ccb.run()`.
+
+
 
 Using the Cold Call Bot
 ======================================
 
 Using the Cold Call Bot
 ======================================
@@ -31,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
 detailed weights as it goes, and it will record data into files in the
 
 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
index 60a60f38df53d7bf798c3ceed00a92cbffb186fb..b26270b6be7809661ee16581ad2eb73bd62b2ac2 100644 (file)
@@ -1,36 +1,23 @@
 ## load in the data
 #################################
 
 ## load in the data
 #################################
 
-case.sessions  <- 15
-myuw <- read.csv("myuw-COM_482_A_autumn_2020_students.csv", stringsAsFactors=FALSE)
+myuw <- read.csv("myuw-COMMLD_570_A_spring_2021_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))
-missed.question.penalty <- (50/3.3) * 0.2 ## 1/5 of a full point on the GPA scale
+question.grades <- c("GOOD"=100, "FAIR"=100-(50/3.3), "WEAK"=100-(50/(3.3)*2))
 
 source("../assessment_and_tracking/track_participation.R")
 setwd("case_grades")
 
 
 source("../assessment_and_tracking/track_participation.R")
 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)
-prop.table(table(call.list.full$assessment))
-table(call.list.full$answered)
-prop.table(table(call.list.full$answered))
+table(call.list$assessment)
+prop.table(table(call.list$assessment))
+table(call.list$answered)
+prop.table(table(call.list$answered))
 
 
-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)])
+total.questions.asked <- nrow(call.list)
 
 ## generate grades
 ##########################################################
 
 ## generate grades
 ##########################################################
@@ -39,81 +26,47 @@ d$part.grade <- NA
 
 ## print the median number of questions for (a) everybody and (b)
 ## people that have been present 75% of the time
 
 ## 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
 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.discord.name) {
-    q.scores <- question.grades[call.list$assessment[call.list$discord.name == x.discord.name]]
+gen.part.grade <- function (x.unique.name) {
+    q.scores <- question.grades[call.list$assessment[call.list$unique.name == x.unique.name]]
     base.score <- mean(q.scores, na.rm=TRUE)
 
     ## number of missing days
     base.score <- mean(q.scores, na.rm=TRUE)
 
     ## 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,
-               part.grade=(base.score - missing.days * missed.question.penalty))
+    data.frame(unique.name=x.unique.name,
+               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
-
-## 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
+tmp <- do.call("rbind", lapply(d$unique.name, gen.part.grade))
 
 
-## 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)]
+d[as.character(tmp$unique.name), "part.grade"] <- tmp$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
 d$part.4point <-round((d$part.grade / (50/3.3)) - 2.6, 2)
 
 
 ## 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$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")]
+d[sort.list(d$part.4point),]
 
 
 ## 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")],
            by.x="student.num", by.y="StudentNo")
 write.csv(d.print, file="final_participation_grades.csv")
 
 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.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=""))
-}
+## 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=""))
+## }
index 563384d98a739fac2e6d9d8e2f2071f952f03d56..f0d0fcbd7ad610d0d0a196f7b0d244e152fdc6f0 100644 (file)
@@ -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")
 
 ## these are students who dropped the class (should be empty)
 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
 
 ## 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..
 
 ## 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]
 
 
index 9a51084d2f5effd1cb2d30dbf02f4efe5fae60cd..28b8a4e8d03712fc0fe70ec586ef06ce620f1307 100644 (file)
+setwd("~/online_communities/coldcallbot/data/")
+
 library(ggplot2)
 library(data.table)
 
 gs <- read.delim("student_information.tsv")
 library(ggplot2)
 library(data.table)
 
 gs <- read.delim("student_information.tsv")
-d <- gs[,c(2,5)]
-colnames(d) <- c("student.num", "discord.name")
+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]}))
 
 
-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
 call.list.full <- call.list
 call.list[!call.list$answered,]
 call.list <- call.list[call.list$answered,]
 
 
 ## 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$discord.name))
-colnames(call.counts) <- c("discord.name", "num.calls")
-
-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")
+call.counts <- data.frame(table(call.list$unique.name))
+colnames(call.counts) <- c("unique.name", "num.calls")
 
 
-dev.off()
+d <- merge(d, call.counts, all.x=TRUE, all.y=TRUE, by="unique.name"); d
 
 
index 190584453c10253981f4341f5cd13f771c70948a..1ba96a582b24104b2c35b4a225eab89c553a8fef 100644 (file)
@@ -8,19 +8,21 @@ from csv import DictReader
 
 import os.path
 import re
 
 import os.path
 import re
-import discord
 
 class ColdCall():
 
 class ColdCall():
-    def __init__ (self):
+    def __init__ (self, record_attendance=True):
         self.today = str(datetime.date(datetime.now()))
         # how much less likely should it be that a student is called upon?
         self.today = str(datetime.date(datetime.now()))
         # how much less likely should it be that a student is called upon?
-        self.weight = 2 
+        self.weight = 2
+        self.record_attendance = record_attendance
 
         # 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"
 
 
         # 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()
+        
     def __load_prev_questions(self):
         previous_questions = defaultdict(int)
 
     def __load_prev_questions(self):
         previous_questions = defaultdict(int)
 
@@ -29,21 +31,24 @@ class ColdCall():
                 with open(f"./data/{fn}", 'r') as f:
                     for row in DictReader(f, delimiter="\t"):
                         if not row["answered"] == "FALSE":
                 with open(f"./data/{fn}", 'r') as f:
                     for row in DictReader(f, delimiter="\t"):
                         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):
-        # translate the discord name into the preferred students name,
-        # if possible, otherwise return the discord name
+
+    def __get_preferred_names(self):
+        # translate the unique name into the preferred students name,
+        # if possible, otherwise return the unique name
 
         preferred_names = {}
         with open(self.__fn_studentinfo, 'r') as f:
             for row in DictReader(f, delimiter="\t"):
 
         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 Discord server"]] = row["Name you'd like to go by in class"]
+                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[selected_student]
+        return(preferred_names)
+        
+    def __get_preferred_name(self, selected_student):
+        if selected_student in self.preferred_names:
+            return self.preferred_names[selected_student]
         else:
             return None
 
         else:
             return None
 
@@ -59,7 +64,7 @@ class ColdCall():
                 weights[s] = weights[s] / self.weight
 
         # choose one student from the weighted list
                 weights[s] = weights[s] / self.weight
 
         # 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]
 
     def __record_attendance(self, students_present):
         return choices(list(weights.keys()), weights=list(weights.values()), k=1)[0]
 
     def __record_attendance(self, students_present):
@@ -78,7 +83,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:
         # 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(["discord_name", "timestamp", "answered", "assessment"]), file=f)
+                print("\t".join(["unique_name", "timestamp", "answered", "assessment"]), file=f)
 
         # open for appending the student
         with open(self.__fn_daily_calllist, "a") as f:
 
         # open for appending the student
         with open(self.__fn_daily_calllist, "a") as f:
@@ -89,7 +94,8 @@ class ColdCall():
         selected_student = self.__select_student_from_list(students_present)
 
         # record the called-upon student in the right place
         selected_student = self.__select_student_from_list(students_present)
 
         # 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)
 
         preferred_name = self.__get_preferred_name(selected_student)
         self.__record_coldcall(selected_student)
 
         preferred_name = self.__get_preferred_name(selected_student)
@@ -100,7 +106,7 @@ class ColdCall():
         return coldcall_message
 
 # cc = ColdCall()
         return coldcall_message
 
 # 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"]
 # print(cc.coldcall(test_student_list))
 
 # 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))
 
diff --git a/coldcallbot-manual.py b/coldcallbot-manual.py
new file mode 100755 (executable)
index 0000000..a4268ea
--- /dev/null
@@ -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")
+
diff --git a/data/download_data.sh b/data/download_data.sh
new file mode 100755 (executable)
index 0000000..8687509
--- /dev/null
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+wget 'https://docs.google.com/spreadsheets/d/FIXME/export?gid=FIXME&format=tsv' -O 'student_information.tsv'
+

Community Data Science Collective || Want to submit a patch?