# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/03_canvas.ipynb.

# %% auto 0
__all__ = ['bcolors', 'CanvasGroup']

# %% ../nbs/api/03_canvas.ipynb 3
from canvasapi import Canvas
from github import Github
import canvasapi
import json
import requests
import time
import sys
import numpy as np
import pandas as pd
from io import StringIO

# %% ../nbs/api/03_canvas.ipynb 4
class bcolors:
    HEADER = '\033[95m'
    OKBLUE = '\033[94m'
    OKCYAN = '\033[96m'
    OKGREEN = '\033[92m'
    WARNING = '\033[93m'
    FAIL = '\033[91m'
    ENDC = '\033[0m'
    BOLD = '\033[1m'
    UNDERLINE = '\033[4m'

# %% ../nbs/api/03_canvas.ipynb 5
class CanvasGroup():
    def __init__(self,
                 credentials_fp = "", # credential file path. [Template of the credentials.json](https://github.com/FleischerResearchLab/CanvasGroupy/blob/main/nbs/credentials.json)
                 API_URL="https://canvas.ucsd.edu", # the domain name of canvas
                 course_id="", # Course ID, can be found in the course url
                 group_category="", # target group category (set) of interests
                 verbosity=1 # Controls the verbosity: 0 = Silent, 1 = print all messages
                ):
        "Initialize Canvas Group within a Group Set and its appropriate memberships"
        self.API_URL = API_URL
        self.canvas = None
        self.course = None
        self.group_category = None
        self.group_categories = None
        self.groups = None
        self.users = None
        self.email_to_canvas_id = None
        self.canvas_id_to_email = None
        self.email_to_name = None
        self.API_KEY = None
        self.github = None
        self.credentials_fp = None
        self.groups = None
        self.group_to_emails = None
        self.assignment = None
        self.verbosity = verbosity
        
        # initialize by the input parameter
        if credentials_fp != "":
            self.auth_canvas(credentials_fp)
        if course_id != "":
            self.set_course(course_id)
            self.get_group_categories()
        if group_category != "":
            self.set_group_category(group_category)
            
    def auth_canvas(self,
                    credentials_fp: str # the Authenticator key generated from canvas
                   ):
        "Authorize the canvas module with API_KEY"
        self.credentials_fp = credentials_fp
        with open(credentials_fp, "r") as f:
            credentials = json.load(f)
        self.API_KEY = credentials["Canvas Token"]
        self.canvas = Canvas(self.API_URL, self.API_KEY)
        # test authorization
        _ = self.canvas.get_activity_stream_summary()
        if self.verbosity != 0:
            print(f"{bcolors.OKGREEN}Authorization Successful!{bcolors.ENDC}")
        
    def set_course(self, 
                   course_id: int # the course id of the target course
                  ):
        "Set the target course by the course ID"
        self.course = self.canvas.get_course(course_id)
        if self.verbosity != 0:
            print(f"Course Set: {bcolors.OKGREEN} {self.course.name} {bcolors.ENDC}")
            print(f"Getting List of Users... This might take a while...")
        self.users = list(self.course.get_users(enrollment_type=['student']))
        if self.verbosity != 0:
            print(f"Users Fetch Complete! The course has {bcolors.OKBLUE}{len(self.users)}{bcolors.ENDC} students.")
        self.email_to_canvas_id = {}
        self.canvas_id_to_email = {}
        self.email_to_name = {}
        for u in self.users:
            try:
                self.email_to_canvas_id[u.email.split("@")[0]] = u.id
                self.canvas_id_to_email[u.id] = u.email.split("@")[0]
                self.email_to_name[u.email.split("@")[0]] = u.short_name
            except Exception:
                if self.verbosity != 0:
                    print(f"{bcolors.WARNING}Failed to Parse email and id"
                          f" for {bcolors.UNDERLINE}{u.short_name}{bcolors.ENDC}{bcolors.ENDC}")

    def link_assignment(self,
                        assignment_id: int # assignment id, found at the url of assignmnet tab
                       ) -> canvasapi.assignment.Assignment: # target assignment
        "Link the target assignment on canvas"
        assignment = self.course.get_assignment(assignment_id)
        if self.verbosity != 0:
            print(f"Assignment {bcolors.OKGREEN+assignment.name+bcolors.ENDC} Link!")
        self.assignment = assignment
        return assignment

    def post_grade(self,
                    student_id: int, # canvas student id of student. found in self.email_to_canvas_id
                    grade: float, # grade of that assignment
                    text_comment="", # text comment of the submission. Can feed
                    force=False, # whether force to post grade for all students. If False (default), it will skip post for the same score.
                  ) -> canvasapi.submission.Submission: # created submission
        "Post grade and comment to canvas to the target assignment"
        submission = self.assignment.get_submission(student_id)
        if not force and submission.score == grade:
            if self.verbosity != 0:
                print(f"Grade for {bcolors.OKGREEN+self.canvas_id_to_email[student_id]+bcolors.ENDC} did not change.\n"
                      f"{bcolors.OKCYAN}Skipped{bcolors.ENDC}.\n"
                     )
            return
        edited = submission.edit(
            submission={
                'posted_grade': grade
            }, comment={
                'text_comment': text_comment
            }
        )
        if self.verbosity != 0:
            print(f"Grade for {bcolors.OKGREEN+self.canvas_id_to_email[student_id]+bcolors.ENDC} Posted!")
        return edited

    def get_email_by_name(self,
                          name_fussy: str # search by first name or last name of a student
                         ) -> str: # email of a search student
        name_fussy = name_fussy.lower()
        for email, name in self.email_to_name.items():
            if name_fussy in name.lower():
                return email
        raise ValueError(f"Name {name_fussy} Not Found.")
        
        
    def set_group_category(self,
                           category_name: str # the target group category
                          ) -> canvasapi.group.GroupCategory: # target group category object
        _ = self.get_group_categories()
        try:
            self.group_category = self.group_categories[category_name]
        except KeyError:
            raise KeyError(f"{category_name} did not found in the group categories. "
                           f"Try to create one with CanvasGroup.create_group_category")
        if self.verbosity != 0:
            print(f"Setting Group Category... ")
        self.groups = list(self.group_category.get_groups())
        self.group_to_emails = {
            group.name: [
                u.login_id for u in list(group.get_users())
            ] for group in self.groups}
        if self.verbosity != 0:
            print(f"Group Category: {bcolors.OKGREEN+category_name+bcolors.ENDC} Set!")
        return self.group_category
    
    def get_groups(self,
                   category_name="" # the target group category. If not provided, will look for self.group_category
                  ) -> dict: # {group_name: [student_emails]}
        if category_name != "":
            self.set_group_category(category_name)
            return self.group_to_emails
        if self.group_category is None:
            raise ValueError("Group Category is not set")
        return self.group_to_emails
        
    def get_course(self):
        return self.course
    
    def get_group_categories(self) -> dict: # return a name / group category object
        "Grab all existing group categories (group set) in this course"
        categories = list(self.course.get_group_categories())
        self.group_categories = {cat.name: cat for cat in categories}
        return {cat.name: cat for cat in categories}
    
    def create_group_category(self,
                              params: dict # the parameter of canvas group category API @ [this link](https://canvas.instructure.com/doc/api/group_categories.html#method.group_categories.create)
                             ) -> canvasapi.group.GroupCategory: # the generated group category object
        "Create group category (group set) in this course"
        self.group_category = self.course.create_group_category(**params)
        return self.group_category
    
    def create_group(self,
                     params: dict, #the parameter of canvas group create API at [this link](https://canvas.instructure.com/doc/api/groups.html#method.groups.create)
                    ) -> canvasapi.group.Group: # the generated target group object
        "Create canvas group under the target group category"
        if self.group_category is None:
            raise ValueError("Have you specified or create a group category (group set)?")
        group = self.group_category.create_group(**params)
        if self.verbosity != 0:
            print(f"In Group Set: {bcolors.OKBLUE+self.group_category.name+bcolors.ENDC},")
            print(f"Group {bcolors.OKGREEN+params['name']+bcolors.ENDC} Created!")
        return group
    
    def join_canvas_group(self,
                          group: canvasapi.group.Group, # the group that students will join
                          group_members:[str], # list of group member's SIS Login (email prefix, before the @.)
                         ) -> [str]: # list of unsuccessful join
        "Add membership access of each group member into the group"
        unsuccessful_join = []
        for group_member in group_members:
            try:
                canvas_id = self.email_to_canvas_id[group_member]
                group.create_membership(canvas_id)
                if self.verbosity != 0:
                    print(f"Member {bcolors.OKGREEN}{group_member}{bcolors.ENDC} Joined group {bcolors.OKGREEN}{group.name}{bcolors.ENDC}")
            except KeyError as e:
                unsuccessful_join.append(group_member)
                print(f"Error adding student {bcolors.WARNING+group_member+bcolors.ENDC} \n into group {group.name}")
                print(e)
        return unsuccessful_join
    
    def fetch_username_from_quiz(self,
                                 quiz_id: int, # quiz id of the username quiz
                                 col_index=7, # canvas quiz generated csv's question field column index
                                ) -> dict: # {SIS Login ID: github username} dictionary
        "Fetch the GitHub user name from the canvas quiz"
        header = {'Authorization': 'Bearer ' + self.API_KEY}
        quiz = self.course.get_quiz(quiz_id)
        if self.verbosity != 0:
            print(f"Quiz: {bcolors.OKGREEN+quiz.title+bcolors.ENDC} "
                  f"fetch! \nGenerating Student Analaysis..."
                 )
        report = quiz.create_report("student_analysis")
        progress_url = report.progress_url
        completed = False
        while not completed:
            status = requests.get(progress_url, headers = header).json()
            if self.verbosity != 0:
                self._progress(status["completion"])
                time.sleep(0.1)
            if status["completion"] == 100:
                completed = True
        if self.verbosity != 0:
            print(f"\n{bcolors.OKGREEN}Report Generated!{bcolors.ENDC}")
        # use requests to download the file 
        file_url = quiz.create_report("student_analysis").file["url"]
        response = requests.get(file_url, headers=header)
        file = StringIO(response.content.decode())
        # use pandas to parse the response csv
        df = pd.read_csv(file, delimiter=",")
        col = list(df.columns)
        # rename column
        if self.verbosity != 0:
            print(f"The Question asked is {bcolors.OKBLUE}{col[col_index]}{bcolors.ENDC}. \n"
                  f"Make sure this is the correct question where you asked student for their GitHub id.\n"
                  f"If you need to change the index of columns, change the col_index argument of this call."
                 )
        col[col_index] = "GitHub Username"
        df.columns = col
        small = df[["id", "GitHub Username"]].copy()
        small["email"] = small["id"].apply(lambda x: self.canvas_id_to_email[x])
        small = small[["email", "GitHub Username"]].set_index("email")
        return small.to_dict()["GitHub Username"]
    
    def _check_single_github_username(self,
                              email:str, # Student email
                              github_username:str, # student input we want to test
                             ) -> bool: # whether the username is valid
        "Check a single GitHub username on GitHub"
        if self.credentials_fp is None:
            raise ValueError("Credentials not set. Set it via self.auth_canvas")
        if self.github is None:
            # if the GitHub object has not been initialized
            with open(self.credentials_fp, "r") as f:
                credentials = json.load(f)
            github_token = credentials["GitHub Token"]
            self.github = Github(github_token)
        try:
            self.github.get_user(github_username)
        except Exception as e:
            print(f"User: {bcolors.WARNING+github_username+bcolors.ENDC} Not Found on GitHub")
            return False
        return True

    def check_github_usernames(self,
                               github_usernames:dict, # {email: github username} of student inputs, generated from self.fetch_username_from_quiz
                               send_canvas_email=False, # whether send a reminder for students who have an invalid GitHub username
                               send_undone_reminder=False, # send quiz undone reminder using canvas email
                               quiz_url="", # include a quiz url in the conversation for student to quickly complete the quiz.
                              ) -> dict: # {email: github username} of unreasonable GitHub id
        "batch check GitHub username from student inputs."
        unsuccessful = {}
        for email, github_username in github_usernames.items():
            valid = self._check_single_github_username(email, github_username)
            if not valid:
                unsuccessful[email] = github_username
                if send_canvas_email:
                    self.create_conversation(
                        self.email_to_canvas_id[email],
                        subject="Unidentifiable GitHub Username",
                        body=(f"Hi {email}, \n Your GitHub Username: {github_username} "
                              f"is unidentifiable on github.com. \n Please complete the quiz GitHub Username Quiz again.\n"
                              f"{quiz_url} \n"
                              f"Thank You."
                             )
                    )
                    if self.verbosity != 0:
                        print(f"{bcolors.OKGREEN}Notification Sent!{bcolors.ENDC}")
        if send_undone_reminder:
            submitted = github_usernames.keys()
            for email in self.email_to_canvas_id.keys():
                if email not in submitted:
                    if self.verbosity != 0:
                        print(f"Student {bcolors.WARNING}{email}{bcolors.ENDC} did not"
                              f" submit their github username."
                             )
                    # means student did not submit the quiz
                    if send_canvas_email:
                        self.create_conversation(
                            self.email_to_canvas_id[email],
                            subject="GitHub Username Quiz Not Completed",
                            body=(f"Hi {email}, \n You did not complete the GitHub Quiz."
                                  f"\n Please complete the quiz GitHub Username Quiz ASAP\n"
                                  f"{quiz_url} \n"
                                  f"Thank You."
                                 )
                        )
                        if self.verbosity != 0:
                            print(f"{bcolors.OKGREEN}Notification Sent!{bcolors.ENDC}")
        return unsuccessful
    
    def _progress(self,
                  percentage:int # percentage of the progress
                 ):
        sys.stdout.write('\r')
        # the exact output you're looking for:
        sys.stdout.write("[%-20s] %d%%" % ('='*int(percentage//5), percentage))
        sys.stdout.flush()
    
    def assign_canvas_group(self,
                            group_name: str, # group name, display on canvas
                            group_members:[str], # list of group member's SIS Login
                            in_group_category: str, # specify which group category the group belongs to
                           ) -> (canvasapi.group.Group, [str]): # list of unsuccessful join
        "Create new groups and assign group member into the class in the `self.group_category`"
        self.set_group_category(in_group_category)
        group = self.create_group({"name": group_name})
        unsuccessful_join = self.join_canvas_group(group, group_members)
        if self.verbosity != 0:
            print(f"Group {bcolors.OKGREEN+group_name+bcolors.ENDC} created!")
        return group, unsuccessful_join
    
    def create_conversation(self,
                            recipients:int, #  recipient ids. These may be user ids or course/group ids prefixed with ‘course_’ or ‘group_’ respectively.
                            subject:str, # subject of the conversation
                            body:str, # The message to be sent
                           ) -> canvasapi.conversation.Conversation: # created conversation
        "Create a conversation with the target user"
        conv = self.canvas.create_conversation(
            [recipients],
            body=body,
            subject=subject,
            context_code=f"course_{self.course.id}",
        )
        return conv

