diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..1ac516b --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[run] +omit = ./src/schedulebot.py, ./src/__init__.py.py, diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..3d15b60 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,16 @@ +language: python +python: + - "3.9" +# command to install dependencies +before_install: + - "pip install -U pip" + - "export PYTHONPATH=$PYTHONPATH:$(pwd)" + - "pip install coverage" + - "pip install coveralls" +install: + - pip install -r requirements.txt +# command to run tests +script: + - "coverage run --source 'src' -m pytest" +after_success: + coveralls diff --git a/README.md b/README.md index a955a71..86250c3 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,14 @@ ![Python v3.9](https://img.shields.io/badge/python-v3.9-blue) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![DOI](https://zenodo.org/badge/403393616.svg)](https://zenodo.org/badge/latestdoi/403393616) -![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/lyonva/ScheduleBot) -[![GitHub issues](https://img.shields.io/github/issues/lyonva/ScheduleBot)](https://github.com/lyonva/ScheduleBot/issues) -[![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/lyonva/ScheduleBot?include_prereleases)](https://github.com/lyonva/ScheduleBot/releases) -[![GitHub all releases](https://img.shields.io/github/downloads/lyonva/ScheduleBot/total)](https://github.com/lyonva/ScheduleBot/releases) +[![DOI](https://zenodo.org/badge/419116957.svg)](https://zenodo.org/badge/latestdoi/419116957) +[![Build Status](https://app.travis-ci.com/qchen59/ScheduleBot.svg?branch=main)](https://app.travis-ci.com/github/qchen59/ScheduleBot) +![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/qchen59/ScheduleBot) +[![GitHub issues](https://img.shields.io/github/issues/qchen59/ScheduleBot)](https://github.com/qchen59/ScheduleBot/issues) +[![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/qchen59/ScheduleBot?include_prereleases)](https://github.com/qchen59/ScheduleBot/releases) +[![GitHub all releases](https://img.shields.io/github/downloads/qchen59/ScheduleBot/total)](https://github.com/qchen59/ScheduleBot/releases) [![Platform](https://img.shields.io/badge/platform-discord-blue)](https://discord.com/) -[![Test cases](https://github.com/lyonva/ScheduleBot/actions/workflows/python-app.yml/badge.svg)](https://github.com/lyonva/ScheduleBot/actions/workflows/python-app.yml) -[![Code coverage](https://raw.githubusercontent.com/lyonva/ScheduleBot/main/docs/img/coverage.svg)](https://github.com/lyonva/ScheduleBot/actions/workflows/python-app.yml) +[![Coverage Status](https://coveralls.io/repos/github/qchen59/ScheduleBot/badge.svg?branch=main)](https://coveralls.io/github/qchen59/ScheduleBot?branch=main) # ScheduleBot @@ -70,7 +70,8 @@ The bot will ask you for the name of the type and your preferred times. ## Releases -- [All releases](https://github.com/lyonva/ScheduleBot/releases) +- [All releases](https://github.com/ +/ScheduleBot/releases) - Latest: [v0](https://github.com/lyonva/ScheduleBot/releases/tag/v0) ## Documentation diff --git a/requirements.txt b/requirements.txt index 2758169..5dc5a3c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ coverage>=5.0.1 coverage-badge lark pdoc3 +pandas \ No newline at end of file diff --git a/src/functionality/create_event_type.py b/src/functionality/create_event_type.py index fa8d308..30a49f3 100644 --- a/src/functionality/create_event_type.py +++ b/src/functionality/create_event_type.py @@ -162,7 +162,7 @@ def check(m): rows.append(line) continue else: - await channel.send("Inalid input, Time range is not changed.") + await channel.send("Invalid input, Time range is not changed.") rows.append(line) continue else: diff --git a/src/functionality/export_file.py b/src/functionality/export_file.py new file mode 100644 index 0000000..12d174b --- /dev/null +++ b/src/functionality/export_file.py @@ -0,0 +1,20 @@ +import os +import csv +import discord +from functionality.shared_functions import create_event_file + + +async def export_file(ctx): + channel = await ctx.author.create_dm() + print(ctx.author.id) + + def check(m): + return m.content is not None and m.channel == channel and m.author == ctx.author + + user_id = str(ctx.author.id) + + # Checks if the calendar csv file exists, and creates it if it does not + if not os.path.exists(os.path.expanduser("~/Documents") + "/ScheduleBot/Event/" + user_id + ".csv"): + create_event_file(user_id) + + await channel.send(file=discord.File(os.path.expanduser("~/Documents") + "/ScheduleBot/Event/" + user_id + ".csv")) diff --git a/src/functionality/import_file.py b/src/functionality/import_file.py new file mode 100644 index 0000000..5b2a7b4 --- /dev/null +++ b/src/functionality/import_file.py @@ -0,0 +1,105 @@ +import os +import csv +import discord +import pandas as pd +import tempfile +from discord import Attachment +from functionality.shared_functions import create_event_tree, create_type_tree, add_event_to_file, turn_types_to_string +from Event import Event +from parse.match import parse_period + +def verify_csv(data): + """ + Function: + verify_csv + Description: + Verifies that CSV data retrieved through pandas matches the expected format + Input: + data - A Pandas Dataframe of data pulled from a CSV + Output: + - True if the data matches the expectation, false otherwise + """ + + if data.columns[0] != "ID": + return False + if data.columns[1] != "Name": + return False + if data.columns[2] != "Start Date": + return False + if data.columns[3] != "End Date": + return False + if data.columns[4] != "Type": + return False + if data.columns[5] != "Notes": + return False + + return True + +def convert_time(old_str): + """ + Function: + convert_time + Description: + Converts a time string from YYYY-MM-DD HH:MM:SS format to mm/dd/yy hh:mm am/pm format + Input: + old_str - The string to be converted + Output: + - the converted string + """ + + new_str = old_str[5:7] + '/' + old_str[8:10] + '/' + old_str[2:4] + ' ' + + hour_int = int(old_str[11:13]) + if (hour_int >= 12): + ap = "pm" + hour_int = hour_int - 12 + else: + ap = "am" + + hour = str(hour_int) + if len(hour) == 1: + hour = '0' + hour + + new_str = new_str + hour + ':' + old_str[14:16] + ap + + return new_str + +async def import_file(ctx, client): + channel = await ctx.author.create_dm() + + def check(m): + return m.content is not None and m.channel == channel and m.author == ctx.author + + user_id = str(ctx.author.id) + + # Checks if the calendar csv file exists, and creates it if it does not + await channel.send("Please upload your file below.") + + # Loops until we receive a file. + while True: + event_msg = await client.wait_for("message", check=check) + + if len(event_msg.attachments) != 1: + await channel.send("No file detected. Please upload your your file below.\nYou can do this by dropping " + "the file directly into Discord. Do not write out the file contents in the message.") + else: + break + + temp_file = tempfile.TemporaryFile() + await event_msg.attachments[0].save(fp=temp_file.file, seek_begin=True, use_cached=False) + data = pd.read_csv(temp_file) + + if not verify_csv(data): + await channel.send("Unexpected CSV Format. Import has failed.") + return + + # creates an event tree if one doesn't exist yet. + create_event_tree(str(ctx.author.id)) + + for index, row in data.iterrows(): + time_period = parse_period(convert_time(row['Start Date']) + ' ' + convert_time(row['End Date'])) + current = Event(row['Name'], time_period[0], time_period[1], row['Type'], row['Notes']) + add_event_to_file(str(ctx.author.id), current) + + await channel.send("Your events were successfully added!") + diff --git a/src/functionality/shared_functions.py b/src/functionality/shared_functions.py index e446a95..00870ee 100644 --- a/src/functionality/shared_functions.py +++ b/src/functionality/shared_functions.py @@ -1,7 +1,7 @@ import os import csv from pathlib import Path -from Event import Event +from src.Event import Event from datetime import datetime @@ -147,7 +147,6 @@ def read_event_file(user_id): with open(os.path.expanduser("~/Documents") + "/ScheduleBot/Event/" + user_id + ".csv", "r") as calendar_lines: calendar_lines = csv.reader(calendar_lines, delimiter=",") rows = [] - # Stores the current row in an array of rows if the row is not a new-line character # This check prevents an accidental empty lines from being kept in the updated file for row in calendar_lines: @@ -168,9 +167,8 @@ def add_event_to_file(user_id, current): line_number = 0 rows = read_event_file(user_id) # If the file already has events - if len(rows) > 0: + if len(rows) > 1: for i in rows: - # Skips check with empty lines if len(i) > 0 and line_number != 0: @@ -182,7 +180,6 @@ def add_event_to_file(user_id, current): "", "", ) - # If the current Event occurs before the temp Event, insert the current at that position if current < temp_event: rows.insert(line_number, [""] + current.to_list()) @@ -193,22 +190,15 @@ def add_event_to_file(user_id, current): rows.insert(len(rows), [""] + current.to_list()) break line_number += 1 - - # Open current user's calendar file for writing - with open( - os.path.expanduser("~/Documents") + "/ScheduleBot/Event/" + user_id + ".csv", - "w", - newline="", - ) as calendar_file: - # Write to column headers and array of rows back to the calendar file - csvwriter = csv.writer(calendar_file) - csvwriter.writerows(rows) - # If the file has no events, add the current Event to the file else: - with open( - os.path.expanduser("~/Documents") + "/ScheduleBot/Event/" + user_id + ".csv", - "w", - newline="", - ) as calendar_file: - csvwriter = csv.writer(calendar_file) - csvwriter.writerow([""] + current.to_list()) + rows.insert(len(rows), [""] + current.to_list()) + # Open current user's calendar file for writing + with open( + os.path.expanduser("~/Documents") + "/ScheduleBot/Event/" + user_id + ".csv", + "w", + newline="", + ) as calendar_file: + # Write to column headers and array of rows back to the calendar file + csvwriter = csv.writer(calendar_file) + csvwriter.writerows(rows) + diff --git a/src/schedulebot.py b/src/schedulebot.py index 082df4d..4c988b1 100644 --- a/src/schedulebot.py +++ b/src/schedulebot.py @@ -11,6 +11,8 @@ from functionality.FindAvailableTime import find_avaialbleTime from functionality.delete_event_type import delete_event_type from functionality.DisplayFreeTime import get_free_time +from functionality.export_file import export_file +from functionality.import_file import import_file bot = commands.Bot(command_prefix="!") # Creates the bot with a command prefix of '!' bot.remove_command("help") # Removes the help command, so it can be created using Discord embed pages later @@ -38,6 +40,8 @@ async def help(ctx): em.add_field(name="day", value="Shows everything on your schedule for today", inline=False) em.add_field(name="typecreate", value="Creates a new event type", inline=True) em.add_field(name="typedelete", value="Deletes an event type", inline=True) + em.add_field(name="exportfile", value="Exports a CSV file of your events", inline=False) + em.add_field(name="importfile", value="Import events from a CSV file", inline=False) await ctx.send(embed=em) @@ -138,6 +142,35 @@ async def day(ctx): """ await get_highlight(ctx) +@bot.command() +async def exportfile(ctx): + """ + Function: + exportfile + Description: + Sends the user a CSV file containing their scheduled events. + Input: + ctx - Discord context window + Output: + - A CSV file sent to the context that contains a user's scheduled events. + """ + + await export_file(ctx) + +@bot.command() +async def importfile(ctx): + """ + Function: + importfile + Description: + Reads a CSV file containing events submitted by the user, and adds those events + Input: + ctx - Discord context window + Output: + - Events are added to a users profile. + """ + + await import_file(ctx, bot) # creating new event type @bot.command()