diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml new file mode 100644 index 000000000..5a98bc708 --- /dev/null +++ b/.github/workflows/black.yml @@ -0,0 +1,10 @@ +name: Black + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: psf/black@stable \ No newline at end of file diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 7f7894456..c21502db8 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -13,7 +13,7 @@ name: "CodeQL" on: push: - branches: [ main, group25-cloud-deployment ] + branches: [ main] pull_request: # The branches below must be a subset of the branches above branches: [ main ] diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 172706798..a3308bc6a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,14 +12,15 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up Python 3.9 + - name: Set up Python 3.11 uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: 3.11 - name: Install dependencies run: | python -m pip install --upgrade pip pip install pylint + sudo apt-get install xvfb libfontconfig wkhtmltopdf if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with pylint run: | @@ -28,12 +29,15 @@ jobs: run: | cat << EOF > .env DATABASE_URL=${{ secrets.DATABASE_URL }} + CALENDAR_ID=${{ secrets.CALENDAR_ID }} + CALENDAR_PATH=${{ secrets.CALENDAR_PATH }} + CALENDAR_ICS=${{ secrets.CALENDAR_ICS }} EOF - name: Test with pytest - run: pytest + run: pytest --tb=line - name: Generate coverage - run: pytest --cov-config=.coveragerc --cov --cov-report=xml - - name: "Upload coverage to Codecov" - uses: codecov/codecov-action@v2 - with: - fail_ci_if_error: true + run: pytest --tb=line --cov-config=.coveragerc --cov --cov-report=xml + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 1bc65b9d8..87e129704 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -12,10 +12,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up Python 3.9 + - name: Set up Python 3.11 uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: 3.11 - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 31741925f..ba5919d44 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -9,19 +9,24 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up Python 3.9 + - name: Set up Python 3.11 uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: 3.11 - name: Install dependencies run: | python -m pip install --upgrade pip pip install pylint + sudo apt-get install xvfb libfontconfig wkhtmltopdf if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Create env file run: | cat << EOF > .env + TOKEN=${{ secrets.TOKEN }} DATABASE_URL=${{ secrets.DATABASE_URL }} + CALENDAR_ID=${{ secrets.CALENDAR_ID }} + CALENDAR_PATH=${{ secrets.CALENDAR_PATH }} + CALENDAR_ICS=${{ secrets.CALENDAR_ICS }} EOF - name: Test with pytest - run: pytest + run: pytest --tb=line diff --git a/.gitignore b/.gitignore index b6e47617d..9a1d86fbd 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,14 @@ dmypy.json # Pyre type checker .pyre/ +bot.env +bot.env + +#Google calendar stuff +credentials.json +token.json +ical.ics +calendar.pdf +dpytest_0.dat +!/test/data/calendar.pdf +!/test/data/ical.ics \ No newline at end of file diff --git a/.pylintrc b/.pylintrc index 6ae597f53..01151360b 100644 --- a/.pylintrc +++ b/.pylintrc @@ -74,85 +74,18 @@ confidence= # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use "--disable=all --enable=classes # --disable=W". -disable=print-statement, - parameter-unpacking, - unpacking-in-except, - old-raise-syntax, - backtick, - long-suffix, - old-ne-operator, - old-octal-literal, - import-star-module-level, - non-ascii-bytes-literal, - raw-checker-failed, +disable=raw-checker-failed, bad-inline-option, locally-disabled, file-ignored, suppressed-message, useless-suppression, + consider-using-enumerate, + undefined-loop-variable, + unused-variable, deprecated-pragma, use-symbolic-message-instead, - apply-builtin, - basestring-builtin, - buffer-builtin, - cmp-builtin, - coerce-builtin, - execfile-builtin, - file-builtin, - long-builtin, - raw_input-builtin, - reduce-builtin, - standarderror-builtin, - unicode-builtin, - xrange-builtin, - coerce-method, - delslice-method, - getslice-method, - setslice-method, - no-absolute-import, - old-division, - dict-iter-method, - dict-view-method, - next-method-called, - metaclass-assignment, - indexing-exception, - raising-string, - reload-builtin, - oct-method, - hex-method, - nonzero-method, - cmp-method, - input-builtin, - round-builtin, - intern-builtin, - unichr-builtin, - map-builtin-not-iterating, - zip-builtin-not-iterating, - range-builtin-not-iterating, - filter-builtin-not-iterating, - using-cmp-argument, - eq-without-hash, - div-method, - idiv-method, - rdiv-method, - exception-message-attribute, - invalid-str-codec, - sys-max-int, - bad-python3-import, - deprecated-string-function, - deprecated-str-translate-call, - deprecated-itertools-function, - deprecated-types-field, - next-method-defined, - dict-items-not-iterating, - dict-keys-not-iterating, - dict-values-not-iterating, - deprecated-operator-function, - deprecated-urllib-function, - xreadlines-attribute, - deprecated-sys-function, - exception-escape, - comprehension-escape, + no-member, C0114, # Missing module docstring (missing-module-docstring) C0115, C0116, @@ -167,10 +100,10 @@ disable=print-statement, R0915, # Too many statements (85/50) (too-many-statements) R0903, # Too few public methods (0/2) (too-few-public-methods) R0904, # Too many public methods (24/20) (too-many-public-methods) + R1732, E0401, C0413, R0801, - R0201, C0301, # Line too long (127/120) (line-too-long) C0411, # (wrong-import-order) W0611, # Unused join imported from os.path (unused-import) @@ -647,5 +580,5 @@ min-public-methods=2 # Exceptions that will emit a warning when being caught. Defaults to # "BaseException, Exception". -overgeneral-exceptions=BaseException, - Exception +overgeneral-exceptions=builtins.BaseException, + builtins.Exception diff --git a/CHAT.md b/CHAT.md new file mode 100644 index 000000000..555173222 --- /dev/null +++ b/CHAT.md @@ -0,0 +1,9 @@ +# ClassMateBot Developers Chat + + +This is a screenshot of what our developers' communication channel looks like. + +## Means of Communication + +As a team, we used Discord voice channels and text channels to communicate about ClassMateBot. +We used ClassMateBot in this channel to test its functionality locally. diff --git a/CITATION.md b/CITATION.md index 9629c65d8..9d17b29b4 100644 --- a/CITATION.md +++ b/CITATION.md @@ -1,5 +1,21 @@ Cite as +Brandon Walia, Nicholas Foster, Robert Kenney, Nathan Kohen. (2023, October 19). nfoster1492/ClassMateBot-1 (Version v4). +Zenodo. https://doi.org/10.5281/zenodo.5673334 + +``` +@software{nfoster1492/ClassMateBot-1, + title = {nfoster1492/ClassMateBot-1 (Version v4)}, + DOI = {10.5281/zenodo.10023404}, + author = {Brandon Walia, Nicholas Foster, Robert Kenney, Nathan Kohen}, + publisher = {Zenodo}, + year = {2023}, + month = {October} +} +``` + +For Version 3 Cite as + Emily Tracey, Leila Moran, Shraddha Mishra, Jonathan Nguyen, & Peeyush Taneja. (2021, November 10). CSC510-Group-25/ClassMateBot (Version v3). Zenodo. https://doi.org/10.5281/zenodo.5673334 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6563fc349..c5a1e9c63 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ The following is a set of guidelines for contributing to ClassMate Bot. These [Code of Conduct](#code-of-conduct) -[I don't want to read this whole thing, I just have a question!!!](#i-dont-want-to-read-this-whole-thing-i-just-have-a-question) +[Quick Support](#quick-support) [How Can I Contribute?](#how-can-i-contribute) * [Pull Requests](#pull-requests) @@ -19,22 +19,24 @@ The following is a set of guidelines for contributing to ClassMate Bot. These * [Git Commit Messages](#git-commit-messages) * [Python Style Guide](#python-style-guide) +[Governal Policies](#governal-policies) + ## Code of Conduct This project and everyone participating in it is governed by the [ClassMate Bot Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to any of the original team members listed at the bottom of [README.md](README.md). -## I don't want to read this whole thing I just have a question!!! +## Quick Support -Reach out to one of the main contributors on Discord using their IDs: -* Emily Tracey: etracey@ncsu.edu -* Jonathan Nguyen: jhnguye4@ncsu.edu -* Leila Moran: lmmoran@ncsu.edu -* Peeysh Taneja: ptaneja@ncsu.edu -* Shraddha Mishra: smishra9@ncsu.edu +Support Email: +* *classmatebot5@gmail.com* -*Note: Due to the dynamic nature of Discord IDs, these may change without prior notice on this page.* +Reach out to one of the main contributors: +* Robert Kenney: *rpkenney@ncsu.edu* +* Brandon Walia: *bswalia@ncsu.edu* +* Nathan Kohen: *nrkohen@ncsu.edu* +* Nicholas Foster: *nsfoster@ncsu.edu* -We do not have an official message board at this time, however, we plan to have one if it will help future contributors! +We would love to help with whatever issues you have or questions about contributing to ClassMateBot! ## How Can I Contribute? @@ -42,21 +44,21 @@ We do not have an official message board at this time, however, we plan to have The process described here has several goals: -- Maintain the projects quality +- Maintain the project's quality - Fix problems that are important to users -- Enable a sustainable system for the projects maintainers to review contributions +- Enable a sustainable system for the projects maintainers' to review contributions Please follow these steps to have your contribution reviewed by the maintainers: 1. Include a clear and descriptive title. -2. Include a description of the change. +2. Include a detailed description of the change. While the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted. ### Adding Commands - Commands can be added in the form of Cogs. View hello.py as a simple example of how a Cog can be added. +Commands can be added in the form of Cogs. View hello.py as a simple example of how a Cog can be added. The basic structure is as follows: @@ -75,7 +77,7 @@ For more information on how to use cogs, refer to the [Cogs Page](https://discor For more information on the API of discord.py you can use the [API Reference Page](https://discordpy.readthedocs.io/en/stable/api.html) -For general Knowledge of discord.py use the [Documentation Page](https://discordpy.readthedocs.io/en/latest/index.html) +For general knowledge of discord.py use the [Documentation Page](https://discordpy.readthedocs.io/en/latest/index.html) ## Reporting Bugs This section guides you through submitting a bug report for ClassMateBot. @@ -118,4 +120,10 @@ Changes to ClassMateBot Python code should conform to [Google Python Style Guide All Python code is linted with Pylint. Ensure that before you commit any changes, your code passes all default pylint checks. Pylint can be installed with `pip install pylint`. +All Python code is formatted using Black. Ensure that before you commit any changes, you run Black on all updated files and ensure that the code passes the Black checks. + +### Governal Policies + +All contributions will be carefully reviewed by our entire development team. We will discuss the advantages and disadvantages of adding the change, and a final decision will be decided based on a vote, with all members' votes having equal weight. + *This document is adapted from the [Atom Code of Conduct](https://github.com/atom/atom/blob/master/CONTRIBUTING.md#code-of-conduct)* diff --git a/Dependencies.md b/Dependencies.md new file mode 100644 index 000000000..ce95c3431 --- /dev/null +++ b/Dependencies.md @@ -0,0 +1,33 @@ +All libraries used with their licenses and links to repo. all are mandatory +| Library | License | Link to Source| +-----------|--------|---------------| +aiohttp| Apache| https://github.com/aio-libs/aiohttp +async-timeout| Apache| https://github.com/aio-libs/async-timeout +attrs| MIT| https://github.com/python-attrs/attrs +black| MIT |https://github.com/psf/black +coverage| MIT |https://github.com/sueastside/python-coverage +coverage-badge |MIT |https://github.com/tj-actions/coverage-badge-py +discord| MIT| https://github.com/Rapptz/discord.py +discord.py |MIT| https://github.com/Rapptz/discord.py +dpytest| MIT| https://github.com/CraftSpider/dpytest +jishaku| MIT| https://github.com/Gorialis/jishaku +packaging| Apache| https://github.com/pypa/packaging +py |GPL-compatible| www.python.org +pdfkit |MIT |https://github.com/foliojs/pdfkit +pytest |MIT |https://github.com/pytest-dev/pytest +pytest-asynico |Apache| https://github.com/pytest-dev/pytest-asyncio +pytest-cov |MIT| https://github.com/pytest-dev/pytest-cov +python-dateutil |Apache| https://github.com/dateutil/dateutil +python-dotenv| BSD-3-Clause "New" or "Revised"| https://github.com/theskumar/python-dotenv +psycopg2-binar| LGPL| https://github.com/psycopg/psycopg2 +better-profanity |MIT| https://github.com/snguyenthanh/better_profanity +requests| Apache| https://github.com/psf/requests +pandas| BSD-3-Clause |https://github.com/pandas-dev/pandas +google-api-core |Apache |https://github.com/googleapis/python-api-core +google-api-python-client |Apache| https://github.com/googleapis/google-api-python-client +google-auth| Apache| https://github.com/googleapis/google-auth-library-python +google-auth-httplib2| Apache| https://github.com/googleapis/google-auth-library-python-httplib2 +google-auth-oauthlib |Apache| https://github.com/googleapis/google-auth-library-python-oauthlib +googleapis-common-protos| Apache |https://github.com/googleapis/api-common-protos +PyPDF2| BSD Clause| https://github.com/py-pdf/pypdf +vobject |Apache| https://github.com/eventable/vobject diff --git a/LICENSE b/LICENSE index fd2417b42..2c0d1c8f4 100644 --- a/LICENSE +++ b/LICENSE @@ -2,6 +2,10 @@ MIT License Copyright (c) 2021 SE21-Team2 +Copyright for portions of project ClassMateBot-1 are held by SE21-Team2,2021 +as part of project ClassMateBot. All other copyright for project ClassMateBot-1 +are held by nfoster1492, 2023. + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights diff --git a/Proj2/Project2Changes.md b/Proj2/Project2Changes.md new file mode 100644 index 000000000..f5c970b70 --- /dev/null +++ b/Proj2/Project2Changes.md @@ -0,0 +1,40 @@ +# Project 2 Changes +## Additions +### Assignments/Grading +An essential part of any course is the delivery of assignments and the grading of these. This feature allows instructors to add assignments into the server, and assign grades to them based on grading categories. Both the students and the instructor are able to have an easy interface to view their grades, and do calculations based on them. For example, the instrcutor can view a grading breakdown for a given grading category or asssignment, and the students can do calculations to determine how well they need to do on a given assignment to maintain a desired grade. + +Details on these additions can be found here: [Assignments Docs](https://github.com/nfoster1492/ClassMateBot-1/tree/main/docs/Assignments) | [Grading Docs](https://github.com/nfoster1492/ClassMateBot-1/tree/main/docs/Grades) + +### Calendar Integration +Although being able to set deadlines on discord is useful, a good number of students would like to have those deadlines on their calendar. This feature allows deadlines to be automically added to a Google calendar that the students can see as well as functionality to move those calendar events to other formats that the student may prefer. After the instructor has added events to the calendar students will be able to download these events either as a .ics file they can upload to outlook or other calendar software, or they can download the events as a pdf. Lastly, the bot will check the calendar daily for events due that day and ping everyone in general of the items that are due that day. + +Details on these additions can be found here: [Calendar Docs](https://github.com/nfoster1492/ClassMateBot-1/tree/main/docs/Calendar) + +### Database +#### grades table +This table was required for the grading functionality to hold grades for students. +|guild_id|member_name|assignment_id|grade| +|-------|---------------|---|--| +|.|.|.|.| + +#### grade_categories table +This table was required for the instructor to be able to assign different weights to different types of grades (ex: Exams, HW, Projects) +|id|guild_id|category_name|category_weight| +|--|----------|--------|--| +|.|.|.|.| + +#### assignments table +This table was required for the instructor to be able to create assignments and assign them a weight based on the category +|id|guild_id|category_id|assignment_name|points| +|--|--------|--|-----------|--| +|.|.|.|.| + +## Modifications + +### Database +Supporting the functionality added in this project required the modification of the existing `reminders` table. To better reflect how we leveraged this table, the `homework_name` column was renamed `reminder_name`. + +## Documentation +This version includes a more detailed [Installation Guide](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/installation.md) including the new database and calendar setup instructions. + +This version includes a [troubleshooting guide](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/troubleshoot.md) to help future developers or users resolve issues. diff --git a/Proj2/README.md b/Proj2/README.md new file mode 100644 index 000000000..0d3c0c86f --- /dev/null +++ b/Proj2/README.md @@ -0,0 +1,111 @@ +# Project 2 Rubric + +Prepare a markdown with **three** columns: + +- Column1 has all the following points PLUS all the points from the + [Software Sustainability Evaluation](https://docs.google.com/forms/d/e/1FAIpQLSf0ccsVdN-nXJCHLluJ-hANZlp8rDKgprJa0oTYiLZSDxh3DA/viewform). +- Column2 is your self-assessment. For each items, score yourself zero (none), one (a litte), two (somewhat), three (a lot). +- Column3 is for any links you are adding to support your claim in column two. +- At the top, show the sum of column2, + + +| Notes| Score (SUM 240)| Evidence| +|------|------|---------| +| Video | 3 | On README.md (https://www.youtube.com/watch?v=8CfEfXnoKMs) +|workload is spread over the whole team (one team member is often Xtimes more productive than the others but nevertheless, here is a track record that everyone is contributing a lot) | 3 | in GH +| Number of commits|3|in GH| +| Number of commits: by different people|3| in GH +| Issues reports: there are **many**|3| https://github.com/nfoster1492/ClassMateBot-1/issues +| Issues are being closed|3| https://github.com/nfoster1492/ClassMateBot-1/issues?q=is%3Aissue+is%3Aclosed +| DOI badge: exists |3| on README +|Docs: doco generated , format not ugly |3| In Docs folder| +|Docs: what: point descriptions of each class/function (in isolation) |3| In Docs folder| +|Docs: how: for common use cases X,Y,Z mini-tutorials showing worked examples on how to do X,Y,Z|3|In Docs Folder| +|Docs: why: docs tell a story, motivate the whole thing, deliver a punchline that makes you want to rush out and use the thing|3|On README.md +|Docs: short video, animated, hosted on your repo. That convinces people why they want to work on your code.|3|On README.md +| Use of version control tools|3| We use GitHub for this and the trunk flow| +|Use of style checkers |3|Pylint| +| Use of code formatters. |3|Black| +| Use of syntax checkers. |3|Pylint| +| Use of code coverage |3|Codecov: badge on readme| +| other automated analysis tools|3|workflows/main.yml and CodeQL| +| test cases exist|3|test/test_bot.py| +| test cases are routinely executed|3|workflows/pytest.yml: see actions| +| the files CONTRIBUTING.md lists coding standards and lots of tips on how to extend the system without screwing things up|3| In Contributing.md| +| issues are discussed before they are closed|3|We discussed issues in discord, and summarized on the issue comments| +| Chat channel: exists|3| We had a chat channel, see CHAT.md| +| test cases:.a large proportion of the issues related to handling failing cases.|3|see our issues page| +| evidence that the whole team is using the same tools: everyone can get to all tools and files|3| we use VSCode and everyone has the software installed via the tools in the installation guide +| evidence that the whole team is using the same tools (e.g. config files in the repo, updated by lots of different people)|3|| +| evidence that the whole team is using the same tools (e.g. tutor can ask anyone to share screen, they demonstrate the system running on their computer)|3|| +| evidence that the members of the team are working across multiple places in the code base|3|We wrote and debugged the tests of the cogs that we were not working on. For example if most of my work was in grades, I worked on testing and debugging calendar| +|short release cycles |2| visible in commit history | +|Does your website and documentation provide a clear, high-level overview of your software?|3|In Readme and docs files| +|Does your website and documentation provide a clear, high-level overview of your software?|3|In Readme and docs files| +|Do you publish case studies to show how your software has been used by yourself and others?|2|In Readme and the images in the docs. The video and the gifs are great examples of this| +|Is the name of your project/software unique?|3|| +|Is your project/software name free from trademark violations?|3|| +|Is your software available as a package that can be deployed without building it?|0| +|Is your software available for free?|3|Yes, all code availabe on github| +|Is your source code publicly available to download, either as a downloadable bundle or via access to a source code repository?|3|Yes, all code availabe on github +|Is your software hosted in an established, third-party repository likeGitHub (https://github.com), BitBucket (https://bitbucket.org),LaunchPad (https://launchpad.net) orSourceForge (https://sourceforge.net)?|3|Yes, all code availabe on github| +|Is your documentation clearly available on your website or within your software?|3|In Readme and docs files| +|Does your documentation include a "quick start" guide, that provides a short overview of how to use your software with some basic examples of use?|1|Installation guide included in Readme| +|If you provide more extensive documentation, does this provide clear, step-by-step instructions on how to deploy and use your software?|3|Instructions in docs folder| +|Do you provide a comprehensive guide to all your software’s commands, functions and options?|3|Instructions in docs folder| +|Do you provide troubleshooting information that describes the symptoms and step-by-step solutions for problems and error messages?|2|see docs/troubleshoot.md +|If your software can be used as a library, package or service by other software, do you provide comprehensive API documentation?|0| +|Do you store your documentation under revision control with your source code?|3|Yes, in docs folder and Readme| +|Do you publish your release history e.g. release data, version numbers, key features of each release etc. on your web site or in your documentation?|2|see badge on github readme and the changes in project 2 docs file| +|Does your software describe how a user can get help with using your software?|3|Info in Contributing, and on the bottom of readme with a help email| +|Does your website and documentation describe what support, if any, you provide to users and developers?|3|Info in Contributing, nd on the bottom of readme with a help email| +|Does your project have an e-mail address or forum that is solely for supporting users?|3|At the bottom of readme| +|Are e-mails to your support e-mail address received by more than one person?|3| All of our developers check the help email| +|Does your project have a ticketing system to manage bug reports and feature requests?|3|In issues and projects tab| +|Is your project's ticketing system publicly visible to your users, so they can view bug reports and feature requests?|3|In issues and projects tab| +|Is your software’s architecture and design modular?|3|Yes, users can add functionality through cogs| +|Does your software use an accepted coding standard or convention?|2|Consistent standards followed across codebase, conventions are defined in our pylint and black configuration| +|Does your software allow communications using open communications protocols?|2|we use http to communicate with google calendar| +|Is your software cross-platform compatible?|3| +|Does your software adhere to appropriate accessibility conventions or standards?|1| +|Does your documentation adhere to appropriate accessibility conventions or standards?|1| +|Is your source code stored in a repository under revision control?|3|All code in github| +|Is each source code release a snapshot of the repository?|2| +|Are releases tagged in the repository?|2|Releases tagged in github| +|Is there a branch of the repository that is always stable? (i.e. tests always pass, code always builds successfully)|1|our final release is stable, but during development we did not have this| +|Do you back-up your repository?|3|On github and local machines| +|Do you provide publicly-available instructions for building your software from the source code?|3|In Readme and installation guide| +|Can you build, or package, your software using an automated tool?|0| +|Do you provide publicly-available instructions for deploying your software?|3|In Readme and installation guide| +|Does your documentation list all third-party dependencies?|3|In requirements| +|Does your documentation list the version number for all third-party dependencies?|3|In requirements| +|Does your software list the web address, and licences for all third-party dependencies and say whether the dependencies are mandatory or optional?|3|see dependencies.md +|Can you download dependencies using a dependency management tool or package manager?|3|Using pip install requirements| +|Do you have tests that can be run after your software has been built or deployed to show whether the build or deployment has been successful?|3|Testing through github actions| +|Do you have an automated test suite for your software? |3|Testing through github actions| +|Do you have a framework to periodically (e.g. nightly) run your tests on the latest version of the source code?|0| +|Do you use continuous integration, automatically running tests whenever changes are made to your source code?|3|On-push testing through github actions| +|Are your test results publicly visible?|3|Test results in actions build tab| +|Are all manually-run tests documented?|0| +|Does your project have resources (e.g. blog, Twitter, RSS feed, Facebook page, wiki, mailing list) that are regularly updated with information about your software?|0| +|Does your website state how many projects and users are associated with your project?|0| +|Do you provide success stories on your website?|1|the gifs show successful runs of the code +|Do you list your important partners and collaborators on your website?|3|Contributors on Readme| +|Do you list your project's publications on your website or link to a resource where these are available?|0| +|Do you list third-party publications that refer to your software on your website or link to a resource where these are available?|0| +|Can users subscribe to notifications to changes to your source code repository?|3| +|If your software is developed as an open source project (and, not just a project developing open source software), do you have a governance model?|3|laid out in docs +|Do you accept contributions (e.g. bug fixes, enhancements, documentation updates, tutorials) from people who are not part of your project?|3| +|Do you have a contributions policy?|3|In Contributing| +|Is your contributions' policy publicly available?|3|In Contributing on Github| +|Do contributors keep the copyright/IP of their contributions? |3|MIT license is used +|Does your website and documentation clearly state the copyright owners of your software and documentation?|3|all files say copytright informations and doi is listed on readme +|Does each of your source code files include a copyright statement?|3|At top of each .py file| +|Does your website and documentation clearly state the licence of your software?|3|MIT License +|Is your software released under an open source licence?|3|MIT Liscense| +|Is your software released under an OSI-approved open-source licence?|3|MIT Liscense| +|Does each of your source code files include a licence header?|0| +|Do you have a recommended citation for your software?|3|Citation.md| +|Does your website or documentation include a project roadmap (a list of project and development milestones for the next 3, 6 and 12 months)?|3|In projects tab| +|Does your website or documentation describe how your project is funded, and the period over which funding is guaranteed?|0| +|Do you make timely announcements of the deprecation of components, APIs, etc.?|0| diff --git a/README.md b/README.md index e4bfa2649..dda98a33b 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,26 @@ -

+

ClassMate Bot

                    -[![DOI](https://zenodo.org/badge/426029685.svg)](https://zenodo.org/badge/latestdoi/426029685) -![GitHub release (latest by date)](https://img.shields.io/github/v/release/CSC510-Group-25/ClassMateBot) + +[![DOI](https://zenodo.org/badge/690393967.svg)](https://zenodo.org/doi/10.5281/zenodo.10023403) +![Release Version](https://img.shields.io/github/v/release/nfoster1492/ClassMateBot-1) [![License](https://img.shields.io/badge/license-MIT-orange.svg)](https://opensource.org/licenses/MIT) ![Python](https://img.shields.io/badge/python-v3.8+-blue.svg) -![Lines of code](https://img.shields.io/tokei/lines/github/CSC510-Group-25/ClassMateBot) -![GitHub repo size](https://img.shields.io/github/repo-size/CSC510-Group-25/ClassMateBot) -[![Codacy Badge](https://app.codacy.com/project/badge/Grade/c1c81dbe55ac4b9ca27533ec23a8493d)](https://www.codacy.com/gh/CSC510-Group-25/ClassMateBot/dashboard?utm_source=github.com&utm_medium=referral&utm_content=CSC510-Group-25/ClassMateBot&utm_campaign=Badge_Grade) -[![codecov](https://codecov.io/gh/CSC510-Group-25/ClassMateBot/branch/main/graph/badge.svg?token=B1E9S0HRRC)](https://codecov.io/gh/CSC510-Group-25/ClassMateBot) -[![GitHub pull requests](https://img.shields.io/github/issues-pr/CSC510-Group-25/ClassMateBot)](https://github.com/CSC510-Group-25/ClassMateBot/pulls) -[![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed/CSC510-Group-25/ClassMateBot)](https://github.com/CSC510-Group-25/ClassMateBot/pulls?q=is%3Apr+is%3Aclosed) -[![GitHub issues](https://img.shields.io/github/issues/CSC510-Group-25/ClassMateBot)](https://github.com/CSC510-Group-25/ClassMateBot/issues) -[![GitHub closed issues](https://img.shields.io/github/issues-closed/CSC510-Group-25/ClassMateBot)](https://github.com/CSC510-Group-25/ClassMateBot/issues?q=is%3Aissue+is%3Aclosed) -[![GitHub issues by-label](https://img.shields.io/github/issues-raw/CSC510-Group-25/ClassMateBot/bug?color=red&label=Active%20bugs)](https://github.com/CSC510-Group-25/ClassMateBot/issues?q=is%3Aissue+is%3Aopen+label%3Abug) -[![GitHub closed issues by-label](https://img.shields.io/github/issues-closed-raw/CSC510-Group-25/ClassMateBot/bug?color=green&label=Squished%20bugs)](https://github.com/CSC510-Group-25/ClassMateBot/issues?q=is%3Aissue+label%3Abug+is%3Aclosed) -[![Python application](https://github.com/CSC510-Group-25/ClassMateBot/actions/workflows/main.yml/badge.svg)](https://github.com/CSC510-Group-25/ClassMateBot/actions/workflows/main.yml) -[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/CSC510-Group-25/ClassMateBot/Python%20application)](https://github.com/CSC510-Group-25/ClassMateBot/actions/workflows/main.yml) -[![Pytest](https://github.com/CSC510-Group-25/ClassMateBot/actions/workflows/pytest.yml/badge.svg)](https://github.com/CSC510-Group-25/ClassMateBot/actions/workflows/pytest.yml) -[![Pylint](https://github.com/CSC510-Group-25/ClassMateBot/actions/workflows/pylint.yml/badge.svg)](https://github.com/CSC510-Group-25/ClassMateBot/actions/workflows/pylint.yml) +![GitHub repo size](https://img.shields.io/github/repo-size/nfoster1492/ClassMateBot-1) +[![GitHub issues](https://img.shields.io/github/issues/nfoster1492/ClassMateBot-1)](https://github.com/nfoster1492/ClassMateBot-1) +[![GitHub closed issues](https://img.shields.io/github/issues-closed/nfoster1492/ClassMateBot-1)](https://github.com/nfoster1492/ClassMateBot-1/issues?q=is%3Aissue+is%3Aclosed) +[![GitHub issues by-label](https://img.shields.io/github/issues-raw/nfoster1492/ClassMateBot-1/bug?color=red&label=Active%20bugs)](https://github.com/nfoster1492/ClassMateBot-1/issues?q=is%3Aissue+is%3Aopen+label%3Abug) +[![GitHub closed issues by-label](https://img.shields.io/github/issues-closed-raw/nfoster1492/ClassMateBot-1/bug?color=green&label=Squished%20bugs)](https://github.com/nfoster1492/ClassMateBot-1/issues?q=is%3Aissue+label%3Abug+is%3Aclosed) +[![Python application](https://github.com/nfoster1492/ClassMateBot-1/actions/workflows/main.yml/badge.svg)](https://github.com/nfoster1492/ClassMateBot-1/actions/workflows/main.yml) +[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/nfoster1492/ClassMateBot-1/Python%20application)](https://github.com/nfoster1492/ClassMateBot-1/actions/workflows/main.yml) +[![Pytest](https://github.com/nfoster1492/ClassMateBot-1/actions/workflows/pytest.yml/badge.svg?branch=main)](https://github.com/nfoster1492/ClassMateBot-1/actions/workflows/pytest.yml) +[![Pylint](https://github.com/nfoster1492/ClassMateBot-1/actions/workflows/pylint.yml/badge.svg)](https://github.com/nfoster1492/ClassMateBot-1/actions/workflows/pylint.yml) +[![Black](https://github.com/nfoster1492/ClassMateBot-1/actions/workflows/black.yml/badge.svg)](https://github.com/nfoster1492/ClassMateBot-1/actions/workflows/black.yml) +[![codecov](https://codecov.io/gh/nfoster1492/ClassMateBot-1/graph/badge.svg?token=GOZIMU10AY)](https://codecov.io/gh/nfoster1492/ClassMateBot-1) +[![Maintainability](https://api.codeclimate.com/v1/badges/260d558f17ae5e1027e5/maintainability)](https://codeclimate.com/github/nfoster1492/ClassMateBot-1/maintainability) +![Discord](https://img.shields.io/discord/1143966088695124110) + @@ -36,29 +37,17 @@ :: Contributors :: - Wiki + Wiki + :: + Troubleshooting

---- - -Project 3 demo video: - -Watch it on youtube! https://www.youtube.com/watch?v=KBBqUkOTuSo - -Watch it here: - -https://user-images.githubusercontent.com/89357283/144730581-46b85493-3f5c-4c65-9d3f-fbcfffc729a3.mp4 - - - ---- - - -https://user-images.githubusercontent.com/89809302/140442405-e043564d-c946-4116-bb79-e9f8a341da21.mp4 - -Watch on YouTube +### New Features 2 minute demo +[![why contribute video](https://img.youtube.com/vi/8CfEfXnoKMs/0.jpg)](https://www.youtube.com/watch?v=8CfEfXnoKMs) +### Why contribute? +[![why contribute video](https://img.youtube.com/vi/zSehBZcbPKU/0.jpg)](https://www.youtube.com/watch?v=zSehBZcbPKU) --- @@ -66,8 +55,7 @@ https://user-images.githubusercontent.com/89809302/140442405-e043564d-c946-4116- This project helps to improve the life of students, TAs and teachers by automating many mundane tasks which are sometimes done manually. ClassMateBot is a discord bot made in Python and could be used for any discord channel. -This is Project 3 for the ClassMateBot. Changes are marked below and listed in [Project 3 Changes](https://github.com/CSC510-Group-25/ClassMateBot/blob/group25-documentation/docs/Project3Changes.md). - +This is Project 2 for team 1 in Fall 2023. Changes are marked below and listed [here](https://github.com/nfoster1492/ClassMateBot-1/blob/main/Proj2/Project2Changes.md) --- ## :orange_book: Description @@ -97,9 +85,9 @@ Voting for projects is a common occurence that many students must endure. With t -### 3 - Deadline Reminder **(Updated in Project 3!)** +### 3 - Deadline Reminder -***Deadlines now send automatic messgages for assignments that are due that day and assignments that are due within the hour!*** Check out more [here](https://github.com/CSC510-Group-25/ClassMateBot/blob/group25-documentation/docs/Project3Changes.md) +***Deadlines now send automatic messgages for assignments that are due that day and assignments that are due within the hour!*** Check out more [here](https://github.com/nfoster1492/ClassMateBot-1/blob/group25-documentation/docs/Project3Changes.md) The next important thing our project covers is the Deadline reminder feature of our bot. Students may add homeworks, links, and due dates using the system, and then view their daily or weekly dues with ease. No longer will a student be vulnerable to those odd submission times like 3:00 PM. See homework specific to one class, due today, or due this week! @@ -139,7 +127,7 @@ Moreover, the group creation feature allows members of the group to join a priva -### 6 - Question and Answer **(Updated in Project 3!)** +### 6 - Question and Answer A common usage for our current class Discord is for students to ask questions about the course. It is helpful for the questions to be numbered and for the answers to be attached to the question so it be easily found. Some students may feel more comfortable asking and answering questions anonymously. It is also helpful for users to know if the question is answered by a student or instructor. This feature keeps the questions and answers all in one channel so it does not clutter other channels and can be more easily viewed. ![image](https://user-images.githubusercontent.com/32313919/140245147-80aca7ff-525a-4cfb-89d0-df5d10afd691.png) @@ -153,24 +141,45 @@ An essential part of studying is going over questions related to the exam topics -### 8 - Polling **(NEW in Project 3!)** +### 8 - Polling Users can now create polls! Instructor can ask for student opinions. - + - + We can also create Quiz Poll\ - + + + + +### 9 - Assignments/Grading **(New in Project 2)** +An essential part of any course is the delivery of assignments and the grading of these. This feature allows instructors to add assignments into the server, and assign grades to them based on grading categories. Both the students and the instructor are able to have an easy interface to view their grades, and do calculations based on them. For example, the instrcutor can view a grading breakdown for a given grading category or asssignment, and the students can do calculations to determine how well they need to do on a given assignment to maintain a desired grade. + +Instructors can input their syllabus! + + +Instructors can add assignments and grades to the system + + +Students can check their grades + - +### 10 - Calendar **(New in Project 2)** +Although being able to set deadlines on discord is useful, a good number of students would like to have those deadlines on their calendar. This feature allows deadlines to be automically added to a Google calendar that the students can see as well as functionality to move those calendar events to other formats that the student may prefer. After the instructor has added events to the calendar students will be able to download these events either as a .ics file they can upload to outlook or other calendar software, or they can download the events as a pdf. Lastly, the bot will check the calendar daily for events due that day and ping everyone in general of the items that are due that day. + +Instructor can add events to the calendar! + + +And students can download the calendar as iCal so they can use in their preffered app! + --- ## :arrow_down: Installation -To install and run the ClassMate Bot, follow instructions in the [Installation Guide](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/installation.md). +To install and run the ClassMate Bot, follow instructions in the [Installation Guide](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/installation.md). --- @@ -178,118 +187,166 @@ To install and run the ClassMate Bot, follow instructions in the [Installation G General commands in bot.py -:open_file_folder: [$whitelist command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/ProfanityFilter/whitelist.md) **(New Command in Project 3)** +:open_file_folder: [$whitelist command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/ProfanityFilter/whitelist.md) -:open_file_folder: [$dewhitelist command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/ProfanityFilter/dewhitelist.md) **(New Command in Project 3)** +:open_file_folder: [$dewhitelist command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/ProfanityFilter/dewhitelist.md) -:open_file_folder: [$toggleFilter command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/ProfanityFilter/togglefilter.md) **(New Command in Project 3)** +:open_file_folder: [$toggleFilter command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/ProfanityFilter/togglefilter.md) For the newComer.py file -:open_file_folder: [$verify command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/Verification/verify.md) +:open_file_folder: [$verify command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Verification/verify.md) For the voting.py file -:open_file_folder: [$projects command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/Voting/projects.md) +:open_file_folder: [$projects command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Voting/projects.md) -:open_file_folder: [$vote command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/Voting/vote.md) +:open_file_folder: [$vote command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Voting/vote.md) -For the deadline.py file **(Updated in Project 3!)** +For the deadline.py file -:open_file_folder: [$add homework command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/Reminders/add_homework.md) **(Updated in Project 3!)** +:open_file_folder: [$due date command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Reminders/due_date.md) **(Modified Command in Project 2)** -:open_file_folder: [$change reminder due date command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/Reminders/change_reminder_due_date.md) **(Updated in Project 3!)** +:open_file_folder: [$change reminder due date command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Reminders/change_reminder_due_date.md) -:open_file_folder: [$clear all reminders command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/Reminders/clear_all_reminders.md) +:open_file_folder: [$clear all reminders command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Reminders/clear_all_reminders.md) -:open_file_folder: [$course due command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/Reminders/course_due.md) +:open_file_folder: [$course due command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Reminders/course_due.md) -:open_file_folder: [$delete reminder command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/Reminders/delete_reminder.md) +:open_file_folder: [$delete reminder command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Reminders/delete_reminder.md) -:open_file_folder: [$due this week command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/Reminders/due_this_week.md) **(Updated in Project 3!)** +:open_file_folder: [$due this week command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Reminders/due_this_week.md) -:open_file_folder: [$duetoday command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/Reminders/due_today.md) **(Updated in Project 3!)** +:open_file_folder: [$duetoday command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Reminders/due_today.md) -:open_file_folder: [$listreminders command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/Reminders/list_reminders.md) +:open_file_folder: [$listreminders command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Reminders/list_reminders.md) -:open_file_folder: [$timenow command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/Reminders/timenow.md) +:open_file_folder: [$timenow command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Reminders/timenow.md) -:open_file_folder: [$overdue command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/Reminders/overdue.md) **(New Command in Project 3)** +:open_file_folder: [$overdue command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Reminders/overdue.md) -:open_file_folder: [$clearoverdue command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/Reminders/clearoverdue.md) **(New Command in Project 3)** +:open_file_folder: [$clearoverdue command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Reminders/clearoverdue.md) For the pinning.py file -:open_file_folder: [$pin command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/PinMessage/pin.md) +:open_file_folder: [$pin command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/PinMessage/pin.md) -:open_file_folder: [$unpin command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/PinMessage/unpin.md) +:open_file_folder: [$unpin command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/PinMessage/unpin.md) -:open_file_folder: [$pinnedmessages command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/PinMessage/pinnedmessages.md) +:open_file_folder: [$pinnedmessages command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/PinMessage/pinnedmessages.md) -:open_file_folder: [$updatepin command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/PinMessage/updatepin.md) +:open_file_folder: [$updatepin command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/PinMessage/updatepin.md) For the groups.py file -:open_file_folder: [$startupgroups command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/Groups/startupgroups.md) +:open_file_folder: [$startupgroups command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Groups/startupgroups.md) -:open_file_folder: [$reset command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/Groups/reset.md) +:open_file_folder: [$reset command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Groups/reset.md) -:open_file_folder: [$connect command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/Groups/connect.md) +:open_file_folder: [$connect command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Groups/connect.md) -:open_file_folder: [$groups command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/Groups/groups.md) +:open_file_folder: [$groups command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Groups/groups.md) -:open_file_folder: [$group command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/Groups/group.md) +:open_file_folder: [$group command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Groups/group.md) -:open_file_folder: [$join command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/Groups/join.md) +:open_file_folder: [$join command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Groups/join.md) -:open_file_folder: [$leave command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/Groups/leave.md) +:open_file_folder: [$leave command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Groups/leave.md) -For the qanda.py file **(Updated in Project 3!)** +For the qanda.py file -:open_file_folder: [$ask command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/QandA/ask.md) **(Updated in Project 3)** +:open_file_folder: [$ask command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/QandA/ask.md) -:open_file_folder: [$answer command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/QandA/answer.md) **(Updated in Project 3)** +:open_file_folder: [$answer command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/QandA/answer.md) -:open_file_folder: [$DALLAF command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/QandA/DALLAF.md) **(New Command in Project 3)** +:open_file_folder: [$DALLAF command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/QandA/DALLAF.md) -:open_file_folder: [$getAnswersFor command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/QandA/getAnswersFor.md) **(New Command in Project 3)** +:open_file_folder: [$getAnswersFor command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/QandA/getAnswersFor.md) -:open_file_folder: [$deleteAllQA command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/QandA/deleteAllQA.md) **(New Command in Project 3)** +:open_file_folder: [$deleteAllQA command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/QandA/deleteAllQA.md) -:open_file_folder: [$deleteQuestion command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/QandA/deleteQuestion.md) **(New Command in Project 3)** +:open_file_folder: [$deleteQuestion command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/QandA/deleteQuestion.md) -:open_file_folder: [$archiveQA command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/QandA/archiveQA.md) **(New Command in Project 3)** +:open_file_folder: [$archiveQA command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/QandA/archiveQA.md) -:open_file_folder: [$spooky command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/QandA/spooky.md) **(New Command in Project 3)** +:open_file_folder: [$spooky command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/QandA/spooky.md) -:open_file_folder: [$allChannelGhosts command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/QandA/allChannelGhosts.md) **(New Command in Project 3)** +:open_file_folder: [$allChannelGhosts command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/QandA/allChannelGhosts.md) -:open_file_folder: [$channelGhost command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/QandA/channelGhost.md) **(New Command in Project 3)** +:open_file_folder: [$channelGhost command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/QandA/channelGhost.md) -:open_file_folder: [$unearthZombies command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/QandA/unearthZombies.md) **(New Command in Project 3)** +:open_file_folder: [$unearthZombies command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/QandA/unearthZombies.md) -:open_file_folder: [$reviveGhost command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/QandA/reviveGhost.md) **(New Command in Project 3)** +:open_file_folder: [$reviveGhost command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/QandA/reviveGhost.md) For the reviewqs.py file -:open_file_folder: [$addQuestion command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/ReviewQs/addQuestion.md) +:open_file_folder: [$addQuestion command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/ReviewQs/addQuestion.md) + +:open_file_folder: [$getQuestion command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/ReviewQs/getQuestion.md) + + +For the polling.py file + +:open_file_folder: [$poll command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Polling/poll.md) -:open_file_folder: [$getQuestion command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/ReviewQs/getQuestion.md) +:open_file_folder: [$quizpoll command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Polling/quizpoll.md) +For the calendar.py file -For the polling.py file **(All new in Project 3!)** +:open_file_folder: [$getiCalDownload command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Calendar/getiCalDownload.md) **(New Command in Project 2)** -:open_file_folder: [$poll command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/Polling/poll.md) +:open_file_folder: [$getPdfDownload command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Calendar/getPdfDownload.md) **(New Command in Project 2)** -:open_file_folder: [$quizpoll command](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/docs/Polling/quizpoll.md) +:open_file_folder: [$subscribeCalendar command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Calendar/subscribeCalendar.md) **(New Command in Project 2)** + +:open_file_folder: [$removeCalendar command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Calendar/removeCalendar.md) **(New Command in Project 2)** + +:open_file_folder: [$clearCalendar command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Calendar/clearCalendar.md) **(New Command in Project 2)** + +:open_file_folder: [$addCalendarEvent command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Calendar/addCalendarEvent.md) **(New Command in Project 2)** + +For the grades.py file + +:open_file_folder: [$add_grade_category command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Grades/add_grade_category.md) **(New Command in Project 2)** + +:open_file_folder: [$edit_grade_category command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Grades/edit_grade_category.md) **(New Command in Project 2)** + +:open_file_folder: [$delete_grade_category command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Grades/delete_grade_category.md) **(New Command in Project 2)** + +:open_file_folder: [$categories command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Grades/categories.md) **(New Command in Project 2)** + +:open_file_folder: [$grade_report_assignment command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Grades/grade_report_assignment.md) **(New Command in Project 2)** + +:open_file_folder: [$grade_report_category command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Grades/grade_report_category.md) **(New Command in Project 2)** + +:open_file_folder: [$input_grades command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Grades/input_grades.md) **(New Command in Project 2)** + +:open_file_folder: [$grade command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Grades/grade.md) **(New Command in Project 2)** + +:open_file_folder: [$gradebycategory command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Grades/gradebycategory.md) **(New Command in Project 2)** + +:open_file_folder: [$gradeforclass command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Grades/gradeforclass.md) **(New Command in Project 2)** + +:open_file_folder: [$graderequired command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Grades/graderequired.md) **(New Command in Project 2)** + +:open_file_folder: [$graderequiredforclass command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Grades/graderequiredforclass.md) **(New Command in Project 2)** + +For the assignments.py file + +:open_file_folder: [$add_assignment command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Assignments/add_assignment.md) **(New Command in Project 2)** + +:open_file_folder: [$edit_assignment command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Assignments/edit_assignment.md) **(New Command in Project 2)** + +:open_file_folder: [$delete_assignment command](https://github.com/nfoster1492/ClassMateBot-1/blob/main/docs/Assignments/delete_assignment.md) **(New Command in Project 2)** --- ## :earth_americas: Future Scope -[Future scope](https://github.com/CSC510-Group-25/ClassMateBot/projects/3) suggested tasks are located in the Projects tab. +[Future scope](https://github.com/nfoster1492/ClassMateBot-1/projects/1) suggested tasks are located in the Projects tab. --- @@ -297,6 +354,18 @@ For the polling.py file **(All new in Project 3!)** ## :pencil2: Contributors +### :pencil2: (Fall 2023) + + + + + + + + + +
Project 2
Brandon Walia

Nathan Kohen


Nicholas Foster


Robert Kenney

+ ### :pencil2: (Fall 2021) @@ -331,6 +400,11 @@ For the polling.py file **(All new in Project 3!)**
+## :grinning: Support +Support Email: *classmatebot5@gmail.com*
+Please reach out with any questions about ClassMateBot!
+Our team is always monitoring the support email address to provide the quickest and easiest support possible.
+ --- diff --git a/bot.py b/bot.py index e2cfc67ae..eb8ccad0d 100644 --- a/bot.py +++ b/bot.py @@ -8,7 +8,8 @@ from discord import Intents from dotenv import load_dotenv from discord.ext.commands import Bot, has_permissions, CheckFailure -#from better_profanity import profanity + +# from better_profanity import profanity import profanity_helper @@ -29,6 +30,7 @@ # Set all bot commands to begin with $ bot = Bot(intents=intents, command_prefix="$") + # ------------------------------------------------------------------------------------------------------------------ # Function: on_guild_join() # Description: Activates when the bot joins a new guild, prints the name of the server it joins and the names of all members @@ -42,33 +44,44 @@ @bot.event async def on_guild_join(guild): for channel in guild.text_channels: - if channel.permissions_for(guild.me).send_messages and channel.name == "general": - - if 'instructor-commands' not in guild.text_channels: - await guild.create_text_channel('instructor-commands') + if ( + channel.permissions_for(guild.me).send_messages + and channel.name == "general" + ): + if "instructor-commands" not in guild.text_channels: + await guild.create_text_channel("instructor-commands") await channel.send("instructor-commands channel has been added!") - if 'q-and-a' not in guild.text_channels: - await guild.create_text_channel('q-and-a') + if "q-and-a" not in guild.text_channels: + await guild.create_text_channel("q-and-a") await channel.send("q-and-a channel has been added!") - if 'reminders' not in guild.text_channels: - await guild.create_text_channel('reminders') + if "reminders" not in guild.text_channels: + await guild.create_text_channel("reminders") await channel.send("reminders channel has been added!") if discord.utils.get(guild.roles, name="verified") is None: - await guild.create_role(name="verified", colour=discord.Colour(0x2ecc71), - permissions=discord.Permissions.general()) + await guild.create_role( + name="verified", + colour=discord.Colour(0x2ECC71), + permissions=discord.Permissions.general(), + ) if discord.utils.get(guild.roles, name="unverified") is None: - await guild.create_role(name="unverified", colour=discord.Colour(0xe74c3c), - permissions=discord.Permissions.none()) + await guild.create_role( + name="unverified", + colour=discord.Colour(0xE74C3C), + permissions=discord.Permissions.none(), + ) unverified = discord.utils.get(guild.roles, name="unverified") # unverified members can only see/send messages in general channel until they verify overwrite = discord.PermissionOverwrite() - overwrite.update(send_messages = True) - overwrite.update(read_messages = True) + overwrite.update(send_messages=True) + overwrite.update(read_messages=True) await channel.set_permissions(unverified, overwrite=overwrite) if discord.utils.get(guild.roles, name="Instructor") is None: - await guild.create_role(name="Instructor", colour=discord.Colour(0x3498db), - permissions=discord.Permissions.all()) + await guild.create_role( + name="Instructor", + colour=discord.Colour(0x3498DB), + permissions=discord.Permissions.all(), + ) # Assign Verified role to Guild owner leader = guild.owner leadrole = get(guild.roles, name="verified") @@ -84,7 +97,7 @@ async def on_guild_join(guild): for member in guild.members: if member != guild.owner: await member.add_roles(unverified, reason=None, atomic=True) - await channel.send("To verify yourself, use \"$verify \"") + await channel.send('To verify yourself, use "$verify "') # ------------------------------------------------------------------------------------------------------------------ @@ -110,8 +123,8 @@ async def on_ready(): for filename in os.listdir("./cogs"): if filename.endswith(".py"): - bot.load_extension(f"cogs.{filename[:-3]}") - bot.load_extension("jishaku") + await bot.load_extension(f"cogs.{filename[:-3]}") + await bot.load_extension("jishaku") await bot.change_presence( activity=discord.Activity( @@ -126,10 +139,11 @@ async def on_ready(): profanity_helper.command_list.append(n.name) profanity_helper.loadwhitelist() - #profanity_helper.loadDefaultWhitelist() + # profanity_helper.loadDefaultWhitelist() print("READY!") + ########################### # Function: on_message # Description: run when a message is sent to a discord the bot occupies @@ -138,7 +152,7 @@ async def on_ready(): ########################### @bot.event async def on_message(message): - ''' run on message sent to a channel ''' + """run on message sent to a channel""" # allow messages from test bot # NOTE from Group25: Not sure if this is actually being used anywhere. if message.author.bot and message.author.id == 889697640411955251: @@ -152,22 +166,23 @@ async def on_message(message): # CHECK CHANNELS. # don't want to accidentally censor a word before it can be whitelisted if profanity_helper.filtering: - if cname != 'instructor-commands': - nustr = message.content.replace('"','') - if profanity_helper.helpChecker(nustr) or profanity_helper.helpChecker(message.content): - #if profanity_helper.helpChecker(message.content): + if cname != "instructor-commands": + nustr = message.content.replace('"', "") + if profanity_helper.helpChecker(nustr) or profanity_helper.helpChecker( + message.content + ): + # if profanity_helper.helpChecker(message.content): badmsg = "Please do not use inappropriate language in this server. Your message:\n" badmsg += profanity_helper.helpCensor(nustr) - #badmsg += profanity_helper.helpCensor(message.content) - #if message.author.bot: # if the author is the bot - #return + # badmsg += profanity_helper.helpCensor(message.content) + # if message.author.bot: # if the author is the bot + # return await message.author.send(badmsg) await message.delete() return await bot.process_commands(message) - ########################### # Function: on_message_edit # Description: run when a user edits a message @@ -177,18 +192,22 @@ async def on_message(message): ########################### @bot.event async def on_message_edit(before, after): - ''' run on message edited ''' + """run on message edited""" if profanity_helper.filtering: if profanity_helper.helpChecker(after.content): if not after.author.bot: - await after.channel.send(after.author.name + ' says: ' + - profanity_helper.helpCensor(after.content)) + await after.channel.send( + after.author.name + + " says: " + + profanity_helper.helpCensor(after.content) + ) await after.delete() else: numsg = profanity_helper.helpCensor(after.content) await after.edit(content=numsg) + # ----------------------------------------------------------------------- # Function: toggleFilter # Description: Command to toggle the filter @@ -200,13 +219,13 @@ async def on_message_edit(before, after): @bot.command(name="toggleFilter", help="Turns the profanity filter on or off") @has_permissions(administrator=True) async def toggleFilter(ctx): - if profanity_helper.filtering: profanity_helper.filtering = False else: profanity_helper.filtering = True await ctx.send(f"Profanity filter set to: {profanity_helper.filtering}") + # ----------------------------------------------------------------------- # Function: whitelistWord # Description: Command to add a word to the whitelist @@ -216,22 +235,25 @@ async def toggleFilter(ctx): # Outputs: # - # ------------------------------------------------------------------------ -@bot.command(name="whitelist", help="adds a word to the whitelist. EX: $whitelist word or sentence") +@bot.command( + name="whitelist", + help="adds a word to the whitelist. EX: $whitelist word or sentence", +) @has_permissions(administrator=True) -async def whitelistWord(ctx, *, word =''): - - if not ctx.channel.name == 'instructor-commands': - await ctx.author.send('Please use this command inside #instructor-commands') +async def whitelistWord(ctx, *, word=""): + if not ctx.channel.name == "instructor-commands": + await ctx.author.send("Please use this command inside #instructor-commands") await ctx.message.delete() return - if word == '': + if word == "": return profanity_helper.wlword(word) await ctx.send(f"**{word}** has been added to the whitelist.") + # ----------------------------------------------------------------------- # Function: dewhitelistWord # Description: Command to remove a word from the whitelist @@ -241,16 +263,18 @@ async def whitelistWord(ctx, *, word =''): # Outputs: # - # ------------------------------------------------------------------------ -@bot.command(name="dewhitelist", help="Removes a word from the whitelist. EX: $dewhitelist word or sentence") +@bot.command( + name="dewhitelist", + help="Removes a word from the whitelist. EX: $dewhitelist word or sentence", +) @has_permissions(administrator=True) -async def dewhitelistWord(ctx, *, word=''): - - if not ctx.channel.name == 'instructor-commands': - await ctx.author.send('Please use this command inside #instructor-commands') +async def dewhitelistWord(ctx, *, word=""): + if not ctx.channel.name == "instructor-commands": + await ctx.author.send("Please use this command inside #instructor-commands") await ctx.message.delete() return - if word == '': + if word == "": return if word in profanity_helper.command_list: @@ -265,8 +289,6 @@ async def dewhitelistWord(ctx, *, word=''): await ctx.send(f"**{word}** not found in whitelist.") - - # ------------------------------------------------------------------------------------------ # Function: on_member_join(member) # Description: Handles on_member_join events, DMs the user and asks for verification through newComer.py @@ -277,15 +299,15 @@ async def dewhitelistWord(ctx, *, word=''): # ------------------------------------------------------------------------------------------ @bot.event async def on_member_join(member): - unverified = discord.utils.get( member.guild.roles, name="unverified" ) # finds the unverified role in the guild - await member.add_roles(unverified) # assigns the unverified role to the new member + await member.add_roles(unverified) # assigns the unverified role to the new member await member.send("Hello " + member.name + "!") await member.send( "Verify yourself before getting started! \n To use the verify command, do: $verify \n \ - ( For example: $verify Jane Doe )") + ( For example: $verify Jane Doe )" + ) # ------------------------------------------------ @@ -318,7 +340,7 @@ async def on_error(event, *args, **kwargs): @bot.command(name="shutdown", help="Shuts down the bot, only usable by the owner") @has_permissions(administrator=True) async def shutdown(ctx): - await ctx.send('Shutting Down bot') + await ctx.send("Shutting Down bot") print("Bot closed successfully") ctx.bot.logout() exit() diff --git a/cogs/assignments.py b/cogs/assignments.py new file mode 100644 index 000000000..84f4b5152 --- /dev/null +++ b/cogs/assignments.py @@ -0,0 +1,215 @@ +# Copyright (c) 2023 nfoster1492 +# This functionality provides various methods to manage assignments +# The isntructor is able to add/edit/and delete assignments +# and specify their grading category and point value. +import os +import sys +import discord +from discord.ext import commands + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +import db + + +class Assignments(commands.Cog): + def __init__(self, bot): + self.bot = bot + + # ----------------------------------------------------------------------------------------------------------------- + # Function: add_assignment(self, ctx, assignmentname, categoryname, points) + # Description: This command lets the instructor add a new gradeable assignment + # Inputs: + # - self: used to access parameters passed to the class through the constructor + # - ctx: used to access the values passed through the current context + # - assignmentname: the name of the assignment + # - categoryname: the name of the grade category if the assignment + # - points: the points that the assignment is worth + # Outputs: Whether or not the add was a success + # ----------------------------------------------------------------------------------------------------------------- + @commands.has_role("Instructor") + @commands.command( + name="addassignment", + help="add a grading assignment and points $addassignment NAME CATEGORY POINTS", + ) + async def add_assignment( + self, ctx, assignmentname: str, categoryname: str, points: str + ): + """Add a grading assignment and points""" + try: + assignmentpoints = int(points) + except ValueError: + await ctx.send("Points could not be parsed") + return + category = db.query( + "SELECT id FROM grade_categories WHERE guild_id = %s AND category_name = %s", + (ctx.guild.id, categoryname), + ) + + if not category: + await ctx.send(f"Category with name {categoryname} does not exist") + return + if assignmentpoints < 0: + await ctx.send("Assignment points must be greater than or equal to zero") + return + existing = db.query( + "SELECT id FROM assignments WHERE guild_id = %s AND assignment_name = %s", + (ctx.guild.id, assignmentname), + ) + + if not existing: + db.query( + "INSERT INTO assignments (guild_id, category_id, assignment_name, points) VALUES (%s, %s, %s, %s)", + (ctx.guild.id, category[0], assignmentname, points), + ) + await ctx.send( + f"A grading assignment has been added for: {assignmentname} with points: {points} and category: {categoryname}" + ) + else: + await ctx.send("This assignment has already been added..!!") + + # ----------------------------------------------------------------------------------------------------------------- + # Function: edit_assignment(self, ctx, assignmentname, categoryname, points) + # Description: This command lets the instructor edit a gradeable assignment with a new categoryname and/or points + # Inputs: + # - self: used to access parameters passed to the class through the constructor + # - ctx: used to access the values passed through the current context + # - assignmentname: the name of the assignment + # - categoryname: the new name of the grade category if the assignment + # - points: the new points that the assignment is worth + # Outputs: Whether or not the edit was a success + # ----------------------------------------------------------------------------------------------------------------- + @commands.has_role("Instructor") + @commands.command( + name="editassignment", + help="edit a grading assignment and points $editassignment NAME CATEGORY POINTS", + ) + async def edit_assignment( + self, ctx, assignmentname: str, categoryname: str, points: str + ): + """edit a grading assignment and points $editassignment NAME CATEGORY POINTS""" + try: + assignmentpoints = int(points) + except ValueError: + await ctx.send("Points could not be parsed") + return + category = db.query( + "SELECT id FROM grade_categories WHERE guild_id = %s AND category_name = %s", + (ctx.guild.id, categoryname), + ) + if not category: + await ctx.send(f"Category with name {categoryname} does not exist") + return + if assignmentpoints < 0: + await ctx.send("Assignment points must be greater than or equal to zero") + return + existing = db.query( + "SELECT id FROM assignments WHERE guild_id = %s AND assignment_name = %s", + (ctx.guild.id, assignmentname), + ) + if existing: + db.query( + "UPDATE assignments SET category_id = %s, points = %s WHERE id = %s", + (category[0], points, existing[0]), + ) + await ctx.send( + f"{assignmentname} assignment has been updated with points:{points} and category: {categoryname}" + ) + else: + await ctx.send("This assignment does not exist") + + # ----------------------------------------------------------------------------------------------------------------- + # Function: delete_assignment(self, ctx, assignmentname) + # Description: This command lets the instructor delete a gradeable assignment + # Inputs: + # - self: used to access parameters passed to the class through the constructor + # - ctx: used to access the values passed through the current context + # - assignmentname: the name of the assignment + # Outputs: Whether or not the delete was a success + # ----------------------------------------------------------------------------------------------------------------- + @commands.has_role("Instructor") + @commands.command( + name="deleteassignment", + help="delete a grading assignment $deleteassignment NAME", + ) + async def delete_assignment(self, ctx, assignmentname: str): + """delete a grading assignment $deleteassignment NAME""" + existing = db.query( + "SELECT id FROM assignments WHERE guild_id = %s AND assignment_name = %s", + (ctx.guild.id, assignmentname), + ) + if existing: + db.query("DELETE FROM assignments WHERE id = %s", (existing[0])) + await ctx.send(f"{assignmentname} assignment has been deleted ") + else: + await ctx.send("This assignment does not exist") + + # ----------------------------------------------------------------------------------------------------------------- + # Function: add_assignment_error(self, ctx, error) + # Description: prints error message for addassignment command + # Inputs: + # - ctx: context of the command + # - error: error message + # Outputs: + # - Error details + # ----------------------------------------------------------------------------------------------------------------- + @add_assignment.error + async def add_assignment_error(self, ctx, error): + """Error handling of addassignment function""" + if isinstance(error, commands.MissingRequiredArgument): + await ctx.send( + "To use the addassignment command, do: $addassignment \n ( For example: $addassignment test1 tests 100 )" + ) + await ctx.message.delete() + else: + await ctx.author.send(error) + print(error) + + # ----------------------------------------------------------------------------------------------------------------- + # Function: edit_assignment_error(self, ctx, error) + # Description: prints error message for editassignment command + # Inputs: + # - ctx: context of the command + # - error: error message + # Outputs: + # - Error details + # ----------------------------------------------------------------------------------------------------------------- + @edit_assignment.error + async def edit_assignment_error(self, ctx, error): + """Error handling of editassignment function""" + if isinstance(error, commands.MissingRequiredArgument): + await ctx.send( + "To use the editassignment command, do: $editassignment \n ( For example: $editassignment test1 tests 95 )" + ) + await ctx.message.delete() + else: + await ctx.author.send(error) + print(error) + + # ----------------------------------------------------------------------------------------------------------------- + # Function: delete_assignment_error(self, ctx, error) + # Description: prints error message for deleteassignment command + # Inputs: + # - ctx: context of the command + # - error: error message + # Outputs: + # - Error details + # ----------------------------------------------------------------------------------------------------------------- + @delete_assignment.error + async def delete_assignment_error(self, ctx, error): + """Error handling of deleteassignment function""" + if isinstance(error, commands.MissingRequiredArgument): + await ctx.send( + "To use the deleteassignment command, do: $deleteassignment \n ( For example: $deleteassignment test1)" + ) + await ctx.message.delete() + else: + await ctx.author.send(error) + print(error) + + +# ------------------------------------- +# add the file to the bot's cog system +# ------------------------------------- +async def setup(bot): + """Adds the file to the bot's cog system""" + await bot.add_cog(Assignments(bot)) diff --git a/cogs/calendar.py b/cogs/calendar.py new file mode 100644 index 000000000..3a4a2d3a7 --- /dev/null +++ b/cogs/calendar.py @@ -0,0 +1,344 @@ +# Copyright (c) 2023 nfoster1492 +from __future__ import print_function + +import os.path +import datetime +import discord +import asyncio +from dotenv import load_dotenv +from discord.ext import commands, tasks + +from google.auth.transport.requests import Request +from datetime import timedelta, datetime, date +from google.oauth2.credentials import Credentials +from urllib.request import urlopen +from google_auth_oauthlib.flow import InstalledAppFlow +from googleapiclient.discovery import build +from googleapiclient.errors import HttpError +import pdfkit +import pandas as pd + + +class Calendar(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.checkForEvents.start() + + # ----------------------------------------------------------------------------------------------------------------- + # Function: credsSetUp(self) + # Description: Sets up the credentials for all calendar actions + # Outputs: + # - The credentials needed to access the google calendar api calls + # ----------------------------------------------------------------------------------------------------------------- + def credsSetUp(self): + """Set up Google Calendar with authentication""" + # If modifying these scopes, delete the file token.json. + SCOPES = ["https://www.googleapis.com/auth/calendar"] + + creds = None + # The file token.json stores the user's access and refresh tokens, and is + # created automatically when the authorization flow completes for the first + # time. + if os.path.exists("token.json"): + creds = Credentials.from_authorized_user_file("token.json", SCOPES) + # If there are no (valid) credentials available, let the user log in. + if not creds or not creds.valid: + if creds and creds.expired and creds.refresh_token: + creds.refresh(Request()) + else: + flow = InstalledAppFlow.from_client_secrets_file( + "credentials.json", SCOPES + ) + creds = flow.run_local_server(port=0) + # Save the credentials for the next run + with open("token.json", "w", encoding="utf-8") as token: + token.write(creds.to_json()) + with open("cogs/token.json", "w", encoding="utf-8") as token: + token.write(creds.to_json()) + return creds + + # ----------------------------------------------------------------------------------------------------------------- + # Function: addCalendarEvent(self, ctx, name, description, eventTime) + # Description: adds an event to the Google Calendar specified in .env configuration + # Inputs: + # - ctx: context of the command + # - name: name of event + # - decription: description of event + # - eventTime: Time of event + # Outputs: + # - Event added to calendar + # ----------------------------------------------------------------------------------------------------------------- + @commands.command( + name="addCalendarEvent", + help="Add an event to the course calendar using the format" + ": $addCalendarEvent NAME DESCRIPTION DATE/TIME", + ) + async def addCalendarEvent(self, ctx, name, description, eventTime): + """Adds specified event to shared Google Calendar""" + creds = self.credsSetUp() + try: + calendar = os.getenv("CALENDAR_ID") + service = build("calendar", "v3", credentials=creds) + event = { + "summary": name, + "description": description, + "colorId": 4, + "start": {"dateTime": str(eventTime), "timeZone": "UTC"}, + "end": {"dateTime": str(eventTime), "timeZone": "UTC"}, + } + event = service.events().insert(calendarId=calendar, body=event).execute() + await ctx.send(f"Event {name} added to calendar!") + + except HttpError as error: + print(f"An error occurred: {error}") + + # ----------------------------------------------------------------------------------------------------------------- + # Function: clearCalendar(self, ctx) + # Description: clears all events from the google calendar + # Inputs: + # - ctx: context of the command + # Outputs: + # - Whether the command was a success or a failure + # ----------------------------------------------------------------------------------------------------------------- + @commands.command(name="clearCalendar", help="Clear all events from calendar") + async def clearCalendar(self, ctx): + """Clears all events from shared Google Calendar""" + creds = self.credsSetUp() + try: + page_token = None + calendar = os.getenv("CALENDAR_ID") + service = build("calendar", "v3", credentials=creds) + calendar_events = [] + while True: + events = ( + service.events() + .list(calendarId=calendar, pageToken=page_token) + .execute() + ) + for event in events["items"]: + calendar_events.append(event["id"]) + page_token = events.get("nextPageToken") + if not page_token: + break + + for cid in calendar_events: + service.events().delete(calendarId=calendar, eventId=cid).execute() + await ctx.send("Calendar has been cleared") + + except HttpError as error: + print(f"An error occurred: {error}") + + # ----------------------------------------------------------------------------------------------------------------- + # Function: getiCalDownload(self, ctx) + # Description: sends an ics file of the class calendar to the channel the command was issued in + # Inputs: + # - ctx: context of the command + # Outputs: + # - The ics file of the calendar + # ----------------------------------------------------------------------------------------------------------------- + @commands.command( + name="getiCalDownload", + help="Enter the command to receive an ics" + " file of the calendar$getiCalDownload", + ) + async def getiCalDownload(self, ctx): + """Generates an ICAL file of the Google Calendar""" + # Get the calendar in ics format + url = os.getenv("CALENDAR_ICS") + text = urlopen(url).read().decode("iso-8859-1") + # parse the received text to remove all \n characters + newText = "" + for character in text: + if character != "\n": + newText = newText + character + # write to the ics file + f = open(os.getenv("CALENDAR_PATH") + "ical.ics", "w", encoding="utf-8") + f.write(newText) + f.close() + await ctx.send(file=discord.File(os.getenv("CALENDAR_PATH") + "ical.ics")) + + # ----------------------------------------------------------------------------------------------------------------- + # Function: getPdfDownload(self, ctx) + # Description: sends an pdf file of the class calendar to the channel the command was issued in + # Inputs: + # - ctx: context of the command + # Outputs: + # - The pdf file of the calendar + # ----------------------------------------------------------------------------------------------------------------- + @commands.command( + name="getPdfDownload", + help="Enter the command to receive an ics" + " file of the calendar$getiCalDownload", + ) + async def getPdfDownload(self, ctx): + """Sends a pdf file of the class calendar to the Discord Channel""" + creds = self.credsSetUp() + try: + service = build("calendar", "v3", credentials=creds) + # Call the Calendar API + now = datetime.utcnow().isoformat() + "Z" # 'Z' indicates UTC time + calendar = os.getenv("CALENDAR_ID") + events_result = ( + service.events() + .list( + calendarId=calendar, + timeMin=now, + maxResults=150, + singleEvents=True, + orderBy="startTime", + ) + .execute() + ) + events = events_result.get("items", []) + + if not events: + await ctx.send("No upcoming events found.") + return + calEvents = [] + for event in events: + start = event["start"].get("dateTime", event["start"].get("date")) + end = event["end"].get("dateTime", event["end"].get("date")) + calEvent = {"Summary": event["summary"], "Start": start, "End": end} + calEvents.append(calEvent) + df = pd.DataFrame(calEvents) + htmlCal = df.to_html() + pdfkit.from_string(htmlCal, os.getenv("CALENDAR_PATH") + "calendar.pdf") + await ctx.send( + file=discord.File(os.getenv("CALENDAR_PATH") + "calendar.pdf") + ) + + except HttpError as error: + print(f"An error occurred: {error}") + + # ----------------------------------------------------------------------------------------------------------------- + # Function: checkForEvents(self) + # Description: Checks the calendar once per day for any events that are due the same day + # Outputs: + # - Message to the general chat where everyone is pinged of what events are due today + # ----------------------------------------------------------------------------------------------------------------- + @tasks.loop(hours=24) + async def checkForEvents(self): + """Checks calendar daily for the events due that day""" + creds = self.credsSetUp() + try: + service = build("calendar", "v3", credentials=creds) + # Call the Calendar API + now = datetime.utcnow().isoformat() + "Z" # 'Z' indicates UTC time + calendar = os.getenv("CALENDAR_ID") + events_result = ( + service.events() + .list( + calendarId=calendar, + timeMin=now, + maxResults=150, + singleEvents=True, + orderBy="startTime", + ) + .execute() + ) + events = events_result.get("items", []) + summary = "" + for event in events: + dt = datetime.strptime( + (event["start"]["dateTime"])[0:18], "%Y-%m-%dT%H:%M:%S" + ) + if dt.day == date.today().day and dt.year == date.today().year: + summary = summary + event["summary"] + "," + if len(summary) != 0: + # If the bot is used in more than one server + for guild in self.bot.guilds: + for channel in guild.text_channels: + # Find the general channel and ping + if channel.name == "general": + await channel.send("@everyone " + summary + "due TODAY!") + break + except HttpError as error: + print(f"An error occurred: {error}") + + # ----------------------------------------------------------------------------------------------------------------- + # Function: subscribeCalendar(self, ctx, userEmail) + # Description: adds specified user to shared Google Calendar + # Inputs: + # - ctx: context of the command + # - target: calendar to modify + # - userEmail: user to add to target Google Calendar + # Outputs: + # - Confirmation string for successful add, error string for failure. + # ----------------------------------------------------------------------------------------------------------------- + @commands.command( + name="subscribeCalendar", + help="Adds user to shared Google Calendar. Ex: subscribeCalendar john.doe@gmail.com", + ) + async def subscribeCalendar(self, ctx, userEmail): + """Adds user to shared Google Calendar""" + creds = self.credsSetUp() + try: + service = build("calendar", "v3", credentials=creds) + calendar = os.getenv("CALENDAR_ID") + acl_rule = { + "scope": {"type": "user", "value": userEmail}, + "role": "reader", # Adjust the role as needed (e.g., reader, owner) + } + acl_rule = ( + service.acl().insert(calendarId=calendar, body=acl_rule).execute() + ) + + await ctx.author.send(f"Added {userEmail} to the calendar.") + except HttpError as e: + print(f"An error occurred: {e}") + await ctx.author.send( + f"Error adding user: {userEmail} is not a valid email." + ) + + # ----------------------------------------------------------------------------------------------------------------- + # Function: removeCalendar(self, ctx, userEmail) + # Description: removes specified user from shared Google Calendar + # Inputs: + # - ctx: context of the command + # - target: calendar to modify + # - userEmail: user to remove from target Google Calendar + # Outputs: + # - Confirmation string for successful removal, error string for failure. + # ----------------------------------------------------------------------------------------------------------------- + @commands.has_role("Instructor") + @commands.command( + name="removeCalendar", + help="Removes user from shared Google Calendar. Ex: removeCalendar john.doe@gmail.com", + ) + async def removeCalendar(self, ctx, userEmail): + """Removes user from shared Google Calendar""" + creds = self.credsSetUp() + try: + service = build("calendar", "v3", credentials=creds) + calendar = os.getenv("CALENDAR_ID") + acl_rule_id = None + # Get the list of ACL rules (permissions) for the calendar. + acl_list = service.acl().list(calendarId=calendar).execute() + for acl_rule in acl_list.get("items", []): + if ( + acl_rule["scope"]["type"] == "user" + and acl_rule["scope"]["value"] == userEmail + ): + acl_rule_id = acl_rule["id"] + break + if acl_rule_id: + # Delete the ACL rule (permission) to remove the user from the calendar. + service.acl().delete(calendarId=calendar, ruleId=acl_rule_id).execute() + await ctx.author.send( + f"User {userEmail} has been removed from the calendar." + ) + else: + await ctx.author.send( + f"User {userEmail} was not found in the calendar's permissions." + ) + except HttpError as e: + print(f"An error occurred: {e}") + await ctx.author.send( + f"Error removing user: {userEmail} is not a valid email." + ) + + +async def setup(bot): + """Adds the file to the bot's cog system""" + n = Calendar(bot) + await bot.add_cog(n) diff --git a/cogs/deadline.py b/cogs/deadline.py index 5b54d1bc2..a14271296 100644 --- a/cogs/deadline.py +++ b/cogs/deadline.py @@ -16,11 +16,18 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import db -class Deadline(commands.Cog): +class Deadline(commands.Cog): def __init__(self, bot): self.bot = bot - self.units = {"second": 1, "minute": 60, "hour": 3600, "day": 86400, "week": 604800, "month": 2592000} + self.units = { + "second": 1, + "minute": 60, + "hour": 3600, + "day": 86400, + "week": 604800, + "month": 2592000, + } # ----------------------------------------------------------------------------------------------------------------- # Function: timenow(self, ctx, *, date: str) @@ -31,10 +38,13 @@ def __init__(self, bot): # - date: current date and 24-hour time # Outputs: offset from the user's current time with UTC. # ----------------------------------------------------------------------------------------------------------------- - @commands.command(name="timenow", - help="put in current time to get offset needed for proper " - "datetime notifications $timenow MMM DD YYYY HH:MM ex. $timenow SEP 25 2024 17:02") + @commands.command( + name="timenow", + help="put in current time to get offset needed for proper " + "datetime notifications $timenow MMM DD YYYY HH:MM ex. $timenow SEP 25 2024 17:02", + ) async def timenow(self, ctx, *, date: str): + """Gets offset for proper datetime notifications compared to UTC""" try: input_time = parser.parse(date) except ValueError: @@ -46,8 +56,9 @@ async def timenow(self, ctx, *, date: str): diff_in_hours = int(difference.total_seconds() / 3600) input_time += timedelta(hours=diff_in_hours) - await ctx.send(f"Current time is {-diff_in_hours} hours from system time (UTC).") - + await ctx.send( + f"Current time is {-diff_in_hours} hours from system time (UTC)." + ) # ----------------------------------------------------------------------------------------------------------------- # Function: timenow_error(self, ctx, error) @@ -60,32 +71,37 @@ async def timenow(self, ctx, *, date: str): # ----------------------------------------------------------------------------------------------------------------- @timenow.error async def timenow_error(self, ctx, error): + """Error handling for timenow command""" if isinstance(error, commands.MissingRequiredArgument): await ctx.send( "To use the timenow command (with current time), do: " - "$timenow MMM DD YYYY HH:MM ex. $timenow SEP 25 2024 17:02") + "$timenow MMM DD YYYY HH:MM ex. $timenow SEP 25 2024 17:02" + ) else: await ctx.author.send(error) - #await ctx.message.delete() + # await ctx.message.delete() print(error) # ----------------------------------------------------------------------------------------------------------------- # Function: duedate(self, ctx, coursename: str, hwcount: str, *, date: str) - # Description: Adds the homework to database in the specified format + # Description: Adds the reminder to database in the specified format # Inputs: # - self: used to access parameters passed to the class through the constructor # - ctx: used to access the values passed through the current context - # - coursename: name of the course for which homework is to be added - # - hwcount: name of the homework + # - coursename: name of the course for which reminder is to be added + # - hwcount: name of the reminder # - date: due date of the assignment # Outputs: returns either an error stating a reason for failure or returns a success message # indicating that the reminder has been added # ----------------------------------------------------------------------------------------------------------------- - @commands.has_role('Instructor') - @commands.command(name="addhw", - help="add homework and due-date $addhw CLASSNAME HW_NAME MMM DD YYYY optional(HH:MM) optional(TIMEZONE)" - "ex. $addhw CSC510 HW2 SEP 25 2024 17:02 EST") + @commands.has_role("Instructor") + @commands.command( + name="duedate", + help="add reminder and due-date $duedate CLASSNAME NAME MMM DD YYYY optional(HH:MM) optional(TIMEZONE)" + "ex. $duedate CSC510 HW2 SEP 25 2024 17:02 EST", + ) async def duedate(self, ctx, coursename: str, hwcount: str, *, date: str): + """Add reminder for specified course, assignment, and date""" author = ctx.message.author try: @@ -95,23 +111,27 @@ async def duedate(self, ctx, coursename: str, hwcount: str, *, date: str): return existing = db.query( - 'SELECT author_id FROM reminders WHERE guild_id = %s AND course = %s AND homework = %s', - (ctx.guild.id, coursename, hwcount) + "SELECT author_id FROM reminders WHERE guild_id = %s AND course = %s AND reminder_name = %s", + (ctx.guild.id, coursename, hwcount), ) if not existing: db.query( - 'INSERT INTO reminders (guild_id, author_id, course, homework, due_date) VALUES (%s, %s, %s, %s, %s)', - (ctx.guild.id, author.id, coursename, hwcount, duedate) + "INSERT INTO reminders (guild_id, author_id, course, reminder_name, due_date) VALUES (%s, %s, %s, %s, %s)", + (ctx.guild.id, author.id, coursename, hwcount, duedate), ) + calduedate = duedate.astimezone(timezone.utc) + isodate = calduedate.isoformat(timespec="seconds")[:-6] await ctx.send( - f"A date has been added for: {coursename} homework named: {hwcount} " - f"which is due on: {duedate} by {author}.") + f"A date has been added for: {coursename} reminder named: {hwcount} " + f"which is due on: {duedate} by {author}." + f"Use this command to add the reminder to the calendar! **`$addCalendarEvent {hwcount} {coursename} {isodate}Z`**" + ) else: - await ctx.send("This homework has already been added..!!") + await ctx.send("This reminder has already been added..!!") # ----------------------------------------------------------------------------------------------------------------- # Function: duedate_error(self, ctx, error) - # Description: prints error message for addhw command + # Description: prints error message for duedate command # Inputs: # - ctx: context of the command # - error: error message @@ -120,13 +140,15 @@ async def duedate(self, ctx, coursename: str, hwcount: str, *, date: str): # ----------------------------------------------------------------------------------------------------------------- @duedate.error async def duedate_error(self, ctx, error): + """Error handling for duedate command""" if isinstance(error, commands.MissingRequiredArgument): await ctx.send( - 'To use the addhw command, do: $addhw CLASSNAME HW_NAME MMM DD YYYY optional(HH:MM) optional(TIMEZONE)\n ' - '( For example: $addhw CSC510 HW2 SEP 25 2024 17:02 EST )') + "To use the duedate command, do: $duedate CLASSNAME NAME MMM DD YYYY optional(HH:MM) optional(TIMEZONE)\n " + "( For example: $duedate CSC510 HW2 SEP 25 2024 17:02 EST )" + ) else: await ctx.author.send(error) - #await ctx.message.delete() + # await ctx.message.delete() print(error) # ----------------------------------------------------------------------------------------------------------------- @@ -140,24 +162,30 @@ async def duedate_error(self, ctx, error): # Outputs: returns either an error stating a reason for failure or # returns a success message indicating that the reminder has been deleted # ----------------------------------------------------------------------------------------------------------------- - @commands.has_role('Instructor') - @commands.command(name="deletereminder", pass_context=True, - help="delete a specific reminder using course name and homework name using " - "$deletereminder CLASSNAME HW_NAME ex. $deletereminder CSC510 HW2 ") + @commands.has_role("Instructor") + @commands.command( + name="deletereminder", + pass_context=True, + help="delete a specific reminder using course name and reminder name using " + "$deletereminder CLASSNAME HW_NAME ex. $deletereminder CSC510 HW2 ", + ) async def deleteReminder(self, ctx, courseName: str, hwName: str): + """Deletes a specified reminder""" reminders_deleted = db.query( - 'SELECT course, homework, due_date FROM reminders WHERE guild_id = %s AND homework = %s AND course = %s', - (ctx.guild.id, hwName, courseName) + "SELECT course, reminder_name, due_date FROM reminders WHERE guild_id = %s AND reminder_name = %s AND course = %s", + (ctx.guild.id, hwName, courseName), ) db.query( - 'DELETE FROM reminders WHERE guild_id = %s AND homework = %s AND course = %s', - (ctx.guild.id, hwName, courseName) + "DELETE FROM reminders WHERE guild_id = %s AND reminder_name = %s AND course = %s", + (ctx.guild.id, hwName, courseName), ) - for course, homework, due_date in reminders_deleted: + for course, reminder_name, due_date in reminders_deleted: due = due_date.strftime("%Y-%m-%d %H:%M:%S") - await ctx.send(f"Following reminder has been deleted: Course: {course}, " - f"Homework Name: {homework}, Due Date: {due}") + await ctx.send( + f"Following reminder has been deleted: Course: {course}, " + f"reminder Name: {reminder_name}, Due Date: {due}" + ) # ----------------------------------------------------------------------------------------------------------------- # Function: deleteReminder_error(self, ctx, error) @@ -170,13 +198,15 @@ async def deleteReminder(self, ctx, courseName: str, hwName: str): # ----------------------------------------------------------------------------------------------------------------- @deleteReminder.error async def deleteReminder_error(self, ctx, error): + """Error handling for deleteReminder""" if isinstance(error, commands.MissingRequiredArgument): await ctx.send( - 'To use the deletereminder command, do: $deletereminder CLASSNAME HW_NAME \n ' - '( For example: $deletereminder CSC510 HW2 )') + "To use the deletereminder command, do: $deletereminder CLASSNAME HW_NAME \n " + "( For example: $deletereminder CSC510 HW2 )" + ) else: await ctx.author.send(error) - #await ctx.message.delete() + # await ctx.message.delete() print(error) # ----------------------------------------------------------------------------------------------------------------- @@ -191,11 +221,15 @@ async def deleteReminder_error(self, ctx, error): # Outputs: returns either an error stating a reason for failure or # returns a success message indicating that the reminder has been updated # ----------------------------------------------------------------------------------------------------------------- - @commands.has_role('Instructor') - @commands.command(name="changeduedate", pass_context=True, - help="update the assignment date. $changeduedate CLASSNAME HW_NAME MMM DD YYYY optional(HH:MM) optional(TIMEZONE)" - "ex. $changeduedate CSC510 HW2 SEP 25 2024 17:02 EST") + @commands.has_role("Instructor") + @commands.command( + name="changeduedate", + pass_context=True, + help="update the assignment date. $changeduedate CLASSNAME HW_NAME MMM DD YYYY optional(HH:MM) optional(TIMEZONE)" + "ex. $changeduedate CSC510 HW2 SEP 25 2024 17:02 EST", + ) async def changeduedate(self, ctx, classid: str, hwid: str, *, date: str): + """Updates an assignment's due date in the database""" author = ctx.message.author try: duedate = parser.parse(date) @@ -206,10 +240,12 @@ async def changeduedate(self, ctx, classid: str, hwid: str, *, date: str): # future = (time.time() + (duedate - datetime.today()).total_seconds()) db.query( - 'UPDATE reminders SET author_id = %s, due_date = %s WHERE guild_id = %s AND homework = %s AND course = %s', - (author.id, duedate, ctx.guild.id, hwid, classid) + "UPDATE reminders SET author_id = %s, due_date = %s WHERE guild_id = %s AND reminder_name = %s AND course = %s", + (author.id, duedate, ctx.guild.id, hwid, classid), + ) + await ctx.send( + f"{classid} {hwid} has been updated with following date: {duedate}" ) - await ctx.send(f"{classid} {hwid} has been updated with following date: {duedate}") # ----------------------------------------------------------------------------------------------------------------- # Function: changeduedate_error(self, ctx, error) @@ -222,13 +258,15 @@ async def changeduedate(self, ctx, classid: str, hwid: str, *, date: str): # ----------------------------------------------------------------------------------------------------------------- @changeduedate.error async def changeduedate_error(self, ctx, error): + """Error handling for changeduedate command""" if isinstance(error, commands.MissingRequiredArgument): await ctx.send( - 'To use the changeduedate command, do: $changeduedate CLASSNAME HW_NAME MMM DD YYYY optional(HH:MM) optional(TIMEZONE)\n' - ' ( For example: $changeduedate CSC510 HW2 SEP 25 2024 17:02 EST)') + "To use the changeduedate command, do: $changeduedate CLASSNAME HW_NAME MMM DD YYYY optional(HH:MM) optional(TIMEZONE)\n" + " ( For example: $changeduedate CSC510 HW2 SEP 25 2024 17:02 EST)" + ) else: await ctx.author.send(error) - #await ctx.message.delete() + # await ctx.message.delete() print(error) # ----------------------------------------------------------------------------------------------------------------- @@ -240,26 +278,31 @@ async def changeduedate_error(self, ctx, error): # Outputs: returns either an error stating a reason for failure # or returns a list of all the assignments that are due this week # ----------------------------------------------------------------------------------------------------------------- - @commands.command(name="duethisweek", pass_context=True, - help="check all the homeworks that are due this week $duethisweek") + @commands.command( + name="duethisweek", + pass_context=True, + help="check all the homeworks that are due this week $duethisweek", + ) async def duethisweek(self, ctx): + """Checks all homeworks or assignments due this week""" reminders = db.query( - "SELECT course, homework, due_date " + "SELECT course, reminder_name, due_date " "FROM reminders " "WHERE guild_id = %s AND date_part('day', due_date - now()) <= 7 AND date_part('minute', due_date - now()) >= 0", - (ctx.guild.id,) + (ctx.guild.id,), ) curr_date = datetime.now(timezone.utc) - for course, homework, due_date in reminders: + for course, reminder_name, due_date in reminders: delta = due_date - curr_date formatted_due_date = due_date.strftime("%b %d %Y %H:%M:%S%z") - await ctx.author.send(f"{course} {homework} is due in {delta.days} days, {delta.seconds//3600}" - f" hours and {(delta.seconds//60)%60} minutes ({formatted_due_date})") + await ctx.author.send( + f"{course} {reminder_name} is due in {delta.days} days, {delta.seconds//3600}" + f" hours and {(delta.seconds//60)%60} minutes ({formatted_due_date})" + ) await ctx.message.delete() - # ----------------------------------------------------------------------------------------------------------------- # Function: duethisweek_error(self, ctx, error) # Description: prints error message for duethisweek command @@ -271,6 +314,7 @@ async def duethisweek(self, ctx): # ----------------------------------------------------------------------------------------------------------------- @duethisweek.error async def duethisweek_error(self, ctx, error): + """Error handling for duethisweek command""" await ctx.author.send(error) print(error) await ctx.message.delete() @@ -284,18 +328,25 @@ async def duethisweek_error(self, ctx, error): # Outputs: returns either an error stating a reason for failure or # returns a list of all the assignments that are due on the day the command is run # ----------------------------------------------------------------------------------------------------------------- - @commands.command(name="duetoday", pass_context=True, help="check all the homeworks that are due today $duetoday") + @commands.command( + name="duetoday", + pass_context=True, + help="check all the reminders that are due today $duetoday", + ) async def duetoday(self, ctx): + """Checks for all reminders that are due today""" due_today = db.query( - "SELECT course, homework, due_date " + "SELECT course, reminder_name, due_date " "FROM reminders " "WHERE guild_id = %s AND date_part('day', due_date - now()) <= 1 AND date_part('minute', due_date - now()) >= 0", - (ctx.guild.id,) + (ctx.guild.id,), ) - for course, homework, due_date in due_today: + for course, reminder_name, due_date in due_today: delta = due_date - datetime.now(timezone.utc) - await ctx.author.send(f"{course} {homework} is due in {delta.days} days, {delta.seconds//3600}" - f" hours and {(delta.seconds//60)%60} minutes") + await ctx.author.send( + f"{course} {reminder_name} is due in {delta.days} days, {delta.seconds//3600}" + f" hours and {(delta.seconds//60)%60} minutes" + ) if len(due_today) == 0: await ctx.author.send("You have no dues today..!!") await ctx.message.delete() @@ -311,33 +362,40 @@ async def duetoday(self, ctx): # ----------------------------------------------------------------------------------------------------------------- @duetoday.error async def duetoday_error(self, ctx, error): + """Error handling for duetoday command""" await ctx.author.send(error) print(error) await ctx.message.delete() # ----------------------------------------------------------------------------------------------------------------- # Function: coursedue(self, ctx, courseid: str) - # Description: Displays all the homeworks that are due for a specific course + # Description: Displays all the reminder_names that are due for a specific course # Inputs: # - self: used to access parameters passed to the class through the constructor # - ctx: used to access the values passed through the current context - # - courseid: name of the course for which homework is to be added + # - courseid: name of the course for which reminder_name is to be added # Outputs: returns either an error stating a reason for failure or # a list of assignments that are due for the provided courseid # ----------------------------------------------------------------------------------------------------------------- - @commands.command(name="coursedue", pass_context=True, - help="check all the homeworks that are due for a specific course $coursedue coursename " - "ex. $coursedue CSC505") + @commands.command( + name="coursedue", + pass_context=True, + help="check all the reminders that are due for a specific course $coursedue coursename " + "ex. $coursedue CSC505", + ) async def coursedue(self, ctx, courseid: str): + """Displays a list of all reminders due for a specific course""" reminders = db.query( - 'SELECT homework, due_date FROM reminders WHERE guild_id = %s AND course = %s', - (ctx.guild.id, courseid) + "SELECT reminder_name, due_date FROM reminders WHERE guild_id = %s AND course = %s", + (ctx.guild.id, courseid), ) - for homework, due_date in reminders: + for reminder_name, due_date in reminders: formatted_due_date = due_date.strftime("%b %d %Y %H:%M:%S") - await ctx.author.send(f"{homework} is due at {formatted_due_date}") + await ctx.author.send(f"{reminder_name} is due at {formatted_due_date}") if len(reminders) == 0: - await ctx.author.send(f"Rejoice..!! You have no pending homeworks for {courseid}..!!") + await ctx.author.send( + f"Rejoice..!! You have no pending reminders for {courseid}..!!" + ) await ctx.message.delete() # ----------------------------------------------------------------------------------------------------------------- @@ -351,9 +409,11 @@ async def coursedue(self, ctx, courseid: str): # ----------------------------------------------------------------------------------------------------------------- @coursedue.error async def coursedue_error(self, ctx, error): + """Error handling for coursedue command""" if isinstance(error, commands.MissingRequiredArgument): await ctx.author.send( - 'To use the coursedue command, do: $coursedue CLASSNAME \n ( For example: $coursedue CSC510 )') + "To use the coursedue command, do: $coursedue CLASSNAME \n ( For example: $coursedue CSC510 )" + ) else: await ctx.author.send(error) print(error) @@ -368,19 +428,26 @@ async def coursedue_error(self, ctx, error): # Outputs: returns either an error stating a reason for failure or # returns a list of all the assignments # --------------------------------------------------------------------------------- - @commands.command(name="listreminders", pass_context=True, help="lists all reminders") + @commands.command( + name="listreminders", pass_context=True, help="lists all reminders" + ) async def listreminders(self, ctx): + """Displays user with list of all reminders""" author = ctx.message.author reminders = db.query( - 'SELECT course, homework, due_date FROM reminders WHERE guild_id = %s and author_id = %s and now() < due_date', - (ctx.guild.id, author.id) + "SELECT course, reminder_name, due_date FROM reminders WHERE guild_id = %s and author_id = %s and now() < due_date", + (ctx.guild.id, author.id), ) - for course, homework, due_date in reminders: + for course, reminder_name, due_date in reminders: formatted_due_date = due_date.strftime("%b %d %Y %H:%M:%S%z") - await ctx.author.send(f"{course} homework named: {homework} which is due on: {formatted_due_date} by {author.name}") + await ctx.author.send( + f"{course} reminder named: {reminder_name} which is due on: {formatted_due_date} by {author.name}" + ) if not reminders: - await ctx.author.send("Mission Accomplished..!! You don't have any more dues..!!") + await ctx.author.send( + "Mission Accomplished..!! You don't have any more dues..!!" + ) await ctx.message.delete() # ----------------------------------------------------------------------------------------------------------------- @@ -394,6 +461,7 @@ async def listreminders(self, ctx): # ----------------------------------------------------------------------------------------------------------------- @listreminders.error async def listreminders_error(self, ctx, error): + """Error handling for listreminders command""" await ctx.author.send(error) print(error) await ctx.message.delete() @@ -409,16 +477,19 @@ async def listreminders_error(self, ctx, error): # --------------------------------------------------------------------------------- @commands.command(name="overdue", pass_context=True, help="lists overdue reminders") async def overdue(self, ctx): + """Displays list of homeworks and assignments that are overdue""" author = ctx.message.author reminders = db.query( - 'SELECT course, homework, due_date FROM reminders WHERE guild_id = %s and author_id = %s' - 'and now() > due_date', - (ctx.guild.id, author.id) + "SELECT course, reminder_name, due_date FROM reminders WHERE guild_id = %s and author_id = %s" + " and now() > due_date", + (ctx.guild.id, author.id), ) - for course, homework, due_date in reminders: + for course, reminder_name, due_date in reminders: formatted_due_date = due_date.strftime("%b %d %Y %H:%M:%S%z") - await ctx.author.send(f"{course} homework named: {homework} which was due on: {formatted_due_date} by {author.name}") + await ctx.author.send( + f"{course} reminder named: {reminder_name} which was due on: {formatted_due_date} by {author.name}" + ) if not reminders: await ctx.author.send("There are no overdue reminders") await ctx.message.delete() @@ -434,6 +505,7 @@ async def overdue(self, ctx): # ----------------------------------------------------------------------------------------------------------------- @overdue.error async def overdue_error(self, ctx, error): + """Error handling for overdue command""" await ctx.author.send(error) print(error) await ctx.message.delete() @@ -448,9 +520,12 @@ async def overdue_error(self, ctx, error): # returns a success message stating that reminders have been deleted # --------------------------------------------------------------------------------- - @commands.command(name="clearreminders", pass_context=True, help="deletes all reminders") + @commands.command( + name="clearreminders", pass_context=True, help="deletes all reminders" + ) async def clearallreminders(self, ctx): - db.query('DELETE FROM reminders WHERE guild_id = %s', (ctx.guild.id,)) + """Clears all reminders from database""" + db.query("DELETE FROM reminders WHERE guild_id = %s", (ctx.guild.id,)) await ctx.send("All reminders have been cleared..!!") # ----------------------------------------------------------------------------------------------------------------- @@ -464,10 +539,10 @@ async def clearallreminders(self, ctx): # ----------------------------------------------------------------------------------------------------------------- @clearallreminders.error async def clearallreminders_error(self, ctx, error): + """Error handling for clearreminders command""" await ctx.author.send(error) print(error) - # --------------------------------------------------------------------------------- # Function: remindme(self, ctx, quantity: int, time_unit : str,*, text :str) # Description: Personal remind me functionality @@ -520,9 +595,12 @@ async def clearallreminders_error(self, ctx, error): # - self: used to access parameters passed to the class through the constructor # - ctx: context of the command # ----------------------------------------------------------------------------------------------------- - @commands.command(name="clearoverdue", pass_context=True, help="deletes overdue reminders") + @commands.command( + name="clearoverdue", pass_context=True, help="deletes overdue reminders" + ) async def clearoverdue(self, ctx): - db.query('DELETE FROM reminders WHERE now() > due_date') + """Clears all overdue reminders from database""" + db.query("DELETE FROM reminders WHERE now() > due_date") await ctx.send("All overdue reminders have been cleared..!!") # ----------------------------------------------------------------------------------------------------------------- @@ -536,6 +614,7 @@ async def clearoverdue(self, ctx): # ----------------------------------------------------------------------------------------------------------------- @clearoverdue.error async def clearoverdue_error(self, ctx, error): + """Error handling for clearoverdue""" await ctx.author.send(error) print(error) @@ -547,14 +626,19 @@ async def clearoverdue_error(self, ctx, error): # ----------------------------------------------------------------------------------------------------- @tasks.loop(hours=24) async def send_reminders_day(self): + """Task running once per day to send a reminder for assignments due""" channel = discord.utils.get(self.bot.get_all_channels(), name="reminders") if channel: - reminders = db.query("SELECT course, homework, due_date " + reminders = db.query( + "SELECT course, reminder_name, due_date " "FROM reminders " - "WHERE due_date::date = now()::date") - for course,homework,due_date in reminders: + "WHERE due_date::date = now()::date" + ) + for course, reminder_name, due_date in reminders: difference = due_date - datetime.now(timezone.utc) - await channel.send(f"{homework} for {course} is due in {(difference.seconds//3600)} hours") + await channel.send( + f"{reminder_name} for {course} is due in {(difference.seconds//3600)} hours" + ) # ----------------------------------------------------------------------------------------------------- # Function: bofore(self) @@ -565,12 +649,13 @@ async def send_reminders_day(self): # ----------------------------------------------------------------------------------------------------- @send_reminders_day.before_loop async def before(self): - WHEN = time(13, 0, 0) # 8:00 AM eastern + """Task that runs once per day and waits until 8am EST to send reminders via send_reminders_day function""" + WHEN = time(13, 0, 0) # 8:00 AM eastern now = datetime.utcnow() target_time = datetime.combine(now.date(), WHEN) seconds_until_target = (target_time - now).total_seconds() if seconds_until_target < 0: - target_time = datetime.combine(now.date()+timedelta(days=1), WHEN) + target_time = datetime.combine(now.date() + timedelta(days=1), WHEN) seconds_until_target = (target_time - now).total_seconds() await asyncio.sleep(seconds_until_target) @@ -582,21 +667,28 @@ async def before(self): # ----------------------------------------------------------------------------------------------------- @tasks.loop(hours=1) async def send_reminders_hour(self): + """Task that runs once per hour ans sends a reminder for assignments due""" channel = discord.utils.get(self.bot.get_all_channels(), name="reminders") if channel: - reminders = db.query("SELECT course, homework, due_date " + reminders = db.query( + "SELECT course, reminder_name, due_date " "FROM reminders " - "WHERE due_date::date = now()::date") - for course,homework,due_date in reminders: + "WHERE due_date::date = now()::date" + ) + for course, reminder_name, due_date in reminders: difference = due_date - datetime.now(timezone.utc) - if difference.seconds//3600 == 0: - await channel.send(f"{homework} for {course} is due within the hour") + if difference.seconds // 3600 == 0: + await channel.send( + f"{reminder_name} for {course} is due within the hour" + ) + # ------------------------------------- # add the file to the bot's cog system # ------------------------------------- -def setup(bot): +async def setup(bot): + """Adds the file to the bot's cog system""" n = Deadline(bot) - n.send_reminders_day.start() # pylint: disable=no-member - n.send_reminders_hour.start() # pylint: disable=no-member - bot.add_cog(n) + n.send_reminders_day.start() # pylint: disable=no-member + n.send_reminders_hour.start() # pylint: disable=no-member + await bot.add_cog(n) diff --git a/cogs/grades.py b/cogs/grades.py new file mode 100644 index 000000000..5778e9d03 --- /dev/null +++ b/cogs/grades.py @@ -0,0 +1,830 @@ +# Copyright (c) 2023 nfoster1492 +# This functionality provides various methods to manage grades +# It allows for the inputing of grades, searching of grades, and several +# different calculations based on existing grades in the system +import os +import sys +import discord +import pandas as pd +import requests +from io import StringIO +from discord.ext import commands + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +import db + + +class Grades(commands.Cog): + def __init__(self, bot): + self.bot = bot + + # ----------------------------------------------------------------------------------------------------------------- + # Function: grade(self, ctx, assignmentName) + # Description: This command lets a student get their grade for a certain assignment + # Inputs: + # - self: used to access parameters passed to the class through the constructor + # - ctx: used to access the values passed through the current context + # - assignmentName: the name of the desired assignment + # Outputs: Grade of the provided assignment + # ----------------------------------------------------------------------------------------------------------------- + @commands.command( + name="grade", help="get your grade for a specific assignment $grade ASSIGNMENT" + ) + async def grade(self, ctx, assignmentName: str): + """Lets a student get their grade for a certain assignment""" + memberName = ctx.author.name + + grade = db.query( + "SELECT grades.grade FROM grades INNER JOIN assignments ON grades.assignment_id = assignments.id WHERE grades.guild_id = %s AND grades.member_name = %s AND assignments.assignment_name = %s", + (ctx.guild.id, memberName, assignmentName), + ) + + points = db.query( + "SELECT assignments.points FROM assignments WHERE assignments.guild_id = %s AND assignments.assignment_name = %s", + (ctx.guild.id, assignmentName), + ) + + if not grade: + await ctx.author.send(f"Grade for {assignmentName} does not exist") + return + + if not points: + await ctx.author.send(f"{assignmentName} does not exist") + return + + await ctx.author.send( + f"Grade for {assignmentName}: {grade[0][0]}%, worth {points[0][0]} points" + ) + + # ----------------------------------------------------------------------------------------------------------------- + # Function: grade_error(self, ctx, error) + # Description: prints error message for grade command + # Inputs: + # - ctx: context of the command + # - error: error message + # Outputs: + # - Error details + # ----------------------------------------------------------------------------------------------------------------- + @grade.error + async def grade_error(self, ctx, error): + """Error handling of grade function""" + if isinstance(error, commands.MissingRequiredArgument): + await ctx.send( + "To use the grade command, do: $grade \n ( For example: $grade test1 )" + ) + await ctx.message.delete() + else: + await ctx.author.send(error) + print(error) + + # ----------------------------------------------------------------------------------------------------------------- + # Function: gradebycategory(self, ctx, categoryName) + # Description: This command lets a student get their average grade for a certain category + # Inputs: + # - self: used to access parameters passed to the class through the constructor + # - ctx: used to access the values passed through the current context + # - categoryName: the name of the desired category + # Outputs: Average grade of all the assignments in the provided category, accounting for assignment point values + # ----------------------------------------------------------------------------------------------------------------- + @commands.command( + name="gradebycategory", + help="get your grade for a specific category $gradebycategory CATEGORY", + ) + async def gradebycategory(self, ctx, categoryName: str): + """Lets a student get their grade for a specific grade category""" + memberName = ctx.author.name + + grades = db.query( + "SELECT grades.grade FROM grades INNER JOIN assignments ON grades.assignment_id = assignments.id INNER JOIN grade_categories ON assignments.category_id = grade_categories.id WHERE grades.guild_id = %s AND grades.member_name = %s AND grade_categories.category_name = %s ORDER BY grades.assignment_id", + (ctx.guild.id, memberName, categoryName), + ) + + points = db.query( + "SELECT assignments.points FROM assignments INNER JOIN grade_categories ON assignments.category_id = grade_categories.id WHERE assignments.guild_id = %s AND grade_categories.category_name = %s ORDER BY assignments.id", + (ctx.guild.id, categoryName), + ) + + if not grades: + await ctx.author.send(f"Grades for {categoryName} do not exist") + return + + if not points: + await ctx.author.send(f"Assignments for {categoryName} do not exist") + return + + actualGrades = [] + for grade in grades: + actualGrades.append(grade[0]) + + actualPoints = [] + for point in points: + actualPoints.append(point[0]) + + total = 0 + pointsTotal = 0 + + for i in range(len(actualGrades)): + total = total + (actualGrades[i] / 100) * actualPoints[i] + pointsTotal = pointsTotal + actualPoints[i] + + average = (total / pointsTotal) * 100 + + await ctx.author.send(f"Grade for {categoryName}: {average:.2f}%") + + # ----------------------------------------------------------------------------------------------------------------- + # Function: gradebycategory_error(self, ctx, error) + # Description: prints error message for gradebycategory command + # Inputs: + # - ctx: context of the command + # - error: error message + # Outputs: + # - Error details + # ----------------------------------------------------------------------------------------------------------------- + @gradebycategory.error + async def gradebycategory_error(self, ctx, error): + """Error handling of gradebycategory function""" + if isinstance(error, commands.MissingRequiredArgument): + await ctx.send( + "To use the gradebycategory command, do: $gradebycategory \n ( For example: $gradebycategory tests )" + ) + await ctx.message.delete() + else: + await ctx.author.send(error) + print(error) + + # ----------------------------------------------------------------------------------------------------------------- + # Function: gradeforclass(self, ctx) + # Description: This command lets a student get their average grade for the whole class + # Inputs: + # - self: used to access parameters passed to the class through the constructor + # - ctx: used to access the values passed through the current context + # Outputs: Average grade of all the assignments in the class, weighted by category, accounting for assignment point values + # ----------------------------------------------------------------------------------------------------------------- + @commands.command( + name="gradeforclass", + help="get your grade for the whole class $gradeforclass", + ) + async def gradeforclass(self, ctx): + """Lets a student get their overall average grade for the class""" + memberName = ctx.author.name + + categories = db.query( + "SELECT category_name, category_weight FROM grade_categories WHERE guild_id = %s ORDER BY category_weight DESC", + (ctx.guild.id,), + ) + + classTotal = 0 + + for category_name, category_weight in categories: + grades = db.query( + "SELECT grades.grade FROM grades INNER JOIN assignments ON grades.assignment_id = assignments.id INNER JOIN grade_categories ON assignments.category_id = grade_categories.id WHERE grades.guild_id = %s AND grades.member_name = %s AND grade_categories.category_name = %s ORDER BY grades.assignment_id", + (ctx.guild.id, memberName, category_name), + ) + + points = db.query( + "SELECT assignments.points FROM assignments INNER JOIN grade_categories ON assignments.category_id = grade_categories.id WHERE assignments.guild_id = %s AND grade_categories.category_name = %s ORDER BY assignments.id", + (ctx.guild.id, category_name), + ) + + if not grades: + await ctx.author.send(f"Grades for {category_name} do not exist") + return + + if not points: + await ctx.author.send(f"Assignments for {category_name} do not exist") + return + + actualGrades = [] + for grade in grades: + actualGrades.append(grade[0]) + + actualPoints = [] + for point in points: + actualPoints.append(point[0]) + + total = 0 + pointsTotal = 0 + + for i in range(len(actualGrades)): + total = total + (actualGrades[i] / 100) * actualPoints[i] + pointsTotal = pointsTotal + actualPoints[i] + + average = (total / pointsTotal) * 100 + + classTotal = classTotal + (average * float(category_weight)) + + await ctx.author.send(f"Grade for class: {classTotal:.2f}%") + + # ----------------------------------------------------------------------------------------------------------------- + # Function: gradeforclass_error(self, ctx, error) + # Description: prints error message for gradeforclass command + # Inputs: + # - ctx: context of the command + # - error: error message + # Outputs: + # - Error details + # ----------------------------------------------------------------------------------------------------------------- + @gradeforclass.error + async def gradeforclass_error(self, ctx, error): + """Error handling of gradeforclass function""" + await ctx.author.send(error) + print(error) + + # ----------------------------------------------------------------------------------------------------------------- + # Function: graderequired(self, ctx, categoryName, pointValue, desiredGrade) + # Description: This command lets a student get the grade they need on the next assignment to keep a desired grade + # Inputs: + # - self: used to access parameters passed to the class through the constructor + # - ctx: used to access the values passed through the current context + # - categoryName: the name of the desired category + # - pointValue: the amount of points the next assignment will be worth + # - desiredGrade: the grade desired for the category + # Outputs: The necessary grade on the next assignment to maintain a certain grade in a category + # ----------------------------------------------------------------------------------------------------------------- + @commands.command( + name="graderequired", + help="get your grade required on the next assignment for a category and a desired grade $graderequired CATEGORY POINTS GRADE", + ) + async def graderequired( + self, ctx, categoryName: str, pointValue: str, desiredGrade: str + ): + """Lets a student calculate the grade they need for a desired grade in a category""" + memberName = ctx.author.name + + grades = db.query( + "SELECT grades.grade FROM grades INNER JOIN assignments ON grades.assignment_id = assignments.id INNER JOIN grade_categories ON assignments.category_id = grade_categories.id WHERE grades.guild_id = %s AND grades.member_name = %s AND grade_categories.category_name = %s ORDER BY grades.assignment_id", + (ctx.guild.id, memberName, categoryName), + ) + + points = db.query( + "SELECT assignments.points FROM assignments INNER JOIN grade_categories ON assignments.category_id = grade_categories.id WHERE assignments.guild_id = %s AND grade_categories.category_name = %s ORDER BY assignments.id", + (ctx.guild.id, categoryName), + ) + + if not grades: + await ctx.author.send(f"Grades for {categoryName} do not exist") + return + + if not points: + await ctx.author.send(f"Assignments for {categoryName} do not exist") + return + + actualGrades = [] + for grade in grades: + actualGrades.append(grade[0]) + + actualPoints = [] + for point in points: + actualPoints.append(point[0]) + + total = 0 + pointsTotal = 0 + + for i in range(len(actualGrades)): + total = total + (actualGrades[i] / 100) * actualPoints[i] + pointsTotal = pointsTotal + actualPoints[i] + + pointsNeeded = ( + (int(desiredGrade) / 100) * (pointsTotal + int(pointValue)) + ) - total + + gradeNeeded = (pointsNeeded / int(pointValue)) * 100 + + if gradeNeeded < 0: + await ctx.author.send( + f"Grade on next assignment needed to keep {desiredGrade}% in {categoryName}: 0%" + ) + return + + await ctx.author.send( + f"Grade on next assignment needed to keep {desiredGrade}% in {categoryName}: {gradeNeeded:.2f}%" + ) + + # ----------------------------------------------------------------------------------------------------------------- + # Function: graderequired_error(self, ctx, error) + # Description: prints error message for graderequired command + # Inputs: + # - ctx: context of the command + # - error: error message + # Outputs: + # - Error details + # ----------------------------------------------------------------------------------------------------------------- + @graderequired.error + async def graderequired_error(self, ctx, error): + """Error handling of graderequired function""" + if isinstance(error, commands.MissingRequiredArgument): + await ctx.send( + "To use the graderequired command, do: $graderequired \n ( For example: $graderequired tests 200 90 )" + ) + await ctx.message.delete() + else: + await ctx.author.send(error) + print(error) + + # ----------------------------------------------------------------------------------------------------------------- + # Function: graderequiredforclass(self, ctx, categoryName, pointValue, desiredGrade) + # Description: This command lets a student get the grade they need on the next assignment to keep a desired grade + # in the class + # Inputs: + # - self: used to access parameters passed to the class through the constructor + # - ctx: used to access the values passed through the current context + # - categoryName: the name of the category the next assignment will fall in + # - pointValue: the amount of points the next assignment will be worth + # - desiredGrade: the grade desired for the class + # Outputs: The necessary grade on the next assignment to maintain a desired grade in the class + # ----------------------------------------------------------------------------------------------------------------- + @commands.command( + name="graderequiredforclass", + help="get your grade required on the next assignment to keep a desired grade $graderequiredforclass CATEGORY POINTS GRADE", + ) + async def graderequiredforclass( + self, ctx, categoryName: str, pointValue: str, desiredGrade: str + ): + """Lets a student calculate the grade required on the next assignment to keep an overall desired class grade""" + memberName = ctx.author.name + + categories = db.query( + "SELECT category_name, category_weight FROM grade_categories WHERE guild_id = %s ORDER BY category_weight DESC", + (ctx.guild.id,), + ) + + classTotal = 0 + + for category_name, category_weight in categories: + if categoryName == category_name: + categoryWeight = category_weight + break + + grades = db.query( + "SELECT grades.grade FROM grades INNER JOIN assignments ON grades.assignment_id = assignments.id INNER JOIN grade_categories ON assignments.category_id = grade_categories.id WHERE grades.guild_id = %s AND grades.member_name = %s AND grade_categories.category_name = %s ORDER BY grades.assignment_id", + (ctx.guild.id, memberName, category_name), + ) + + points = db.query( + "SELECT assignments.points FROM assignments INNER JOIN grade_categories ON assignments.category_id = grade_categories.id WHERE assignments.guild_id = %s AND grade_categories.category_name = %s ORDER BY assignments.id", + (ctx.guild.id, category_name), + ) + + if not grades: + await ctx.author.send(f"Grades for {category_name} do not exist") + return + + if not points: + await ctx.author.send(f"Assignments for {categoryName} do not exist") + return + + actualGrades = [] + for grade in grades: + actualGrades.append(grade[0]) + + actualPoints = [] + for point in points: + actualPoints.append(point[0]) + + total = 0 + pointsTotal = 0 + + for i in range(len(actualGrades)): + total = total + (actualGrades[i] / 100) * actualPoints[i] + pointsTotal = pointsTotal + actualPoints[i] + + average = (total / pointsTotal) * 100 + + classTotal = classTotal + average * float(category_weight) + + categoryGradeNeeded = (int(desiredGrade) - classTotal) / float(category_weight) + + if categoryGradeNeeded < 0: + await ctx.author.send( + f"Grade on next assignment needed to keep {int(desiredGrade)}%: 0%" + ) + return + + grades = db.query( + "SELECT grades.grade FROM grades INNER JOIN assignments ON grades.assignment_id = assignments.id INNER JOIN grade_categories ON assignments.category_id = grade_categories.id WHERE grades.guild_id = %s AND grades.member_name = %s AND grade_categories.category_name = %s ORDER BY grades.assignment_id", + (ctx.guild.id, memberName, categoryName), + ) + + points = db.query( + "SELECT assignments.points FROM assignments INNER JOIN grade_categories ON assignments.category_id = grade_categories.id WHERE assignments.guild_id = %s AND grade_categories.category_name = %s ORDER BY assignments.id", + (ctx.guild.id, categoryName), + ) + + if not grades: + await ctx.author.send(f"Grades for {categoryName} do not exist") + return + + if not points: + await ctx.author.send(f"Assignments for {categoryName} do not exist") + return + + actualGrades = [] + for grade in grades: + actualGrades.append(grade[0]) + + actualPoints = [] + for point in points: + actualPoints.append(point[0]) + + total = 0 + pointsTotal = 0 + + for i in range(len(actualGrades)): + total = total + (actualGrades[i] / 100) * actualPoints[i] + pointsTotal = pointsTotal + actualPoints[i] + + # pointsNeeded = ((int(desiredGrade) / 100) * (pointsTotal + int(pointValue))) - total + + pointsNeeded = ( + (categoryGradeNeeded / 100) * (pointsTotal + int(pointValue)) + ) - total + + gradeNeeded = (pointsNeeded / int(pointValue)) * 100 + + if gradeNeeded < 0: + await ctx.author.send( + f"Grade on next assignment needed to keep {int(desiredGrade)}%: 0%" + ) + return + + await ctx.author.send( + f"Grade on next assignment needed to keep {int(desiredGrade)}%: {gradeNeeded:.2f}%" + ) + + # ----------------------------------------------------------------------------------------------------------------- + # Function: graderequiredforclass_error(self, ctx, error) + # Description: prints error message for graderequiredforclass command + # Inputs: + # - ctx: context of the command + # - error: error message + # Outputs: + # - Error details + # ----------------------------------------------------------------------------------------------------------------- + @graderequiredforclass.error + async def graderequiredforclass_error(self, ctx, error): + """Error handling of graderequiredforclass function""" + if isinstance(error, commands.MissingRequiredArgument): + await ctx.send( + "To use the graderequiredforclass command, do: $graderequiredforclass \n ( For example: $graderequiredforclass tests 200 90 )" + ) + await ctx.message.delete() + else: + await ctx.author.send(error) + print(error) + + # ----------------------------------------------------------------------------------------------------------------- + # Function: categories(self, ctx) + # Description: This command lets the user list the categories of grades that are in the system + # Inputs: + # - self: used to access parameters passed to the class through the constructor + # - ctx: used to access the values passed through the current context + # Outputs: A list of the grade categories in the system + # ----------------------------------------------------------------------------------------------------------------- + @commands.command( + name="categories", help="display all grading categories and weights $categories" + ) + async def categories(self, ctx): + """Lets the user list the categories of grades that are in the database""" + categories = db.query( + "SELECT category_name, category_weight FROM grade_categories WHERE guild_id = %s ORDER BY category_weight DESC", + (ctx.guild.id,), + ) + + await ctx.author.send("Category | Weight") + await ctx.author.send("================") + + for category_name, category_weight in categories: + await ctx.author.send(f"{category_name} | {category_weight}") + + # ----------------------------------------------------------------------------------------------------------------- + # Function: input_grades(self, ctx, assignmentname) + # Description: This command allows the instructor to input grades into the system for a given assignment + # Inputs: + # - self: used to access parameters passed to the class through the constructor + # - ctx: used to access the values passed through the current context, including the attached csv file + # - assignmentname: the assignment that grades are being input for + # Outputs: A report on how the grades in the system were altered + # ----------------------------------------------------------------------------------------------------------------- + @commands.has_role("Instructor") + @commands.command(name="inputgrades", help="Insert grades using a csv file") + async def input_grades(self, ctx, assignmentname: str, test="False", path=""): + """Lets the instructor input grades into the system for a given assignment""" + print(assignmentname) + assignment = db.query( + "SELECT id FROM assignments WHERE guild_id = %s AND assignment_name = %s", + (ctx.guild.id, assignmentname), + ) + + if not assignment: + await ctx.send(f"Assignment with name {assignmentname} does not exist") + return + if len(ctx.message.attachments) != 1 and test == "False": + await ctx.send("Must have exactly one attachment") + return + if ( + test == "False" + and ctx.message.attachments[0].content_type != "text/csv; charset=utf-8" + ): + await ctx.send("Invalid filetype") + data = None + if test == "False": + attachmenturl = ctx.message.attachments[0].url + response = requests.get(attachmenturl, timeout=10) + data = StringIO(response.text) + if test == "TestingTrue": + data = path + df = pd.read_csv(data) + edited = 0 + added = 0 + for i in range(len(df)): + name = df.loc[i, "name"] + grade = df.loc[i, "grade"].item() + if grade < 0 or grade > 100: + await ctx.send( + f"Invalid grade value for student {name}, skipping entry" + ) + continue + student = db.query( + "SELECT username FROM name_mapping WHERE username = %s", (name,) + ) + if not student: + await ctx.send(f"Invalid student name {name}, skipping entry") + continue + existing = db.query( + "SELECT member_name FROM grades WHERE assignment_id = %s AND member_name = %s", + (assignment[0], name), + ) + + if existing: + edited += 1 + db.query( + "UPDATE grades SET grade = %s WHERE assignment_id = %s AND member_name = %s", + (grade, assignment[0], name), + ) + else: + added += 1 + db.query( + "INSERT INTO grades (guild_id, member_name, assignment_id, grade) VALUES (%s, %s, %s, %s)", + (ctx.guild.id, name, assignment[0], grade), + ) + await ctx.send( + f"Entered grades for {assignmentname}, {added} new grades entered, {edited} grades edited" + ) + + # ----------------------------------------------------------------------------------------------------------------- + # Function: input_grades_error(self, ctx, error) + # Description: prints error message for inputgrades command + # Inputs: + # - ctx: context of the command + # - error: error message + # Outputs: + # - Error details + # ----------------------------------------------------------------------------------------------------------------- + @input_grades.error + async def input_grades_error(self, ctx, error): + """Error handling for inputgrades command""" + if isinstance(error, commands.MissingRequiredArgument): + await ctx.send( + "To use the inputgrades command, do: $inputgrades and add your csv file attachment\n ( For example: $editgradecategory test1 )" + ) + await ctx.message.delete() + else: + await ctx.author.send(error) + print(error) + + # ----------------------------------------------------------------------------------------------------------------- + # Function: add_grade_category(self, ctx, categoryname, weight) + # Description: This command lets the instructor add a grade category, and set the weight for it + # Inputs: + # - self: used to access parameters passed to the class through the constructor + # - ctx: used to access the values passed through the current context + # - categoryname: the name of the grade category + # - weight: the weight of the category, must be greater than 0 + # Outputs: Whether or not the add was a success + # ----------------------------------------------------------------------------------------------------------------- + @commands.has_role("Instructor") + @commands.command( + name="addgradecategory", + help="add a grading category and weight $addgradecategory NAME WEIGHT", + ) + async def add_grade_category(self, ctx, categoryname: str, weight: str): + """Lets the instructor add a grade category with a specified weight""" + try: + categoryweight = float(weight) + except ValueError: + await ctx.send("Weight could not be parsed") + return + if categoryweight < 0: + await ctx.send("Weight must be greater than 0") + return + existing = db.query( + "SELECT id FROM grade_categories WHERE guild_id = %s AND category_name = %s", + (ctx.guild.id, categoryname), + ) + if not existing: + db.query( + "INSERT INTO grade_categories (guild_id, category_name, category_weight) VALUES (%s, %s, %s)", + (ctx.guild.id, categoryname, weight), + ) + await ctx.send( + f"A grading category has been added for: {categoryname} with weight: {weight} " + ) + else: + await ctx.send("This category has already been added..!!") + + # ----------------------------------------------------------------------------------------------------------------- + # Function: add_grade_category_error(self, ctx, error) + # Description: prints error message for addgradecategory command + # Inputs: + # - ctx: context of the command + # - error: error message + # Outputs: + # - Error details + # ----------------------------------------------------------------------------------------------------------------- + @add_grade_category.error + async def add_grade_category_error(self, ctx, error): + """Error handling for add_grade_category command""" + if isinstance(error, commands.MissingRequiredArgument): + await ctx.send( + "To use the gradecategory command, do: $gradecategory \n ( For example: $gradecategory tests .5 )" + ) + await ctx.message.delete() + else: + await ctx.author.send(error) + print(error) + + # ----------------------------------------------------------------------------------------------------------------- + # Function: edit_grade_category(self, ctx, categoryname, weight) + # Description: This command lets the instructor edit a grade category, and set a new weight for it + # Inputs: + # - self: used to access parameters passed to the class through the constructor + # - ctx: used to access the values passed through the current context + # - categoryname: the name of the grade category + # - weight: the weight of the category, must be greater than 0 + # Outputs: Whether or not the edit was a success + # ----------------------------------------------------------------------------------------------------------------- + @commands.has_role("Instructor") + @commands.command( + name="editgradecategory", + help="edit a grading category and weight $editgradecategory NAME WEIGHT", + ) + async def edit_grade_category(self, ctx, categoryname: str, weight: str): + """Lets the instructor edit a grade category and weight""" + try: + categoryweight = float(weight) + except ValueError: + await ctx.send("Weight could not be parsed") + return + if categoryweight < 0: + await ctx.send("Weight must be greater than 0") + return + existing = db.query( + "SELECT id FROM grade_categories WHERE guild_id = %s AND category_name = %s", + (ctx.guild.id, categoryname), + ) + if existing: + db.query( + "UPDATE grade_categories SET category_weight = %s WHERE id = %s", + (weight, existing[0]), + ) + await ctx.send( + f"{categoryname} category has been updated with weight:{weight} " + ) + else: + await ctx.send("This category does not exist") + + # ----------------------------------------------------------------------------------------------------------------- + # Function: edit_grade_category_error(self, ctx, error) + # Description: prints error message for editgradecategory command + # Inputs: + # - ctx: context of the command + # - error: error message + # Outputs: + # - Error details + # ----------------------------------------------------------------------------------------------------------------- + @edit_grade_category.error + async def edit_grade_category_error(self, ctx, error): + """Error handling for edit_grade_category command""" + if isinstance(error, commands.MissingRequiredArgument): + await ctx.send( + "To use the editgradecategory command, do: $editgradecategory \n ( For example: $editgradecategory tests .5 )" + ) + await ctx.message.delete() + else: + await ctx.author.send(error) + print(error) + + # ----------------------------------------------------------------------------------------------------------------- + # Function: delete_grade_category(self, ctx, categoryname) + # Description: This command lets the instructor delete a grade category + # Inputs: + # - self: used to access parameters passed to the class through the constructor + # - ctx: used to access the values passed through the current context + # - categoryname: the name of the grade category + # Outputs: Whether or not the delete was a success + # ----------------------------------------------------------------------------------------------------------------- + @commands.has_role("Instructor") + @commands.command( + name="deletegradecategory", + help="delete a grading category $deletegradecategory NAME", + ) + async def delete_grade_category(self, ctx, categoryname: str): + """Lets the user delete a grade category from the database""" + existing = db.query( + "SELECT id FROM grade_categories WHERE guild_id = %s AND category_name = %s", + (ctx.guild.id, categoryname), + ) + if existing: + db.query("DELETE FROM grade_categories WHERE id = %s", (existing[0])) + await ctx.send(f"{categoryname} category has been deleted ") + else: + await ctx.send("This category does not exist") + + # ----------------------------------------------------------------------------------------------------------------- + # Function: delete_grade_category_error(self, ctx, error) + # Description: prints error message for deletegradecategory command + # Inputs: + # - ctx: context of the command + # - error: error message + # Outputs: + # - Error details + # ----------------------------------------------------------------------------------------------------------------- + @delete_grade_category.error + async def delete_grade_category_error(self, ctx, error): + """Error handling for delete_grade_category command""" + if isinstance(error, commands.MissingRequiredArgument): + await ctx.send( + "To use the deletegradecategory command, do: $deletegradecategory \n ( For example: $deletegradecategory tests)" + ) + await ctx.message.delete() + else: + await ctx.author.send(error) + print(error) + + # ----------------------------------------------------------------------------------------------------------------- + # Function: grade_report_category(self, ctx) + # Description: This command lets the instructor generate a report on the average, low, and high score on each category + # Inputs: + # - self: used to access parameters passed to the class through the constructor + # - ctx: used to access the values passed through the current context + # Outputs: A breakdown on the performance of each category + # ----------------------------------------------------------------------------------------------------------------- + @commands.has_role("Instructor") + @commands.command( + name="gradereportcategory", + help="Report on the classes scores all grade categories", + ) + async def grade_report_category(self, ctx): + """Lets the instructor generate a report on the average, low, and high score for each category""" + result = db.query( + """SELECT category_name, AVG(grade_percent), MAX(grade_percent), MIN(grade_percent) + FROM (SELECT category_name, CAST(grade AS float) / CAST(points AS float) AS grade_percent + FROM grade_categories AS categ JOIN + (SELECT category_id, grade, points + FROM grades AS grades JOIN assignments AS assign ON grades.assignment_id = assign.id) AS grades + ON grades.category_id = categ.id) AS grade_percents + GROUP BY category_name""" + ) + + await ctx.author.send("Grade Breakdown by Category") + for category_name, avg, maxgrade, mingrade in result: + await ctx.author.send( + f"{category_name} | Average: {avg:.2f}, Max: {maxgrade:.2f}, Min: {mingrade:.2f}" + ) + + # ----------------------------------------------------------------------------------------------------------------- + # Function: grade_report_assignment(self, ctx) + # Description: This command lets the instructor generate a report on the average, low, and high score on each assignment + # Inputs: + # - self: used to access parameters passed to the class through the constructor + # - ctx: used to access the values passed through the current context + # Outputs: A breakdown on the performance of each assignment + # ----------------------------------------------------------------------------------------------------------------- + @commands.has_role("Instructor") + @commands.command( + name="gradereportassignment", + help="Report on the classes scores all assignments", + ) + async def grade_report_assignment(self, ctx): + """Lets the instructor generate a report on the average, low, and high score for each assignment""" + result = db.query( + """SELECT assignment_name, AVG(grade_percent), MAX(grade_percent), MIN(grade_percent) + FROM (SELECT assignment_name, CAST(grade AS float) / CAST(points AS float) AS grade_percent + FROM grades AS grades JOIN assignments AS assign + ON grades.assignment_id = assign.id) AS assignment_grades + GROUP BY assignment_name;""" + ) + + await ctx.author.send("Grade Breakdown by Assignment") + for assignment_name, avg, maxgrade, mingrade in result: + await ctx.author.send( + f"{assignment_name} | Average: {avg:.2f}, Max: {maxgrade:.2f}, Min: {mingrade:.2f}" + ) + + +# ------------------------------------- +# add the file to the bot's cog system +# ------------------------------------- +async def setup(bot): + """Adds the file to the bot's cog system""" + await bot.add_cog(Grades(bot)) diff --git a/cogs/groups.py b/cogs/groups.py index ccd2f6000..1a4bf7af2 100644 --- a/cogs/groups.py +++ b/cogs/groups.py @@ -31,9 +31,12 @@ def __init__(self, bot): # - ctx: used to access the values passed through the current context # Outputs: confirms role deletion # ------------------------------------------------------------------------------------------------------- - @commands.command(name="reset", help="Resets group channels and roles. DO NOT USE IN PRODUCTION!") + @commands.command( + name="reset", help="Resets group channels and roles. DO NOT USE IN PRODUCTION!" + ) async def reset(self, ctx): - await ctx.send('Deleting all roles...') + """Deletes all group roles in the server""" + await ctx.send("Deleting all roles...") for i in range(100): role_name = "group_" + str(i) @@ -56,9 +59,9 @@ async def reset(self, ctx): # ------------------------------------------------------------------------------------------------------- @reset.error async def reset_error(self, ctx, error): + """Error handling for reset command""" await ctx.author.send(error) - # ------------------------------------------------------------------------------------------------------- # Function: startupgroups(self, ctx) # Description: creates roles for the groups @@ -69,7 +72,8 @@ async def reset_error(self, ctx, error): # ------------------------------------------------------------------------------------------------------- @commands.command(name="startupgroups", help="Creates group roles for members") async def startupgroups(self, ctx): - await ctx.send('Creating roles....') + """Creates roles for the groups""" + await ctx.send("Creating roles....") for i in range(100): role_name = "group_" + str(i) @@ -91,9 +95,9 @@ async def startupgroups(self, ctx): # ------------------------------------------------------------------------------------------------------- @startupgroups.error async def startupgroups_error(self, ctx, error): + """Error handling for startupgroups command""" await ctx.author.send(error) - # ------------------------------------------------------------------------------------------------------- # Function: connect(self, ctx) # Description: connects all users with their groups @@ -104,6 +108,7 @@ async def startupgroups_error(self, ctx, error): # ------------------------------------------------------------------------------------------------------- @commands.command(name="connect", help="Creates group roles for members") async def connect(self, ctx): + """Connects all users with their groups""" for i in range(100): group_name = "group-" + str(i) existing_channel = get(ctx.guild.text_channels, name=group_name) @@ -111,9 +116,9 @@ async def connect(self, ctx): await existing_channel.delete() groups = db.query( - 'SELECT group_num, array_agg(member_name) ' - 'FROM group_members WHERE guild_id = %s GROUP BY group_num ORDER BY group_num', - (ctx.guild.id,) + "SELECT group_num, array_agg(member_name) " + "FROM group_members WHERE guild_id = %s GROUP BY group_num ORDER BY group_num", + (ctx.guild.id,), ) for group_num, *_ in groups: @@ -121,12 +126,16 @@ async def connect(self, ctx): user_role = get(ctx.guild.roles, name=role_string) overwrites = { - ctx.guild.default_role: discord.PermissionOverwrite(read_messages=False), + ctx.guild.default_role: discord.PermissionOverwrite( + read_messages=False + ), ctx.author: discord.PermissionOverwrite(read_messages=True), - user_role: discord.PermissionOverwrite(read_messages=True) + user_role: discord.PermissionOverwrite(read_messages=True), } group_channel_name = "group-" + str(group_num) - await ctx.guild.create_text_channel(group_channel_name, overwrites=overwrites) + await ctx.guild.create_text_channel( + group_channel_name, overwrites=overwrites + ) # ------------------------------------------------------------------------------------------------------- # Function: connect_error(self, ctx, error) @@ -139,6 +148,7 @@ async def connect(self, ctx): # ------------------------------------------------------------------------------------------------------- @connect.error async def connect_error(self, ctx, error): + """Error handling for connect command""" await ctx.author.send(error) # ------------------------------------------------------------------------------------------------------- @@ -151,40 +161,47 @@ async def connect_error(self, ctx, error): # Outputs: adds the user to the given group or returns an error if the group is invalid or in case of # syntax errors # ------------------------------------------------------------------------------------------------------- - @commands.command(name='join', help='To use the join command, do: $join \n \ - ( For example: $join 0 )', pass_context=True) + @commands.command( + name="join", + help="To use the join command, do: $join \n \ + ( For example: $join 0 )", + pass_context=True, + ) async def join(self, ctx, group_num: int): + """Joins the user to given group""" # get the name of the caller member_name = ctx.message.author.display_name.upper() member = ctx.message.author if group_num < 0 or group_num > 99: - await ctx.send('Not a valid group') - await ctx.send("To use the join command, do: $join " - "where 0 <= <= 99 \n ( For example: $join 0 )") + await ctx.send("Not a valid group") + await ctx.send( + "To use the join command, do: $join " + "where 0 <= <= 99 \n ( For example: $join 0 )" + ) return group_count = db.query( - 'SELECT COUNT(group_num) FROM group_members WHERE guild_id = %s AND group_num = %s', - (ctx.guild.id, group_num) + "SELECT COUNT(group_num) FROM group_members WHERE guild_id = %s AND group_num = %s", + (ctx.guild.id, group_num), ) if group_count == 6: - await ctx.send('A group cannot have more than 6 people!') + await ctx.send("A group cannot have more than 6 people!") return current_group_num = db.query( - 'SELECT group_num FROM group_members WHERE guild_id = %s AND member_name = %s', - (ctx.guild.id, member_name) + "SELECT group_num FROM group_members WHERE guild_id = %s AND member_name = %s", + (ctx.guild.id, member_name), ) if current_group_num: - await ctx.send(f'You are already in Group {current_group_num[0][0]}') + await ctx.send(f"You are already in Group {current_group_num[0][0]}") return db.query( - 'INSERT INTO group_members (guild_id, group_num, member_name) VALUES (%s, %s, %s)', - (ctx.guild.id, group_num, member_name) + "INSERT INTO group_members (guild_id, group_num, member_name) VALUES (%s, %s, %s)", + (ctx.guild.id, group_num, member_name), ) identifier = "group_" + str(group_num) role = get(ctx.guild.roles, name=identifier) @@ -195,7 +212,9 @@ async def join(self, ctx, group_num: int): await member.add_roles(role) - await ctx.send(f'You are now in Group {group_num}! There are now {group_count[0][0] + 1}/6 members.') + await ctx.send( + f"You are now in Group {group_num}! There are now {group_count[0][0] + 1}/6 members." + ) # ------------------------------------------------------------------------------------------------------- # Function: join_error(self, ctx, error) @@ -208,11 +227,14 @@ async def join(self, ctx, group_num: int): # ------------------------------------------------------------------------------------------------------- @join.error async def join_error(self, ctx, error): + """Error handling for join command""" if isinstance(error, commands.MissingRequiredArgument): - await ctx.send('To use the join command, do: $join \n ( For example: $join 0 )') + await ctx.send( + "To use the join command, do: $join \n ( For example: $join 0 )" + ) else: await ctx.author.send(error) - #await ctx.message.delete() + # await ctx.message.delete() print(error) # ------------------------------------------------------------------------------------------------------- @@ -224,32 +246,38 @@ async def join_error(self, ctx, error): # Outputs: removes the user from the given group or returns an error if the group is invalid or in # case of syntax errors # ------------------------------------------------------------------------------------------------------- - @commands.command(name='leave', help='To use the leave command, do: $leave \n \ - ( For example: $leave )', pass_context=True) + @commands.command( + name="leave", + help="To use the leave command, do: $leave \n \ + ( For example: $leave )", + pass_context=True, + ) async def leave(self, ctx): + """Removes the user from the given group""" # get the name of the caller member_name = ctx.message.author.display_name.upper() member = ctx.message.author current_group_num = db.query( - 'SELECT group_num FROM group_members WHERE guild_id = %s AND member_name = %s', - (ctx.guild.id, member_name) + "SELECT group_num FROM group_members WHERE guild_id = %s AND member_name = %s", + (ctx.guild.id, member_name), ) if current_group_num: db.query( - 'DELETE FROM group_members WHERE guild_id = %s AND member_name = %s', - (ctx.guild.id, member_name) + "DELETE FROM group_members WHERE guild_id = %s AND member_name = %s", + (ctx.guild.id, member_name), + ) + await ctx.send( + f"You have been removed from Group {current_group_num[0][0]}!" ) - await ctx.send(f'You have been removed from Group {current_group_num[0][0]}!') identifier = "group_" + str(current_group_num[0][0]) role = get(ctx.guild.roles, name=identifier) await member.remove_roles(role) else: - await ctx.send('You are not in a group!') - + await ctx.send("You are not in a group!") # ------------------------------------------------------------------------------------------------------- # Function: leave_error(self, ctx, error) @@ -262,11 +290,11 @@ async def leave(self, ctx): # ------------------------------------------------------------------------------------------------------- @leave.error async def leave_error(self, ctx, error): + """Error handling for leave command""" await ctx.author.send(error) - #await ctx.message.delete() + # await ctx.message.delete() print(error) - # ------------------------------------------------------------------------------------------------------- # Function: group(self, ctx) # Description: prints the list of groups @@ -275,23 +303,28 @@ async def leave_error(self, ctx, error): # - ctx: used to access the values passed through the current context # Outputs: prints the list of groups # ------------------------------------------------------------------------------------------------------- - @commands.command(name='groups', help='prints group counts', pass_context=True) + @commands.command(name="groups", help="prints group counts", pass_context=True) # @commands.dm_only() # TODO maybe include channel where all groups displayed async def groups(self, ctx): + """Prints the list of groups""" # load groups csv groups = db.query( - 'SELECT group_num, array_agg(member_name) ' - 'FROM group_members WHERE guild_id = %s GROUP BY group_num ORDER BY group_num', - (ctx.guild.id,) + "SELECT group_num, array_agg(member_name) " + "FROM group_members WHERE guild_id = %s GROUP BY group_num ORDER BY group_num", + (ctx.guild.id,), ) # create embedded objects - embed = discord.Embed(title='Group List', color=discord.Color.teal()) - embed.set_thumbnail(url="https://i.pinimg.com/474x/e7/e3/bd/e7e3bd1b5628510a4e9d7a9a098b7be8.jpg") + embed = discord.Embed(title="Group List", color=discord.Color.teal()) + embed.set_thumbnail( + url="https://i.pinimg.com/474x/e7/e3/bd/e7e3bd1b5628510a4e9d7a9a098b7be8.jpg" + ) for group_num, members in groups: - embed.add_field(name=f'Group {group_num}', value=str(len(members)), inline=True) + embed.add_field( + name=f"Group {group_num}", value=str(len(members)), inline=True + ) # print the embedded objects embed.set_footer(text="Number Represents the Group Size") @@ -308,12 +341,13 @@ async def groups(self, ctx): # ------------------------------------------------------------------------------------------------------- @groups.error async def groups_error(self, ctx, error): + """Error handling for groups command""" await ctx.author.send(error) - #await ctx.message.delete() + # await ctx.message.delete() print(error) - # ------------------------------------------------------------------------------------------------------- + # Function: group(self, ctx, group_num) # Description: prints the members of the group, or the current members group if they have a group # Inputs: @@ -322,50 +356,55 @@ async def groups_error(self, ctx, error): # - group_num: the group number to list names for # Outputs: prints the name of people in the group # ------------------------------------------------------------------------------------------------------- - @commands.command(name='group', help='print names of members in a group, or current groups members \n \ - ( For example: $group or $group 8 )', pass_context=True) + @commands.command( + name="group", + help="print names of members in a group, or current groups members \n \ + ( For example: $group or $group 8 )", + pass_context=True, + ) # @commands.dm_only() # TODO maybe include channel where all groups displayed async def group(self, ctx, group_num: int = -1): - + """Prints the members of the group, or the current member's group if they have joined one""" if group_num == -1: member_name = ctx.message.author.display_name.upper() group_num = db.query( - 'SELECT group_num FROM group_members WHERE guild_id = %s and member_name = %s', - (ctx.guild.id, member_name) + "SELECT group_num FROM group_members WHERE guild_id = %s and member_name = %s", + (ctx.guild.id, member_name), ) if not group_num: - await ctx.send('You are not in a group!') + await ctx.send("You are not in a group!") return group_num = group_num[0][0] # load groups csv group = db.query( - 'SELECT member_name FROM group_members WHERE guild_id = %s and group_num = %s', - (ctx.guild.id, group_num) + "SELECT member_name FROM group_members WHERE guild_id = %s and group_num = %s", + (ctx.guild.id, group_num), ) # create embedded objects - embed = discord.Embed(title='Group Members', color=discord.Color.teal()) - embed.set_thumbnail(url="https://i.pinimg.com/474x/e7/e3/bd/e7e3bd1b5628510a4e9d7a9a098b7be8.jpg") + embed = discord.Embed(title="Group Members", color=discord.Color.teal()) + embed.set_thumbnail( + url="https://i.pinimg.com/474x/e7/e3/bd/e7e3bd1b5628510a4e9d7a9a098b7be8.jpg" + ) members = "" for member in group: - members += member[0] + '\n' + members += member[0] + "\n" if members == "": members = "None" - embed.add_field(name=f'Group {group_num}: ', value=members, inline=True) + embed.add_field(name=f"Group {group_num}: ", value=members, inline=True) # print the embedded objects await ctx.send(embed=embed) - # ------------------------------------------------------------------------------------------------------- # Function: group_error(self, ctx, error) # Description: prints error message for group command @@ -377,14 +416,16 @@ async def group(self, ctx, group_num: int = -1): # ------------------------------------------------------------------------------------------------------- @group.error async def group_error(self, ctx, error): + """Error handling for group command""" if isinstance(error, commands.MissingRequiredArgument): - await ctx.send('To use the group command, do: $group \n ( For example: $group 0 )') + await ctx.send( + "To use the group command, do: $group \n ( For example: $group 0 )" + ) else: await ctx.author.send(error) - #await ctx.message.delete() + # await ctx.message.delete() print(error) - # ----------------------------------------------------------- # This is a testing arg, not really used for anything else but adding to the csv file # ----------------------------------------------------------- @@ -403,7 +444,6 @@ async def group_error(self, ctx, error): # print_pool(student_pool) - # # ------------------------------------------------------------ # # Used to load the members from the csv file into a dictionary # # ------------------------------------------------------------ @@ -435,5 +475,6 @@ async def group_error(self, ctx, error): # ----------------------------------------------------------- # add the file to the bot's cog system # ----------------------------------------------------------- -def setup(bot): - bot.add_cog(Groups(bot)) +async def setup(bot): + """Adds the file to the bot's cog system""" + await bot.add_cog(Groups(bot)) diff --git a/cogs/newComer.py b/cogs/newComer.py index d6e2d5fe9..b98b5f7c8 100644 --- a/cogs/newComer.py +++ b/cogs/newComer.py @@ -33,15 +33,20 @@ def __init__(self, bot): name="verify", pass_context=True, help="User self-verifies by attaching their real name to their discord username in this server: " - "$verify ", + "$verify ", ) async def verify(self, ctx, *, name: str = None): + """Gives the user the `verified` role in the server""" member = ctx.message.author # check if verified and unverified roles exist - if discord.utils.get(ctx.guild.roles, name="unverified") is None \ - or discord.utils.get(ctx.guild.roles, name="verified") is None: - await ctx.send("Warning: Please make sure the verified and unverified roles exist in this server!") + if ( + discord.utils.get(ctx.guild.roles, name="unverified") is None + or discord.utils.get(ctx.guild.roles, name="verified") is None + ): + await ctx.send( + "Warning: Please make sure the verified and unverified roles exist in this server!" + ) return # finds the unverified role in the guild @@ -55,21 +60,27 @@ async def verify(self, ctx, *, name: str = None): "To use the verify command, do: $verify \n ( For example: $verify Jane Doe )" ) else: - db.query('INSERT INTO name_mapping (guild_id, username, real_name) VALUES (%s, %s, %s)', - (ctx.guild.id, member.name, name)) + db.query( + "INSERT INTO name_mapping (guild_id, username, real_name) VALUES (%s, %s, %s)", + (ctx.guild.id, member.name, name), + ) await member.add_roles(verified) # adding verified role await member.remove_roles(unverified) # removed unverified role - await ctx.send(f"Thank you for verifying! You can start using {ctx.guild.name}!") + await ctx.send( + f"Thank you for verifying! You can start using {ctx.guild.name}!" + ) embed = discord.Embed( - description="Click [Here](https://github.com/txt/se21) for the home page of the class Github page" + description="Click [Here](https://github.com/txt/se23) for the home page of the class Github page" ) await member.send(embed=embed) else: # user has verified role - db.query('SELECT real_name from name_mapping where guild_id = %s and username = %s', - (ctx.guild.id, member.name)) + db.query( + "SELECT real_name from name_mapping where guild_id = %s and username = %s", + (ctx.guild.id, member.name), + ) await ctx.send("You are already verified!") embed = discord.Embed( - description="Click [Here](https://github.com/txt/se21) for the home page of the class Github page" + description="Click [Here](https://github.com/txt/se23) for the home page of the class Github page" ) await member.send(embed=embed) @@ -84,21 +95,24 @@ async def verify(self, ctx, *, name: str = None): # ----------------------------------------------------------------------------------------------------------------- @verify.error async def verify_error(self, ctx, error): + """Error handling for verify command""" if isinstance(error, commands.MissingRequiredArgument): await ctx.send( - "To use the verify command, do: $verify \n ( For example: $verify Jane Doe )") + "To use the verify command, do: $verify \n ( For example: $verify Jane Doe )" + ) else: await ctx.author.send(error) - #await ctx.message.delete() + # await ctx.message.delete() print(error) # -------------------------------------- # add the file to the bot's cog system # -------------------------------------- -def setup(bot): +async def setup(bot): + """Adds the file to the bot's cog system""" n = NewComer(bot) - bot.add_cog(n) + await bot.add_cog(n) # Copyright (c) 2021 War-Keeper diff --git a/cogs/ping.py b/cogs/ping.py index f7b47b75d..551c8c1d0 100644 --- a/cogs/ping.py +++ b/cogs/ping.py @@ -6,7 +6,6 @@ # Returns the ping of the bot, useful for testing bot lag and as a simple functionality command # ---------------------------------------------------------------------------------------------- class Helpful(commands.Cog): - def __init__(self, bot): self.bot = bot @@ -20,9 +19,11 @@ def __init__(self, bot): # ------------------------------------------------------------------------------------------------------- @commands.command() async def ping(self, ctx): + """Prints the current ping of the bot, used as a test function""" # We set an upper bound on the ping of the bot to prevent float_infinity situations which crash testing - await ctx.send(f"Pong! My ping currently is {round(min(999999999, self.bot.latency * 1000))}ms") - + await ctx.send( + f"Pong! My ping currently is {round(min(999999999, self.bot.latency * 1000))}ms" + ) # ----------------------------------------------------------------------------------------------------------------- # Function: ping_error(self, ctx, error) @@ -33,8 +34,8 @@ async def ping(self, ctx): # Outputs: # - Error details # ----------------------------------------------------------------------------------------------------------------- - #@ping.error - #async def ping_error(self, ctx, error): + # @ping.error + # async def ping_error(self, ctx, error): # await ctx.author.send(error) # print(error) @@ -42,5 +43,6 @@ async def ping(self, ctx): # ------------------------------------- # add the file to the bot's cog system # ------------------------------------- -def setup(bot): - bot.add_cog(Helpful(bot)) +async def setup(bot): + """Adds the file to the bot's cog system""" + await bot.add_cog(Helpful(bot)) diff --git a/cogs/pinning.py b/cogs/pinning.py index f9fdd3878..8b121f736 100644 --- a/cogs/pinning.py +++ b/cogs/pinning.py @@ -13,13 +13,13 @@ class Pinning(commands.Cog): - def __init__(self, bot): self.bot = bot # Test command to check if the bot is working @commands.command() async def helpful3(self, ctx): + """Test command to chheck if the bot it working""" await ctx.send(f"Pong! My ping currently is {round(self.bot.latency * 1000)}ms") # ----------------------------------------------------------------------------------------------------------------- @@ -31,19 +31,23 @@ async def helpful3(self, ctx): # - tagname: a tag given by the user to their pinned message. # - description: description of the pinned message given by the user. # ----------------------------------------------------------------------------------------------------------------- - @commands.command(name="pin", - help="Pin a message by adding a tagname (single word) " - "and a description(can be multi word). EX: $pin Homework Resources for HW2") + @commands.command( + name="pin", + help="Pin a message by adding a tagname (single word) " + "and a description(can be multi word). EX: $pin Homework Resources for HW2", + ) async def addMessage(self, ctx, tagname: str, *, description: str): + """Used to pin a message by the user""" author = ctx.message.author db.query( - 'INSERT INTO pinned_messages (guild_id, author_id, tag, description) VALUES (%s, %s, %s, %s)', - (ctx.guild.id, author.id, tagname, description) + "INSERT INTO pinned_messages (guild_id, author_id, tag, description) VALUES (%s, %s, %s, %s)", + (ctx.guild.id, author.id, tagname, description), ) await ctx.send( - f"A new message has been pinned with tag: {tagname} and description: {description} by {author}.") + f"A new message has been pinned with tag: {tagname} and description: {description} by {author}." + ) # ----------------------------------------------------------------------------------------------------------------- # Function: addMessage_error(self, ctx, error) @@ -56,13 +60,15 @@ async def addMessage(self, ctx, tagname: str, *, description: str): # ----------------------------------------------------------------------------------------------------------------- @addMessage.error async def addMessage_error(self, ctx, error): + """Error handling for pin(addMessage) command""" if isinstance(error, commands.MissingRequiredArgument): await ctx.send( "To use the pin command, do: $pin TAGNAME DESCRIPTION \n ( For example: $pin HW8 https://" - "discordapp.com/channels/139565116151562240/139565116151562240/890813190433292298 HW8 reminder )") + "discordapp.com/channels/139565116151562240/139565116151562240/890813190433292298 HW8 reminder )" + ) else: await ctx.author.send(error) - #await ctx.message.delete() + # await ctx.message.delete() print(error) # ----------------------------------------------------------------------------------------------------------------- @@ -75,23 +81,26 @@ async def addMessage_error(self, ctx, error): # ----------------------------------------------------------------------------------------------------------------- @commands.command(name="unpin", help="Unpin a message by passing the tagname.") async def deleteMessage(self, ctx, tagname: str): + """Unpins the pinned messages with provided tagname""" author = ctx.message.author rows_deleted = db.query( - 'SELECT * FROM pinned_messages WHERE guild_id = %s AND tag = %s AND author_id = %s', - (ctx.guild.id, tagname, author.id) + "SELECT * FROM pinned_messages WHERE guild_id = %s AND tag = %s AND author_id = %s", + (ctx.guild.id, tagname, author.id), ) db.query( - 'DELETE FROM pinned_messages WHERE guild_id = %s AND tag = %s AND author_id = %s', - (ctx.guild.id, tagname, author.id) + "DELETE FROM pinned_messages WHERE guild_id = %s AND tag = %s AND author_id = %s", + (ctx.guild.id, tagname, author.id), ) if len(rows_deleted) == 0: await ctx.send( - f"No message found with the combination of tagname: {tagname}, and author: {author}.") + f"No message found with the combination of tagname: {tagname}, and author: {author}." + ) else: await ctx.send( - f"{len(rows_deleted)} pinned message(s) has been deleted with tag: {tagname}.") + f"{len(rows_deleted)} pinned message(s) has been deleted with tag: {tagname}." + ) # ----------------------------------------------------------------------------------------------------------------- # Function: deleteMessage_error(self, ctx, error) @@ -104,12 +113,14 @@ async def deleteMessage(self, ctx, tagname: str): # ----------------------------------------------------------------------------------------------------------------- @deleteMessage.error async def deleteMessage_error(self, ctx, error): + """Error handling for unpin(deleteMessage) command""" if isinstance(error, commands.MissingRequiredArgument): await ctx.send( - 'To use the unpin command, do: $unpin TAGNAME \n ( For example: $unpin HW8 )') + "To use the unpin command, do: $unpin TAGNAME \n ( For example: $unpin HW8 )" + ) else: await ctx.author.send(error) - #await ctx.message.delete() + # await ctx.message.delete() print(error) # ---------------------------------------------------------------------------------- @@ -121,23 +132,29 @@ async def deleteMessage_error(self, ctx, error): # - ctx: used to access the values passed through the current context # - tagname: the tag used to identify which pinned messages are to be retrieved. # ---------------------------------------------------------------------------------- - @commands.command(name="pinnedmessages", help="Retrieve the pinned messages by a particular tag or all messages.") + @commands.command( + name="pinnedmessages", + help="Retrieve the pinned messages by a particular tag or all messages.", + ) async def retrieveMessages(self, ctx, tagname: str = ""): + """Retrieves all pinned messages under a given tagname by either everyone or a particular user""" author = ctx.message.author if tagname == "": messages = db.query( - 'SELECT tag, description FROM pinned_messages WHERE guild_id = %s AND author_id = %s', - (ctx.guild.id, author.id) + "SELECT tag, description FROM pinned_messages WHERE guild_id = %s AND author_id = %s", + (ctx.guild.id, author.id), ) else: messages = db.query( - 'SELECT tag, description FROM pinned_messages WHERE guild_id = %s AND author_id = %s AND tag = %s', - (ctx.guild.id, author.id, tagname) + "SELECT tag, description FROM pinned_messages WHERE guild_id = %s AND author_id = %s AND tag = %s", + (ctx.guild.id, author.id, tagname), ) if len(messages) == 0: - await ctx.send("No messages found with the given tagname and author combination") + await ctx.send( + "No messages found with the given tagname and author combination" + ) for tag, description in messages: await ctx.send(f"Tag: {tag}, Description: {description}") @@ -152,13 +169,15 @@ async def retrieveMessages(self, ctx, tagname: str = ""): # ----------------------------------------------------------------------------------------------------------------- @retrieveMessages.error async def retrieveMessages_error(self, ctx, error): + """Error handling for retrievemessages function""" if isinstance(error, commands.MissingRequiredArgument): await ctx.send( "To use the pinnedmessages command, do: $pinnedmessages:" - " TAGNAME \n ( For example: $pinnedmessages HW8 )") + " TAGNAME \n ( For example: $pinnedmessages HW8 )" + ) else: await ctx.author.send(error) - #await ctx.message.delete() + # await ctx.message.delete() print(error) # ---------------------------------------------------------------------------------------------------------- @@ -170,12 +189,17 @@ async def retrieveMessages_error(self, ctx, error): # - tagname: tag to be updated # - description: new description # ---------------------------------------------------------------------------------------------------------- - @commands.command(name="updatepin", - help="Update a previously pinned message by passing the " - "tagname and old description in the same order") + @commands.command( + name="updatepin", + help="Update a previously pinned message by passing the " + "tagname and old description in the same order", + ) async def updatePinnedMessage(self, ctx, tagname: str, *, description: str): - await ctx.invoke(self.bot.get_command('unpin'), tagname) - await ctx.invoke(self.bot.get_command('pin'), tagname=tagname, description=description) + """Updates a pinned message with a given tagname, deletes old messages for the tag""" + await ctx.invoke(self.bot.get_command("unpin"), tagname) + await ctx.invoke( + self.bot.get_command("pin"), tagname=tagname, description=description + ) # ----------------------------------------------------------------------------------------------------------------- # Function: updatePinnedMessage_error(self, ctx, error) @@ -188,20 +212,22 @@ async def updatePinnedMessage(self, ctx, tagname: str, *, description: str): # ----------------------------------------------------------------------------------------------------------------- @updatePinnedMessage.error async def updatePinnedMessage_error(self, ctx, error): + """Error handling for updatepinnedmessage function""" if isinstance(error, commands.MissingRequiredArgument): await ctx.send( "To use the updatepin command, do: $pin TAGNAME DESCRIPTION \n ( $updatepin HW8 https://discordapp" - ".com/channels/139565116151562240/139565116151562240/890814489480531969 HW8 reminder )") + ".com/channels/139565116151562240/139565116151562240/890814489480531969 HW8 reminder )" + ) else: await ctx.author.send(error) - #await ctx.message.delete() + # await ctx.message.delete() print(error) - # ------------------------------------- # add the file to the bot's cog system # ------------------------------------- -def setup(bot): +async def setup(bot): + """Adds the file to the bot's cog system""" n = Pinning(bot) - bot.add_cog(n) + await bot.add_cog(n) diff --git a/cogs/polling.py b/cogs/polling.py index 20b83b9bc..b9933cf3f 100644 --- a/cogs/polling.py +++ b/cogs/polling.py @@ -3,8 +3,8 @@ from discord.ext import commands import re -class Poll(commands.Cog): +class Poll(commands.Cog): def __init__(self, bot): self.bot = bot @@ -18,36 +18,35 @@ def __init__(self, bot): ] # parses the title, which should be in between curly brackets ('{ title }') - #def find_title(self, message): - # this is the index of the first character of the title - #first = message.find('{') + 1 - # index of the last character of the title - #last = message.find('}') + # def find_title(self, message): + # this is the index of the first character of the title + # first = message.find('{') + 1 + # index of the last character of the title + # last = message.find('}') - #if first == last: # if the character after '{' is '}' ... does not check for whitespace. - # return "" + # if first == last: # if the character after '{' is '}' ... does not check for whitespace. + # return "" - #if first == 0 or last == -1: - # return "" - #return message[first:last] + # if first == 0 or last == -1: + # return "" + # return message[first:last] # parses the options (recursively), which should be in between square brackets ('[ option n ]') - #def find_options(self, message, options): - # first index of the first character of the option - #first = message.find('[') + 1 - # index of the last character of the title - #last = message.find(']') - #if (first == 0 or last == -1): - # if len(options) < 2: - # return "Not using the command correctly" - # else: - # return options - #options.append(message[first:last]) - #message = message[last + 1:] - #return self.find_options(message, options) - - # @commands.Cog.listener() - + # def find_options(self, message, options): + # first index of the first character of the option + # first = message.find('[') + 1 + # index of the last character of the title + # last = message.find(']') + # if (first == 0 or last == -1): + # if len(options) < 2: + # return "Not using the command correctly" + # else: + # return options + # options.append(message[first:last]) + # message = message[last + 1:] + # return self.find_options(message, options) + + # @commands.Cog.listener() # @commands.cooldown(2, 60, BucketType.user) # ----------------------------------------------------------------------------------------------------------------- @@ -62,15 +61,17 @@ def __init__(self, bot): # ----------------------------------------------------------------------------------------------------------------- @commands.command( name="quizpoll", - help = 'Create a multi reaction poll by typing \n$poll "TITLE" [option 1] ... [option 6]\n ' - 'Be sure to enclose title with quotes and options with brackets!\n' - 'EX: $quizpoll "I am a poll" [Vote for me!] [I am option 2]') + help='Create a multi reaction poll by typing \n$poll "TITLE" [option 1] ... [option 6]\n ' + "Be sure to enclose title with quotes and options with brackets!\n" + 'EX: $quizpoll "I am a poll" [Vote for me!] [I am option 2]', + ) async def quizpoll(self, ctx, title: str, *, ops): - #message = ctx.message - #messageContent = message.clean_content + """Allows the user to begin quiz polls; that is, multi-reaction polls with listed questions""" + # message = ctx.message + # messageContent = message.clean_content - #title = self.find_title(messageContent) - #options = self.find_options(messageContent, []) + # title = self.find_title(messageContent) + # options = self.find_options(messageContent, []) # if title is blank, whitespace only, or just too short! if not title or title.isspace(): @@ -84,17 +85,15 @@ async def quizpoll(self, ctx, title: str, *, ops): return # regex: extracts every string between brackets - options = re.findall(r'\[([^[\]]*)\]', ops) + options = re.findall(r"\[([^[\]]*)\]", ops) if len(options) < 2: - await ctx.author.send( - "Polls need at least two options.") + await ctx.author.send("Polls need at least two options.") await ctx.message.delete() return if len(options) > 6: - await ctx.author.send( - "Polls cannot have more than six options.") + await ctx.author.send("Polls cannot have more than six options.") await ctx.message.delete() return @@ -102,37 +101,41 @@ async def quizpoll(self, ctx, title: str, *, ops): pollMessage = "" i = 0 for choice in options: - if not choice or choice.isspace(): # if empty or whitespace only + if not choice or choice.isspace(): # if empty or whitespace only await ctx.author.send("Options cannot be blank or whitespace only.") await ctx.message.delete() return if not i == len(options): - pollMessage = pollMessage + "\n\n" + self.emojiLetters[i] + " " + choice + pollMessage = ( + pollMessage + "\n\n" + self.emojiLetters[i] + " " + choice + ) i += 1 ads = [""] - e = discord.Embed(title="**" + title + "**", - description=pollMessage + ads[0], - colour=0x83bae3) + e = discord.Embed( + title="**" + title + "**", + description=pollMessage + ads[0], + colour=0x83BAE3, + ) pollMessage = await ctx.send(embed=e) i = 0 - #final_options = [] # There is a better way to do this for sure, but it also works that way + # final_options = [] # There is a better way to do this for sure, but it also works that way for choice in options: if not i == len(options) and not options[i] == "": - #final_options.append(choice) + # final_options.append(choice) await pollMessage.add_reaction(self.emojiLetters[i]) i += 1 except KeyError: await ctx.author.send( 'To use the quizpoll command, do: $quizpoll "TITLE" [option1] [option2] ... [option6]\n ' - 'Be sure to enclose title with quotes and options with brackets!\n' - 'EX: $quizpoll "I am a poll" [Vote for me!] [I am option 2]') + "Be sure to enclose title with quotes and options with brackets!\n" + 'EX: $quizpoll "I am a poll" [Vote for me!] [I am option 2]' + ) await ctx.message.delete() return - # delete user message - else: - await ctx.message.delete() + # else delete user message + await ctx.message.delete() # ----------------------------------------------------------------------------------------------------------------- # Function: quizpoll_error(self, ctx, error) @@ -145,11 +148,13 @@ async def quizpoll(self, ctx, title: str, *, ops): # ----------------------------------------------------------------------------------------------------------------- @quizpoll.error async def quizpoll_error(self, ctx, error): + """Error handling for quizpoll command""" if isinstance(error, commands.MissingRequiredArgument): await ctx.author.send( 'To use the quizpoll command, do: $quizpoll "TITLE" [option1] [option2] ... [option6]\n ' - 'Be sure to enclose title with quotes and options with brackets!\n' - 'EX: $quizpoll "I am a poll" [Vote for me!] [I am option 2]') + "Be sure to enclose title with quotes and options with brackets!\n" + 'EX: $quizpoll "I am a poll" [Vote for me!] [I am option 2]' + ) else: await ctx.author.send(error) await ctx.message.delete() @@ -163,25 +168,28 @@ async def quizpoll_error(self, ctx, error): # Outputs: # - an embedded reaction poll # ----------------------------------------------------------------------------------------------------------------- - @commands.command(name="poll", help = 'Create a reaction poll by typing $poll QUESTION\n' - 'EX: $poll What do you think about cats?') - async def poll(self, ctx, *, qs=''): - - if qs == '': + @commands.command( + name="poll", + help="Create a reaction poll by typing $poll QUESTION\n" + "EX: $poll What do you think about cats?", + ) + async def poll(self, ctx, *, qs=""): + """Allows the user to create a simple reaction poll with thumbs up, thumbs down, and unsure""" + if qs == "": await ctx.author.send("Please enter a question for your poll.") - #await ctx.send( - #'To use the poll command, do: $poll QUESTION\n' - #'EX: $poll Is this a good idea?') + # await ctx.send( + #'To use the poll command, do: $poll QUESTION\n' + #'EX: $poll Is this a good idea?') await ctx.message.delete() return # if using qs:str instead of *; checks for empty and whitespace only strings - #if not qs or qs.isspace(): + # if not qs or qs.isspace(): # await ctx.author.send("Please enter a question for your poll.") - #await ctx.send( - # 'To use the poll command, do: $poll QUESTION\n' - # 'EX: $poll Is this a good idea?') - #await ctx.message.delete() + # await ctx.send( + # 'To use the poll command, do: $poll QUESTION\n' + # 'EX: $poll Is this a good idea?') + # await ctx.message.delete() # return if len(qs) <= 2: @@ -191,24 +199,24 @@ async def poll(self, ctx, *, qs=''): # can make it anonymous or not, is anonymous by default. if "instructor" in [y.name.lower() for y in ctx.author.roles]: - author = 'Instructor' + author = "Instructor" else: - author = 'Student' + author = "Student" - #author = ctx.message.author.id - #author_str = (await self.bot.fetch_user(author)).name + # author = ctx.message.author.id + # author_str = (await self.bot.fetch_user(author)).name # create a poll, post to channel, and add reactions. - #pollmsg = f"**POLL by {author_str}**\n\n{pollstr}\n** **" + # pollmsg = f"**POLL by {author_str}**\n\n{pollstr}\n** **" pollmsg = f"**POLL by {author}**\n\n{qs}\n** **" message = await ctx.send(pollmsg) - #TODO: ADD POLL ID TO DATABASE. - #Need to check for deleted IDs when fetching poll results later. + # TODO: ADD POLL ID TO DATABASE. + # Need to check for deleted IDs when fetching poll results later. - await message.add_reaction('👍') - await message.add_reaction('👎') - await message.add_reaction('🤷') + await message.add_reaction("👍") + await message.add_reaction("👎") + await message.add_reaction("🤷") # delete original message await ctx.message.delete() @@ -224,14 +232,18 @@ async def poll(self, ctx, *, qs=''): # ----------------------------------------------------------------------------------------------------------------- @poll.error async def poll_error(self, ctx, error): + """Error handling for poll command""" if isinstance(error, commands.MissingRequiredArgument): await ctx.author.send( - 'To use the poll command, do: $poll QUESTION\n' - 'EX: $poll Is this a good idea?') + "To use the poll command, do: $poll QUESTION\n" + "EX: $poll Is this a good idea?" + ) else: await ctx.author.send(error) await ctx.message.delete() -def setup(bot): + +async def setup(bot): + """Adds the file to the bot's cog system""" n = Poll(bot) - bot.add_cog(n) + await bot.add_cog(n) diff --git a/cogs/qanda.py b/cogs/qanda.py index b8582fb69..3683d444a 100644 --- a/cogs/qanda.py +++ b/cogs/qanda.py @@ -5,8 +5,8 @@ import db import re -class Qanda(commands.Cog): +class Qanda(commands.Cog): def __init__(self, bot): self.bot = bot @@ -20,13 +20,16 @@ def __init__(self, bot): # Outputs: # - User question in new post # ----------------------------------------------------------------------------------------------------------------- - @commands.command(name='ask', help='Ask question. Please put question text in quotes. Add *anonymous* or *anon* if desired.' - 'EX: $ask /"When is the exam?/" anonymous') - async def askQuestion(self, ctx, qs: str, anonymous=''): - + @commands.command( + name="ask", + help="Ask question. Please put question text in quotes. Add *anonymous* or *anon* if desired." + 'EX: $ask /"When is the exam?/" anonymous', + ) + async def askQuestion(self, ctx, qs: str, anonymous=""): + """Takes question from the user the reposts it anonymously and numbered""" # make sure to check that this is actually being asked in the Q&A channel - if not ctx.channel.name == 'q-and-a': - await ctx.author.send('Please send questions to the #q-and-a channel.') + if not ctx.channel.name == "q-and-a": + await ctx.author.send("Please send questions to the #q-and-a channel.") await ctx.message.delete() return @@ -36,35 +39,44 @@ async def askQuestion(self, ctx, qs: str, anonymous=''): return if len(qs) <= 2: - await ctx.author.send('Question too short.') + await ctx.author.send("Question too short.") await ctx.message.delete() return # get author - if anonymous == '': + if anonymous == "": author = ctx.message.author.id - elif anonymous == 'anonymous': + elif anonymous == "anonymous": author = None - elif anonymous == 'anon': + elif anonymous == "anon": author = None else: - await ctx.author.send('Unknown input for *anonymous* option. Please type **anonymous**, **anon**, or leave blank.') + await ctx.author.send( + "Unknown input for *anonymous* option. Please type **anonymous**, **anon**, or leave blank." + ) await ctx.message.delete() return # get number of questions + 1 - num = db.query('SELECT COUNT(*) FROM questions WHERE guild_id = %s', (ctx.guild.id,))[0][0] + 1 + num = ( + db.query( + "SELECT COUNT(*) FROM questions WHERE guild_id = %s", (ctx.guild.id,) + )[0][0] + + 1 + ) # format question - author_str = 'anonymous' if author is None else (await self.bot.fetch_user(author)).name + author_str = ( + "anonymous" if author is None else (await self.bot.fetch_user(author)).name + ) q_str = f"Q{num}: {qs} by {author_str}" message = await ctx.send(q_str) # add to db db.query( - 'INSERT INTO questions (guild_id, number, question, author_id, msg_id) VALUES (%s, %s, %s, %s, %s)', - (ctx.guild.id, num, qs, author, message.id) + "INSERT INTO questions (guild_id, number, question, author_id, msg_id) VALUES (%s, %s, %s, %s, %s)", + (ctx.guild.id, num, qs, author, message.id), ) # delete original question @@ -81,10 +93,12 @@ async def askQuestion(self, ctx, qs: str, anonymous=''): # ----------------------------------------------------------------------------------------------------------------- @askQuestion.error async def ask_error(self, ctx, error): + """Error handling for ask command""" if isinstance(error, commands.MissingRequiredArgument): await ctx.author.send( - 'To use the ask command, do: $ask \"QUESTION\" anonymous** \n ' - '(For example: $ask \"What class is this?\" anonymous)') + 'To use the ask command, do: $ask "QUESTION" anonymous** \n ' + '(For example: $ask "What class is this?" anonymous)' + ) else: await ctx.author.send(error) await ctx.message.delete() @@ -100,14 +114,16 @@ async def ask_error(self, ctx, error): # Outputs: # - User answer added to question post # ----------------------------------------------------------------------------------------------------------------- - @commands.command(name='answer', - help='Answer question. Please put answer text in quotes. Add *anonymous* or *anon* if desired.' - 'EX: $answer 1 /"Oct 12/" anonymous') - async def answer(self, ctx, num, ans: str, anonymous=''): - ''' answer the specific question ''' + @commands.command( + name="answer", + help="Answer question. Please put answer text in quotes. Add *anonymous* or *anon* if desired." + 'EX: $answer 1 /"Oct 12/" anonymous', + ) + async def answer(self, ctx, num, ans: str, anonymous=""): + """Adds user to specific question and post anonymously""" # make sure to check that this is actually being asked in the Q&A channel - if not ctx.channel.name == 'q-and-a': - await ctx.author.send('Please send answers to the #q-and-a channel.') + if not ctx.channel.name == "q-and-a": + await ctx.author.send("Please send answers to the #q-and-a channel.") await ctx.message.delete() return @@ -116,34 +132,40 @@ async def answer(self, ctx, num, ans: str, anonymous=''): await ctx.message.delete() return -# if len(ans) == 0: -# await ctx.author.send('Answer too short.') -# await ctx.message.delete() -# return + # if len(ans) == 0: + # await ctx.author.send('Answer too short.') + # await ctx.message.delete() + # return # get author - if anonymous == '': + if anonymous == "": author = ctx.message.author.id - elif anonymous == 'anonymous': + elif anonymous == "anonymous": author = None - elif anonymous == 'anon': + elif anonymous == "anon": author = None else: - await ctx.author.send('Unknown input for *anonymous* option. Please type **anonymous**, **anon**, or leave blank.') + await ctx.author.send( + "Unknown input for *anonymous* option. Please type **anonymous**, **anon**, or leave blank." + ) await ctx.message.delete() return # to stop SQL from freezing. Only allows valid numbers. - if not re.match(r'^([1-9]\d*|0)$', num): - await ctx.author.send('Please include a valid question number. EX: $answer 1 /"Oct 12/" anonymous') + if not re.match(r"^([1-9]\d*|0)$", num): + await ctx.author.send( + 'Please include a valid question number. EX: $answer 1 /"Oct 12/" anonymous' + ) await ctx.message.delete() return # check if question number exists - q = db.query('SELECT number, question, author_id, msg_id, is_ghost FROM questions WHERE guild_id = %s AND number = %s', - (ctx.guild.id, num)) + q = db.query( + "SELECT number, question, author_id, msg_id, is_ghost FROM questions WHERE guild_id = %s AND number = %s", + (ctx.guild.id, num), + ) if len(q) == 0: - await ctx.author.send('No such question with the number: ' + str(num)) + await ctx.author.send("No such question with the number: " + str(num)) # delete user msg await ctx.message.delete() return @@ -167,23 +189,31 @@ async def answer(self, ctx, num, ans: str, anonymous=''): # add answer to db if "instructor" in [y.name.lower() for y in ctx.author.roles]: - role = 'Instructor' + role = "Instructor" else: - role = 'Student' + role = "Student" db.query( - 'INSERT INTO answers (guild_id, q_number, answer, author_id, author_role) VALUES (%s, %s, %s, %s, %s)', - (ctx.guild.id, num, ans, author, role) + "INSERT INTO answers (guild_id, q_number, answer, author_id, author_role) VALUES (%s, %s, %s, %s, %s)", + (ctx.guild.id, num, ans, author, role), ) # generate and edit msg with answer - q_author_str = 'anonymous' if q[2] is None else (await self.bot.fetch_user(q[2])).name + q_author_str = ( + "anonymous" if q[2] is None else (await self.bot.fetch_user(q[2])).name + ) new_answer = f"Q{q[0]}: {q[1]} by {q_author_str}\n" # get all answers for question and add to msg - answers = db.query('SELECT answer, author_id, author_role FROM answers WHERE guild_id = %s AND q_number = %s', - (ctx.guild.id, num)) + answers = db.query( + "SELECT answer, author_id, author_role FROM answers WHERE guild_id = %s AND q_number = %s", + (ctx.guild.id, num), + ) for answer, author, role in answers: - a_author = 'anonymous' if author is None else (await self.bot.fetch_user(author)).name + a_author = ( + "anonymous" + if author is None + else (await self.bot.fetch_user(author)).name + ) new_answer += f"{a_author} ({role}) Ans: {answer}\n" # edit message @@ -192,7 +222,7 @@ async def answer(self, ctx, num, ans: str, anonymous=''): except NotFound: nf_str = f"Question {num} not found. It's a zombie!" await ctx.author.send(nf_str) - #await ctx.author.send('Invalid question number: ' + str(num)) + # await ctx.author.send('Invalid question number: ' + str(num)) # delete user msg await ctx.message.delete() @@ -208,15 +238,16 @@ async def answer(self, ctx, num, ans: str, anonymous=''): # ----------------------------------------------------------------------------------------------------------------- @answer.error async def answer_error(self, ctx, error): + """Error handling for answer command""" if isinstance(error, commands.MissingRequiredArgument): await ctx.author.send( - 'To use the answer command, do: $answer QUESTION_NUMBER \"ANSWER\" anonymous**\n ' - '(For example: $answer 2 \"Yes\")') + 'To use the answer command, do: $answer QUESTION_NUMBER "ANSWER" anonymous**\n ' + '(For example: $answer 2 "Yes")' + ) else: await ctx.author.send(error) await ctx.message.delete() - # ----------------------------------------------------------------------------------------------------------------- # Function: deleteAllAnswersFor # Description: Deletes all answers for a question. Instructor only. @@ -226,58 +257,68 @@ async def answer_error(self, ctx, error): # Outputs: # - # ----------------------------------------------------------------------------------------------------------------- - @commands.has_role('Instructor') - @commands.command(name='DALLAF', help='(PLACEHOLDER NAME) Delete all answers for a question.\n' - 'EX: $DALLAF 1\n' - 'THIS ACTION IS IRREVERSIBLE.\n' - 'Before deletion, archive the question and its answers with\n' - '$getAnswersFor QUESTION_NUMBER' - ) + @commands.has_role("Instructor") + @commands.command( + name="DALLAF", + help="(PLACEHOLDER NAME) Delete all answers for a question.\n" + "EX: $DALLAF 1\n" + "THIS ACTION IS IRREVERSIBLE.\n" + "Before deletion, archive the question and its answers with\n" + "$getAnswersFor QUESTION_NUMBER", + ) async def deleteAllAnsFor(self, ctx, num): - + """Lets instructor delete all answers for a question""" # make sure to check that this is actually being asked in the Q&A channel - if not ctx.channel.name == 'q-and-a': - await ctx.author.send('Please use this command inside the #q-and-a channel.') + if not ctx.channel.name == "q-and-a": + await ctx.author.send( + "Please use this command inside the #q-and-a channel." + ) await ctx.message.delete() return # to stop SQL from freezing. Only allows valid numbers. - if not re.match(r'^([1-9]\d*|0)$', num): - await ctx.author.send('Please include a valid question number. EX: $DALLAF 1') + if not re.match(r"^([1-9]\d*|0)$", num): + await ctx.author.send( + "Please include a valid question number. EX: $DALLAF 1" + ) await ctx.message.delete() return # check if question number exists - q = db.query('SELECT number, question, author_id, msg_id, is_ghost FROM questions WHERE guild_id = %s AND number = %s', - (ctx.guild.id, num)) + q = db.query( + "SELECT number, question, author_id, msg_id, is_ghost FROM questions WHERE guild_id = %s AND number = %s", + (ctx.guild.id, num), + ) if len(q) == 0: - await ctx.author.send('No such question with the number: ' + str(num)) + await ctx.author.send("No such question with the number: " + str(num)) # delete user msg await ctx.message.delete() return q = q[0] # retrieve question msg - q_author_str = 'anonymous' if q[2] is None else (await self.bot.fetch_user(q[2])).name + q_author_str = ( + "anonymous" if q[2] is None else (await self.bot.fetch_user(q[2])).name + ) qstr = f"Q{q[0]}: {q[1]} by {q_author_str}\n" rows_deleted = db.query( - 'SELECT * FROM answers WHERE guild_id = %s AND q_number = %s', - (ctx.guild.id, num) + "SELECT * FROM answers WHERE guild_id = %s AND q_number = %s", + (ctx.guild.id, num), ) rd = len(rows_deleted) # Delete all answers for question - db.query('DELETE FROM answers WHERE guild_id = %s AND q_number = %s', - (ctx.guild.id, num)) + db.query( + "DELETE FROM answers WHERE guild_id = %s AND q_number = %s", + (ctx.guild.id, num), + ) if rd == 0: - await ctx.author.send( - f"No answers exist for Q{num}") + await ctx.author.send(f"No answers exist for Q{num}") else: - await ctx.author.send( - f"deleted {rd} answers for Q{num}") + await ctx.author.send(f"deleted {rd} answers for Q{num}") # if it's a ghost, return. if q[4]: @@ -311,15 +352,16 @@ async def deleteAllAnsFor(self, ctx, num): # ----------------------------------------------------------------------------------------------------------------- @deleteAllAnsFor.error async def deleteAllAnsFor_error(self, ctx, error): + """Error handling for deleteAllAnswersFor command""" if isinstance(error, commands.MissingRequiredArgument): await ctx.author.send( - 'To use the deleteAllAnswersFor command, do: $DALLAF QUESTION_NUMBER\n ' - '(Example: $DALLAF 1)') + "To use the deleteAllAnswersFor command, do: $DALLAF QUESTION_NUMBER\n " + "(Example: $DALLAF 1)" + ) else: await ctx.author.send(error) await ctx.message.delete() - # ----------------------------------------------------------------------------------------------------------------- # Function: getAllAnsFor # Description: gets all answers for a question and DMs them to the user. @@ -329,27 +371,35 @@ async def deleteAllAnsFor_error(self, ctx, error): # Outputs: # - All answers for a question, if any # ----------------------------------------------------------------------------------------------------------------- - @commands.command(name='getAnswersFor', help='Get a question and all its answers\n' - 'EX: $getAnswersFor 1') + @commands.command( + name="getAnswersFor", + help="Get a question and all its answers\n" "EX: $getAnswersFor 1", + ) async def getAllAnsFor(self, ctx, num): - + """Gets all answers for a question and DMs them to the user""" # make sure to check that this is actually being used in the Q&A channel - if not ctx.channel.name == 'q-and-a': - await ctx.author.send('Please use this command inside the #q-and-a channel.') + if not ctx.channel.name == "q-and-a": + await ctx.author.send( + "Please use this command inside the #q-and-a channel." + ) await ctx.message.delete() return # to stop SQL from screaming. Only allows valid numbers. - if not re.match(r'^([1-9]\d*|0)$', num): - await ctx.author.send('Please include a valid question number. EX: $getAnswersFor 1') + if not re.match(r"^([1-9]\d*|0)$", num): + await ctx.author.send( + "Please include a valid question number. EX: $getAnswersFor 1" + ) await ctx.message.delete() return # check if question number exists - q = db.query('SELECT number, question, author_id, msg_id, is_ghost FROM questions WHERE guild_id = %s AND number = %s', - (ctx.guild.id, num)) + q = db.query( + "SELECT number, question, author_id, msg_id, is_ghost FROM questions WHERE guild_id = %s AND number = %s", + (ctx.guild.id, num), + ) if len(q) == 0: - await ctx.author.send('No such question with the number: ' + str(num)) + await ctx.author.send("No such question with the number: " + str(num)) # delete user msg await ctx.message.delete() return @@ -373,12 +423,16 @@ async def getAllAnsFor(self, ctx, num): return # retrieve question msg - q_author_str = 'anonymous' if q[2] is None else (await self.bot.fetch_user(q[2])).name + q_author_str = ( + "anonymous" if q[2] is None else (await self.bot.fetch_user(q[2])).name + ) qstr = f"Q{q[0]}: {q[1]} by {q_author_str}\n" # get all answers for question and add to msg - answers = db.query('SELECT answer, author_id, author_role FROM answers WHERE guild_id = %s AND q_number = %s', - (ctx.guild.id, num)) + answers = db.query( + "SELECT answer, author_id, author_role FROM answers WHERE guild_id = %s AND q_number = %s", + (ctx.guild.id, num), + ) # if there are no answers, return if len(answers) == 0: @@ -388,14 +442,17 @@ async def getAllAnsFor(self, ctx, num): return for answer, author, role in answers: - a_author = 'anonymous' if author is None else (await self.bot.fetch_user(author)).name + a_author = ( + "anonymous" + if author is None + else (await self.bot.fetch_user(author)).name + ) qstr += f"{a_author} ({role}) Ans: {answer}\n" # send the question and answers to user await ctx.author.send(qstr) await ctx.message.delete() - # ----------------------------------------------------------------------------------------------------------------- # Function: getAllAnsFor_error(self, ctx, error) # Description: prints error message for getAnswersFor command @@ -407,10 +464,12 @@ async def getAllAnsFor(self, ctx, num): # ----------------------------------------------------------------------------------------------------------------- @getAllAnsFor.error async def getAllAnsFor_error(self, ctx, error): + """Error handling for getAllAnswersFor command""" if isinstance(error, commands.MissingRequiredArgument): await ctx.author.send( - 'To use the getAnswersFor command, do: $getAnswersFor QUESTION_NUMBER\n ' - '(Example: $getAnswersFor 1)') + "To use the getAnswersFor command, do: $getAnswersFor QUESTION_NUMBER\n " + "(Example: $getAnswersFor 1)" + ) else: await ctx.author.send(error) await ctx.message.delete() @@ -423,32 +482,37 @@ async def getAllAnsFor_error(self, ctx, error): # Outputs: # - DMs all questions and answers to the user # ----------------------------------------------------------------------------------------------------------------- - @commands.command(name='archiveQA', help='(PLACEHOLDER NAME) DM all questions and their answers\n' - 'EX: $archiveQA') + @commands.command( + name="archiveQA", + help="(PLACEHOLDER NAME) DM all questions and their answers\n" "EX: $archiveQA", + ) async def archiveQA(self, ctx): - + """DM all questions and their answers to the user""" # make sure to check that this is actually being used in the Q&A channel - if not ctx.channel.name == 'q-and-a': - await ctx.author.send('Please use this command inside the #q-and-a channel.') + if not ctx.channel.name == "q-and-a": + await ctx.author.send( + "Please use this command inside the #q-and-a channel." + ) await ctx.message.delete() return # get questions - q = db.query('SELECT number, question, author_id, msg_id, is_ghost FROM questions WHERE guild_id = %s ORDER BY number ASC', - (ctx.guild.id,)) + q = db.query( + "SELECT number, question, author_id, msg_id, is_ghost FROM questions WHERE guild_id = %s ORDER BY number ASC", + (ctx.guild.id,), + ) if len(q) == 0: - await ctx.author.send('No questions found in database.') + await ctx.author.send("No questions found in database.") # delete user msg await ctx.message.delete() return for number, question, author_id, msg_id, is_ghost in q: - # prevent receiving any hidden questions. if is_ghost: nf_str = f"Q{number} is a ghost!" await ctx.author.send(nf_str) - #go to next question + # go to next question continue # prevent receiving any deleted questions. @@ -458,21 +522,31 @@ async def archiveQA(self, ctx): if not is_ghost: nf_str = f"Q{number} was deleted. It's a zombie!" await ctx.author.send(nf_str) - #go to next question + # go to next question continue - q_author_str = 'anonymous' if author_id is None else (await self.bot.fetch_user(author_id)).name + q_author_str = ( + "anonymous" + if author_id is None + else (await self.bot.fetch_user(author_id)).name + ) qstr = f"Q{number}: {question} by {q_author_str}\n" # get all answers for question and add to msg - answers = db.query('SELECT answer, author_id, author_role FROM answers WHERE guild_id = %s AND q_number = %s', - (ctx.guild.id, number)) + answers = db.query( + "SELECT answer, author_id, author_role FROM answers WHERE guild_id = %s AND q_number = %s", + (ctx.guild.id, number), + ) if len(answers) == 0: qstr += f"No answers for Q{number}\n" else: for answer, author, role in answers: - a_author = 'anonymous' if author is None else (await self.bot.fetch_user(author)).name + a_author = ( + "anonymous" + if author is None + else (await self.bot.fetch_user(author)).name + ) qstr += f"{a_author} ({role}) Ans: {answer}\n" # send the question and answers to user @@ -492,10 +566,10 @@ async def archiveQA(self, ctx): # ----------------------------------------------------------------------------------------------------------------- @archiveQA.error async def archiveqa_error(self, ctx, error): + """Error handling for archiveQA command""" await ctx.author.send(error) await ctx.message.delete() - # ----------------------------------------------------------------------------------------------------------------- # Function: deleteAllQAs # Description: Deletes all questions and answers from the database and channel. @@ -504,29 +578,35 @@ async def archiveqa_error(self, ctx, error): # Outputs: # - # ----------------------------------------------------------------------------------------------------------------- - @commands.has_role('Instructor') - @commands.command(name='deleteAllQA', help='Delete all questions and answers from the database and channel.\n' - 'EX: $deleteAllQA\n' - 'THIS COMMAND IS IRREVERSIBLE.\n' - 'BE SURE TO ARCHIVE ALL QUESTIONS BEFORE DELETION.\n' - 'To archive, use the $unearthZombies command followed by $allChannelGhosts,' - ' and then use $archiveQA.' - ) + @commands.has_role("Instructor") + @commands.command( + name="deleteAllQA", + help="Delete all questions and answers from the database and channel.\n" + "EX: $deleteAllQA\n" + "THIS COMMAND IS IRREVERSIBLE.\n" + "BE SURE TO ARCHIVE ALL QUESTIONS BEFORE DELETION.\n" + "To archive, use the $unearthZombies command followed by $allChannelGhosts," + " and then use $archiveQA.", + ) async def deleteAllQAs(self, ctx): - + """Deletes all quetsions and answers from the database and channel""" # make sure to check that this is actually being used in the Q&A channel - if not ctx.channel.name == 'q-and-a': - await ctx.author.send('Please use this command inside the #q-and-a channel.') + if not ctx.channel.name == "q-and-a": + await ctx.author.send( + "Please use this command inside the #q-and-a channel." + ) await ctx.message.delete() return # get questions - q = db.query('SELECT msg_id, is_ghost FROM questions WHERE guild_id = %s', - (ctx.guild.id,)) + q = db.query( + "SELECT msg_id, is_ghost FROM questions WHERE guild_id = %s", + (ctx.guild.id,), + ) numqs = len(q) if numqs == 0: - await ctx.author.send('No questions found in database.') + await ctx.author.send("No questions found in database.") return # Zombies are questions that were manually deleted from the channel. They need to be @@ -550,20 +630,18 @@ async def deleteAllQAs(self, ctx): await message.delete() # count ghosts - #spooks = db.query( + # spooks = db.query( # 'SELECT * FROM questions WHERE guild_id = %s AND is_ghost IS TRUE', # (ctx.guild.id,) - #) + # ) - #spooky = len(spooks) + # spooky = len(spooks) # Delete all questions - db.query('DELETE FROM questions WHERE guild_id = %s', - (ctx.guild.id,)) + db.query("DELETE FROM questions WHERE guild_id = %s", (ctx.guild.id,)) # Delete all answers - db.query('DELETE FROM answers WHERE guild_id = %s', - (ctx.guild.id,)) + db.query("DELETE FROM answers WHERE guild_id = %s", (ctx.guild.id,)) report = f"Deleted {numqs} questions from the database, including {zombies} zombies and {ghosts} ghosts." await ctx.author.send(report) @@ -582,10 +660,10 @@ async def deleteAllQAs(self, ctx): # ----------------------------------------------------------------------------------------------------------------- @deleteAllQAs.error async def deleteAllQAs_error(self, ctx, error): + """Error handling for deleteAllQA command""" await ctx.author.send(error) await ctx.message.delete() - # ----------------------------------------------------------------------------------------------------------------- # Function: deleteOneQuestion # Description: Delete one question but leave answers untouched. Instructor only. @@ -595,30 +673,38 @@ async def deleteAllQAs_error(self, ctx, error): # Outputs: # - # ----------------------------------------------------------------------------------------------------------------- - @commands.has_role('Instructor') - @commands.command(name='deleteQuestion', help='Delete (hide) one question but leave answers untouched.' - ' Leaves database ghosts.\n' - 'EX: $deleteQuestion QUESTION_NUMBER\n' - ) + @commands.has_role("Instructor") + @commands.command( + name="deleteQuestion", + help="Delete (hide) one question but leave answers untouched." + " Leaves database ghosts.\n" + "EX: $deleteQuestion QUESTION_NUMBER\n", + ) async def deleteOneQuestion(self, ctx, num): - + """Lets the instructor delete one question, but leave the answers untouched""" # make sure to check that this is actually being used in the Q&A channel - if not ctx.channel.name == 'q-and-a': - await ctx.author.send('Please use this command inside the #q-and-a channel.') + if not ctx.channel.name == "q-and-a": + await ctx.author.send( + "Please use this command inside the #q-and-a channel." + ) await ctx.message.delete() return # to stop SQL from freezing. Only allows valid numbers. - if not re.match(r'^([1-9]\d*|0)$', num): - await ctx.author.send('Please include a valid question number. EX: $deleteQuestion 1') + if not re.match(r"^([1-9]\d*|0)$", num): + await ctx.author.send( + "Please include a valid question number. EX: $deleteQuestion 1" + ) await ctx.message.delete() return - # check if question number exists - q = db.query('SELECT number, msg_id, is_ghost FROM questions WHERE guild_id = %s AND number = %s', - (ctx.guild.id, num)) + # check if question number exists + q = db.query( + "SELECT number, msg_id, is_ghost FROM questions WHERE guild_id = %s AND number = %s", + (ctx.guild.id, num), + ) if len(q) == 0: - await ctx.author.send('Question number not in database: ' + str(num)) + await ctx.author.send("Question number not in database: " + str(num)) # delete user msg await ctx.message.delete() return @@ -627,8 +713,10 @@ async def deleteOneQuestion(self, ctx, num): # if is_ghost is false, set to true if not q[2]: - db.query('UPDATE questions SET is_ghost = NOT is_ghost WHERE guild_id = %s AND number = %s', - (ctx.guild.id, num)) + db.query( + "UPDATE questions SET is_ghost = NOT is_ghost WHERE guild_id = %s AND number = %s", + (ctx.guild.id, num), + ) else: sendme = f"Q{q[0]} is already a ghost!" await ctx.author.send(sendme) @@ -659,15 +747,16 @@ async def deleteOneQuestion(self, ctx, num): # ----------------------------------------------------------------------------------------------------------------- @deleteOneQuestion.error async def deleteOneQuestion_error(self, ctx, error): + """Error handling for deleteQuestion command""" if isinstance(error, commands.MissingRequiredArgument): await ctx.author.send( - 'To use the deleteQuestion command, do: $deleteQuestion QUESTION_NUMBER\n ' - '(Example: $deleteQuestion 1') + "To use the deleteQuestion command, do: $deleteQuestion QUESTION_NUMBER\n " + "(Example: $deleteQuestion 1" + ) else: await ctx.author.send(error) await ctx.message.delete() - # ----------------------------------------------------------------------------------------------------------------- # Function: channelOneGhost # Description: Gets a specific ghost question. Instructor only. @@ -677,28 +766,37 @@ async def deleteOneQuestion_error(self, ctx, error): # Outputs: # - All answers for a ghost question, if any # ----------------------------------------------------------------------------------------------------------------- - @commands.has_role('Instructor') - @commands.command(name='channelGhost', help='Gets a specific ghost (question deleted with command) and all its answers.\n' - 'EX: $channelGhost 1') + @commands.has_role("Instructor") + @commands.command( + name="channelGhost", + help="Gets a specific ghost (question deleted with command) and all its answers.\n" + "EX: $channelGhost 1", + ) async def channelOneGhost(self, ctx, num): - + """Lets the instructor get a specific ghost question""" # make sure to check that this is actually being used in the Q&A channel - if not ctx.channel.name == 'q-and-a': - await ctx.author.send('Please use this command inside the #q-and-a channel.') + if not ctx.channel.name == "q-and-a": + await ctx.author.send( + "Please use this command inside the #q-and-a channel." + ) await ctx.message.delete() return # to stop SQL from screaming. Only allows valid numbers. - if not re.match(r'^([1-9]\d*|0)$', num): - await ctx.author.send('Please include a valid question number. EX: $channelGhost 1') + if not re.match(r"^([1-9]\d*|0)$", num): + await ctx.author.send( + "Please include a valid question number. EX: $channelGhost 1" + ) await ctx.message.delete() return # check if question number exists - q = db.query('SELECT number, question, author_id, is_ghost FROM questions WHERE guild_id = %s AND number = %s', - (ctx.guild.id, num)) + q = db.query( + "SELECT number, question, author_id, is_ghost FROM questions WHERE guild_id = %s AND number = %s", + (ctx.guild.id, num), + ) if len(q) == 0: - await ctx.author.send('No such question with the number: ' + str(num)) + await ctx.author.send("No such question with the number: " + str(num)) # delete user msg await ctx.message.delete() return @@ -706,15 +804,19 @@ async def channelOneGhost(self, ctx, num): q = q[0] if not q[3]: - await ctx.author.send('This question is not a ghost. Fetching anyway. . .') + await ctx.author.send("This question is not a ghost. Fetching anyway. . .") # retrieve question msg - q_author_str = 'anonymous' if q[2] is None else (await self.bot.fetch_user(q[2])).name + q_author_str = ( + "anonymous" if q[2] is None else (await self.bot.fetch_user(q[2])).name + ) qstr = f"Q{q[0]}: {q[1]} by {q_author_str}\n" # get all answers for question and add to msg - answers = db.query('SELECT answer, author_id, author_role FROM answers WHERE guild_id = %s AND q_number = %s', - (ctx.guild.id, num)) + answers = db.query( + "SELECT answer, author_id, author_role FROM answers WHERE guild_id = %s AND q_number = %s", + (ctx.guild.id, num), + ) # if there are no answers, return if len(answers) == 0: @@ -724,14 +826,17 @@ async def channelOneGhost(self, ctx, num): return for answer, author, role in answers: - a_author = 'anonymous' if author is None else (await self.bot.fetch_user(author)).name + a_author = ( + "anonymous" + if author is None + else (await self.bot.fetch_user(author)).name + ) qstr += f"{a_author} ({role}) Ans: {answer}\n" # send the question and answers to user await ctx.author.send(qstr) await ctx.message.delete() - # ----------------------------------------------------------------------------------------------------------------- # Function: channelOneGhost_error(self, ctx, error) # Description: prints error message for channelGhost command @@ -743,15 +848,16 @@ async def channelOneGhost(self, ctx, num): # ----------------------------------------------------------------------------------------------------------------- @channelOneGhost.error async def channelOneGhost_error(self, ctx, error): + """Error handling for channelGhost command""" if isinstance(error, commands.MissingRequiredArgument): await ctx.author.send( - 'To use the channelGhost command, do: $channelGhost QUESTION_NUMBER\n ' - '(Example: $channelGhost 1)') + "To use the channelGhost command, do: $channelGhost QUESTION_NUMBER\n " + "(Example: $channelGhost 1)" + ) else: await ctx.author.send(error) await ctx.message.delete() - # ----------------------------------------------------------------------------------------------------------------- # Function: channelGhostQs # Description: Get the questions that are in the database but not in the channel. Instructor only. @@ -760,43 +866,58 @@ async def channelOneGhost_error(self, ctx, error): # Outputs: # - # ----------------------------------------------------------------------------------------------------------------- - @commands.has_role('Instructor') - @commands.command(name='allChannelGhosts', help='Get all the questions that are in the database but ' - 'not in the channel. Does not detect zombies.\n' - 'EX: $allChannelGhosts\n' - 'To detect zombies and convert them to ghosts, use $unearthZombies' - ) + @commands.has_role("Instructor") + @commands.command( + name="allChannelGhosts", + help="Get all the questions that are in the database but " + "not in the channel. Does not detect zombies.\n" + "EX: $allChannelGhosts\n" + "To detect zombies and convert them to ghosts, use $unearthZombies", + ) async def channelGhostQs(self, ctx): - + """Lets the instructor get the questions that are in the database but not inthe channel""" # make sure to check that this is actually being used in the Q&A channel - if not ctx.channel.name == 'q-and-a': - await ctx.author.send('Please use this command inside the #q-and-a channel.') + if not ctx.channel.name == "q-and-a": + await ctx.author.send( + "Please use this command inside the #q-and-a channel." + ) await ctx.message.delete() return - # get questions - q = db.query('SELECT number, question, author_id FROM questions WHERE guild_id = %s AND is_ghost IS TRUE ORDER BY number ASC', - (ctx.guild.id,)) + # get questions + q = db.query( + "SELECT number, question, author_id FROM questions WHERE guild_id = %s AND is_ghost IS TRUE ORDER BY number ASC", + (ctx.guild.id,), + ) if len(q) == 0: - await ctx.author.send('No ghosts found in database.') + await ctx.author.send("No ghosts found in database.") # delete user msg await ctx.message.delete() return for number, question, author_id in q: - - q_author_str = 'anonymous' if author_id is None else (await self.bot.fetch_user(author_id)).name + q_author_str = ( + "anonymous" + if author_id is None + else (await self.bot.fetch_user(author_id)).name + ) qstr = f"Q{number}: {question} by {q_author_str}\n" # get all answers for question and add to msg - answers = db.query('SELECT answer, author_id, author_role FROM answers WHERE guild_id = %s AND q_number = %s', - (ctx.guild.id, number)) + answers = db.query( + "SELECT answer, author_id, author_role FROM answers WHERE guild_id = %s AND q_number = %s", + (ctx.guild.id, number), + ) if len(answers) == 0: qstr += f"No answers for Q{number}\n" else: for answer, author, role in answers: - a_author = 'anonymous' if author is None else (await self.bot.fetch_user(author)).name + a_author = ( + "anonymous" + if author is None + else (await self.bot.fetch_user(author)).name + ) qstr += f"{a_author} ({role}) Ans: {answer}\n" # send the question and answers to user @@ -816,6 +937,7 @@ async def channelGhostQs(self, ctx): # ----------------------------------------------------------------------------------------------------------------- @channelGhostQs.error async def channelGhostQs_error(self, ctx, error): + """Error handling for allChannelGhosts command""" await ctx.author.send(error) await ctx.message.delete() @@ -828,24 +950,30 @@ async def channelGhostQs_error(self, ctx, error): # Outputs: # - # ----------------------------------------------------------------------------------------------------------------- - @commands.has_role('Instructor') - @commands.command(name='unearthZombies', help='Assign ghost status to all manually deleted questions ' - 'in case there is a need to restore them.\n' - 'EX: $unearthZombies\n' - ) + @commands.has_role("Instructor") + @commands.command( + name="unearthZombies", + help="Assign ghost status to all manually deleted questions " + "in case there is a need to restore them.\n" + "EX: $unearthZombies\n", + ) async def unearthZombieQs(self, ctx): - + """Assigns ghost status to all manually deleted questions in case there is a need to restore them""" # make sure to check that this is actually being used in the Q&A channel - if not ctx.channel.name == 'q-and-a': - await ctx.author.send('Please use this command inside the #q-and-a channel.') + if not ctx.channel.name == "q-and-a": + await ctx.author.send( + "Please use this command inside the #q-and-a channel." + ) await ctx.message.delete() return # get questions - q = db.query('SELECT number, msg_id FROM questions WHERE guild_id = %s AND is_ghost IS FALSE', - (ctx.guild.id,)) + q = db.query( + "SELECT number, msg_id FROM questions WHERE guild_id = %s AND is_ghost IS FALSE", + (ctx.guild.id,), + ) if len(q) == 0: - await ctx.author.send('No zombies detected.') + await ctx.author.send("No zombies detected.") # delete user msg await ctx.message.delete() return @@ -856,8 +984,10 @@ async def unearthZombieQs(self, ctx): await ctx.fetch_message(msg_id) except NotFound: zombies += 1 - db.query('UPDATE questions SET is_ghost = NOT is_ghost WHERE guild_id = %s AND number = %s', - (ctx.guild.id, number)) + db.query( + "UPDATE questions SET is_ghost = NOT is_ghost WHERE guild_id = %s AND number = %s", + (ctx.guild.id, number), + ) if zombies == 0: await ctx.author.send("No zombies detected.") @@ -881,6 +1011,7 @@ async def unearthZombieQs(self, ctx): # ----------------------------------------------------------------------------------------------------------------- @unearthZombieQs.error async def unearthZombieQs_error(self, ctx, error): + """Error handling for unearthZombies command""" await ctx.author.send(error) await ctx.message.delete() @@ -893,28 +1024,37 @@ async def unearthZombieQs_error(self, ctx, error): # Outputs: # - All answers for a ghost question, if any # ----------------------------------------------------------------------------------------------------------------- - @commands.has_role('Instructor') - @commands.command(name='reviveGhost', help='Restores a ghost or deleted/hidden question to the channel.\n' - 'EX: $reviveGhost 1') + @commands.has_role("Instructor") + @commands.command( + name="reviveGhost", + help="Restores a ghost or deleted/hidden question to the channel.\n" + "EX: $reviveGhost 1", + ) async def restoreGhost(self, ctx, num): - + """Restores a ghost of deleted question to the channel""" # make sure to check that this is actually being used in the Q&A channel - if not ctx.channel.name == 'q-and-a': - await ctx.author.send('Please use this command inside the #q-and-a channel.') + if not ctx.channel.name == "q-and-a": + await ctx.author.send( + "Please use this command inside the #q-and-a channel." + ) await ctx.message.delete() return # to stop SQL from screaming. Only allows valid numbers. - if not re.match(r'^([1-9]\d*|0)$', num): - await ctx.author.send('Please include a valid question number. EX: $reviveGhost 1') + if not re.match(r"^([1-9]\d*|0)$", num): + await ctx.author.send( + "Please include a valid question number. EX: $reviveGhost 1" + ) await ctx.message.delete() return # check if question number exists - q = db.query('SELECT number, question, author_id, msg_id, is_ghost FROM questions WHERE guild_id = %s AND number = %s', - (ctx.guild.id, num)) + q = db.query( + "SELECT number, question, author_id, msg_id, is_ghost FROM questions WHERE guild_id = %s AND number = %s", + (ctx.guild.id, num), + ) if len(q) == 0: - await ctx.author.send('No such question with the number: ' + str(num)) + await ctx.author.send("No such question with the number: " + str(num)) # delete user msg await ctx.message.delete() return @@ -922,17 +1062,25 @@ async def restoreGhost(self, ctx, num): q = q[0] # retrieve question msg - q_author_str = 'anonymous' if q[2] is None else (await self.bot.fetch_user(q[2])).name + q_author_str = ( + "anonymous" if q[2] is None else (await self.bot.fetch_user(q[2])).name + ) qstr = f"Q{q[0]}: {q[1]} by {q_author_str}\n" # get all answers for question and add to msg - answers = db.query('SELECT answer, author_id, author_role FROM answers WHERE guild_id = %s AND q_number = %s', - (ctx.guild.id, num)) + answers = db.query( + "SELECT answer, author_id, author_role FROM answers WHERE guild_id = %s AND q_number = %s", + (ctx.guild.id, num), + ) # if there are answers, restore them if len(answers) != 0: for answer, author, role in answers: - a_author = 'anonymous' if author is None else (await self.bot.fetch_user(author)).name + a_author = ( + "anonymous" + if author is None + else (await self.bot.fetch_user(author)).name + ) qstr += f"{a_author} ({role}) Ans: {answer}\n" try: @@ -940,17 +1088,20 @@ async def restoreGhost(self, ctx, num): except NotFound: # if the question was manually deleted, post it again (with all its answers)! message = await ctx.send(qstr) - db.query('UPDATE questions SET msg_id = %s WHERE guild_id = %s AND number = %s', - (message.id, ctx.guild.id, num)) + db.query( + "UPDATE questions SET msg_id = %s WHERE guild_id = %s AND number = %s", + (message.id, ctx.guild.id, num), + ) else: # restore the question await msg.edit(content=qstr) # if is_ghost is true, set to false if q[4]: - db.query('UPDATE questions SET is_ghost = NOT is_ghost WHERE guild_id = %s AND number = %s', - (ctx.guild.id, num)) - + db.query( + "UPDATE questions SET is_ghost = NOT is_ghost WHERE guild_id = %s AND number = %s", + (ctx.guild.id, num), + ) await ctx.message.delete() @@ -965,19 +1116,16 @@ async def restoreGhost(self, ctx, num): # ----------------------------------------------------------------------------------------------------------------- @restoreGhost.error async def restoreGhost_error(self, ctx, error): + """Error handling for reviveGhost command""" if isinstance(error, commands.MissingRequiredArgument): await ctx.author.send( - 'To use the reviveGhost command, do: $reviveGhost QUESTION_NUMBER\n ' - '(Example: $reviveGhost 1)') + "To use the reviveGhost command, do: $reviveGhost QUESTION_NUMBER\n " + "(Example: $reviveGhost 1)" + ) else: await ctx.author.send(error) await ctx.message.delete() - - - - - # ----------------------------------------------------------------------------------------------------------------- # Function: countGhosts # Description: Counts the number of ghost and zombie questions in the channel. Just for fun, but may be useful @@ -986,18 +1134,21 @@ async def restoreGhost_error(self, ctx, error): # Outputs: # - The number of ghost questions # ----------------------------------------------------------------------------------------------------------------- - @commands.command(name='spooky', help='Is this channel haunted?\n' - 'EX: $spooky') + @commands.command(name="spooky", help="Is this channel haunted?\n" "EX: $spooky") async def countGhosts(self, ctx): - + """Counts the number of ghost and zombie questions in the channel. Mainly for fun but could be useful""" # make sure to check that this is actually being used in the Q&A channel - if not ctx.channel.name == 'q-and-a': - await ctx.author.send('Please use this command inside the #q-and-a channel.') + if not ctx.channel.name == "q-and-a": + await ctx.author.send( + "Please use this command inside the #q-and-a channel." + ) await ctx.message.delete() return - q = db.query('SELECT msg_id, is_ghost FROM questions WHERE guild_id = %s', - (ctx.guild.id,)) + q = db.query( + "SELECT msg_id, is_ghost FROM questions WHERE guild_id = %s", + (ctx.guild.id,), + ) # if database is empty if len(q) == 0: await ctx.author.send("This channel isn't haunted.") @@ -1026,7 +1177,6 @@ async def countGhosts(self, ctx): await ctx.author.send(spookstr) await ctx.message.delete() - # ----------------------------------------------------------------------------------------------------------------- # Function: countGhosts_error(self, ctx, error) # Description: prints error message for spooky command @@ -1038,10 +1188,12 @@ async def countGhosts(self, ctx): # ----------------------------------------------------------------------------------------------------------------- @countGhosts.error async def countGhosts_error(self, ctx, error): + """Error handling for spooky command""" await ctx.author.send(error) await ctx.message.delete() -def setup(bot): +async def setup(bot): + """Adds the file to the bot's cog system""" n = Qanda(bot) - bot.add_cog(n) + await bot.add_cog(n) diff --git a/cogs/reviewQs.py b/cogs/reviewQs.py index 15273e519..f8c188c4d 100644 --- a/cogs/reviewQs.py +++ b/cogs/reviewQs.py @@ -2,8 +2,8 @@ from discord.ext import commands import db -class ReviewQs(commands.Cog): +class ReviewQs(commands.Cog): def __init__(self, bot): self.bot = bot @@ -15,12 +15,15 @@ def __init__(self, bot): # Outputs: # - a random question from the database (in user guild) is sent by the bot # ----------------------------------------------------------------------------------------------------------------- - @commands.command(name='getQuestion', help='Get a review question. EX: $getQuestion') + @commands.command( + name="getQuestion", help="Get a review question. EX: $getQuestion" + ) async def getQuestion(self, ctx): + """Prints a random question from the database""" # get random question from db rand = db.query( - 'SELECT question, answer FROM review_questions WHERE guild_id = %s ORDER BY RANDOM() LIMIT 1', - (ctx.guild.id, ) + "SELECT question, answer FROM review_questions WHERE guild_id = %s ORDER BY RANDOM() LIMIT 1", + (ctx.guild.id,), ) # send question to guild @@ -38,9 +41,9 @@ async def getQuestion(self, ctx): # ----------------------------------------------------------------------------------------------------------------- @getQuestion.error async def get_question_error(self, ctx, error): + """Error handling for getQuestion command""" if isinstance(error, commands.MissingRequiredArgument): - await ctx.send( - 'To use the getQuestion command, do: $getQuestion \n') + await ctx.send("To use the getQuestion command, do: $getQuestion \n") else: await ctx.author.send(error) print(error) @@ -56,18 +59,23 @@ async def get_question_error(self, ctx, error): # Outputs: # - success message # ----------------------------------------------------------------------------------------------------------------- - @commands.has_role('Instructor') - @commands.command(name='addQuestion', help='Add a review question. ' - 'EX: $addQuestion \"What class is this?\" \"Software Engineering\"') + @commands.has_role("Instructor") + @commands.command( + name="addQuestion", + help="Add a review question. " + 'EX: $addQuestion "What class is this?" "Software Engineering"', + ) async def addQuestion(self, ctx, qs: str, ans: str): + """Allows instructors to add review questions""" # add question to database db.query( - 'INSERT INTO review_questions (guild_id, question, answer) VALUES (%s, %s, %s)', - (ctx.guild.id, qs, ans) + "INSERT INTO review_questions (guild_id, question, answer) VALUES (%s, %s, %s)", + (ctx.guild.id, qs, ans), ) await ctx.send( - f"A new review question has been added! Question: {qs} and Answer: {ans}.") + f"A new review question has been added! Question: {qs} and Answer: {ans}." + ) # ----------------------------------------------------------------------------------------------------------------- # Function: add_question_error(self, ctx, error) @@ -80,15 +88,19 @@ async def addQuestion(self, ctx, qs: str, ans: str): # ----------------------------------------------------------------------------------------------------------------- @addQuestion.error async def add_question_error(self, ctx, error): + """Error handling for addQuestion command""" if isinstance(error, commands.MissingRequiredArgument): await ctx.send( - 'To use the addQuestion command, do: $addQuestion \"Question\" \"Answer\" \n' - '(For example: $addQuestion \"What class is this?\" "CSC510")') + 'To use the addQuestion command, do: $addQuestion "Question" "Answer" \n' + '(For example: $addQuestion "What class is this?" "CSC510")' + ) else: await ctx.author.send(error) print(error) await ctx.message.delete() -def setup(bot): + +async def setup(bot): + """Adds the file to the bot's cog system""" n = ReviewQs(bot) - bot.add_cog(n) + await bot.add_cog(n) diff --git a/cogs/token.json b/cogs/token.json new file mode 100644 index 000000000..401c192cf --- /dev/null +++ b/cogs/token.json @@ -0,0 +1 @@ +{"token": "ya29.a0AfB_byACl-7iB6FJ01k0q-UuoBCW4_sxfDEV6BQSP1POWYMqVjBniIYu41ln_orKyXOP6cc6jHpry4jKtyqJ0trWoTAofV5ABUF9egterjhgyKvm_2GShfkCqwHpG-JH90Hnxw6YNoWPZG8j5dIv0gaia9lLuCHiUplJaCgYKAXISARESFQGOcNnClbqwddntpmh_8J2zZCGIwg0171", "refresh_token": "1//01AkCtePnnMFcCgYIARAAGAESNwF-L9IrHnpE8plwxcfMpAQICAiMLjTquY2ij1id9rN6xgYoVO9l-8ZzI1dzunOkSXHr04xSxaU", "token_uri": "https://oauth2.googleapis.com/token", "client_id": "947515131770-5ngv90mctfkdklfopgk7r89sk3dbq4tr.apps.googleusercontent.com", "client_secret": "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", "scopes": ["https://www.googleapis.com/auth/calendar"], "expiry": "2023-10-17T22:18:59.607413Z"} diff --git a/cogs/voting.py b/cogs/voting.py index 1ba631618..47f4f3f1c 100644 --- a/cogs/voting.py +++ b/cogs/voting.py @@ -6,12 +6,13 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import db + + # ----------------------------------------------------------- # This File contains commands for voting on projects, # displaying which groups have signed up for which project # ----------------------------------------------------------- class Voting(commands.Cog): - # ----------- # initialize # ----------- @@ -28,10 +29,15 @@ def __init__(self, bot): # Outputs: adds the user to the given project, switching if already in a project # or returns an error if the project is invalid or the user is not in a valid group # ---------------------------------------------------------------------------------------------------------- - @commands.command(name='vote', help='Used for voting for Projects, \ + @commands.command( + name="vote", + help="Used for voting for Projects, \ To use the vote command, do: $vote \n \ - (For example: $vote 0)', pass_context=True) - async def vote(self, ctx, project_num : int): + (For example: $vote 0)", + pass_context=True, + ) + async def vote(self, ctx, project_num: int): + """Used for voting for projects. "Votes" for the given project by adding the user's group to it""" # get the name of the caller member_name = ctx.message.author.display_name.upper() @@ -40,50 +46,54 @@ async def vote(self, ctx, project_num : int): return group = db.query( - 'SELECT group_num FROM group_members WHERE guild_id = %s AND member_name = %s', - (ctx.guild.id, member_name) + "SELECT group_num FROM group_members WHERE guild_id = %s AND member_name = %s", + (ctx.guild.id, member_name), ) # error handle if member is not in a group if len(group) == 0: - await ctx.send("You are not in a group. You must join a group before voting on a project.") + await ctx.send( + "You are not in a group. You must join a group before voting on a project." + ) return group = group[0][0] num_groups = db.query( - 'SELECT COUNT(*) FROM project_groups WHERE guild_id = %s AND project_num = %s', - (ctx.guild.id, project_num) + "SELECT COUNT(*) FROM project_groups WHERE guild_id = %s AND project_num = %s", + (ctx.guild.id, project_num), )[0] # check if project has more than 6 groups voting on it if num_groups == 3: - await ctx.send('Projects are limited to 3 groups, please select another project.') + await ctx.send( + "Projects are limited to 3 groups, please select another project." + ) return voted_for = db.query( - 'SELECT project_num FROM project_groups WHERE guild_id = %s AND group_num = %s', - (ctx.guild.id, group) + "SELECT project_num FROM project_groups WHERE guild_id = %s AND group_num = %s", + (ctx.guild.id, group), ) if voted_for: voted_for = voted_for[0][0] if voted_for == project_num: - await ctx.send(f'You already voted for Project {voted_for}') + await ctx.send(f"You already voted for Project {voted_for}") return db.query( - 'DELETE FROM project_groups WHERE guild_id = %s AND group_num = %s', - (ctx.guild.id, group) + "DELETE FROM project_groups WHERE guild_id = %s AND group_num = %s", + (ctx.guild.id, group), ) - await ctx.send(f'Group {group} removed vote for Project {voted_for}') + await ctx.send(f"Group {group} removed vote for Project {voted_for}") # add the group to the project list db.query( - 'INSERT INTO project_groups (guild_id, project_num, group_num) VALUES (%s, %s, %s)', - (ctx.guild.id, project_num, group) + "INSERT INTO project_groups (guild_id, project_num, group_num) VALUES (%s, %s, %s)", + (ctx.guild.id, project_num, group), ) - await ctx.send(f'Group {group} has voted for Project {project_num}!') + await ctx.send(f"Group {group} has voted for Project {project_num}!") # ----------------------------------------------------------------------------------------------------------------- # Function: vote_error(self, ctx, error) @@ -96,12 +106,15 @@ async def vote(self, ctx, project_num : int): # ----------------------------------------------------------------------------------------------------------------- @vote.error async def vote_error(self, ctx, error): + """Error handling for vote command""" if isinstance(error, commands.UserInputError): - await ctx.send('To join a project, use the join command, do: $vote \n' - '( For example: $vote 0 )') + await ctx.send( + "To join a project, use the join command, do: $vote \n" + "( For example: $vote 0 )" + ) else: await ctx.author.send(error) - #await ctx.message.delete() + # await ctx.message.delete() # ---------------------------------------------------------------------------------- # Function: projects(self, ctx) @@ -111,20 +124,29 @@ async def vote_error(self, ctx, error): # - ctx: used to access the values passed through the current context # Outputs: prints the list of current projects # ---------------------------------------------------------------------------------- - @commands.command(name='projects', help='print projects with groups assigned to them', pass_context=True) + @commands.command( + name="projects", + help="print projects with groups assigned to them", + pass_context=True, + ) # @commands.dm_only() async def projects(self, ctx): + """Prints the list of current projects""" projects = db.query( "SELECT project_num, string_agg(group_num::text, ', ') AS group_members " "FROM project_groups WHERE guild_id = %s GROUP BY project_num", - (ctx.guild.id,) + (ctx.guild.id,), ) if len(projects) > 0: - await ctx.send('\n'.join(f'Project {project_num}: Group(s) {group_members}' - for project_num, group_members in projects)) + await ctx.send( + "\n".join( + f"Project {project_num}: Group(s) {group_members}" + for project_num, group_members in projects + ) + ) else: - await ctx.send('There are currently no votes for any project numbers.') + await ctx.send("There are currently no votes for any project numbers.") # ----------------------------------------------------------------------------------------------------------------- # Function: project_error(self, ctx, error) @@ -137,10 +159,13 @@ async def projects(self, ctx): # ----------------------------------------------------------------------------------------------------------------- @projects.error async def project_error(self, ctx, error): + """Error handling for projects command""" await ctx.author.send(error) + # ----------------------------------------------------------- # add the file to the bot's cog system # ----------------------------------------------------------- -def setup(bot): - bot.add_cog(Voting(bot)) +async def setup(bot): + """Adds the file to the bot's cog system""" + await bot.add_cog(Voting(bot)) diff --git a/cogs/wordfilter.py b/cogs/wordfilter.py index c890fdbbe..463bf4951 100644 --- a/cogs/wordfilter.py +++ b/cogs/wordfilter.py @@ -5,14 +5,14 @@ from discord import NotFound from discord.ext import commands import db -#import profanity_helper -class WordFilter(commands.Cog): +# import profanity_helper + +class WordFilter(commands.Cog): def __init__(self, bot): self.bot = bot - # TODO # whitelistWord # remove from white list @@ -25,9 +25,8 @@ def __init__(self, bot): # reset filter (doesn't clear any saved lists. It just prevents better-profanity from using any saved lists.) # prevent commands from being blacklisted - #custom_badwords = ['happy', 'jolly', 'merry'] - #profanity.add_censor_words(custom_badwords) - + # custom_badwords = ['happy', 'jolly', 'merry'] + # profanity.add_censor_words(custom_badwords) # ----------------------------------------------------------------------------------------------------------------- # Function: whitelistWord @@ -38,23 +37,19 @@ def __init__(self, bot): # Outputs: # - success message # ----------------------------------------------------------------------------------------------------------------- - @commands.has_role('Instructor') + @commands.has_role("Instructor") @commands.command( - name='whitelisttest', - help='Add a word to the censor whitelist. Enclose in quotation marks. EX: $whitelist \"WORD\"') - async def whitelistWordTest(self, ctx, word: str =''): - - #if not ctx.channel.name == 'instructor-commands': + name="whitelisttest", + help='Add a word to the censor whitelist. Enclose in quotation marks. EX: $whitelist "WORD"', + ) + async def whitelistWordTest(self, ctx, word: str = ""): + """Allows instructors to add words to censor whitelist""" + # if not ctx.channel.name == 'instructor-commands': # await ctx.author.send('Please use this command inside #instructor-commands') # await ctx.message.delete() # return - - - - - await ctx.send( - f"_{word}_ has been added to the whitelist. TODO") + await ctx.send(f"_{word}_ has been added to the whitelist. TODO") # ----------------------------------------------------------------------------------------------------------------- # Function: whitelistWord_error(self, ctx, error) @@ -67,15 +62,14 @@ async def whitelistWordTest(self, ctx, word: str =''): # ----------------------------------------------------------------------------------------------------------------- @whitelistWordTest.error async def whitelistWord_error(self, ctx, error): + """Error handling for whitelist command""" if isinstance(error, commands.MissingRequiredArgument): - await ctx.send( - 'Todo') + await ctx.send("Todo") else: await ctx.send(error) print(error) await ctx.message.delete() - # ----------------------------------------------------------------------------------------------------------------- # Function: clearwhitelist # Description: allow instructors to clear their saved whitelist @@ -84,20 +78,20 @@ async def whitelistWord_error(self, ctx, error): # Outputs: # - success message # ----------------------------------------------------------------------------------------------------------------- - @commands.has_role('Instructor') + @commands.has_role("Instructor") @commands.command( - name='clearWhitelist', - help='Clears all words from the saved whitelist. EX: $clearwhitelist') + name="clearWhitelist", + help="Clears all words from the saved whitelist. EX: $clearwhitelist", + ) async def clearWhitelist(self, ctx): - - if not ctx.channel.name == 'instructor-commands': - await ctx.author.send('Please use this command inside #instructor-commands') + """Allows instructors to clea their saved whitelist""" + if not ctx.channel.name == "instructor-commands": + await ctx.author.send("Please use this command inside #instructor-commands") await ctx.message.delete() return # clear whitelist and reconstruct. - await ctx.send("Whitelist has been cleared. TODO") # ----------------------------------------------------------------------------------------------------------------- @@ -111,6 +105,7 @@ async def clearWhitelist(self, ctx): # ----------------------------------------------------------------------------------------------------------------- @clearWhitelist.error async def clearWhitelist_error(self, ctx, error): + """Error handling for whitelist command""" print(error) await ctx.message.delete() @@ -122,18 +117,18 @@ async def clearWhitelist_error(self, ctx, error): # Outputs: # - success message # ----------------------------------------------------------------------------------------------------------------- - @commands.has_role('Instructor') + @commands.has_role("Instructor") @commands.command( - name='loadWhitelist', - help='Adds all words in the saved whitelist to the censor whitelist. EX: $loadWhitelist') + name="loadWhitelist", + help="Adds all words in the saved whitelist to the censor whitelist. EX: $loadWhitelist", + ) async def loadWhitelist(self, ctx): - - if not ctx.channel.name == 'instructor-commands': - await ctx.author.send('Please use this command inside #instructor-commands') + """Allows instructors to load their saved whitelist""" + if not ctx.channel.name == "instructor-commands": + await ctx.author.send("Please use this command inside #instructor-commands") await ctx.message.delete() return - await ctx.send("Whitelist has been loaded. TODO") # ----------------------------------------------------------------------------------------------------------------- @@ -147,9 +142,12 @@ async def loadWhitelist(self, ctx): # ----------------------------------------------------------------------------------------------------------------- @loadWhitelist.error async def loadWhitelist_error(self, ctx, error): + """Error handling for loadWhitelist command""" print(error) await ctx.message.delete() -def setup(bot): + +async def setup(bot): + """Adds the file the bot's cog system""" n = WordFilter(bot) - bot.add_cog(n) + await bot.add_cog(n) diff --git a/cogsical.ics b/cogsical.ics new file mode 100644 index 000000000..9f592fb5a --- /dev/null +++ b/cogsical.ics @@ -0,0 +1 @@ +BEGIN:VCALENDAR PRODID:-//Google Inc//Google Calendar 70.9054//EN VERSION:2.0 CALSCALE:GREGORIAN METHOD:PUBLISH X-WR-CALNAME:ClassMateBot X-WR-TIMEZONE:America/New_York BEGIN:VEVENT DTSTART:20231018T175523Z DTSTAMP:20231017T215524Z UID:kp5t1bmpbb8p3cghkhvgrtfbj4@google.com CREATED:20231017T215523Z DESCRIPTION:CSC510 LAST-MODIFIED:20231017T215523Z SEQUENCE:0 STATUS:CONFIRMED SUMMARY:HW3 TRANSP:OPAQUE END:VEVENT END:VCALENDAR \ No newline at end of file diff --git a/conftest.py b/conftest.py index ee0cb10a0..af415a7a7 100644 --- a/conftest.py +++ b/conftest.py @@ -2,7 +2,7 @@ import sys import discord.ext.test as dpytest -import pytest +import pytest_asyncio from discord import Intents from discord.ext.commands import Bot from setuptools import glob @@ -17,18 +17,20 @@ root_dir = d(d(abspath("test/test_bot.py"))) sys.path.append(root_dir) + # Default parameters for the simulated dpytest bot. Loads the bot with commands from the /cogs directory # Ran everytime pytest is called -@pytest.fixture -def bot(event_loop): +@pytest_asyncio.fixture +async def bot(event_loop): bot = Bot(intents=intents, command_prefix="$", loop=event_loop) dir = os.path.dirname(os.path.abspath(__file__)) os.chdir(dir) - os.chdir('cogs') + os.chdir("cogs") for filename in os.listdir(os.getcwd()): - if filename.endswith('.py'): - bot.load_extension(f"cogs.{filename[:-3]}") - bot.load_extension('jishaku') + if filename.endswith(".py"): + await bot.load_extension(f"cogs.{filename[:-3]}") + await bot.load_extension("jishaku") + await bot._async_setup_hook() dpytest.configure(bot) return bot @@ -36,7 +38,7 @@ def bot(event_loop): # Cleans up leftover files generated through dpytest def pytest_sessionfinish(): # Clean up attachment files - files = glob.glob('./dpytest_*.dat') + files = glob.glob("./dpytest_*.dat") for path in files: try: os.remove(path) @@ -46,4 +48,5 @@ def pytest_sessionfinish(): # rollback all db modifications made db.CONN.rollback() -# Copyright (c) 2021 War-Keeper \ No newline at end of file + +# Copyright (c) 2021 War-Keeper diff --git a/data/proj2media/addAssignment.PNG b/data/proj2media/addAssignment.PNG new file mode 100644 index 000000000..028de3786 Binary files /dev/null and b/data/proj2media/addAssignment.PNG differ diff --git a/data/proj2media/addAssignmentHelp.PNG b/data/proj2media/addAssignmentHelp.PNG new file mode 100644 index 000000000..bfaa15022 Binary files /dev/null and b/data/proj2media/addAssignmentHelp.PNG differ diff --git a/data/proj2media/addCalendarEvent.png b/data/proj2media/addCalendarEvent.png new file mode 100644 index 000000000..22fd74e62 Binary files /dev/null and b/data/proj2media/addCalendarEvent.png differ diff --git a/data/proj2media/addGradeCategory.PNG b/data/proj2media/addGradeCategory.PNG new file mode 100644 index 000000000..d55e0fc63 Binary files /dev/null and b/data/proj2media/addGradeCategory.PNG differ diff --git a/data/proj2media/addGradeCategoryHelp.PNG b/data/proj2media/addGradeCategoryHelp.PNG new file mode 100644 index 000000000..2dd6c3296 Binary files /dev/null and b/data/proj2media/addGradeCategoryHelp.PNG differ diff --git a/data/proj2media/assignments.gif b/data/proj2media/assignments.gif new file mode 100644 index 000000000..b661f74f0 Binary files /dev/null and b/data/proj2media/assignments.gif differ diff --git a/data/proj2media/calendar.gif b/data/proj2media/calendar.gif new file mode 100644 index 000000000..5c5ad826e Binary files /dev/null and b/data/proj2media/calendar.gif differ diff --git a/data/proj2media/categories.PNG b/data/proj2media/categories.PNG new file mode 100644 index 000000000..c265ff261 Binary files /dev/null and b/data/proj2media/categories.PNG differ diff --git a/data/proj2media/categoriesDM.PNG b/data/proj2media/categoriesDM.PNG new file mode 100644 index 000000000..d9f4a5fc1 Binary files /dev/null and b/data/proj2media/categoriesDM.PNG differ diff --git a/data/proj2media/categoriesHelp.PNG b/data/proj2media/categoriesHelp.PNG new file mode 100644 index 000000000..3a3df9a84 Binary files /dev/null and b/data/proj2media/categoriesHelp.PNG differ diff --git a/data/proj2media/clearCalendar.PNG b/data/proj2media/clearCalendar.PNG new file mode 100644 index 000000000..18c6e4364 Binary files /dev/null and b/data/proj2media/clearCalendar.PNG differ diff --git a/data/proj2media/deleteAssignment.PNG b/data/proj2media/deleteAssignment.PNG new file mode 100644 index 000000000..374c0e6b7 Binary files /dev/null and b/data/proj2media/deleteAssignment.PNG differ diff --git a/data/proj2media/deleteAssignmentHelp.PNG b/data/proj2media/deleteAssignmentHelp.PNG new file mode 100644 index 000000000..06cadcadf Binary files /dev/null and b/data/proj2media/deleteAssignmentHelp.PNG differ diff --git a/data/proj2media/deleteGradeCategory.PNG b/data/proj2media/deleteGradeCategory.PNG new file mode 100644 index 000000000..3e0e7a45f Binary files /dev/null and b/data/proj2media/deleteGradeCategory.PNG differ diff --git a/data/proj2media/deleteGradeCategoryHelp.PNG b/data/proj2media/deleteGradeCategoryHelp.PNG new file mode 100644 index 000000000..1ed3269f8 Binary files /dev/null and b/data/proj2media/deleteGradeCategoryHelp.PNG differ diff --git a/data/proj2media/discordScreenshot.png b/data/proj2media/discordScreenshot.png new file mode 100644 index 000000000..7e5dcd704 Binary files /dev/null and b/data/proj2media/discordScreenshot.png differ diff --git a/data/proj2media/editAssignment.PNG b/data/proj2media/editAssignment.PNG new file mode 100644 index 000000000..6e65eebc3 Binary files /dev/null and b/data/proj2media/editAssignment.PNG differ diff --git a/data/proj2media/editAssignmentHelp.PNG b/data/proj2media/editAssignmentHelp.PNG new file mode 100644 index 000000000..5f219af45 Binary files /dev/null and b/data/proj2media/editAssignmentHelp.PNG differ diff --git a/data/proj2media/editGradeCategory.PNG b/data/proj2media/editGradeCategory.PNG new file mode 100644 index 000000000..8f364037c Binary files /dev/null and b/data/proj2media/editGradeCategory.PNG differ diff --git a/data/proj2media/editGradeCategoryHelp.PNG b/data/proj2media/editGradeCategoryHelp.PNG new file mode 100644 index 000000000..fb2173038 Binary files /dev/null and b/data/proj2media/editGradeCategoryHelp.PNG differ diff --git a/data/proj2media/getPdfDownload.JPG b/data/proj2media/getPdfDownload.JPG new file mode 100644 index 000000000..a0f0f4963 Binary files /dev/null and b/data/proj2media/getPdfDownload.JPG differ diff --git a/data/proj2media/getiCalPic.JPG b/data/proj2media/getiCalPic.JPG new file mode 100644 index 000000000..b720f9c7a Binary files /dev/null and b/data/proj2media/getiCalPic.JPG differ diff --git a/data/proj2media/gradeHelp.png b/data/proj2media/gradeHelp.png new file mode 100644 index 000000000..c9f4aa7ad Binary files /dev/null and b/data/proj2media/gradeHelp.png differ diff --git a/data/proj2media/gradeReportAssignment.PNG b/data/proj2media/gradeReportAssignment.PNG new file mode 100644 index 000000000..043aa6134 Binary files /dev/null and b/data/proj2media/gradeReportAssignment.PNG differ diff --git a/data/proj2media/gradeReportAssignmentDM.PNG b/data/proj2media/gradeReportAssignmentDM.PNG new file mode 100644 index 000000000..5bcb66df2 Binary files /dev/null and b/data/proj2media/gradeReportAssignmentDM.PNG differ diff --git a/data/proj2media/gradeReportAssignmentHelp.PNG b/data/proj2media/gradeReportAssignmentHelp.PNG new file mode 100644 index 000000000..98862fe64 Binary files /dev/null and b/data/proj2media/gradeReportAssignmentHelp.PNG differ diff --git a/data/proj2media/gradeReportCategory.PNG b/data/proj2media/gradeReportCategory.PNG new file mode 100644 index 000000000..8f8daec29 Binary files /dev/null and b/data/proj2media/gradeReportCategory.PNG differ diff --git a/data/proj2media/gradeReportCategoryDM.PNG b/data/proj2media/gradeReportCategoryDM.PNG new file mode 100644 index 000000000..caf396354 Binary files /dev/null and b/data/proj2media/gradeReportCategoryDM.PNG differ diff --git a/data/proj2media/gradeReportCategoryHelp.PNG b/data/proj2media/gradeReportCategoryHelp.PNG new file mode 100644 index 000000000..f656650f5 Binary files /dev/null and b/data/proj2media/gradeReportCategoryHelp.PNG differ diff --git a/data/proj2media/gradebycategoryHelp.png b/data/proj2media/gradebycategoryHelp.png new file mode 100644 index 000000000..2ea37c4b3 Binary files /dev/null and b/data/proj2media/gradebycategoryHelp.png differ diff --git a/data/proj2media/gradeforclassHelp.png b/data/proj2media/gradeforclassHelp.png new file mode 100644 index 000000000..34c52409e Binary files /dev/null and b/data/proj2media/gradeforclassHelp.png differ diff --git a/data/proj2media/graderequiredHelp.png b/data/proj2media/graderequiredHelp.png new file mode 100644 index 000000000..94c29aef7 Binary files /dev/null and b/data/proj2media/graderequiredHelp.png differ diff --git a/data/proj2media/graderequiredforclassHelp.png b/data/proj2media/graderequiredforclassHelp.png new file mode 100644 index 000000000..e94cfbaf5 Binary files /dev/null and b/data/proj2media/graderequiredforclassHelp.png differ diff --git a/data/proj2media/ical.gif b/data/proj2media/ical.gif new file mode 100644 index 000000000..ab6bb1216 Binary files /dev/null and b/data/proj2media/ical.gif differ diff --git a/data/proj2media/inputGrades.PNG b/data/proj2media/inputGrades.PNG new file mode 100644 index 000000000..c59aadbbb Binary files /dev/null and b/data/proj2media/inputGrades.PNG differ diff --git a/data/proj2media/inputGradesHelp.PNG b/data/proj2media/inputGradesHelp.PNG new file mode 100644 index 000000000..cf9178e4c Binary files /dev/null and b/data/proj2media/inputGradesHelp.PNG differ diff --git a/data/proj2media/quizzes.gif b/data/proj2media/quizzes.gif new file mode 100644 index 000000000..897550ba0 Binary files /dev/null and b/data/proj2media/quizzes.gif differ diff --git a/data/proj2media/subscribeCalendar1.png b/data/proj2media/subscribeCalendar1.png new file mode 100644 index 000000000..d2c144525 Binary files /dev/null and b/data/proj2media/subscribeCalendar1.png differ diff --git a/data/proj2media/subscribeCalendar2.png b/data/proj2media/subscribeCalendar2.png new file mode 100644 index 000000000..7b696ff76 Binary files /dev/null and b/data/proj2media/subscribeCalendar2.png differ diff --git a/data/proj2media/sylabus.gif b/data/proj2media/sylabus.gif new file mode 100644 index 000000000..f51194d20 Binary files /dev/null and b/data/proj2media/sylabus.gif differ diff --git a/db.py b/db.py index 9c3edc9b0..7ba6aee82 100644 --- a/db.py +++ b/db.py @@ -7,18 +7,18 @@ CONN = None TESTING_MODE = False # def connect(): - # global CONN +# global CONN -DATABASE_URL = os.getenv('DATABASE_URL') +DATABASE_URL = os.getenv("DATABASE_URL") try: - CONN = psycopg2.connect(DATABASE_URL, sslmode='require') - print('PostgreSQL connection successful') + CONN = psycopg2.connect(DATABASE_URL, sslmode="require") + print("PostgreSQL connection successful") except (Exception, psycopg2.DatabaseError) as error: print(error) def query(sql, args=()): - ''' query the database and get back rows selected/modified ''' + """query the database and get back rows selected/modified""" cur = CONN.cursor() try: cur.execute(sql, args) diff --git a/docs/Assignments/add_assignment.md b/docs/Assignments/add_assignment.md new file mode 100644 index 000000000..c492e210d --- /dev/null +++ b/docs/Assignments/add_assignment.md @@ -0,0 +1,26 @@ +# About $add_assignment _(New Project 2 Command)_ +This command lets the instructor add a new gradeable assignment +## Changes + +This command was introduced by [CSC510-Group-1](https://github.com/nfoster1492/ClassMateBot-1/). + +# Location of Code +The code that implements the above mentioned functionality is located in `[cogs/grades.py](https://github.com/nfoster1492/ClassMateBot-1/tree/main/cogs/assignments.py)`. + +# Code Description +## Functions +add_assignment(self, ctx, assignmentname: str, categoryname: str, points: str):
+This function takes as arguments the values provided by the constructor through self, context in which the command was called, the name of the assignment being added, the category it belongs to, and the maximum amount of points attainable on the assignment. + +# How to run it? (Small Example) +You are in the server that has the Classmate Bot active and online. You are an Instructor. From the instructor commands channel, enter the command `$add_assignment ` with the desired assignment name, category, and weight. + +``` +$add_assignment ASSIGNMENT_NAME CATEGORY_NAME POINTS +$add_assignment test1 tests 100 +``` +Successful execution of this command will add an assignment into the database with the desired name, category, and points. The bot will report on the success or failure of the command. + + + + \ No newline at end of file diff --git a/docs/Assignments/delete_assignment.md b/docs/Assignments/delete_assignment.md new file mode 100644 index 000000000..f5b3715b9 --- /dev/null +++ b/docs/Assignments/delete_assignment.md @@ -0,0 +1,25 @@ +# About $delete_assignment _(New Project 2 Command)_ +This command lets the instructor delete an existing gradeable assignment +## Changes + +This command was introduced by [CSC510-Group-1](https://github.com/nfoster1492/ClassMateBot-1/). + +# Location of Code +The code that implements the above mentioned functionality is located in `[cogs/grades.py](https://github.com/nfoster1492/ClassMateBot-1/tree/main/cogs/assignments.py)`. + +# Code Description +## Functions +delete_assignment(self, ctx, assignmentname: str):
+This function takes as arguments the values provided by the constructor through self, context in which the command was called, and the name of the assignment being deleted. +# How to run it? (Small Example) +You are in the server that has the Classmate Bot active and online. You are an Instructor. From the instructor commands channel, enter the command `$delete_assignment ` with the desired assignment name. + +``` +$delete_assignment ASSIGNMENT_NAME +$delete_assignment test1 +``` +Successful execution of this command will delete an assignment from the database. The bot will report on the success or failure of the command. + + + + \ No newline at end of file diff --git a/docs/Assignments/edit_assignment.md b/docs/Assignments/edit_assignment.md new file mode 100644 index 000000000..cf85f84ff --- /dev/null +++ b/docs/Assignments/edit_assignment.md @@ -0,0 +1,26 @@ +# About $edit_assignment _(New Project 2 Command)_ +This command lets the instructor edit a new gradeable assignment +## Changes + +This command was introduced by [CSC510-Group-1](https://github.com/nfoster1492/ClassMateBot-1/). + +# Location of Code +The code that implements the above mentioned functionality is located in `[cogs/grades.py](https://github.com/nfoster1492/ClassMateBot-1/tree/main/cogs/assignments.py)`. + +# Code Description +## Functions +edit_assignment(self, ctx, assignmentname: str, categoryname: str, points: str):
+This function takes as arguments the values provided by the constructor through self, context in which the command was called, the name of the assignment being edited, the category it will now belong to, and the maximum amount of points now attainable on the assignment. + +# How to run it? (Small Example) +You are in the server that has the Classmate Bot active and online. You are an Instructor. From the instructor commands channel, enter the command `$edit_assignment ` with the desired assignment name, category, and weight. + +``` +$edit_assignment ASSIGNMENT_NAME CATEGORY_NAME POINTS +$edit_assignment test1 tests 100 +``` +Successful execution of this command will edit an assignment in the database with the given name, with the desired category, and points. The bot will report on the success or failure of the command. + + + + \ No newline at end of file diff --git a/docs/Calendar/addCalendarEvent.md b/docs/Calendar/addCalendarEvent.md new file mode 100644 index 000000000..c25004032 --- /dev/null +++ b/docs/Calendar/addCalendarEvent.md @@ -0,0 +1,20 @@ +# About $addCalendarEvent _(Modified Command in Project 2)_ +This command lets the user add an event to the shared Google Calendar + +# Location of Code +The code that implements the above mentioned gets functionality is located [here](https://github.com/nfoster1492/ClassMateBot-1/blob/main/cogs/calendar.py) + +# Code Description +## Functions +addCalendarEvent(self, ctx, name, description, eventTime)::
+This function takes as arguments the values provided by the constructor through self, the context in which the command was called, the name and description of the event, as well as the event date and time information. + +# How to run it? (Small Example) +An example of this command's usage would be when a user would like to place an item on the Google Calendar. For example, Homework 1a for CSC510 is due on 10/11/2023 at 12:00pm +enter the command '$addCalendarEvent HW1a CSC510 2023-10-11T12:00:00Z' and the bot will provide confirmation if the calendar add was successful. +``` +$addCalendarEvent HW1a CSC510 2023-10-11T12:00:00Z +``` +Successful execution of this command will result in an event being placed on the shared calendar for all subscribers to see and confirmation output. + +![image](https://github.com/nfoster1492/ClassMateBot-1/blob/main/data/proj2media/addCalendarEvent.png) diff --git a/docs/Calendar/clearCalendar.md b/docs/Calendar/clearCalendar.md new file mode 100644 index 000000000..bec769a9e --- /dev/null +++ b/docs/Calendar/clearCalendar.md @@ -0,0 +1,20 @@ +# About $getiCalDownload _(Modified Command in Project 2)_ +This command lets the instructor clear all events from the google calendar. + +# Location of Code +The code that implements the above mentioned gets functionality is located [here](https://github.com/nfoster1492/ClassMateBot-1/blob/main/cogs/calendar.py) + +# Code Description +## Functions +clearCalendar(self, ctx)::
+This function takes as arguments the values provided by the constructor through self, and the context in which the command was called. + +# How to run it? (Small Example) +Let's say that you are in the server that has the Classmate Bot active and online, and you are the instructor. All you have to do is +enter the command 'clearCalendar': in the instructor commands channel and the bot will let you know the success or failure of your command +``` +$clearCalendar +``` +Successful execution of this command will result in the google calendar being cleared + +![image](https://github.com/nfoster1492/ClassMateBot-1/blob/main/data/proj2media/clearCalendar.PNG) diff --git a/docs/Calendar/getPdfDownload.md b/docs/Calendar/getPdfDownload.md new file mode 100644 index 000000000..c6f01f3f7 --- /dev/null +++ b/docs/Calendar/getPdfDownload.md @@ -0,0 +1,20 @@ +# About $getpdfDownload _(Modified Command in Project 2)_ +This command lets students download a pdf so that they can import the class calendar into their calendar software of choice. + +# Location of Code +The code that implements the above mentioned gets functionality is located [here](https://github.com/nfoster1492/ClassMateBot-1/blob/main/cogs/calendar.py) + +# Code Description +## Functions +getpdfDownload(self, ctx)::
+This function takes as arguments the values provided by the constructor through self, and the context in which the command was called. + +# How to run it? (Small Example) +Let's say that you are in the server that has the Classmate Bot active and online, and the instructor has been updating the calendar with events. All you have to do is +enter the command 'getPdfDownload': and the bot will return a file that the student can download +``` +$getPdfDownload +``` +Successful execution of this command will result in a pdf being sent in the same channel + +![image](https://github.com/nfoster1492/ClassMateBot-1/blob/main/data/proj2media/getPdfDownload.JPG) diff --git a/docs/Calendar/getiCalDownload.md b/docs/Calendar/getiCalDownload.md new file mode 100644 index 000000000..c89a0f852 --- /dev/null +++ b/docs/Calendar/getiCalDownload.md @@ -0,0 +1,20 @@ +# About $getiCalDownload _(Modified Command in Project 2)_ +This command lets students download an .ics file so that they can import the class calendar into their calendar software of choice. + +# Location of Code +The code that implements the above mentioned gets functionality is located [here](https://github.com/nfoster1492/ClassMateBot-1/blob/main/cogs/calendar.py) + +# Code Description +## Functions +getiCalDownload(self, ctx)::
+This function takes as arguments the values provided by the constructor through self, and the context in which the command was called. + +# How to run it? (Small Example) +Let's say that you are in the server that has the Classmate Bot active and online, and the instructor has been updating the calendar with events. All you have to do is +enter the command 'getiCalDownload': and the bot will return a file that the student can download +``` +$getiCalDownload +``` +Successful execution of this command will result in an .ics file being sent in the same channel + +![image](https://github.com/nfoster1492/ClassMateBot-1/blob/main/data/proj2media/getiCalPic.JPG) diff --git a/docs/Calendar/removeCalendar.md b/docs/Calendar/removeCalendar.md new file mode 100644 index 000000000..bfb6f1efd --- /dev/null +++ b/docs/Calendar/removeCalendar.md @@ -0,0 +1,18 @@ +# About $removeCalendar _(Modified Command in Project 2)_ +This command allows the instructor to remove a specified user from the shared class Google Calendar. + +# Location of Code +The code that implements the above mentioned gets functionality is located [here](https://github.com/nfoster1492/ClassMateBot-1/blob/main/cogs/calendar.py) + +# Code Description +## Functions +removeCalendar(self, ctx, userEmail)::
+This function takes as arguments the values provided by the constructor through self, the context in which the command was called, and the user email that the instructor wishes to be removed. + +# How to run it? (Small Example) +A basic application of this command would be if a student in the class decided to drop. The instructor can remove this user from the calendar using +``` +$removeCalendar *email-to-remove* +``` +Successful execution of this command will result in a Discord DM confirmation and the user being removed from the calendar. + diff --git a/docs/Calendar/subscribeCalendar.md b/docs/Calendar/subscribeCalendar.md new file mode 100644 index 000000000..2b277f025 --- /dev/null +++ b/docs/Calendar/subscribeCalendar.md @@ -0,0 +1,22 @@ +# About $subscribeCalendar _(Modified Command in Project 2)_ +This command allows a user to subscribe to the shared class Google Calendar. + +# Location of Code +The code that implements the above mentioned gets functionality is located [here](https://github.com/nfoster1492/ClassMateBot-1/blob/main/cogs/calendar.py) + +# Code Description +## Functions +subscribeCalendar(self, ctx, userEmail)::
+This function takes as arguments the values provided by the constructor through self, the context in which the command was called, and the user email to be added to the calendar. + +# How to run it? (Small Example) +A basic application of this command would be if a student wanted to add themselves to the shared class calendar. Another possibility would be for the instructor to enroll students on their behalf. This is accomplished using +``` +$subscribeCalendar *email-to-add* +``` +Successful execution of this command will result in a Discord DM confirmation and the user being added the calendar. + +![image](https://github.com/nfoster1492/ClassMateBot-1/blob/main/data/proj2media/subscribeCalendar1.png) + +![image](https://github.com/nfoster1492/ClassMateBot-1/blob/main/data/proj2media/subscribeCalendar2.png) + diff --git a/docs/Grades/add_grade_category.md b/docs/Grades/add_grade_category.md new file mode 100644 index 000000000..0c54ac921 --- /dev/null +++ b/docs/Grades/add_grade_category.md @@ -0,0 +1,26 @@ +# About $add_grade_category _(New Project 2 Command)_ +This command allows the instructor to add a new grade category with a designated weight +## Changes + +This command was introduced by [CSC510-Group-1](https://github.com/nfoster1492/ClassMateBot-1/). + +# Location of Code +The code that implements the above mentioned functionality is located in `[cogs/grades.py](https://github.com/nfoster1492/ClassMateBot-1/tree/main/cogs/grades.py)`. + +# Code Description +## Functions +add_grade_category(self, ctx, categoryname: str, weight: str):
+This function takes as arguments the values provided by the constructor through self, context in which the command was called, the name of the category being added, and the weight of the category being added. + +# How to run it? (Small Example) +You are in the server that has the Classmate Bot active and online. You are an Instructor. From the instructor commands channel, enter the command `$add_grade_category ` with the desired category name and weight. + +``` +$add_grade_category CATEGORY_NAME, WEIGHT +$add_grade_category Tests .5 +``` +Successful execution of this command will add a grade category into the database with the desired weight. The bot will report on the success or failure of the command. + + + + \ No newline at end of file diff --git a/docs/Grades/categories.md b/docs/Grades/categories.md new file mode 100644 index 000000000..0ef8b3d13 --- /dev/null +++ b/docs/Grades/categories.md @@ -0,0 +1,28 @@ +# About $categories _(New Project 2 Command)_ +This command allows the user to see a list the current grade categories in the course. + +## Changes + +This command was introduced by [CSC510-Group-1](https://github.com/nfoster1492/ClassMateBot-1/). + +# Location of Code +The code that implements the above mentioned functionality is located in `[cogs/grades.py](https://github.com/nfoster1492/ClassMateBot-1/tree/main/cogs/grades.py)`. + +# Code Description +## Functions +categories(self, ctx):
+This function takes as arguments the values provided by the constructor through self and context in which the command was called + +# How to run it? (Small Example) +You are in the server that has the Classmate Bot active and online. From any channel, enter the command `$categories`. + +``` +$categories +``` +Successful execution of this command will list the categories and corresponding weights in a dm to the user + + + + + + diff --git a/docs/Grades/delete_grade_category.md b/docs/Grades/delete_grade_category.md new file mode 100644 index 000000000..0cea7986d --- /dev/null +++ b/docs/Grades/delete_grade_category.md @@ -0,0 +1,26 @@ +# About $delete_grade_category _(New Project 2 Command)_ +This command allows the instructor to delete an existing grade category with a designated weight +## Changes + +This command was introduced by [CSC510-Group-1](https://github.com/nfoster1492/ClassMateBot-1/). + +# Location of Code +The code that implements the above mentioned functionality is located in `[cogs/grades.py](https://github.com/nfoster1492/ClassMateBot-1/tree/main/cogs/grades.py)`. + +# Code Description +## Functions +delete_grade_category(self, ctx, categoryname: str):
+This function takes as arguments the values provided by the constructor through self, context in which the command was called, and the name of the category being deleted + +# How to run it? (Small Example) +You are in the server that has the Classmate Bot active and online. You are an Instructor. From the instructor commands channel, enter the command `$delete_grade_category ` with the desired category to delete. + +``` +$delete_grade_category CATEGORY_NAME +$delete_grade_category Tests +``` +Successful execution of this command will delete a grade category in the database. The bot will report on the success or failure of the command. + + + + \ No newline at end of file diff --git a/docs/Grades/edit_grade_category.md b/docs/Grades/edit_grade_category.md new file mode 100644 index 000000000..d2857cdfb --- /dev/null +++ b/docs/Grades/edit_grade_category.md @@ -0,0 +1,26 @@ +# About $edit_grade_category _(New Project 2 Command)_ +This command allows the instructor to edit an existing grade category with a designated weight +## Changes + +This command was introduced by [CSC510-Group-1](https://github.com/nfoster1492/ClassMateBot-1/). + +# Location of Code +The code that implements the above mentioned functionality is located in `[cogs/grades.py](https://github.com/nfoster1492/ClassMateBot-1/tree/main/cogs/grades.py)`. + +# Code Description +## Functions +edit_grade_category(self, ctx, categoryname: str, weight: str):
+This function takes as arguments the values provided by the constructor through self, context in which the command was called, the name of the category being edited, and the weight of the category being edited. + +# How to run it? (Small Example) +You are in the server that has the Classmate Bot active and online. You are an Instructor. From the instructor commands channel, enter the command `$edit_grade_category ` with the desired category name and weight. + +``` +$edit_grade_category CATEGORY_NAME, WEIGHT +$edit_grade_category Tests, .5 +``` +Successful execution of this command will edit a grade category in the database with the new desired weight. The bot will report on the success or failure of the command. + + + + \ No newline at end of file diff --git a/docs/Grades/grade.md b/docs/Grades/grade.md new file mode 100644 index 000000000..6b5386c52 --- /dev/null +++ b/docs/Grades/grade.md @@ -0,0 +1,24 @@ +# About $grade _(New Project 2 Command)_ +This command lets a student get their grade for a certain assignment +## Changes + +This command was introduced by [CSC510-Group-1](https://github.com/nfoster1492/ClassMateBot-1/). + +# Location of Code +The code that implements the above mentioned functionality is located in `[cogs/grades.py](https://github.com/nfoster1492/ClassMateBot-1/tree/main/cogs/grades.py)`. + +# Code Description +## Functions +grade(self, ctx, assignmentName: str):
+This function takes as arguments the values provided by the constructor through self, context in which the command was called, and the name of the assignment whose grade is desired. + +# How to run it? (Small Example) +You are in the server that has the Classmate Bot active and online. You are a Student. From the general channel, enter the command `$grade ` with the desired assignment name. + +``` +$grade ASSIGNMENT_NAME +$grade hw1 +``` +Successful execution of this command will DM the student their grade for that specific assignment. + + diff --git a/docs/Grades/grade_report_assignment.md b/docs/Grades/grade_report_assignment.md new file mode 100644 index 000000000..26b02e2f7 --- /dev/null +++ b/docs/Grades/grade_report_assignment.md @@ -0,0 +1,28 @@ +# About $grade_report_assignment _(New Project 2 Command)_ + This command lets the instructor generate a report on the average, low, and high score on each assignment + +## Changes + +This command was introduced by [CSC510-Group-1](https://github.com/nfoster1492/ClassMateBot-1/). + +# Location of Code +The code that implements the above mentioned functionality is located in `[cogs/grades.py](https://github.com/nfoster1492/ClassMateBot-1/tree/main/cogs/grades.py)`. + +# Code Description +## Functions +grade_report_assignment(self, ctx):
+This function takes as arguments the values provided by the constructor through self and context in which the command was called + +# How to run it? (Small Example) +You are in the server that has the Classmate Bot active and online. From the instructor commands channel, enter the command `$grade_report_assignment` + +``` +$grade_report_assignment +``` +Successful execution of this command will send a DM to the instructor with the grading breakdown. + + + + + + diff --git a/docs/Grades/grade_report_category.md b/docs/Grades/grade_report_category.md new file mode 100644 index 000000000..698b74220 --- /dev/null +++ b/docs/Grades/grade_report_category.md @@ -0,0 +1,28 @@ +# About $grade_report_category _(New Project 2 Command)_ + This command lets the instructor generate a report on the average, low, and high score on each category + +## Changes + +This command was introduced by [CSC510-Group-1](https://github.com/nfoster1492/ClassMateBot-1/). + +# Location of Code +The code that implements the above mentioned functionality is located in `[cogs/grades.py](https://github.com/nfoster1492/ClassMateBot-1/tree/main/cogs/grades.py)`. + +# Code Description +## Functions +grade_report_category(self, ctx):
+This function takes as arguments the values provided by the constructor through self and context in which the command was called + +# How to run it? (Small Example) +You are in the server that has the Classmate Bot active and online. From the instructor commands channel, enter the command `$grade_report_category` + +``` +$grade_report_category +``` +Successful execution of this command will send a DM to the instructor with the grading breakdown. + + + + + + diff --git a/docs/Grades/gradebycategory.md b/docs/Grades/gradebycategory.md new file mode 100644 index 000000000..e4199396f --- /dev/null +++ b/docs/Grades/gradebycategory.md @@ -0,0 +1,25 @@ +# About $gradebycategory _(New Project 2 Command)_ + This command lets a student get their average grade for a certain category. + +## Changes + +This command was introduced by [CSC510-Group-1](https://github.com/nfoster1492/ClassMateBot-1/). + +# Location of Code +The code that implements the above mentioned functionality is located in `[cogs/grades.py](https://github.com/nfoster1492/ClassMateBot-1/tree/main/cogs/grades.py)`. + +# Code Description +## Functions +gradebycateogory(self, ctx, categoryName: str):
+This function takes as arguments the values provided by the constructor through self, context in which the command was called, and the name of the category whose average grade is desired. + +# How to run it? (Small Example) +You are in the server that has the Classmate Bot active and online. From the general channel, enter the command `$gradebycategory categoryName` + +``` +$gradebycategory CATEGORY_NAME +$gradebycategory projects +``` +Successful execution of this command will send a DM to the student with their average grade for the given category. + + diff --git a/docs/Grades/gradeforclass.md b/docs/Grades/gradeforclass.md new file mode 100644 index 000000000..5b0d5c008 --- /dev/null +++ b/docs/Grades/gradeforclass.md @@ -0,0 +1,23 @@ +# About $gradeforclass _(New Project 2 Command)_ +This command lets a student get their average grade for the whole class. +## Changes + +This command was introduced by [CSC510-Group-1](https://github.com/nfoster1492/ClassMateBot-1/). + +# Location of Code +The code that implements the above mentioned functionality is located in `[cogs/grades.py](https://github.com/nfoster1492/ClassMateBot-1/tree/main/cogs/grades.py)`. + +# Code Description +## Functions +gradeforclass(self, ctx):
+This function takes as arguments the values provided by the constructor through self and context in which the command was called. + +# How to run it? (Small Example) +You are in the server that has the Classmate Bot active and online. You are a Student. From the general channel, enter the command `$gradeforclass`. + +``` +$gradeforclass +``` +Successful execution of this command will DM the student their overall grade for the course. + + diff --git a/docs/Grades/graderequired.md b/docs/Grades/graderequired.md new file mode 100644 index 000000000..bae040c12 --- /dev/null +++ b/docs/Grades/graderequired.md @@ -0,0 +1,24 @@ +# About $graderequired _(New Project 2 Command)_ +This command lets a student get the grade they need on the next assignment to keep a desired grade in a certain category. +## Changes + +This command was introduced by [CSC510-Group-1](https://github.com/nfoster1492/ClassMateBot-1/). + +# Location of Code +The code that implements the above mentioned functionality is located in `[cogs/grades.py](https://github.com/nfoster1492/ClassMateBot-1/tree/main/cogs/grades.py)`. + +# Code Description +## Functions +graderequired(self, ctx, categoryName: str, pointValue: str, desiredGrade: str):
+This function takes as arguments the values provided by the constructor through self, context in which the command was called, the name of the category the assignment will be part of, the amount of points the next assignment will be worth, and the overall grade desired for the category. + +# How to run it? (Small Example) +You are in the server that has the Classmate Bot active and online. You are a Student. From the general channel, enter the command `$graderequired `. + +``` +$graderequired CATEGORY_NAME POINT_VALUE DESIRED_GRADE +$graderequired tests 200 90 +``` +Successful execution of this command will DM the student the grade required on the next hypothetical assignment. + + diff --git a/docs/Grades/graderequiredforclass.md b/docs/Grades/graderequiredforclass.md new file mode 100644 index 000000000..fae582b93 --- /dev/null +++ b/docs/Grades/graderequiredforclass.md @@ -0,0 +1,24 @@ +# About $graderequiredforclass _(New Project 2 Command)_ +This command lets a student get the grade they need on the next assignment to keep a desired grade in the class. +## Changes + +This command was introduced by [CSC510-Group-1](https://github.com/nfoster1492/ClassMateBot-1/). + +# Location of Code +The code that implements the above mentioned functionality is located in `[cogs/grades.py](https://github.com/nfoster1492/ClassMateBot-1/tree/main/cogs/grades.py)`. + +# Code Description +## Functions +graderequiredforclass(self, ctx, categoryName: str, pointValue: str, desiredGrade: str):
+This function takes as arguments the values provided by the constructor through self, context in which the command was called, the name of the category the assignment will be part of, the amount of points the next assignment will be worth, and the overall grade desired for the course. + +# How to run it? (Small Example) +You are in the server that has the Classmate Bot active and online. You are a Student. From the general channel, enter the command `$graderequired `. + +``` +$graderequired CATEGORY_NAME POINT_VALUE DESIRED_GRADE +$graderequired tests 200 90 +``` +Successful execution of this command will DM the student the grade required on the next hypothetical assignment to keep a desired grade in the course. + + diff --git a/docs/Grades/input_grades.md b/docs/Grades/input_grades.md new file mode 100644 index 000000000..600c55a4a --- /dev/null +++ b/docs/Grades/input_grades.md @@ -0,0 +1,27 @@ +# About $input_grades _(New Project 2 Command)_ +This command allows the instructor to input grades into the system for a given assignment. + +## Changes + +This command was introduced by [CSC510-Group-1](https://github.com/nfoster1492/ClassMateBot-1/). + +# Location of Code +The code that implements the above mentioned functionality is located in `[cogs/grades.py](https://github.com/nfoster1492/ClassMateBot-1/tree/main/cogs/grades.py)`. + +# Code Description +## Functions +input_grades(self, ctx, assignmentname: str):
+This function takes as arguments the values provided by the constructor through self, context in which the command was called, the assignment that the grades are being input for, and a csv file that is attached with a mappin gof the student names to the grades. + +# How to run it? (Small Example) +You are in the server that has the Classmate Bot active and online. You are an Instructor. From the instructor commands channel, enter the command `$input_grades ` and attach a csv file with two columns, the first being student names and the second being the grades for the assignment + +``` +$inputgrades ASSIGNMENT_NAME +$inputgrades HW1 +``` +Successful execution of this command will update the grades in the database and report to the instructor the amount that were added, if any were skipped over, and the amount of grades that were edited. + + + + \ No newline at end of file diff --git a/docs/Groups/connect.md b/docs/Groups/connect.md index b83f325d2..af184b1ea 100644 --- a/docs/Groups/connect.md +++ b/docs/Groups/connect.md @@ -1,4 +1,4 @@ -# About $connect _(New Project 2 Command)_ +# About $connect This command lets the user create private text channels for all groups with members. Note: Running this command will delete all current private group channels! diff --git a/docs/Groups/group.md b/docs/Groups/group.md index d4050907d..697088139 100644 --- a/docs/Groups/group.md +++ b/docs/Groups/group.md @@ -1,4 +1,4 @@ -# About $group _(New Project 2 Command)_ +# About $group This command lets the student view the names of members in their group or the names of members in a specified group. This is useful for students to contact each other and for instructors to find out the names of people in a specific group. # Location of Code diff --git a/docs/Groups/groups.md b/docs/Groups/groups.md index 393014ac7..9d56a7090 100644 --- a/docs/Groups/groups.md +++ b/docs/Groups/groups.md @@ -1,4 +1,4 @@ -# About $groups _(Modified Command in Project 2)_ +# About $groups This command lets the student view the amount of people there are in each group from the private discord DM with the bot. This is useful for viewing which group numbers are taken, which groups are still empty and which groups are already full. # Location of Code diff --git a/docs/Groups/join.md b/docs/Groups/join.md index 1301221d9..f3ba858a6 100644 --- a/docs/Groups/join.md +++ b/docs/Groups/join.md @@ -1,4 +1,4 @@ -# About $join _(Modified Command in Project 2)_ +# About $join This command lets the student add their name to the group member list. This is used to ensure that all students can get into a group and making sure no duplicates occur in the process # Location of Code diff --git a/docs/Groups/leave.md b/docs/Groups/leave.md index d3c84e282..0b4c38b8f 100644 --- a/docs/Groups/leave.md +++ b/docs/Groups/leave.md @@ -1,4 +1,4 @@ -# About $leave _(Modified Command in Project 2)_ +# About $leave This command lets the student leave their current group. This is used to ensure that if a member switches groups or drops the class, then they can be removed from a group. # Location of Code diff --git a/docs/Groups/reset.md b/docs/Groups/reset.md index a6a5072b9..dcd8370e7 100644 --- a/docs/Groups/reset.md +++ b/docs/Groups/reset.md @@ -1,4 +1,4 @@ -# About $reset _(New Project 2 Command)_ +# About $reset This command lets the user delete all group roles in the server. Note: This must be used in the TEST environment only! diff --git a/docs/Groups/startupgroups.md b/docs/Groups/startupgroups.md index b9edb61b7..afe435689 100644 --- a/docs/Groups/startupgroups.md +++ b/docs/Groups/startupgroups.md @@ -1,4 +1,4 @@ -# About $startupgroups _(New Project 2 Command)_ +# About $startupgroups This command lets the user set up the roles required for the grouping. This is required as a part of the group making/joining/leaving functionality. # Location of Code diff --git a/docs/PinMessage/pin.md b/docs/PinMessage/pin.md index 1fc798469..d442f090d 100644 --- a/docs/PinMessage/pin.md +++ b/docs/PinMessage/pin.md @@ -1,4 +1,4 @@ -# About $pin _(Modified Command in Project 2)_ +# About $pin This command lets the student to pin a message from the discord channel to their private pinning board. # Location of Code diff --git a/docs/PinMessage/pinnedmessages.md b/docs/PinMessage/pinnedmessages.md index 98b5ac28f..c69ca949f 100644 --- a/docs/PinMessage/pinnedmessages.md +++ b/docs/PinMessage/pinnedmessages.md @@ -1,4 +1,4 @@ -# About $pinnedmessages _(Modified Command in Project 2)_ +# About $pinnedmessages This command lets the student to retrieve all the pinned messages from their private pinning board with an optional given tagname. # Location of Code diff --git a/docs/PinMessage/unpin.md b/docs/PinMessage/unpin.md index ac39dd970..57a46f5cd 100644 --- a/docs/PinMessage/unpin.md +++ b/docs/PinMessage/unpin.md @@ -1,4 +1,4 @@ -# About $unpin _(Modified Command in Project 2)_ +# About $unpin This command lets the student to delete a pinned message from their private pinning board. # Location of Code diff --git a/docs/PinMessage/updatepin.md b/docs/PinMessage/updatepin.md index b8e85f97d..1d444adc4 100644 --- a/docs/PinMessage/updatepin.md +++ b/docs/PinMessage/updatepin.md @@ -1,4 +1,4 @@ -# About $updatepin _(Modified Command in Project 2)_ +# About $updatepin This command lets the student to update a pinned message with a new link from the discord channel to their private pinning board. # Location of Code diff --git a/docs/Polling/poll.md b/docs/Polling/poll.md index 3741fc537..c7dd9cab3 100644 --- a/docs/Polling/poll.md +++ b/docs/Polling/poll.md @@ -1,4 +1,4 @@ -# About $poll _(New Project 3 Command)_ +# About $poll This command allows the user to create a simple reaction poll with thumbs up, thumbs down, and unsure reactions. diff --git a/docs/Polling/quizpoll.md b/docs/Polling/quizpoll.md index 737ddda9f..47fc55159 100644 --- a/docs/Polling/quizpoll.md +++ b/docs/Polling/quizpoll.md @@ -1,4 +1,4 @@ -# About $quizpoll _(New Project 3 Command)_ +# About $quizpoll This command allows the user to start a quiz-style reaction poll with a given title and six options or less. diff --git a/docs/ProfanityFilter/dewhitelist.md b/docs/ProfanityFilter/dewhitelist.md index 86baaf70e..7215a843a 100644 --- a/docs/ProfanityFilter/dewhitelist.md +++ b/docs/ProfanityFilter/dewhitelist.md @@ -1,4 +1,4 @@ -# About $dewhitelist _(New Project 3 Command)_ +# About $dewhitelist Command that adds removes a word or sentence from the censor whitelist. Instructor only. diff --git a/docs/ProfanityFilter/togglefilter.md b/docs/ProfanityFilter/togglefilter.md index 6f087d37f..ec6fbeccb 100644 --- a/docs/ProfanityFilter/togglefilter.md +++ b/docs/ProfanityFilter/togglefilter.md @@ -1,4 +1,4 @@ -# About $toggleFilter _(New Project 3 Command)_ +# About $toggleFilter Enable or disable the profanity filter. Instructor only. diff --git a/docs/ProfanityFilter/whitelist.md b/docs/ProfanityFilter/whitelist.md index 0eadd63df..05029288e 100644 --- a/docs/ProfanityFilter/whitelist.md +++ b/docs/ProfanityFilter/whitelist.md @@ -1,4 +1,4 @@ -# About $whitelist _(New Project 3 Command)_ +# About $whitelist Command that adds a word or sentence to the censor whitelist. Instructor only. diff --git a/docs/QandA/DALLAF.md b/docs/QandA/DALLAF.md index b3e2ce821..ac5b9c830 100644 --- a/docs/QandA/DALLAF.md +++ b/docs/QandA/DALLAF.md @@ -1,4 +1,4 @@ -# About $DALLAF (deleteAllAnswersFor) _(New Project 3 Command)_ +# About $DALLAF (deleteAllAnswersFor) This command lets instructors remove all answers for a question in the #q-and-a channel. Deletes all answers for a question. Instructor only. diff --git a/docs/QandA/allChannelGhosts.md b/docs/QandA/allChannelGhosts.md index 0a8da63a4..fee8cdbf6 100644 --- a/docs/QandA/allChannelGhosts.md +++ b/docs/QandA/allChannelGhosts.md @@ -1,4 +1,4 @@ -# About $allChannelGhosts _(New Project 3 Command)_ +# About $allChannelGhosts Get the hidden (deleted with $deleteQuestion) questions that are in the database but not in the channel. Instructor only. diff --git a/docs/QandA/answer.md b/docs/QandA/answer.md index 7c0a21b8c..9a6a01aa0 100644 --- a/docs/QandA/answer.md +++ b/docs/QandA/answer.md @@ -1,4 +1,4 @@ -# About $answer _(Modified in project 3)_ +# About $answer This command lets users answer a question in the #q-and-a channel. The answers are automatically appended to the question and the role (Instructor/Student) of the sender will be shown. The user can choose to display their name or answer the question anonymously. ## Changes diff --git a/docs/QandA/archiveQA.md b/docs/QandA/archiveQA.md index 8936af3ea..3f869ab26 100644 --- a/docs/QandA/archiveQA.md +++ b/docs/QandA/archiveQA.md @@ -1,4 +1,4 @@ -# About $archiveQA _(New Project 3 Command)_ +# About $archiveQA DMs the user all the questions and answers on the channel, excluding deleted (zombie) and hidden (ghost) questions. diff --git a/docs/QandA/ask.md b/docs/QandA/ask.md index a0c90e09f..a6a01f5d0 100644 --- a/docs/QandA/ask.md +++ b/docs/QandA/ask.md @@ -1,4 +1,4 @@ -# About $ask _(Modified in project 3)_ +# About $ask This command lets users ask a question in the #q-and-a channel. The questions are automatically numbered and can be asked anonymously or display the author. ## Changes diff --git a/docs/QandA/channelGhost.md b/docs/QandA/channelGhost.md index 90405b4f2..169e65c3e 100644 --- a/docs/QandA/channelGhost.md +++ b/docs/QandA/channelGhost.md @@ -1,4 +1,4 @@ -# About $channelGhost _(New Project 3 Command)_ +# About $channelGhost Gets a specific ghost or hidden (deleted with $deleteQuestion) question and all its answers. Instructor only. diff --git a/docs/QandA/deleteAllQA.md b/docs/QandA/deleteAllQA.md index 114d30dfc..e4e53e9a5 100644 --- a/docs/QandA/deleteAllQA.md +++ b/docs/QandA/deleteAllQA.md @@ -1,4 +1,4 @@ -# About $deleteAllQA _(New Project 3 Command)_ +# About $deleteAllQA Deletes all questions and answers from the database and channel (for that server only), including ghost (hidden) and zombie (deleted) questions. Instructor only. Note: may take some time to complete. diff --git a/docs/QandA/deleteQuestion.md b/docs/QandA/deleteQuestion.md index 33f47b688..64f45c609 100644 --- a/docs/QandA/deleteQuestion.md +++ b/docs/QandA/deleteQuestion.md @@ -1,4 +1,4 @@ -# About $deleteQuestion _(New Project 3 Command)_ +# About $deleteQuestion Would be more accurate to call it hideQuestion. Delete one question but leave answers untouched. Instructor only. diff --git a/docs/QandA/getAnswersFor.md b/docs/QandA/getAnswersFor.md index 9995d5514..87424dd3e 100644 --- a/docs/QandA/getAnswersFor.md +++ b/docs/QandA/getAnswersFor.md @@ -1,4 +1,4 @@ -# About $getAnswersFor _(New Project 3 Command)_ +# About $getAnswersFor This command gets a question and all its answers and DMs them to the user. diff --git a/docs/QandA/reviveGhost.md b/docs/QandA/reviveGhost.md index 6a55cb2b6..0461d69e2 100644 --- a/docs/QandA/reviveGhost.md +++ b/docs/QandA/reviveGhost.md @@ -1,4 +1,4 @@ -# About $reviveGhost _(New Project 3 Command)_ +# About $reviveGhost Unhides a ghost (question hidden with the deleteQuestion command) or restores a manually deleted question (zombie) to the channel. Instructor only. diff --git a/docs/QandA/spooky.md b/docs/QandA/spooky.md index ac24c0cae..3cf66c02e 100644 --- a/docs/QandA/spooky.md +++ b/docs/QandA/spooky.md @@ -1,4 +1,4 @@ -# About $spooky _(New Project 3 Command)_ +# About $spooky Counts the number of ghost (hidden) and zombie (deleted) questions in the channel. Just for fun, but may be useful. diff --git a/docs/QandA/unearthZombies.md b/docs/QandA/unearthZombies.md index 5ccaf5c0e..3de16308e 100644 --- a/docs/QandA/unearthZombies.md +++ b/docs/QandA/unearthZombies.md @@ -1,4 +1,4 @@ -# About $unearthZombies _(New Project 3 Command)_ +# About $unearthZombies Assigns ghost status to all manually deleted questions in case there is a need to restore them. Instructor only. diff --git a/docs/Reminders/add_homework.md b/docs/Reminders/add_homework.md deleted file mode 100644 index a7f0982f3..000000000 --- a/docs/Reminders/add_homework.md +++ /dev/null @@ -1,22 +0,0 @@ -# About $addhw -This command lets the user (either the TAs or professor) to add a homework as a reminder to the discord channel - -# Location of Code -The code that implements the above mentioned gits functionality is located [here](https://github.com/SE21-Team2/ClassMateBot/blob/main/cogs/deadline.py). - -# Code Description -## Functions -duedate(self, ctx, coursename: str, hwcount: str, *, date: str):
-This function takes as arguments the values provided by the constructor through self, context in which the command was called, name of the course, name of the homework, and the date and time when the homework is due. - -# How to run it? (Small Example) -Let's say that you are in the server that has the Classmate Bot active and online. All you have to do is -enter the command 'addhw' pass in all the parameters as a space seperated inputs in the following order: -coursename, homeworkname, duedate (in MMM DD YYYY optional(HH:MM) optional(timezone) format) -``` -$addhw CLASSNAME HW_NAME MMM DD YYYY optional(HH:MM) optional(timezone) -$addhw CSC510 HW2 SEP 25 2024 17:02 EST -``` -Successful execution of this command will add the reminder for the specified coursework and homework on the specified time. - -![$addhw CSC510 HW2 SEP 25 2024 17:02](https://github.com/SE21-Team2/ClassMateBot/blob/main/data/media/addhomework.gif) diff --git a/docs/Reminders/clearoverdue.md b/docs/Reminders/clearoverdue.md index d57e753f9..cb407052a 100644 --- a/docs/Reminders/clearoverdue.md +++ b/docs/Reminders/clearoverdue.md @@ -1,5 +1,4 @@ -# About $clearoverdue _(New Project 3 Command)_ - +# About $clearoverdue Clears overdue reminders from the database. ## Changes diff --git a/docs/Reminders/due_date.md b/docs/Reminders/due_date.md new file mode 100644 index 000000000..7d563ba6a --- /dev/null +++ b/docs/Reminders/due_date.md @@ -0,0 +1,24 @@ +# About $due_date +This command lets the user (either the TAs or professor) to add a reminder to the discord channel. + +New in project two, this command now provides the runner with a command that will allow them to add the due date to the google calendar programmatically. + +# Location of Code +The code that implements the above mentioned gits functionality is located [here](https://github.com/SE21-Team2/ClassMateBot/blob/main/cogs/deadline.py). + +# Code Description +## Functions +duedate(self, ctx, coursename: str, hwcount: str, *, date: str):
+This function takes as arguments the values provided by the constructor through self, context in which the command was called, name of the course, name of the deadline, and the date and time when the deadline is due. + +# How to run it? (Small Example) +Let's say that you are in the server that has the Classmate Bot active and online. All you have to do is +enter the command 'duedate' pass in all the parameters as a space seperated inputs in the following order: +coursename, deadline name, duedate (in MMM DD YYYY optional(HH:MM) optional(timezone) format) +``` +$addhw CLASSNAME NAME MMM DD YYYY optional(HH:MM) optional(timezone) +$addhw CSC510 HW2 SEP 25 2024 17:02 EST +``` +Successful execution of this command will add the reminder for the specified coursework on the specified time. + +![$duedate CSC510 HW2 SEP 25 2024 17:02](https://github.com/SE21-Team2/ClassMateBot/blob/main/data/media/addhomework.gif) diff --git a/docs/Reminders/due_this_week.md b/docs/Reminders/due_this_week.md index c015a681c..22b5cd7b2 100644 --- a/docs/Reminders/due_this_week.md +++ b/docs/Reminders/due_this_week.md @@ -1,4 +1,4 @@ -# About $duethisweek _(Project 3 Update)_ +# About $duethisweek This command lets the user display all the homeworks that are due this week for all the courses. # Location of Code diff --git a/docs/Reminders/due_today.md b/docs/Reminders/due_today.md index 9514d59c7..2d26ac40d 100644 --- a/docs/Reminders/due_today.md +++ b/docs/Reminders/due_today.md @@ -1,4 +1,4 @@ -# About $duetoday _(Project 3 Update)_ +# About $duetoday This command lets the user display all the homeworks that are due today for all the courses. # Location of Code diff --git a/docs/Reminders/overdue.md b/docs/Reminders/overdue.md index 96da43b54..1cee372c7 100644 --- a/docs/Reminders/overdue.md +++ b/docs/Reminders/overdue.md @@ -1,4 +1,4 @@ -# About $overdue _(New Project 3 Command)_ +# About $overdue This command lists all overdue reminders/assignments. diff --git a/docs/ReviewQs/addQuestion.md b/docs/ReviewQs/addQuestion.md index 3c08b9bc2..95f20b40b 100644 --- a/docs/ReviewQs/addQuestion.md +++ b/docs/ReviewQs/addQuestion.md @@ -1,4 +1,4 @@ -# About $addQuestion _(New Project 2 Command)_ +# About $addQuestion This command lets instructors add review questions to the system. # Location of Code diff --git a/docs/ReviewQs/getQuestion.md b/docs/ReviewQs/getQuestion.md index e059de030..513a7e774 100644 --- a/docs/ReviewQs/getQuestion.md +++ b/docs/ReviewQs/getQuestion.md @@ -1,4 +1,4 @@ -# About $getQuestion _(New Project 2 Command)_ +# About $getQuestion This command lets users get a random review question added by instructors. The answer to the question will be hidden as a spoiler. # Location of Code diff --git a/docs/Voting/projects.md b/docs/Voting/projects.md index 2416e28b2..5f144ea5f 100644 --- a/docs/Voting/projects.md +++ b/docs/Voting/projects.md @@ -1,4 +1,4 @@ -# About $projects _(Modified Command in Project 2)_ +# About $projects This command lets the student view the projects and the groups that have voted for each of them. # Location of Code The code that implements the above-mentioned gits functionality is located [here](https://github.com/SE21-Team2/ClassMateBot/blob/main/cogs/voting.py) diff --git a/docs/Voting/vote.md b/docs/Voting/vote.md index 7cf14f909..b43a84ee0 100644 --- a/docs/Voting/vote.md +++ b/docs/Voting/vote.md @@ -1,4 +1,4 @@ -# About $vote _(Modified Command in Project 2)_ +# About $vote This command lets the groups vote on projects that they want to be working on next. # Location of Code diff --git a/docs/generated_documentation/QandA_pydoc.md b/docs/generated_documentation/QandA_pydoc.md new file mode 100644 index 000000000..f33527ae7 --- /dev/null +++ b/docs/generated_documentation/QandA_pydoc.md @@ -0,0 +1,383 @@ +# Table of Contents + +* [qanda](#qanda) + * [Qanda](#qanda.Qanda) + * [askQuestion](#qanda.Qanda.askQuestion) + * [ask\_error](#qanda.Qanda.ask_error) + * [answer](#qanda.Qanda.answer) + * [answer\_error](#qanda.Qanda.answer_error) + * [deleteAllAnsFor](#qanda.Qanda.deleteAllAnsFor) + * [deleteAllAnsFor\_error](#qanda.Qanda.deleteAllAnsFor_error) + * [getAllAnsFor](#qanda.Qanda.getAllAnsFor) + * [getAllAnsFor\_error](#qanda.Qanda.getAllAnsFor_error) + * [archiveQA](#qanda.Qanda.archiveQA) + * [archiveqa\_error](#qanda.Qanda.archiveqa_error) + * [deleteAllQAs](#qanda.Qanda.deleteAllQAs) + * [deleteAllQAs\_error](#qanda.Qanda.deleteAllQAs_error) + * [deleteOneQuestion](#qanda.Qanda.deleteOneQuestion) + * [deleteOneQuestion\_error](#qanda.Qanda.deleteOneQuestion_error) + * [channelOneGhost](#qanda.Qanda.channelOneGhost) + * [channelOneGhost\_error](#qanda.Qanda.channelOneGhost_error) + * [channelGhostQs](#qanda.Qanda.channelGhostQs) + * [channelGhostQs\_error](#qanda.Qanda.channelGhostQs_error) + * [unearthZombieQs](#qanda.Qanda.unearthZombieQs) + * [unearthZombieQs\_error](#qanda.Qanda.unearthZombieQs_error) + * [restoreGhost](#qanda.Qanda.restoreGhost) + * [restoreGhost\_error](#qanda.Qanda.restoreGhost_error) + * [countGhosts](#qanda.Qanda.countGhosts) + * [countGhosts\_error](#qanda.Qanda.countGhosts_error) + * [setup](#qanda.setup) + + + +# qanda + + + +## Qanda Objects + +```python +class Qanda(commands.Cog) +``` + + + +#### askQuestion + +```python +@commands.command( + name="ask", + help= + "Ask question. Please put question text in quotes. Add *anonymous* or *anon* if desired." + 'EX: $ask /"When is the exam?/" anonymous', +) +async def askQuestion(ctx, qs: str, anonymous="") +``` + +Takes question from the user the reposts it anonymously and numbered + + + +#### ask\_error + +```python +@askQuestion.error +async def ask_error(ctx, error) +``` + +Error handling for ask command + + + +#### answer + +```python +@commands.command( + name="answer", + help= + "Answer question. Please put answer text in quotes. Add *anonymous* or *anon* if desired." + 'EX: $answer 1 /"Oct 12/" anonymous', +) +async def answer(ctx, num, ans: str, anonymous="") +``` + +Adds user to specific question and post anonymously + + + +#### answer\_error + +```python +@answer.error +async def answer_error(ctx, error) +``` + +Error handling for answer command + + + +#### deleteAllAnsFor + +```python +@commands.has_role("Instructor") +@commands.command( + name="DALLAF", + help="(PLACEHOLDER NAME) Delete all answers for a question.\n" + "EX: $DALLAF 1\n" + "THIS ACTION IS IRREVERSIBLE.\n" + "Before deletion, archive the question and its answers with\n" + "$getAnswersFor QUESTION_NUMBER", +) +async def deleteAllAnsFor(ctx, num) +``` + +Lets instructor delete all answers for a question + + + +#### deleteAllAnsFor\_error + +```python +@deleteAllAnsFor.error +async def deleteAllAnsFor_error(ctx, error) +``` + +Error handling for deleteAllAnswersFor command + + + +#### getAllAnsFor + +```python +@commands.command( + name="getAnswersFor", + help="Get a question and all its answers\n" + "EX: $getAnswersFor 1", +) +async def getAllAnsFor(ctx, num) +``` + +Gets all answers for a question and DMs them to the user + + + +#### getAllAnsFor\_error + +```python +@getAllAnsFor.error +async def getAllAnsFor_error(ctx, error) +``` + +Error handling for getAllAnswersFor command + + + +#### archiveQA + +```python +@commands.command( + name="archiveQA", + help="(PLACEHOLDER NAME) DM all questions and their answers\n" + "EX: $archiveQA", +) +async def archiveQA(ctx) +``` + +DM all questions and their answers to the user + + + +#### archiveqa\_error + +```python +@archiveQA.error +async def archiveqa_error(ctx, error) +``` + +Error handling for archiveQA command + + + +#### deleteAllQAs + +```python +@commands.has_role("Instructor") +@commands.command( + name="deleteAllQA", + help="Delete all questions and answers from the database and channel.\n" + "EX: $deleteAllQA\n" + "THIS COMMAND IS IRREVERSIBLE.\n" + "BE SURE TO ARCHIVE ALL QUESTIONS BEFORE DELETION.\n" + "To archive, use the $unearthZombies command followed by $allChannelGhosts," + " and then use $archiveQA.", +) +async def deleteAllQAs(ctx) +``` + +Deletes all quetsions and answers from the database and channel + + + +#### deleteAllQAs\_error + +```python +@deleteAllQAs.error +async def deleteAllQAs_error(ctx, error) +``` + +Error handling for deleteAllQA command + + + +#### deleteOneQuestion + +```python +@commands.has_role("Instructor") +@commands.command( + name="deleteQuestion", + help="Delete (hide) one question but leave answers untouched." + " Leaves database ghosts.\n" + "EX: $deleteQuestion QUESTION_NUMBER\n", +) +async def deleteOneQuestion(ctx, num) +``` + +Lets the instructor delete one question, but leave the answers untouched + + + +#### deleteOneQuestion\_error + +```python +@deleteOneQuestion.error +async def deleteOneQuestion_error(ctx, error) +``` + +Error handling for deleteQuestion command + + + +#### channelOneGhost + +```python +@commands.has_role("Instructor") +@commands.command( + name="channelGhost", + help= + "Gets a specific ghost (question deleted with command) and all its answers.\n" + "EX: $channelGhost 1", +) +async def channelOneGhost(ctx, num) +``` + +Lets the instructor get a specific ghost question + + + +#### channelOneGhost\_error + +```python +@channelOneGhost.error +async def channelOneGhost_error(ctx, error) +``` + +Error handling for channelGhost command + + + +#### channelGhostQs + +```python +@commands.has_role("Instructor") +@commands.command( + name="allChannelGhosts", + help="Get all the questions that are in the database but " + "not in the channel. Does not detect zombies.\n" + "EX: $allChannelGhosts\n" + "To detect zombies and convert them to ghosts, use $unearthZombies", +) +async def channelGhostQs(ctx) +``` + +Lets the instructor get the questions that are in the database but not inthe channel + + + +#### channelGhostQs\_error + +```python +@channelGhostQs.error +async def channelGhostQs_error(ctx, error) +``` + +Error handling for allChannelGhosts command + + + +#### unearthZombieQs + +```python +@commands.has_role("Instructor") +@commands.command( + name="unearthZombies", + help="Assign ghost status to all manually deleted questions " + "in case there is a need to restore them.\n" + "EX: $unearthZombies\n", +) +async def unearthZombieQs(ctx) +``` + +Assigns ghost status to all manually deleted questions in case there is a need to restore them + + + +#### unearthZombieQs\_error + +```python +@unearthZombieQs.error +async def unearthZombieQs_error(ctx, error) +``` + +Error handling for unearthZombies command + + + +#### restoreGhost + +```python +@commands.has_role("Instructor") +@commands.command( + name="reviveGhost", + help="Restores a ghost or deleted/hidden question to the channel.\n" + "EX: $reviveGhost 1", +) +async def restoreGhost(ctx, num) +``` + +Restores a ghost of deleted question to the channel + + + +#### restoreGhost\_error + +```python +@restoreGhost.error +async def restoreGhost_error(ctx, error) +``` + +Error handling for reviveGhost command + + + +#### countGhosts + +```python +@commands.command(name="spooky", + help="Is this channel haunted?\n" + "EX: $spooky") +async def countGhosts(ctx) +``` + +Counts the number of ghost and zombie questions in the channel. Mainly for fun but could be useful + + + +#### countGhosts\_error + +```python +@countGhosts.error +async def countGhosts_error(ctx, error) +``` + +Error handling for spooky command + + + +#### setup + +```python +async def setup(bot) +``` + +Adds the file to the bot's cog system + diff --git a/docs/generated_documentation/assignments_pydoc.md b/docs/generated_documentation/assignments_pydoc.md new file mode 100644 index 000000000..d43c43516 --- /dev/null +++ b/docs/generated_documentation/assignments_pydoc.md @@ -0,0 +1,116 @@ +# Table of Contents + +* [assignments](#assignments) + * [Assignments](#assignments.Assignments) + * [add\_assignment](#assignments.Assignments.add_assignment) + * [edit\_assignment](#assignments.Assignments.edit_assignment) + * [delete\_assignment](#assignments.Assignments.delete_assignment) + * [add\_assignment\_error](#assignments.Assignments.add_assignment_error) + * [edit\_assignment\_error](#assignments.Assignments.edit_assignment_error) + * [delete\_assignment\_error](#assignments.Assignments.delete_assignment_error) + * [setup](#assignments.setup) + + + +# assignments + + + +## Assignments Objects + +```python +class Assignments(commands.Cog) +``` + + + +#### add\_assignment + +```python +@commands.has_role("Instructor") +@commands.command( + name="addassignment", + help= + "add a grading assignment and points $addassignment NAME CATEGORY POINTS", +) +async def add_assignment(ctx, assignmentname: str, categoryname: str, + points: str) +``` + +Add a grading assignment and points + + + +#### edit\_assignment + +```python +@commands.has_role("Instructor") +@commands.command( + name="editassignment", + help= + "edit a grading assignment and points $editassignment NAME CATEGORY POINTS", +) +async def edit_assignment(ctx, assignmentname: str, categoryname: str, + points: str) +``` + +edit a grading assignment and points $editassignment NAME CATEGORY POINTS + + + +#### delete\_assignment + +```python +@commands.has_role("Instructor") +@commands.command( + name="deleteassignment", + help="delete a grading assignment $deleteassignment NAME", +) +async def delete_assignment(ctx, assignmentname: str) +``` + +delete a grading assignment $deleteassignment NAME + + + +#### add\_assignment\_error + +```python +@add_assignment.error +async def add_assignment_error(ctx, error) +``` + +Error handling of addassignment function + + + +#### edit\_assignment\_error + +```python +@edit_assignment.error +async def edit_assignment_error(ctx, error) +``` + +Error handling of editassignment function + + + +#### delete\_assignment\_error + +```python +@delete_assignment.error +async def delete_assignment_error(ctx, error) +``` + +Error handling of deleteassignment function + + + +#### setup + +```python +async def setup(bot) +``` + +Adds the file to the bot's cog system + diff --git a/docs/generated_documentation/calendar_pydoc.md b/docs/generated_documentation/calendar_pydoc.md new file mode 100644 index 000000000..2c27f144c --- /dev/null +++ b/docs/generated_documentation/calendar_pydoc.md @@ -0,0 +1,144 @@ +# Table of Contents + +* [calendar](#calendar) + * [Calendar](#calendar.Calendar) + * [credsSetUp](#calendar.Calendar.credsSetUp) + * [addCalendarEvent](#calendar.Calendar.addCalendarEvent) + * [clearCalendar](#calendar.Calendar.clearCalendar) + * [getiCalDownload](#calendar.Calendar.getiCalDownload) + * [getPdfDownload](#calendar.Calendar.getPdfDownload) + * [checkForEvents](#calendar.Calendar.checkForEvents) + * [subscribeCalendar](#calendar.Calendar.subscribeCalendar) + * [removeCalendar](#calendar.Calendar.removeCalendar) + * [setup](#calendar.setup) + + + +# calendar + + + +## Calendar Objects + +```python +class Calendar(commands.Cog) +``` + + + +#### credsSetUp + +```python +def credsSetUp() +``` + +Set up Google Calendar with authentication + + + +#### addCalendarEvent + +```python +@commands.command( + name="addCalendarEvent", + help="Add an event to the course calendar using the format" + ": $addCalendarEvent NAME DESCRIPTION DATE/TIME", +) +async def addCalendarEvent(ctx, name, description, eventTime) +``` + +Adds specified event to shared Google Calendar + + + +#### clearCalendar + +```python +@commands.command(name="clearCalendar", help="Clear all events from calendar") +async def clearCalendar(ctx) +``` + +Clears all events from shared Google Calendar + + + +#### getiCalDownload + +```python +@commands.command( + name="getiCalDownload", + help="Enter the command to receive an ics" + " file of the calendar$getiCalDownload", +) +async def getiCalDownload(ctx) +``` + +Generates an ICAL file of the Google Calendar + + + +#### getPdfDownload + +```python +@commands.command( + name="getPdfDownload", + help="Enter the command to receive an ics" + " file of the calendar$getiCalDownload", +) +async def getPdfDownload(ctx) +``` + +Sends a pdf file of the class calendar to the Discord Channel + + + +#### checkForEvents + +```python +@tasks.loop(hours=24) +async def checkForEvents() +``` + +Checks calendar daily for the events due that day + + + +#### subscribeCalendar + +```python +@commands.command( + name="subscribeCalendar", + help= + "Adds user to shared Google Calendar. Ex: subscribeCalendar john.doe@gmail.com", +) +async def subscribeCalendar(ctx, userEmail) +``` + +Adds user to shared Google Calendar + + + +#### removeCalendar + +```python +@commands.has_role("Instructor") +@commands.command( + name="removeCalendar", + help= + "Removes user from shared Google Calendar. Ex: removeCalendar john.doe@gmail.com", +) +async def removeCalendar(ctx, userEmail) +``` + +Removes user from shared Google Calendar + + + +#### setup + +```python +async def setup(bot) +``` + +Adds the file to the bot's cog system + diff --git a/docs/generated_documentation/deadline_pydoc.md b/docs/generated_documentation/deadline_pydoc.md new file mode 100644 index 000000000..334224053 --- /dev/null +++ b/docs/generated_documentation/deadline_pydoc.md @@ -0,0 +1,373 @@ +# Table of Contents + +* [deadline](#deadline) + * [Deadline](#deadline.Deadline) + * [timenow](#deadline.Deadline.timenow) + * [timenow\_error](#deadline.Deadline.timenow_error) + * [duedate](#deadline.Deadline.duedate) + * [duedate\_error](#deadline.Deadline.duedate_error) + * [deleteReminder](#deadline.Deadline.deleteReminder) + * [deleteReminder\_error](#deadline.Deadline.deleteReminder_error) + * [changeduedate](#deadline.Deadline.changeduedate) + * [changeduedate\_error](#deadline.Deadline.changeduedate_error) + * [duethisweek](#deadline.Deadline.duethisweek) + * [duethisweek\_error](#deadline.Deadline.duethisweek_error) + * [duetoday](#deadline.Deadline.duetoday) + * [duetoday\_error](#deadline.Deadline.duetoday_error) + * [coursedue](#deadline.Deadline.coursedue) + * [coursedue\_error](#deadline.Deadline.coursedue_error) + * [listreminders](#deadline.Deadline.listreminders) + * [listreminders\_error](#deadline.Deadline.listreminders_error) + * [overdue](#deadline.Deadline.overdue) + * [overdue\_error](#deadline.Deadline.overdue_error) + * [clearallreminders](#deadline.Deadline.clearallreminders) + * [clearallreminders\_error](#deadline.Deadline.clearallreminders_error) + * [clearoverdue](#deadline.Deadline.clearoverdue) + * [clearoverdue\_error](#deadline.Deadline.clearoverdue_error) + * [send\_reminders\_day](#deadline.Deadline.send_reminders_day) + * [before](#deadline.Deadline.before) + * [send\_reminders\_hour](#deadline.Deadline.send_reminders_hour) + * [setup](#deadline.setup) + + + +# deadline + + + +## Deadline Objects + +```python +class Deadline(commands.Cog) +``` + + + +#### timenow + +```python +@commands.command( + name="timenow", + help="put in current time to get offset needed for proper " + "datetime notifications $timenow MMM DD YYYY HH:MM ex. $timenow SEP 25 2024 17:02", +) +async def timenow(ctx, *, date: str) +``` + +Gets offset for proper datetime notifications compared to UTC + + + +#### timenow\_error + +```python +@timenow.error +async def timenow_error(ctx, error) +``` + +Error handling for timenow command + + + +#### duedate + +```python +@commands.has_role("Instructor") +@commands.command( + name="duedate", + help= + "add reminder and due-date $duedate CLASSNAME NAME MMM DD YYYY optional(HH:MM) optional(TIMEZONE)" + "ex. $duedate CSC510 HW2 SEP 25 2024 17:02 EST", +) +async def duedate(ctx, coursename: str, hwcount: str, *, date: str) +``` + +Add reminder for specified course, assignment, and date + + + +#### duedate\_error + +```python +@duedate.error +async def duedate_error(ctx, error) +``` + +Error handling for duedate command + + + +#### deleteReminder + +```python +@commands.has_role("Instructor") +@commands.command( + name="deletereminder", + pass_context=True, + help="delete a specific reminder using course name and reminder name using " + "$deletereminder CLASSNAME HW_NAME ex. $deletereminder CSC510 HW2 ", +) +async def deleteReminder(ctx, courseName: str, hwName: str) +``` + +Deletes a specified reminder + + + +#### deleteReminder\_error + +```python +@deleteReminder.error +async def deleteReminder_error(ctx, error) +``` + +Error handling for deleteReminder + + + +#### changeduedate + +```python +@commands.has_role("Instructor") +@commands.command( + name="changeduedate", + pass_context=True, + help= + "update the assignment date. $changeduedate CLASSNAME HW_NAME MMM DD YYYY optional(HH:MM) optional(TIMEZONE)" + "ex. $changeduedate CSC510 HW2 SEP 25 2024 17:02 EST", +) +async def changeduedate(ctx, classid: str, hwid: str, *, date: str) +``` + +Updates an assignment's due date in the database + + + +#### changeduedate\_error + +```python +@changeduedate.error +async def changeduedate_error(ctx, error) +``` + +Error handling for changeduedate command + + + +#### duethisweek + +```python +@commands.command( + name="duethisweek", + pass_context=True, + help="check all the homeworks that are due this week $duethisweek", +) +async def duethisweek(ctx) +``` + +Checks all homeworks or assignments due this week + + + +#### duethisweek\_error + +```python +@duethisweek.error +async def duethisweek_error(ctx, error) +``` + +Error handling for duethisweek command + + + +#### duetoday + +```python +@commands.command( + name="duetoday", + pass_context=True, + help="check all the reminders that are due today $duetoday", +) +async def duetoday(ctx) +``` + +Checks for all reminders that are due today + + + +#### duetoday\_error + +```python +@duetoday.error +async def duetoday_error(ctx, error) +``` + +Error handling for duetoday command + + + +#### coursedue + +```python +@commands.command( + name="coursedue", + pass_context=True, + help= + "check all the reminders that are due for a specific course $coursedue coursename " + "ex. $coursedue CSC505", +) +async def coursedue(ctx, courseid: str) +``` + +Displays a list of all reminders due for a specific course + + + +#### coursedue\_error + +```python +@coursedue.error +async def coursedue_error(ctx, error) +``` + +Error handling for coursedue command + + + +#### listreminders + +```python +@commands.command(name="listreminders", + pass_context=True, + help="lists all reminders") +async def listreminders(ctx) +``` + +Displays user with list of all reminders + + + +#### listreminders\_error + +```python +@listreminders.error +async def listreminders_error(ctx, error) +``` + +Error handling for listreminders command + + + +#### overdue + +```python +@commands.command(name="overdue", + pass_context=True, + help="lists overdue reminders") +async def overdue(ctx) +``` + +Displays list of homeworks and assignments that are overdue + + + +#### overdue\_error + +```python +@overdue.error +async def overdue_error(ctx, error) +``` + +Error handling for overdue command + + + +#### clearallreminders + +```python +@commands.command(name="clearreminders", + pass_context=True, + help="deletes all reminders") +async def clearallreminders(ctx) +``` + +Clears all reminders from database + + + +#### clearallreminders\_error + +```python +@clearallreminders.error +async def clearallreminders_error(ctx, error) +``` + +Error handling for clearreminders command + + + +#### clearoverdue + +```python +@commands.command(name="clearoverdue", + pass_context=True, + help="deletes overdue reminders") +async def clearoverdue(ctx) +``` + +Clears all overdue reminders from database + + + +#### clearoverdue\_error + +```python +@clearoverdue.error +async def clearoverdue_error(ctx, error) +``` + +Error handling for clearoverdue + + + +#### send\_reminders\_day + +```python +@tasks.loop(hours=24) +async def send_reminders_day() +``` + +Task running once per day to send a reminder for assignments due + + + +#### before + +```python +@send_reminders_day.before_loop +async def before() +``` + +Task that runs once per day and waits until 8am EST to send reminders via send_reminders_day function + + + +#### send\_reminders\_hour + +```python +@tasks.loop(hours=1) +async def send_reminders_hour() +``` + +Task that runs once per hour ans sends a reminder for assignments due + + + +#### setup + +```python +async def setup(bot) +``` + +Adds the file to the bot's cog system + diff --git a/docs/generated_documentation/grades_pydoc.md b/docs/generated_documentation/grades_pydoc.md new file mode 100644 index 000000000..1aeaa9533 --- /dev/null +++ b/docs/generated_documentation/grades_pydoc.md @@ -0,0 +1,261 @@ +# Table of Contents + +* [grades](#grades) + * [Grades](#grades.Grades) + * [grade](#grades.Grades.grade) + * [gradebycategory](#grades.Grades.gradebycategory) + * [gradeforclass](#grades.Grades.gradeforclass) + * [graderequired](#grades.Grades.graderequired) + * [graderequiredforclass](#grades.Grades.graderequiredforclass) + * [categories](#grades.Grades.categories) + * [input\_grades](#grades.Grades.input_grades) + * [input\_grades\_error](#grades.Grades.input_grades_error) + * [add\_grade\_category](#grades.Grades.add_grade_category) + * [add\_grade\_category\_error](#grades.Grades.add_grade_category_error) + * [edit\_grade\_category](#grades.Grades.edit_grade_category) + * [edit\_grade\_category\_error](#grades.Grades.edit_grade_category_error) + * [delete\_grade\_category](#grades.Grades.delete_grade_category) + * [delete\_grade\_category\_error](#grades.Grades.delete_grade_category_error) + * [grade\_report\_category](#grades.Grades.grade_report_category) + * [grade\_report\_assignment](#grades.Grades.grade_report_assignment) + * [setup](#grades.setup) + + + +# grades + + + +## Grades Objects + +```python +class Grades(commands.Cog) +``` + + + +#### grade + +```python +@commands.command( + name="grade", + help="get your grade for a specific assignment $grade ASSIGNMENT") +async def grade(ctx, assignmentName: str) +``` + +Lets a student get their grade for a certain assignment + + + +#### gradebycategory + +```python +@commands.command( + name="gradebycategory", + help="get your grade for a specific category $gradebycategory CATEGORY", +) +async def gradebycategory(ctx, categoryName: str) +``` + +Lets a student get their grade for a specific grade category + + + +#### gradeforclass + +```python +@commands.command( + name="gradeforclass", + help="get your grade for the whole class $gradeforclass", +) +async def gradeforclass(ctx) +``` + +Lets a student get their overall average grade for the class + + + +#### graderequired + +```python +@commands.command( + name="graderequired", + help= + "get your grade required on the next assignment for a category and a desired grade $graderequired CATEGORY POINTS GRADE", +) +async def graderequired(ctx, categoryName: str, pointValue: str, + desiredGrade: str) +``` + +Lets a student calculate the grade they need for a desired grade in a category + + + +#### graderequiredforclass + +```python +@commands.command( + name="graderequiredforclass", + help= + "get your grade required on the next assignment to keep a desired grade $graderequiredforclass CATEGORY POINTS GRADE", +) +async def graderequiredforclass(ctx, categoryName: str, pointValue: str, + desiredGrade: str) +``` + +Lets a student calculate the grade required on the next assignment to keep an overall desired class grade + + + +#### categories + +```python +@commands.command(name="categories", + help="display all grading categories and weights $categories" + ) +async def categories(ctx) +``` + +Lets the user list the categories of grades that are in the database + + + +#### input\_grades + +```python +@commands.has_role("Instructor") +@commands.command(name="inputgrades", help="Insert grades using a csv file") +async def input_grades(ctx, assignmentname: str) +``` + +Lets the instructor input grades into the system for a given assignment + + + +#### input\_grades\_error + +```python +@input_grades.error +async def input_grades_error(ctx, error) +``` + +Error handling for inputgrades command + + + +#### add\_grade\_category + +```python +@commands.has_role("Instructor") +@commands.command( + name="addgradecategory", + help="add a grading category and weight $addgradecategory NAME WEIGHT", +) +async def add_grade_category(ctx, categoryname: str, weight: str) +``` + +Lets the instructor add a grade category with a specified weight + + + +#### add\_grade\_category\_error + +```python +@add_grade_category.error +async def add_grade_category_error(ctx, error) +``` + +Error handling for add_grade_category command + + + +#### edit\_grade\_category + +```python +@commands.has_role("Instructor") +@commands.command( + name="editgradecategory", + help="edit a grading category and weight $editgradecategory NAME WEIGHT", +) +async def edit_grade_category(ctx, categoryname: str, weight: str) +``` + +Lets the instructor edit a grade category and weight + + + +#### edit\_grade\_category\_error + +```python +@edit_grade_category.error +async def edit_grade_category_error(ctx, error) +``` + +Error handling for edit_grade_category command + + + +#### delete\_grade\_category + +```python +@commands.has_role("Instructor") +@commands.command( + name="deletegradecategory", + help="delete a grading category $deletegradecategory NAME", +) +async def delete_grade_category(ctx, categoryname: str) +``` + +Lets the user delete a grade category from the database + + + +#### delete\_grade\_category\_error + +```python +@delete_grade_category.error +async def delete_grade_category_error(ctx, error) +``` + +Error handling for delete_grade_category command + + + +#### grade\_report\_category + +```python +@commands.has_role("Instructor") +@commands.command( + name="gradereportcategory", + help="Report on the classes scores all grade categories", +) +async def grade_report_category(ctx) +``` + +Lets the instructor generate a report on the average, low, and high score for each category + + + +#### grade\_report\_assignment + +```python +@commands.has_role("Instructor") +@commands.command( + name="gradereportassignment", + help="Report on the classes scores all assignments", +) +async def grade_report_assignment(ctx) +``` + +Lets the instructor generate a report on the average, low, and high score for each assignment + + + +#### setup + +```python +async def setup(bot) +``` + +Adds the file to the bot's cog system + diff --git a/docs/generated_documentation/groups_pydoc.md b/docs/generated_documentation/groups_pydoc.md new file mode 100644 index 000000000..27c5a8df3 --- /dev/null +++ b/docs/generated_documentation/groups_pydoc.md @@ -0,0 +1,213 @@ +# Table of Contents + +* [groups](#groups) + * [Groups](#groups.Groups) + * [reset](#groups.Groups.reset) + * [reset\_error](#groups.Groups.reset_error) + * [startupgroups](#groups.Groups.startupgroups) + * [startupgroups\_error](#groups.Groups.startupgroups_error) + * [connect](#groups.Groups.connect) + * [connect\_error](#groups.Groups.connect_error) + * [join](#groups.Groups.join) + * [join\_error](#groups.Groups.join_error) + * [leave](#groups.Groups.leave) + * [leave\_error](#groups.Groups.leave_error) + * [groups](#groups.Groups.groups) + * [groups\_error](#groups.Groups.groups_error) + * [group](#groups.Groups.group) + * [group\_error](#groups.Groups.group_error) + * [setup](#groups.setup) + + + +# groups + + + +## Groups Objects + +```python +class Groups(commands.Cog) +``` + + + +#### reset + +```python +@commands.command( + name="reset", + help="Resets group channels and roles. DO NOT USE IN PRODUCTION!") +async def reset(ctx) +``` + +Deletes all group roles in the server + + + +#### reset\_error + +```python +@reset.error +async def reset_error(ctx, error) +``` + +Error handling for reset command + + + +#### startupgroups + +```python +@commands.command(name="startupgroups", help="Creates group roles for members") +async def startupgroups(ctx) +``` + +Creates roles for the groups + + + +#### startupgroups\_error + +```python +@startupgroups.error +async def startupgroups_error(ctx, error) +``` + +Error handling for startupgroups command + + + +#### connect + +```python +@commands.command(name="connect", help="Creates group roles for members") +async def connect(ctx) +``` + +Connects all users with their groups + + + +#### connect\_error + +```python +@connect.error +async def connect_error(ctx, error) +``` + +Error handling for connect command + + + +#### join + +```python +@commands.command( + name="join", + help="To use the join command, do: $join \n \ + ( For example: $join 0 )", + pass_context=True, +) +async def join(ctx, group_num: int) +``` + +Joins the user to given group + + + +#### join\_error + +```python +@join.error +async def join_error(ctx, error) +``` + +Error handling for join command + + + +#### leave + +```python +@commands.command( + name="leave", + help="To use the leave command, do: $leave \n \ + ( For example: $leave )", + pass_context=True, +) +async def leave(ctx) +``` + +Removes the user from the given group + + + +#### leave\_error + +```python +@leave.error +async def leave_error(ctx, error) +``` + +Error handling for leave command + + + +#### groups + +```python +@commands.command(name="groups", help="prints group counts", pass_context=True) +async def groups(ctx) +``` + +Prints the list of groups + + + +#### groups\_error + +```python +@groups.error +async def groups_error(ctx, error) +``` + +Error handling for groups command + + + +#### group + +```python +@commands.command( + name="group", + help="print names of members in a group, or current groups members \n \ + ( For example: $group or $group 8 )", + pass_context=True, +) +async def group(ctx, group_num: int = -1) +``` + +Prints the members of the group, or the current member's group if they have joined one + + + +#### group\_error + +```python +@group.error +async def group_error(ctx, error) +``` + +Error handling for group command + + + +#### setup + +```python +async def setup(bot) +``` + +Adds the file to the bot's cog system + diff --git a/docs/generated_documentation/newComer_pydoc.md b/docs/generated_documentation/newComer_pydoc.md new file mode 100644 index 000000000..576ecdf98 --- /dev/null +++ b/docs/generated_documentation/newComer_pydoc.md @@ -0,0 +1,58 @@ +# Table of Contents + +* [newComer](#newComer) + * [NewComer](#newComer.NewComer) + * [verify](#newComer.NewComer.verify) + * [verify\_error](#newComer.NewComer.verify_error) + * [setup](#newComer.setup) + + + +# newComer + + + +## NewComer Objects + +```python +class NewComer(commands.Cog) +``` + + + +#### verify + +```python +@commands.command( + name="verify", + pass_context=True, + help= + "User self-verifies by attaching their real name to their discord username in this server: " + "$verify ", +) +async def verify(ctx, *, name: str = None) +``` + +Gives the user the `verified` role in the server + + + +#### verify\_error + +```python +@verify.error +async def verify_error(ctx, error) +``` + +Error handling for verify command + + + +#### setup + +```python +async def setup(bot) +``` + +Adds the file to the bot's cog system + diff --git a/docs/generated_documentation/ping_pydoc.md b/docs/generated_documentation/ping_pydoc.md new file mode 100644 index 000000000..3a09f948f --- /dev/null +++ b/docs/generated_documentation/ping_pydoc.md @@ -0,0 +1,40 @@ +# Table of Contents + +* [ping](#ping) + * [Helpful](#ping.Helpful) + * [ping](#ping.Helpful.ping) + * [setup](#ping.setup) + + + +# ping + + + +## Helpful Objects + +```python +class Helpful(commands.Cog) +``` + + + +#### ping + +```python +@commands.command() +async def ping(ctx) +``` + +Prints the current ping of the bot, used as a test function + + + +#### setup + +```python +async def setup(bot) +``` + +Adds the file to the bot's cog system + diff --git a/docs/generated_documentation/pinning_pydoc.md b/docs/generated_documentation/pinning_pydoc.md new file mode 100644 index 000000000..e6a42b2b1 --- /dev/null +++ b/docs/generated_documentation/pinning_pydoc.md @@ -0,0 +1,147 @@ +# Table of Contents + +* [pinning](#pinning) + * [Pinning](#pinning.Pinning) + * [helpful3](#pinning.Pinning.helpful3) + * [addMessage](#pinning.Pinning.addMessage) + * [addMessage\_error](#pinning.Pinning.addMessage_error) + * [deleteMessage](#pinning.Pinning.deleteMessage) + * [deleteMessage\_error](#pinning.Pinning.deleteMessage_error) + * [retrieveMessages](#pinning.Pinning.retrieveMessages) + * [retrieveMessages\_error](#pinning.Pinning.retrieveMessages_error) + * [updatePinnedMessage](#pinning.Pinning.updatePinnedMessage) + * [updatePinnedMessage\_error](#pinning.Pinning.updatePinnedMessage_error) + * [setup](#pinning.setup) + + + +# pinning + + + +## Pinning Objects + +```python +class Pinning(commands.Cog) +``` + + + +#### helpful3 + +```python +@commands.command() +async def helpful3(ctx) +``` + +Test command to chheck if the bot it working + + + +#### addMessage + +```python +@commands.command( + name="pin", + help="Pin a message by adding a tagname (single word) " + "and a description(can be multi word). EX: $pin Homework Resources for HW2", +) +async def addMessage(ctx, tagname: str, *, description: str) +``` + +Used to pin a message by the user + + + +#### addMessage\_error + +```python +@addMessage.error +async def addMessage_error(ctx, error) +``` + +Error handling for pin(addMessage) command + + + +#### deleteMessage + +```python +@commands.command(name="unpin", help="Unpin a message by passing the tagname.") +async def deleteMessage(ctx, tagname: str) +``` + +Unpins the pinned messages with provided tagname + + + +#### deleteMessage\_error + +```python +@deleteMessage.error +async def deleteMessage_error(ctx, error) +``` + +Error handling for unpin(deleteMessage) command + + + +#### retrieveMessages + +```python +@commands.command( + name="pinnedmessages", + help="Retrieve the pinned messages by a particular tag or all messages.", +) +async def retrieveMessages(ctx, tagname: str = "") +``` + +Retrieves all pinned messages under a given tagname by either everyone or a particular user + + + +#### retrieveMessages\_error + +```python +@retrieveMessages.error +async def retrieveMessages_error(ctx, error) +``` + +Error handling for retrievemessages function + + + +#### updatePinnedMessage + +```python +@commands.command( + name="updatepin", + help="Update a previously pinned message by passing the " + "tagname and old description in the same order", +) +async def updatePinnedMessage(ctx, tagname: str, *, description: str) +``` + +Updates a pinned message with a given tagname, deletes old messages for the tag + + + +#### updatePinnedMessage\_error + +```python +@updatePinnedMessage.error +async def updatePinnedMessage_error(ctx, error) +``` + +Error handling for updatepinnedmessage function + + + +#### setup + +```python +async def setup(bot) +``` + +Adds the file to the bot's cog system + diff --git a/docs/generated_documentation/polling_pydoc.md b/docs/generated_documentation/polling_pydoc.md new file mode 100644 index 000000000..50e68665c --- /dev/null +++ b/docs/generated_documentation/polling_pydoc.md @@ -0,0 +1,86 @@ +# Table of Contents + +* [polling](#polling) + * [Poll](#polling.Poll) + * [quizpoll](#polling.Poll.quizpoll) + * [quizpoll\_error](#polling.Poll.quizpoll_error) + * [poll](#polling.Poll.poll) + * [poll\_error](#polling.Poll.poll_error) + * [setup](#polling.setup) + + + +# polling + + + +## Poll Objects + +```python +class Poll(commands.Cog) +``` + + + +#### quizpoll + +```python +@commands.command( + name="quizpoll", + help= + 'Create a multi reaction poll by typing \n$poll "TITLE" [option 1] ... [option 6]\n ' + "Be sure to enclose title with quotes and options with brackets!\n" + 'EX: $quizpoll "I am a poll" [Vote for me!] [I am option 2]', +) +async def quizpoll(ctx, title: str, *, ops) +``` + +Allows the user to begin quiz polls; that is, multi-reaction polls with listed questions + + + +#### quizpoll\_error + +```python +@quizpoll.error +async def quizpoll_error(ctx, error) +``` + +Error handling for quizpoll command + + + +#### poll + +```python +@commands.command( + name="poll", + help="Create a reaction poll by typing $poll QUESTION\n" + "EX: $poll What do you think about cats?", +) +async def poll(ctx, *, qs="") +``` + +Allows the user to create a simple reaction poll with thumbs up, thumbs down, and unsure + + + +#### poll\_error + +```python +@poll.error +async def poll_error(ctx, error) +``` + +Error handling for poll command + + + +#### setup + +```python +async def setup(bot) +``` + +Adds the file to the bot's cog system + diff --git a/docs/generated_documentation/reviewQs_pydoc.md b/docs/generated_documentation/reviewQs_pydoc.md new file mode 100644 index 000000000..fe0bd4232 --- /dev/null +++ b/docs/generated_documentation/reviewQs_pydoc.md @@ -0,0 +1,82 @@ +# Table of Contents + +* [reviewQs](#reviewQs) + * [ReviewQs](#reviewQs.ReviewQs) + * [getQuestion](#reviewQs.ReviewQs.getQuestion) + * [get\_question\_error](#reviewQs.ReviewQs.get_question_error) + * [addQuestion](#reviewQs.ReviewQs.addQuestion) + * [add\_question\_error](#reviewQs.ReviewQs.add_question_error) + * [setup](#reviewQs.setup) + + + +# reviewQs + + + +## ReviewQs Objects + +```python +class ReviewQs(commands.Cog) +``` + + + +#### getQuestion + +```python +@commands.command(name="getQuestion", + help="Get a review question. EX: $getQuestion") +async def getQuestion(ctx) +``` + +Prints a random question from the database + + + +#### get\_question\_error + +```python +@getQuestion.error +async def get_question_error(ctx, error) +``` + +Error handling for getQuestion command + + + +#### addQuestion + +```python +@commands.has_role("Instructor") +@commands.command( + name="addQuestion", + help="Add a review question. " + 'EX: $addQuestion "What class is this?" "Software Engineering"', +) +async def addQuestion(ctx, qs: str, ans: str) +``` + +Allows instructors to add review questions + + + +#### add\_question\_error + +```python +@addQuestion.error +async def add_question_error(ctx, error) +``` + +Error handling for addQuestion command + + + +#### setup + +```python +async def setup(bot) +``` + +Adds the file to the bot's cog system + diff --git a/docs/generated_documentation/voting_pydoc.md b/docs/generated_documentation/voting_pydoc.md new file mode 100644 index 000000000..32158e5fb --- /dev/null +++ b/docs/generated_documentation/voting_pydoc.md @@ -0,0 +1,86 @@ +# Table of Contents + +* [voting](#voting) + * [Voting](#voting.Voting) + * [vote](#voting.Voting.vote) + * [vote\_error](#voting.Voting.vote_error) + * [projects](#voting.Voting.projects) + * [project\_error](#voting.Voting.project_error) + * [setup](#voting.setup) + + + +# voting + + + +## Voting Objects + +```python +class Voting(commands.Cog) +``` + + + +#### vote + +```python +@commands.command( + name="vote", + help="Used for voting for Projects, \ + To use the vote command, do: $vote \n \ + (For example: $vote 0)", + pass_context=True, +) +async def vote(ctx, project_num: int) +``` + +Used for voting for projects. "Votes" for the given project by adding the user's group to it + + + +#### vote\_error + +```python +@vote.error +async def vote_error(ctx, error) +``` + +Error handling for vote command + + + +#### projects + +```python +@commands.command( + name="projects", + help="print projects with groups assigned to them", + pass_context=True, +) +async def projects(ctx) +``` + +Prints the list of current projects + + + +#### project\_error + +```python +@projects.error +async def project_error(ctx, error) +``` + +Error handling for projects command + + + +#### setup + +```python +async def setup(bot) +``` + +Adds the file to the bot's cog system + diff --git a/docs/generated_documentation/wordfilter_pydoc.md b/docs/generated_documentation/wordfilter_pydoc.md new file mode 100644 index 000000000..ce173f1d3 --- /dev/null +++ b/docs/generated_documentation/wordfilter_pydoc.md @@ -0,0 +1,114 @@ +# Table of Contents + +* [wordfilter](#wordfilter) + * [WordFilter](#wordfilter.WordFilter) + * [whitelistWordTest](#wordfilter.WordFilter.whitelistWordTest) + * [whitelistWord\_error](#wordfilter.WordFilter.whitelistWord_error) + * [clearWhitelist](#wordfilter.WordFilter.clearWhitelist) + * [clearWhitelist\_error](#wordfilter.WordFilter.clearWhitelist_error) + * [loadWhitelist](#wordfilter.WordFilter.loadWhitelist) + * [loadWhitelist\_error](#wordfilter.WordFilter.loadWhitelist_error) + * [setup](#wordfilter.setup) + + + +# wordfilter + + + +## WordFilter Objects + +```python +class WordFilter(commands.Cog) +``` + + + +#### whitelistWordTest + +```python +@commands.has_role("Instructor") +@commands.command( + name="whitelisttest", + help= + 'Add a word to the censor whitelist. Enclose in quotation marks. EX: $whitelist "WORD"', +) +async def whitelistWordTest(ctx, word: str = "") +``` + +Allows instructors to add words to censor whitelist + + + +#### whitelistWord\_error + +```python +@whitelistWordTest.error +async def whitelistWord_error(ctx, error) +``` + +Error handling for whitelist command + + + +#### clearWhitelist + +```python +@commands.has_role("Instructor") +@commands.command( + name="clearWhitelist", + help="Clears all words from the saved whitelist. EX: $clearwhitelist", +) +async def clearWhitelist(ctx) +``` + +Allows instructors to clea their saved whitelist + + + +#### clearWhitelist\_error + +```python +@clearWhitelist.error +async def clearWhitelist_error(ctx, error) +``` + +Error handling for whitelist command + + + +#### loadWhitelist + +```python +@commands.has_role("Instructor") +@commands.command( + name="loadWhitelist", + help= + "Adds all words in the saved whitelist to the censor whitelist. EX: $loadWhitelist", +) +async def loadWhitelist(ctx) +``` + +Allows instructors to load their saved whitelist + + + +#### loadWhitelist\_error + +```python +@loadWhitelist.error +async def loadWhitelist_error(ctx, error) +``` + +Error handling for loadWhitelist command + + + +#### setup + +```python +async def setup(bot) +``` + +Adds the file the bot's cog system + diff --git a/docs/installation.md b/docs/installation.md index e027be607..a8f7d8f88 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -32,10 +32,11 @@ To set up and run the ClassMate Bot: DATABASE_URL={your-database-url} ``` 7. TO COMPLETELY SET UP THE DATABASE, SEE THE SECTIONS BELOW. Under the **Heroku** section, follow the **Installation** and **Database Setup** guides. DO THIS BEFORE CONTINUING ON TO THE NEXT STEP. -8. Start the bot. From the project root directory, run `python3 bot` (or `python bot.py` on Windows) -9. Invite the bot to your server ([Follow instructions here](https://realpython.com/how-to-make-a-discord-bot-python/)) (**Please ensure the bot is running and that the database is set up in order for server initialization to happen properly**) +8. Create a Google Cloud Project ([Follow instructions here](https://developers.google.com/calendar/api/quickstart/python)) +9. Start the bot. From the project root directory, run `python3 bot` (or `python bot.py` on Windows) +10. Invite the bot to your server ([Follow instructions here](https://realpython.com/how-to-make-a-discord-bot-python/)) (**Please ensure the bot is running and that the database is set up in order for server initialization to happen properly**) * NOTE: When using the OAuth2 URL Generator, make sure you check the box which gives your bot Administrative permissions -10. You should now be able to input commands and get responses from the bot as appropriate. +11. You should now be able to input commands and get responses from the bot as appropriate. ## Heroku @@ -79,11 +80,47 @@ If you used the PostgreSQL installer, you should have a program called pgAdmin4. 6. Go to the `Advanced` tab. Inside the **DB Restriction** field, enter your Heroku **Database** credential. (This is the same string you entered into **Maintenance Database**.) DO NOT SKIP THIS STEP. 7. _Now_ you can press save. 8. Inside your newly created server, open up the `Databases > Schemas > Public` drop down lists. Right click on `Tables` and select the `Query Tool`. -9. Inside the query tool, click on `Query Editor.` Copy and paste the contents of [this SQL file](https://github.com/CSC510-Group-25/ClassMateBot/blob/main/init.sql) into the editor and then click on the execute button (looks like a play button). +9. Inside the query tool, click on `Query Editor.` Copy and paste the contents of [this SQL file](https://github.com/nfoster1492/ClassMateBot-1/blob/main/init.sql) into the editor and then click on the execute button (looks like a play button). 10. Right click on `Tables` and select `refresh`. Congratulations! You now have your tables set up. +## Google Calendar setup: + +1. Create a Calendar category on your Google Calendar that will be used for the shared bot calendar +2. Find the Calendar ID of this calendar in the Google Calendar settings under "Integrate calendar" and add it to your .env file + ``` + # .env + TOKEN={your-bot-token} + DATABASE_URL={your-database-url} + CALENDAR_ID={your-calendar-id} + ``` +3. In the same settings menu, find the secret address of this calendar and add it to your .env file + ``` + # .env + TOKEN={your-bot-token} + DATABASE_URL={your-database-url} + CALENDAR_ID={your-calendar-id} + CALENDAR_ICS={your-secret-address} + ``` +4. In your directory of choice create two files, calendar.pdf and ical.ics +5. Copy the path to this directory and add it to your .env file + ``` + # .env + TOKEN={your-bot-token} + DATABASE_URL={your-database-url} + CALENDAR_ID={your-calendar-id} + CALENDAR_ICS={your-secret-address} + CALENDAR_PATH={path-to-files} + ``` +6. Download wkhtmltopdf ([Download here](https://wkhtmltopdf.org/downloads.html)) Note: If you are on Windows add the path of the downloaded file to your PATH environment variable + +#### Resolving calendar issues + +Until the Google Cloud App is pushed to production, the user that is managing the bot will need to generate a new token every seven days: +1. Delete the `token.json` file from the project root directory. +2. Run one of the calendar commands specified in /docs/Calendar: this will open a browser window allowing you to reauthenticate the application and generate a new `token.json` + ## Running the bot locally This is recommended if you plan on testing your changes before pushing them to github. Be sure to turn off any Heroku dynos that may be active! diff --git a/docs/Project2Changes.md b/docs/previous_contributions/Project2Changes.md similarity index 100% rename from docs/Project2Changes.md rename to docs/previous_contributions/Project2Changes.md diff --git a/docs/Project3Changes.md b/docs/previous_contributions/Project3Changes.md similarity index 100% rename from docs/Project3Changes.md rename to docs/previous_contributions/Project3Changes.md diff --git a/docs/proj1rubric.md b/docs/previous_contributions/proj1rubric.md similarity index 100% rename from docs/proj1rubric.md rename to docs/previous_contributions/proj1rubric.md diff --git a/docs/proj1rubricCommentsc.pdf b/docs/previous_contributions/proj1rubricCommentsc.pdf similarity index 100% rename from docs/proj1rubricCommentsc.pdf rename to docs/previous_contributions/proj1rubricCommentsc.pdf diff --git a/docs/proj2rubric.md b/docs/previous_contributions/proj2rubric.md similarity index 100% rename from docs/proj2rubric.md rename to docs/previous_contributions/proj2rubric.md diff --git a/docs/proj3rubric.md b/docs/previous_contributions/proj3rubric.md similarity index 100% rename from docs/proj3rubric.md rename to docs/previous_contributions/proj3rubric.md diff --git a/docs/troubleshoot.md b/docs/troubleshoot.md new file mode 100644 index 000000000..14e706688 --- /dev/null +++ b/docs/troubleshoot.md @@ -0,0 +1,24 @@ +# Troubleshooting Guide + +## Google Calendar Issues + +### Invalid Authentication + +Until the Google Cloud App is pushed to production, the user that is managing the bot will need to generate a new token every seven days: +1. Delete the `token.json` file from the project root directory. +2. Run one of the calendar commands specified in /docs/Calendar: this will open a browser window allowing you to reauthenticate the application and generate a new `token.json` + +## Pytest/Dyptest + +### Pytest VS Code issue +If you are on Windows and using VS code to edit the code and your tests are not being picked up by the editor, then follow these [steps](https://stackoverflow.com/questions/54387442/vs-code-not-finding-pytest-tests). + +### Dpytest +If you are creating new tests you may find that tests fail for seemingly no reason. One aspect of dpytest that is helpful to know in debugging your failing test is that the messages returned from dpytest are put into a queue. An example of this is the following where in $command1 the bot returns two messages +``` +await dpytest.message("$command1") +assert (dpytest.verify().message().content("Command one has run")) +await dpytest.message("$command2") +assert (dpytest.verify().message().content("Command two has run")) +``` +Line 4 will fail because the second message from $command1 is at the head of the queue and so is returned instead of the message from $command2. diff --git a/init.sql b/init.sql index 860513bbd..51dcab9aa 100644 --- a/init.sql +++ b/init.sql @@ -2,10 +2,35 @@ CREATE TABLE reminders ( guild_id BIGINT NOT NULL, author_id BIGINT NOT NULL, course VARCHAR NOT NULL, - homework VARCHAR NOT NULL, + reminder_name VARCHAR NOT NULL, due_date TIMESTAMP WITH TIME ZONE NOT NULL ); + +CREATE TABLE grade_categories( + id bigserial primary key, + guild_id BIGINT NOT NULL, + category_name VARCHAR NOT NULL, + category_weight DECIMAL(3, 3) + +); + +CREATE TABLE assignments ( + id bigserial primary key, + guild_id BIGINT NOT NULL, + category_id BIGINT NOT NULL REFERENCES grade_categories(id) ON DELETE CASCADE, + assignment_name VARCHAR NOT NULL, + points INTEGER NOT NULL DEFAULT 100 + +); + +CREATE TABLE grades ( + guild_id BIGINT NOT NULL, + member_name VARCHAR NOT NULL, + assignment_id INT NOT NULL REFERENCES assignments(id) ON DELETE CASCADE, + grade INT NOT NULL +); + CREATE TABLE group_members ( guild_id BIGINT NOT NULL, group_num INTEGER NOT NULL, diff --git a/profanity_helper.py b/profanity_helper.py index 0ce844851..aac8c21b6 100644 --- a/profanity_helper.py +++ b/profanity_helper.py @@ -1,6 +1,7 @@ import os from better_profanity import profanity + profanity.load_censor_words() # profanity filter is on by default @@ -11,29 +12,34 @@ # newly added words. Will be loaded directly into profanity. newwhitelist = [] # commands -command_list = [] # needs to be separate so that it's possible to slice later. See saveCurrentWL +command_list = ( + [] +) # needs to be separate so that it's possible to slice later. See saveCurrentWL # list of words to censor; loaded from file censorlist = [] # list of new words to censor newcensorlist = [] + # loads whitelist into filter def loadwhitelist(): profanity.load_censor_words(whitelist_words=whitelist) + # loads default whitelist. Includes commands. TODO def loadDefaultWhitelist(): - #wl_set = set(whitelist) - #NOTE: THIS MAY CAUSE BUGS IF FILE DOESN'T EXIST, HAS FINAL NEWLINE, OR IS EMPTY... maybe? - with open('default_whitelist.txt', 'r', encoding='UTF-8') as file: + # wl_set = set(whitelist) + # NOTE: THIS MAY CAUSE BUGS IF FILE DOESN'T EXIST, HAS FINAL NEWLINE, OR IS EMPTY... maybe? + with open("default_whitelist.txt", "r", encoding="UTF-8") as file: for line in file: thing = line.rstrip() whitelist.append(thing) - #wl_set.add(thing) + # wl_set.add(thing) profanity.load_censor_words(whitelist_words=whitelist) + # a helper to use contains_profanity because load_censor_words won't use the whitelist otherwise. def helpChecker(content): # some words really, really like being censored. @@ -43,15 +49,18 @@ def helpChecker(content): return profanity.contains_profanity(content) + # a helper to censor profanity because it triggers discord formatting otherwise. def helpCensor(content): - return profanity.censor(content, r'\*') + return profanity.censor(content, r"\*") + # a function to add a word to the whitelist and reload the filter. def wlword(word): whitelist.append(word) profanity.load_censor_words(whitelist_words=whitelist) + # a function to remove a word from the whitelist and reload the filter. def unwlword(word): whitelist.remove(word) @@ -59,45 +68,45 @@ def unwlword(word): # loads saved whitelist. TODO -#def loadSavedWhitelist(): +# def loadSavedWhitelist(): # profanity.load_censor_words(whitelist_words=command_list) - # for each word in words: - # read and strip newline - # if in array: skip, else: add to array +# for each word in words: +# read and strip newline +# if in array: skip, else: add to array - # pass array to profanity whitelist? +# pass array to profanity whitelist? # saves the current whitelist to the existing list (excluding commands): TODO -#def saveCurrentWhitelist(): - #ex = len(command_list) - #saveList = whitelist[ex:] # remove commands... - # convert to set to prevent duplicates... - # return? save it? +# def saveCurrentWhitelist(): +# ex = len(command_list) +# saveList = whitelist[ex:] # remove commands... +# convert to set to prevent duplicates... +# return? save it? # clears the current whitelist but leaves the saved list untouched (excluding commands): TODO -#def clearCurrentWhitelist(): - # ex = len(command_list) - # saveList = whitelist[ex:] - # convert to set to prevent duplicates... - # return? save it? - # how to handle newly added words if the user decides to load saved words? +# def clearCurrentWhitelist(): +# ex = len(command_list) +# saveList = whitelist[ex:] +# convert to set to prevent duplicates... +# return? save it? +# how to handle newly added words if the user decides to load saved words? # to prevent duplication, reconstruct the list... -#def loadSavedWL(): +# def loadSavedWL(): # profanity.load_censor_words(whitelist_words=command_list) -#TODO -#def censorWord(word): +# TODO +# def censorWord(word): # if word.startswith("$"): # pass - #cannot censor a command +# cannot censor a command - # if all white space: do nothing - # cannot censor words < 2 chars - # if word is on command list: cannot censor command. - # if word is on whitelist: remove from list and add to censor +# if all white space: do nothing +# cannot censor words < 2 chars +# if word is on command list: cannot censor command. +# if word is on whitelist: remove from list and add to censor # load saved words into an array from a file -#def loadSavedBadWords(): +# def loadSavedBadWords(): # pass diff --git a/requirements.txt b/requirements.txt index 6e9b5aaf3..eb991c9ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,19 +1,30 @@ aiohttp==3.7.4.post0 async-timeout==3.0.1 -attrs==21.2.0 -black==21.7b0 -coverage==5.5 -coverage-badge==1.0.1 -discord==1.7.3 -discord.py==1.7.3 -dpytest==0.5.3 -jishaku==2.3.0 -packaging==21.0 -py==1.10.0 -pytest==6.2.5 -pytest-asyncio==0.15.1 -pytest-cov==2.12.1 +attrs==23.1.0 +black==23.7.0 +coverage==7.3.0 +coverage-badge==1.1.0 +discord==2.3.2 +discord.py==2.3.2 +dpytest==0.7.0 +jishaku==2.5.1 +packaging==23.1 +py==1.11.0 +pdfkit==1.0.0 +pytest==7.4.1 +pytest-asyncio==0.21.1 +pytest-cov==4.1.0 python-dateutil==2.8.2 -python-dotenv==0.19.0 -psycopg2-binary==2.9.1 -better-profanity==0.7.0 \ No newline at end of file +python-dotenv==1.0.0 +psycopg2-binary==2.9.7 +better-profanity==0.7.0 +requests==2.31.0 +pandas==2.1.1 +google-api-core==2.12.0 +google-api-python-client==2.101.0 +google-auth==2.23.2 +google-auth-httplib2==0.1.1 +google-auth-oauthlib==1.1.0 +googleapis-common-protos==1.60.0 +PyPDF2==3.0.1 +vobject==0.9.6.1 diff --git a/setup.py b/setup.py index 0de94aa74..7cd3dec8d 100644 --- a/setup.py +++ b/setup.py @@ -1,19 +1,20 @@ from setuptools import setup setup( - name='ClassMateBot', - version='0.2', - description='A Discord bot for classrom discord channels', + name="ClassMateBot", + version="0.2", + description="A Discord bot for classrom discord channels", long_description=""" A Discord bot which provides utility commands for students and teachers in classrom discord channels """, - author='Chaitanya Patel, Walter Evan Brown, Kunwar Vidhan, Sumedh Sanjay Salvi, Sunil Dattatraya Upare', - author_emails='cpatel3@ncsu.edu, webrown2@ncsu.edu, kvidhan@ncsu.edu, ssalvi@ncsu.edu, supare@ncsu.edu', - url='https://github.com/War-Keeper/ClassMateBot', - liscense='MIT', - install_requires=['pytest', 'discord.py'], - classifiers=['Development Status :: 3 - Alpha', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 3.9' - ] + author="Chaitanya Patel, Walter Evan Brown, Kunwar Vidhan, Sumedh Sanjay Salvi, Sunil Dattatraya Upare", + author_emails="cpatel3@ncsu.edu, webrown2@ncsu.edu, kvidhan@ncsu.edu, ssalvi@ncsu.edu, supare@ncsu.edu", + url="https://github.com/War-Keeper/ClassMateBot", + liscense="MIT", + install_requires=["pytest", "discord.py"], + classifiers=[ + "Development Status :: 3 - Alpha", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.9", + ], ) # Copyright (c) 2021 War-Keeper diff --git a/test/data/calendar.pdf b/test/data/calendar.pdf new file mode 100644 index 000000000..e69de29bb diff --git a/test/data/examGrades.csv b/test/data/examGrades.csv new file mode 100644 index 000000000..0e68443f4 --- /dev/null +++ b/test/data/examGrades.csv @@ -0,0 +1,4 @@ +name,grade +TestUser0,-1 +TestUser0,75 +InvalidUser0,50 \ No newline at end of file diff --git a/test/data/grades.csv b/test/data/grades.csv new file mode 100644 index 000000000..89941508a --- /dev/null +++ b/test/data/grades.csv @@ -0,0 +1,2 @@ +name,grade +TestUser0,25 \ No newline at end of file diff --git a/test/data/hwGrades.csv b/test/data/hwGrades.csv new file mode 100644 index 000000000..917b3a4f6 --- /dev/null +++ b/test/data/hwGrades.csv @@ -0,0 +1,3 @@ +name,grade +TestUser0,69 +TestUser0,70 \ No newline at end of file diff --git a/test/data/ical.ics b/test/data/ical.ics new file mode 100644 index 000000000..b67f6f6de --- /dev/null +++ b/test/data/ical.ics @@ -0,0 +1 @@ +BEGIN:VCALENDAR PRODID:-//Google Inc//Google Calendar 70.9054//EN VERSION:2.0 CALSCALE:GREGORIAN METHOD:PUBLISH X-WR-CALNAME:ClassMateBot X-WR-TIMEZONE:America/New_York BEGIN:VEVENT DTSTART:20231018T192823Z DTSTAMP:20231017T232824Z UID:5m02bl6eq69esng4ffpn8am0s4@google.com CREATED:20231017T232822Z DESCRIPTION:CSC510 LAST-MODIFIED:20231017T232822Z SEQUENCE:0 STATUS:CONFIRMED SUMMARY:HW3 TRANSP:OPAQUE END:VEVENT END:VCALENDAR \ No newline at end of file diff --git a/test/test_bot.py b/test/test_bot.py index b65c8472d..8b3b94c4f 100644 --- a/test/test_bot.py +++ b/test/test_bot.py @@ -1,11 +1,17 @@ # Copyright (c) 2021 War-Keeper import discord import os +import asyncio from datetime import datetime, timedelta +from zoneinfo import ZoneInfo import discord.ext.test as dpytest from dotenv import load_dotenv import pytest +from PyPDF2 import PdfReader from discord.ext import commands +import vobject + +import db # ------------------------------------------------------------------------------------------------------ @@ -36,7 +42,11 @@ async def test_groupJoin(bot): # Try to join a group await dpytest.message("$join 99") - assert dpytest.verify().message().content("You are now in Group 99! There are now 1/6 members.") + assert ( + dpytest.verify() + .message() + .content("You are now in Group 99! There are now 1/6 members.") + ) # try to join a different group await dpytest.message("$join 1") @@ -55,10 +65,11 @@ async def test_groupJoin(bot): print(dpytest.get_message()) assert dpytest.verify().message().content("Roles deleted!") - await dpytest.message('$startupgroups') + await dpytest.message("$startupgroups") print(dpytest.get_message()) - await dpytest.message('$connect') + await dpytest.message("$connect") + # ------------------------------------ # Tests cogs/groups.py error handling @@ -67,14 +78,511 @@ async def test_groupJoin(bot): async def test_groupError(bot): # Try to join a group that doesn't exist await dpytest.message("$join -1") - assert dpytest.verify().message().content('Not a valid group') - assert dpytest.verify().message().content( - 'To use the join command, do: $join where 0 <= <= 99 \n ( For example: $join 0 )') + assert dpytest.verify().message().content("Not a valid group") + assert ( + dpytest.verify() + .message() + .content( + "To use the join command, do: $join where 0 <= <= 99 \n ( For example: $join 0 )" + ) + ) with pytest.raises(commands.MissingRequiredArgument): await dpytest.message("$join") - assert dpytest.verify().message().content( - 'To use the join command, do: $join \n ( For example: $join 0 )') + assert ( + dpytest.verify() + .message() + .content("To use the join command, do: $join \n ( For example: $join 0 )") + ) + + +# --------------------- +# Tests cogs/assignments.py +# --------------------- +@pytest.mark.asyncio +async def test_assignments(bot): + # create instuctor user + user = dpytest.get_config().members[0] + guild = dpytest.get_config().guilds[0] + irole = await guild.create_role(name="Instructor") + await irole.edit(permissions=discord.Permissions(8)) + role = discord.utils.get(guild.roles, name="Instructor") + await dpytest.add_role(user, role) + # Set up grade categories + await dpytest.message("$addgradecategory Homework 0.3") + assert ( + dpytest.verify() + .message() + .content("A grading category has been added for: Homework with weight: 0.3 ") + ) + await dpytest.message("$addgradecategory Project 0.7") + assert ( + dpytest.verify() + .message() + .content("A grading category has been added for: Project with weight: 0.7 ") + ) + ##Adding Assignments + # Test adding a valid assignment with new category + await dpytest.message("$addassignment HW1 Homework 30") + assert ( + dpytest.verify() + .message() + .content( + "A grading assignment has been added for: HW1 with points: 30 and category: Homework" + ) + ) + # Test adding into an existing category + await dpytest.message("$addassignment HW2 Homework 30") + assert ( + dpytest.verify() + .message() + .content( + "A grading assignment has been added for: HW2 with points: 30 and category: Homework" + ) + ) + ##Editing Assignments + # Test editing an assignment points + await dpytest.message("$editassignment HW2 Homework 20") + assert ( + dpytest.verify() + .message() + .content( + "HW2 assignment has been updated with points:20 and category: Homework" + ) + ) + # Test editing an assignment category + await dpytest.message("$editassignment HW1 Project 70") + assert ( + dpytest.verify() + .message() + .content("HW1 assignment has been updated with points:70 and category: Project") + ) + ##Deleting Assignments + # Test deleting an assignment + await dpytest.message("$deleteassignment HW1") + assert dpytest.verify().message().content("HW1 assignment has been deleted ") + + +# --------------------- +# Tests cogs/assignments.py +# --------------------- +@pytest.mark.asyncio +async def test_assignments_error(bot): + # create instuctor user + user = dpytest.get_config().members[0] + guild = dpytest.get_config().guilds[0] + irole = await guild.create_role(name="Instructor") + await irole.edit(permissions=discord.Permissions(8)) + role = discord.utils.get(guild.roles, name="Instructor") + await dpytest.add_role(user, role) + # Set up grade categories + await dpytest.message("$addgradecategory Homework 0.3") + assert ( + dpytest.verify() + .message() + .content("A grading category has been added for: Homework with weight: 0.3 ") + ) + await dpytest.message("$addgradecategory Project 0.7") + assert ( + dpytest.verify() + .message() + .content("A grading category has been added for: Project with weight: 0.7 ") + ) + ##Adding Assignments + # Test invalid points + await dpytest.message("$addassignment HW2 Homework points") + assert dpytest.verify().message().content("Points could not be parsed") + await dpytest.message("$addassignment HW3 Homework -1") + assert ( + dpytest.verify() + .message() + .content("Assignment points must be greater than or equal to zero") + ) + # Test invalid parameters + with pytest.raises(commands.MissingRequiredArgument): + await dpytest.message("$addassignment HW1 Homework") + assert ( + dpytest.verify() + .message() + .content( + "To use the addassignment command, do: $addassignment \n ( For example: $addassignment test1 tests 100 )" + ) + ) + # Test duplicate assignment + await dpytest.message("$addassignment HW2 Homework 20") + assert ( + dpytest.verify() + .message() + .content( + "A grading assignment has been added for: HW2 with points: 20 and category: Homework" + ) + ) + await dpytest.message("$addassignment HW2 Homework 20") + assert ( + dpytest.verify().message().content("This assignment has already been added..!!") + ) + ##Editing Assignments + # Test invalid points + await dpytest.message("$editassignment HW2 Homework points") + assert dpytest.verify().message().content("Points could not be parsed") + await dpytest.message("$editassignment HW2 Homework -1") + assert ( + dpytest.verify() + .message() + .content("Assignment points must be greater than or equal to zero") + ) + # Test assignment that does not exist + await dpytest.message("$editassignment HW1 Homework 30") + assert dpytest.verify().message().content("This assignment does not exist") + # Test invalid parameters + with pytest.raises(commands.MissingRequiredArgument): + await dpytest.message("$editassignment HW2 Homework") + assert ( + dpytest.verify() + .message() + .content( + "To use the editassignment command, do: $editassignment \n ( For example: $editassignment test1 tests 95 )" + ) + ) + ##Deleting Assignments + # Test non existing assignment + await dpytest.message("$deleteassignment HW1") + assert dpytest.verify().message().content("This assignment does not exist") + # Test invalid parameters + with pytest.raises(commands.MissingRequiredArgument): + await dpytest.message("$deleteassignment") + assert ( + dpytest.verify() + .message() + .content( + "To use the deleteassignment command, do: $deleteassignment \n ( For example: $deleteassignment test1)" + ) + ) + + +# ----------------------- +# Tests cogs/grades.py +# ----------------------- +@pytest.mark.asyncio +async def test_gradesStudent(bot): + # create instuctor user + user = dpytest.get_config().members[0] + guild = dpytest.get_config().guilds[0] + irole = await guild.create_role(name="Instructor") + await irole.edit(permissions=discord.Permissions(8)) + role = discord.utils.get(guild.roles, name="Instructor") + await dpytest.add_role(user, role) + await dpytest.message("$addgradecategory Homework 0.3") + assert ( + dpytest.verify() + .message() + .content("A grading category has been added for: Homework with weight: 0.3 ") + ) + await dpytest.message("$addassignment HW1 Homework 30") + assert ( + dpytest.verify() + .message() + .content( + "A grading assignment has been added for: HW1 with points: 30 and category: Homework" + ) + ) + await guild.create_role(name="unverified") + await guild.create_role(name="verified") + role = discord.utils.get(guild.roles, name="unverified") + await dpytest.add_role(user, role) + channel = await guild.create_text_channel("general") + await dpytest.message("$verify TestUser0", channel=channel) + assert dpytest.verify().message().contains().content("Thank you for verifying!") + # this is to clear the empty spot on the queue + dpytest.get_message() + await dpytest.message("$inputgrades HW1 TestingTrue ../test/data/grades.csv") + assert dpytest.verify().message().contains().content("Entered grades for") + await dpytest.message("$grade HW1") + assert dpytest.verify().message().content("Grade for HW1: 25%, worth 30 points") + await dpytest.message("$gradebycategory Homework") + assert dpytest.verify().message().content("Grade for Homework: 25.00%") + await dpytest.message("$gradeforclass") + assert dpytest.verify().message().content("Grade for class: 7.50%") + await dpytest.message("$graderequired Homework 50 30") + assert ( + dpytest.verify() + .message() + .content("Grade on next assignment needed to keep 30% in Homework: 33.00%") + ) + await dpytest.message("$graderequiredforclass Homework 50 60") + assert ( + dpytest.verify() + .message() + .content("Grade on next assignment needed to keep 60%: 305.00%") + ) + await dpytest.message("$categories") + assert dpytest.verify().message().content("Category | Weight") + assert dpytest.verify().message().content("================") + assert dpytest.verify().message().content("Homework | 0.300") + + +# ----------------------- +# Tests cogs/grades.py +# ----------------------- +@pytest.mark.asyncio +async def test_gradesStudentError(bot): + # create instuctor user + user = dpytest.get_config().members[0] + guild = dpytest.get_config().guilds[0] + irole = await guild.create_role(name="Instructor") + await irole.edit(permissions=discord.Permissions(8)) + role = discord.utils.get(guild.roles, name="Instructor") + await dpytest.add_role(user, role) + await dpytest.message("$addgradecategory Homework 0.3") + assert ( + dpytest.verify() + .message() + .content("A grading category has been added for: Homework with weight: 0.3 ") + ) + await dpytest.message("$addassignment HW1 Homework 30") + assert ( + dpytest.verify() + .message() + .content( + "A grading assignment has been added for: HW1 with points: 30 and category: Homework" + ) + ) + await guild.create_role(name="unverified") + await guild.create_role(name="verified") + role = discord.utils.get(guild.roles, name="unverified") + await dpytest.add_role(user, role) + channel = await guild.create_text_channel("general") + await dpytest.message("$verify TestUser0", channel=channel) + assert dpytest.verify().message().contains().content("Thank you for verifying!") + # this is to clear the empty spot on the queue + dpytest.get_message() + await dpytest.message("$inputgrades HW1 TestingTrue ../test/data/grades.csv") + assert dpytest.verify().message().contains().content("Entered grades for") + with pytest.raises(commands.MissingRequiredArgument): + await dpytest.message("$grade") + assert ( + dpytest.verify() + .message() + .content( + "To use the grade command, do: $grade \n ( For example: $grade test1 )" + ) + ) + await dpytest.message("$grade FakeHW") + assert dpytest.verify().message().content("Grade for FakeHW does not exist") + with pytest.raises(commands.MissingRequiredArgument): + await dpytest.message("$gradebycategory") + assert ( + dpytest.verify() + .message() + .content( + "To use the gradebycategory command, do: $gradebycategory \n ( For example: $gradebycategory tests )" + ) + ) + await dpytest.message("$gradebycategory FakeCat") + assert dpytest.verify().message().content("Grades for FakeCat do not exist") + with pytest.raises(commands.MissingRequiredArgument): + await dpytest.message("$graderequired") + assert ( + dpytest.verify() + .message() + .content( + "To use the graderequired command, do: $graderequired \n ( For example: $graderequired tests 200 90 )" + ) + ) + with pytest.raises(commands.MissingRequiredArgument): + await dpytest.message("$graderequiredforclass") + assert ( + dpytest.verify() + .message() + .content( + "To use the graderequiredforclass command, do: $graderequiredforclass \n ( For example: $graderequiredforclass tests 200 90 )" + ) + ) + await dpytest.message("$graderequiredforclass Testing33 50 60") + assert dpytest.verify().message().content("Grades for Testing33 do not exist") + + +# ----------------------- +# Tests cogs/grades.py +# ----------------------- +@pytest.mark.asyncio +async def test_gradesInstructor(bot): + # create instuctor user + user = dpytest.get_config().members[0] + guild = dpytest.get_config().guilds[0] + irole = await guild.create_role(name="Instructor") + await irole.edit(permissions=discord.Permissions(8)) + role = discord.utils.get(guild.roles, name="Instructor") + await dpytest.add_role(user, role) + await dpytest.message("$addgradecategory Homework 0.2") + assert ( + dpytest.verify() + .message() + .content("A grading category has been added for: Homework with weight: 0.2 ") + ) + await dpytest.message("$editgradecategory Homework 0.3") + assert ( + dpytest.verify() + .message() + .content("Homework category has been updated with weight:0.3 ") + ) + await dpytest.message("$addgradecategory Exams 0.7") + assert ( + dpytest.verify() + .message() + .content("A grading category has been added for: Exams with weight: 0.7 ") + ) + await dpytest.message("$addgradecategory Projects 0.5") + assert ( + dpytest.verify() + .message() + .content("A grading category has been added for: Projects with weight: 0.5 ") + ) + await dpytest.message("$deletegradecategory Projects") + assert dpytest.verify().message().content("Projects category has been deleted ") + await dpytest.message("$addassignment Midterm1 Exams 100") + assert ( + dpytest.verify() + .message() + .content( + "A grading assignment has been added for: Midterm1 with points: 100 and category: Exams" + ) + ) + await dpytest.message("$addassignment HW1 Homework 10") + assert ( + dpytest.verify() + .message() + .content( + "A grading assignment has been added for: HW1 with points: 10 and category: Homework" + ) + ) + + await dpytest.message("$categories") + assert dpytest.verify().message().contains().content("Category | Weight") + assert dpytest.verify().message().contains().content("================") + assert dpytest.verify().message().contains().content("Exams | 0.700") + assert dpytest.verify().message().contains().content("Homework | 0.300") + + # Create TestUser0 + await guild.create_role(name="unverified") + await guild.create_role(name="verified") + role = discord.utils.get(guild.roles, name="unverified") + await dpytest.add_role(user, role) + channel = await guild.create_text_channel("general") + await dpytest.message("$verify TestUser0", channel=channel) + assert dpytest.verify().message().contains().content("Thank you for verifying!") + + # this is to clear the empty spot on the queue + dpytest.get_message() + + # Enters new grade and editing existing grade + await dpytest.message("$inputgrades HW1 TestingTrue ../test/data/hwGrades.csv") + assert ( + dpytest.verify() + .message() + .contains() + .content("Entered grades for HW1, 1 new grades entered, 1 grades edited") + ) + + await dpytest.message( + "$inputgrades Midterm1 TestingTrue ../test/data/examGrades.csv" + ) + assert ( + dpytest.verify() + .message() + .contains() + .content("Invalid grade value for student TestUser0, skipping entry") + ) + assert ( + dpytest.verify() + .message() + .contains() + .content("Invalid student name InvalidUser0, skipping entry") + ) + assert ( + dpytest.verify() + .message() + .contains() + .content("Entered grades for Midterm1, 1 new grades entered, 0 grades edited") + ) + + # await dpytest.message("$gradereportcategory") + # assert dpytest.verify().message().contains().content("Grade Breakdown by Category") + # assert dpytest.verify().message().contains().content("Exams | Average:") + # assert dpytest.verify().message().contains().content("Homework | Average:") + + # await dpytest.message("$gradereportassignment") + # assert ( + # dpytest.verify().message().contains().content("Grade Breakdown by Assignment") + # ) + # assert dpytest.verify().message().contains().content("HW1 | Average:") + # assert dpytest.verify().message().contains().content("Midterm1 | Average:") + + +# ----------------------- +# Tests cogs/grades.py +# ----------------------- +@pytest.mark.asyncio +async def test_gradesInstructorError(bot): + # pytest.set_trace() + # create instuctor user + user = dpytest.get_config().members[0] + guild = dpytest.get_config().guilds[0] + irole = await guild.create_role(name="Instructor") + await irole.edit(permissions=discord.Permissions(8)) + role = discord.utils.get(guild.roles, name="Instructor") + await dpytest.add_role(user, role) + await dpytest.message("$addgradecategory Homework 0.2") + assert ( + dpytest.verify() + .message() + .content("A grading category has been added for: Homework with weight: 0.2 ") + ) + await dpytest.message("$addgradecategory Homework asdf") + assert dpytest.verify().message().content("Weight could not be parsed") + await dpytest.message("$addgradecategory Homework -1") + assert dpytest.verify().message().content("Weight must be greater than 0") + await dpytest.message("$addgradecategory Homework 0.5") + assert ( + dpytest.verify().message().content("This category has already been added..!!") + ) + with pytest.raises(commands.MissingRequiredArgument): + await dpytest.message("$addgradecategory") + assert ( + dpytest.verify() + .message() + .contains() + .content("To use the gradecategory command") + ) + await dpytest.message("$editgradecategory Homework asdf") + assert dpytest.verify().message().content("Weight could not be parsed") + await dpytest.message("$editgradecategory Homework -1") + assert dpytest.verify().message().content("Weight must be greater than 0") + await dpytest.message("$editgradecategory Invalid 0.5") + assert dpytest.verify().message().content("This category does not exist") + with pytest.raises(commands.MissingRequiredArgument): + await dpytest.message("$editgradecategory") + assert ( + dpytest.verify() + .message() + .contains() + .content("To use the editgradecategory command") + ) + await dpytest.message("$deletegradecategory Invalid") + assert dpytest.verify().message().content("This category does not exist") + with pytest.raises(commands.MissingRequiredArgument): + await dpytest.message("$deletegradecategory") + assert ( + dpytest.verify() + .message() + .contains() + .content("To use the deletegradecategory command") + ) + with pytest.raises(commands.MissingRequiredArgument): + await dpytest.message("$inputgrades") + assert ( + dpytest.verify().message().contains().content("To use the inputgrades command") + ) # ----------------------- @@ -94,28 +602,59 @@ async def test_deadline(bot): # assert dpytest.verify().message().contains().content("All reminders have been cleared..!!") # Test reminders while none have been set await dpytest.message("$coursedue CSC505") - assert dpytest.verify().message().content("Rejoice..!! You have no pending homeworks for CSC505..!!") + assert ( + dpytest.verify() + .message() + .content("Rejoice..!! You have no pending reminders for CSC505..!!") + ) # Test setting 1 reminder - await dpytest.message("$addhw CSC505 DANCE SEP 21 2050 10:00") - assert dpytest.verify().message().contains().content( - "A date has been added for: CSC505 homework named: DANCE which is due on: 2050-09-21 10:00:00") + await dpytest.message("$duedate CSC505 DANCE SEP 21 2050 10:00") + assert ( + dpytest.verify() + .message() + .contains() + .content( + "A date has been added for: CSC505 reminder named: DANCE which is due on: 2050-09-21 10:00:00" + ) + ) # Test setting a 2nd reminder - await dpytest.message("$addhw CSC510 HW1 DEC 21 2050 19:59") - assert dpytest.verify().message().contains().content( - "A date has been added for: CSC510 homework named: HW1 which is due on: 2050-12-21 19:59:00") + await dpytest.message("$duedate CSC510 HW1 DEC 21 2050 19:59") + assert ( + dpytest.verify() + .message() + .contains() + .content( + "A date has been added for: CSC510 reminder named: HW1 which is due on: 2050-12-21 19:59:00" + ) + ) # Test deleting reminder await dpytest.message("$deletereminder CSC510 HW1") - assert dpytest.verify().message().content( - "Following reminder has been deleted: Course: CSC510, Homework Name: HW1, Due Date: 2050-12-21 19:59:00") + assert ( + dpytest.verify() + .message() + .content( + "Following reminder has been deleted: Course: CSC510, reminder Name: HW1, Due Date: 2050-12-21 19:59:00" + ) + ) # Test re-adding a reminder - await dpytest.message("$addhw CSC510 HW1 DEC 21 2050 19:59") - assert dpytest.verify().message().contains().content( - "A date has been added for: CSC510 homework named: HW1 which is due on: 2050-12-21 19:59:00") + await dpytest.message("$duedate CSC510 HW1 DEC 21 2050 19:59") + assert ( + dpytest.verify() + .message() + .contains() + .content( + "A date has been added for: CSC510 reminder named: HW1 which is due on: 2050-12-21 19:59:00" + ) + ) # Test adding an assignment twice - await dpytest.message("$addhw CSC510 HW1 DEC 21 2050 19:59") - assert dpytest.verify().message().contains().content( - "This homework has already been added..!!") + await dpytest.message("$duedate CSC510 HW1 DEC 21 2050 19:59") + assert ( + dpytest.verify() + .message() + .contains() + .content("This reminder has already been added..!!") + ) # Clear reminders at the end of testing since we're using a local JSON file to store them await dpytest.message("$clearreminders") @@ -135,25 +674,49 @@ async def test_listreminders(bot): role = discord.utils.get(guild.roles, name="Instructor") await dpytest.add_role(user, role) # Test listing multiple reminders - await dpytest.message("$addhw CSC505 DANCE SEP 21 2050 10:00") - assert dpytest.verify().message().contains().content( - "A date has been added for: CSC505 homework named: DANCE which is due on: 2050-09-21 10:00:00") + await dpytest.message("$duedate CSC505 DANCE SEP 21 2050 10:00") + assert ( + dpytest.verify() + .message() + .contains() + .content( + "A date has been added for: CSC505 reminder named: DANCE which is due on: 2050-09-21 10:00:00" + ) + ) # Test setting a 2nd reminder - await dpytest.message("$addhw CSC510 HW1 DEC 21 2050 19:59") - assert dpytest.verify().message().contains().content( - "A date has been added for: CSC510 homework named: HW1 which is due on: ") + await dpytest.message("$duedate CSC510 HW1 DEC 21 2050 19:59") + assert ( + dpytest.verify() + .message() + .contains() + .content( + "A date has been added for: CSC510 reminder named: HW1 which is due on: " + ) + ) await dpytest.message("$listreminders") - assert dpytest.verify().message().contains().content( - "CSC505 homework named: DANCE which is due on:") - assert dpytest.verify().message().contains().content( - "CSC510 homework named: HW1 which is due on:") + assert ( + dpytest.verify() + .message() + .contains() + .content("CSC505 reminder named: DANCE which is due on:") + ) + assert ( + dpytest.verify() + .message() + .contains() + .content("CSC510 reminder named: HW1 which is due on:") + ) # Test $coursedue await dpytest.message("$coursedue CSC505") - assert dpytest.verify().message().contains().content( - "DANCE is due at ") + assert dpytest.verify().message().contains().content("DANCE is due at ") # Clear reminders at the end of testing since we're using a local JSON file to store them await dpytest.message("$clearreminders") - assert dpytest.verify().message().contains().content("All reminders have been cleared..!!") + assert ( + dpytest.verify() + .message() + .contains() + .content("All reminders have been cleared..!!") + ) # Tests cogs/deadline.py @@ -173,15 +736,25 @@ async def test_duethisweek(bot): # Try adding a reminder due in an hour now = datetime.now() + timedelta(hours=1) dt_string = now.strftime("%b %d %Y %H:%M") - await dpytest.message(f'$addhw CSC600 HW0 {dt_string}') - assert dpytest.verify().message().contains().content( - "A date has been added for: CSC600 homework named: HW0") + await dpytest.message(f"$duedate CSC600 HW0 {dt_string}") + assert ( + dpytest.verify() + .message() + .contains() + .content("A date has been added for: CSC600 reminder named: HW0") + ) # Check to see that the reminder is due this week await dpytest.message("$duethisweek") assert dpytest.verify().message().contains().content("CSC600 HW0 is due ") # Clear reminders at the end of testing since we're using a local JSON file to store them await dpytest.message("$clearreminders") - assert dpytest.verify().message().contains().content("All reminders have been cleared..!!") + assert ( + dpytest.verify() + .message() + .contains() + .content("All reminders have been cleared..!!") + ) + # ------------------------------ # Tests reminders due today @@ -198,15 +771,25 @@ async def test_duetoday(bot): # Try adding a reminder due in an hour now = datetime.now() + timedelta(hours=6) dt_string = now.strftime("%b %d %Y %H:%M") - await dpytest.message(f'$addhw CSC600 HW0 {dt_string}') - assert dpytest.verify().message().contains().content( - "A date has been added for: CSC600 homework named: HW0") + await dpytest.message(f"$duedate CSC600 HW0 {dt_string}") + assert ( + dpytest.verify() + .message() + .contains() + .content("A date has been added for: CSC600 reminder named: HW0") + ) # Check to see that the reminder is due today await dpytest.message("$duetoday") assert dpytest.verify().message().contains().content("CSC600 HW0 is due ") # Clear reminders at the end of testing since we're using a local JSON file to store them await dpytest.message("$clearreminders") - assert dpytest.verify().message().contains().content("All reminders have been cleared..!!") + assert ( + dpytest.verify() + .message() + .contains() + .content("All reminders have been cleared..!!") + ) + # ------------------------------ # Tests overdue reminders @@ -221,18 +804,37 @@ async def test_overdue(bot): role = discord.utils.get(guild.roles, name="Instructor") await dpytest.add_role(user, role) # Try adding a reminder due in the past - await dpytest.message('$addhw CSC600 HW0 SEP 21 2000 10:00') - assert dpytest.verify().message().contains().content( - "A date has been added for: CSC600 homework named: HW0") + await dpytest.message("$duedate CSC600 HW0 SEP 21 2000 10:00") + assert ( + dpytest.verify() + .message() + .contains() + .content("A date has been added for: CSC600 reminder named: HW0") + ) # Check to see that the reminder is overdue await dpytest.message("$overdue") - assert dpytest.verify().message().contains().content("CSC600 homework named: HW0 which was due on: Sep 21 2000 10:00:00+0000") + assert ( + dpytest.verify() + .message() + .contains() + .content( + "CSC600 reminder named: HW0 which was due on: Sep 21 2000 10:00:00+0000" + ) + ) # Clear reminders at the end of testing since we're using a local JSON file to store them await dpytest.message("$clearoverdue") - assert dpytest.verify().message().contains().content("All overdue reminders have been cleared..!!") + assert ( + dpytest.verify() + .message() + .contains() + .content("All overdue reminders have been cleared..!!") + ) # Confirm overdue was removed await dpytest.message("$overdue") - assert dpytest.verify().message().contains().content("There are no overdue reminders") + assert ( + dpytest.verify().message().contains().content("There are no overdue reminders") + ) + # ------------------------------ # Tests deadline errors @@ -250,57 +852,74 @@ async def test_deadline_errors(bot): # Tests timenow without an argument with pytest.raises(commands.MissingRequiredArgument): await dpytest.message("$timenow") - assert dpytest.verify().message().content( + assert ( + dpytest.verify() + .message() + .content( "To use the timenow command (with current time), do: " - "$timenow MMM DD YYYY HH:MM ex. $timenow SEP 25 2024 17:02") + "$timenow MMM DD YYYY HH:MM ex. $timenow SEP 25 2024 17:02" + ) + ) # Test timenow with bad argument - #with pytest.raises(commands.MissingRequiredArgument): + # with pytest.raises(commands.MissingRequiredArgument): await dpytest.message("$timenow blab") - assert dpytest.verify().message().content( - "Due date could not be parsed") + assert dpytest.verify().message().content("Due date could not be parsed") - # Test addhw with bad argument - #with pytest.raises(commands.MissingRequiredArgument): - await dpytest.message("$addhw blab blab blab") - assert dpytest.verify().message().content( - "Due date could not be parsed") - - # Tests addhw without an argument + # Test duedate with bad argument + # with pytest.raises(commands.MissingRequiredArgument): + await dpytest.message("$duedate blab blab blab") + assert dpytest.verify().message().content("Due date could not be parsed") + # Tests duedate without an argument with pytest.raises(commands.MissingRequiredArgument): - await dpytest.message("$addhw") - assert dpytest.verify().message().content( - 'To use the addhw command, do: $addhw CLASSNAME HW_NAME MMM DD YYYY optional(HH:MM) optional(TIMEZONE)\n ' - '( For example: $addhw CSC510 HW2 SEP 25 2024 17:02 EST )') - + await dpytest.message("$duedate") + assert ( + dpytest.verify() + .message() + .content( + "To use the duedate command, do: $duedate CLASSNAME NAME MMM DD YYYY optional(HH:MM) optional(TIMEZONE)\n ( For example: $duedate CSC510 HW2 SEP 25 2024 17:02 EST )" + ) + ) # Tests deletereminder without an argument with pytest.raises(commands.MissingRequiredArgument): await dpytest.message("$deletereminder") - assert dpytest.verify().message().content( - 'To use the deletereminder command, do: $deletereminder CLASSNAME HW_NAME \n ' - '( For example: $deletereminder CSC510 HW2 )') - - + assert ( + dpytest.verify() + .message() + .content( + "To use the deletereminder command, do: $deletereminder CLASSNAME HW_NAME \n " + "( For example: $deletereminder CSC510 HW2 )" + ) + ) # Tests changeduedate without an argument with pytest.raises(commands.MissingRequiredArgument): await dpytest.message("$changeduedate") - assert dpytest.verify().message().content( - 'To use the changeduedate command, do: $changeduedate CLASSNAME HW_NAME MMM DD YYYY optional(HH:MM) optional(TIMEZONE)\n' - ' ( For example: $changeduedate CSC510 HW2 SEP 25 2024 17:02 EST)') + assert ( + dpytest.verify() + .message() + .content( + "To use the changeduedate command, do: $changeduedate CLASSNAME HW_NAME MMM DD YYYY optional(HH:MM) optional(TIMEZONE)\n" + " ( For example: $changeduedate CSC510 HW2 SEP 25 2024 17:02 EST)" + ) + ) # Test changeduedate with bad argument - #with pytest.raises(commands.MissingRequiredArgument): + # with pytest.raises(commands.MissingRequiredArgument): await dpytest.message("$changeduedate blab blab blab") - assert dpytest.verify().message().content( - "Due date could not be parsed") + assert dpytest.verify().message().content("Due date could not be parsed") # Tests coursedue without an argument with pytest.raises(commands.MissingRequiredArgument): await dpytest.message("$coursedue") - assert dpytest.verify().message().content( - 'To use the coursedue command, do: $coursedue CLASSNAME \n ( For example: $coursedue CSC510 )') + assert ( + dpytest.verify() + .message() + .content( + "To use the coursedue command, do: $coursedue CLASSNAME \n ( For example: $coursedue CSC510 )" + ) + ) # -------------------- @@ -308,169 +927,289 @@ async def test_deadline_errors(bot): # -------------------- @pytest.mark.asyncio async def test_pinning(bot): - # Test pinning a message await dpytest.message("$pin TestMessage www.google.com this is a test") # print(dpytest.get_message().content) - assert dpytest.verify().message().contains().content( - "A new message has been pinned with tag: TestMessage and description: www.google.com this is a test") + assert ( + dpytest.verify() + .message() + .contains() + .content( + "A new message has been pinned with tag: TestMessage and description: www.google.com this is a test" + ) + ) await dpytest.message("$pin TestMessage www.discord.com this is also a test") - assert dpytest.verify().message().contains().content( - "A new message has been pinned with tag: TestMessage and description: www.discord.com this is also a test") + assert ( + dpytest.verify() + .message() + .contains() + .content( + "A new message has been pinned with tag: TestMessage and description: www.discord.com this is also a test" + ) + ) + + # clean up + # await dpytest.message("$unpin TestMessage") - #clean up - #await dpytest.message("$unpin TestMessage") # ---------------- # Tests unpinning # ---------------- @pytest.mark.asyncio async def test_unpinning(bot): - # Test pinning a message await dpytest.message("$pin TestMessage www.google.com this is a test") - assert dpytest.verify().message().contains().content( - "A new message has been pinned with tag: TestMessage and description: www.google.com this is a test") + assert ( + dpytest.verify() + .message() + .contains() + .content( + "A new message has been pinned with tag: TestMessage and description: www.google.com this is a test" + ) + ) await dpytest.message("$pin TestMessage www.discord.com this is also a test") - assert dpytest.verify().message().contains().content( - "A new message has been pinned with tag: TestMessage and description: www.discord.com this is also a test") + assert ( + dpytest.verify() + .message() + .contains() + .content( + "A new message has been pinned with tag: TestMessage and description: www.discord.com this is also a test" + ) + ) # Tests unpinning a message that doesn't exist await dpytest.message("$unpin None") - assert dpytest.verify().message().contains().content( - "No message found with the combination of tagname: None, and author:") + assert ( + dpytest.verify() + .message() + .contains() + .content("No message found with the combination of tagname: None, and author:") + ) # Tests unpinning messages that DO exist await dpytest.message("$unpin TestMessage") - assert dpytest.verify().message().contains().content( - "2 pinned message(s) has been deleted with tag: TestMessage") + assert ( + dpytest.verify() + .message() + .contains() + .content("2 pinned message(s) has been deleted with tag: TestMessage") + ) + # --------------------- # Tests updating pins # --------------------- @pytest.mark.asyncio async def test_updatepin(bot): - # Tests adding another message to update pins await dpytest.message("$pin TestMessage2 www.discord.com test") - assert dpytest.verify().message().contains().content( - "A new message has been pinned with tag: TestMessage2 and description: www.discord.com test") + assert ( + dpytest.verify() + .message() + .contains() + .content( + "A new message has been pinned with tag: TestMessage2 and description: www.discord.com test" + ) + ) # Tests updatepin await dpytest.message("$updatepin TestMessage2 www.zoom.com test") - assert dpytest.verify().message().contains().content( - "1 pinned message(s) has been deleted with tag: TestMessage2") - assert dpytest.verify().message().contains().content( - "A new message has been pinned with tag: TestMessage2 and description: www.zoom.com test") + assert ( + dpytest.verify() + .message() + .contains() + .content("1 pinned message(s) has been deleted with tag: TestMessage2") + ) + assert ( + dpytest.verify() + .message() + .contains() + .content( + "A new message has been pinned with tag: TestMessage2 and description: www.zoom.com test" + ) + ) # Tests updating a non-existent pin await dpytest.message("$updatepin Tag Test") # Confirm no message exists - assert dpytest.verify().message().contains().content( - "No message found with the combination of tagname: Tag, and author:") + assert ( + dpytest.verify() + .message() + .contains() + .content("No message found with the combination of tagname: Tag, and author:") + ) # Ensure that a message is pinned. - assert dpytest.verify().message().contains().content( - "A new message has been pinned with tag: Tag and description: Test") + assert ( + dpytest.verify() + .message() + .contains() + .content("A new message has been pinned with tag: Tag and description: Test") + ) + # ------------------------ # Tests pinnedmessages # ------------------------ @pytest.mark.asyncio async def test_pinnedmessages(bot): - # Tests getting pins by tag: no pinned messages await dpytest.message("$pinnedmessages TestTag") - assert dpytest.verify().message().contains().content( - "No messages found with the given tagname and author combination") + assert ( + dpytest.verify() + .message() + .contains() + .content("No messages found with the given tagname and author combination") + ) # pin and dequeue await dpytest.message("$pin Tag1 never gonna give you up") - assert dpytest.verify().message().contains().content( - "A new message has been pinned with tag: Tag1 and description: never gonna give you up") + assert ( + dpytest.verify() + .message() + .contains() + .content( + "A new message has been pinned with tag: Tag1 and description: never gonna give you up" + ) + ) # pin and dequeue await dpytest.message("$pin Tag1 never gonna let you down") - assert dpytest.verify().message().contains().content( - "A new message has been pinned with tag: Tag1 and description: never gonna let you down") + assert ( + dpytest.verify() + .message() + .contains() + .content( + "A new message has been pinned with tag: Tag1 and description: never gonna let you down" + ) + ) # pin and dequeue await dpytest.message("$pin Tag2 never gonna run around and desert you") - assert dpytest.verify().message().contains().content( - "A new message has been pinned with tag: Tag2 and description: never gonna run around and desert you") - + assert ( + dpytest.verify() + .message() + .contains() + .content( + "A new message has been pinned with tag: Tag2 and description: never gonna run around and desert you" + ) + ) + # Tests getting pins by tag await dpytest.message("$pinnedmessages Tag1") - assert dpytest.verify().message().contains().content( - "Tag: Tag1, Description: never gonna give you up") - assert dpytest.verify().message().contains().content( - "Tag: Tag1, Description: never gonna let you down") + assert ( + dpytest.verify() + .message() + .contains() + .content("Tag: Tag1, Description: never gonna give you up") + ) + assert ( + dpytest.verify() + .message() + .contains() + .content("Tag: Tag1, Description: never gonna let you down") + ) # Tests getting all pins await dpytest.message("$pinnedmessages") - assert dpytest.verify().message().contains().content( - "Tag: Tag1, Description: never gonna give you up") - assert dpytest.verify().message().contains().content( - "Tag: Tag1, Description: never gonna let you down") - assert dpytest.verify().message().contains().content( - "Tag: Tag2, Description: never gonna run around and desert you") - - + assert ( + dpytest.verify() + .message() + .contains() + .content("Tag: Tag1, Description: never gonna give you up") + ) + assert ( + dpytest.verify() + .message() + .contains() + .content("Tag: Tag1, Description: never gonna let you down") + ) + assert ( + dpytest.verify() + .message() + .contains() + .content("Tag: Tag2, Description: never gonna run around and desert you") + ) + # ------------------------ # Tests pin-related errors # ------------------------ @pytest.mark.asyncio async def test_pinningErrors(bot): - # Tests pinning without a message with pytest.raises(commands.MissingRequiredArgument): await dpytest.message("$pin") - assert dpytest.verify().message().contains().content( - "To use the pin command, do: $pin TAGNAME DESCRIPTION \n ( For example: $pin HW8 https://" - "discordapp.com/channels/139565116151562240/139565116151562240/890813190433292298 HW8 reminder )") + assert ( + dpytest.verify() + .message() + .contains() + .content( + "To use the pin command, do: $pin TAGNAME DESCRIPTION \n ( For example: $pin HW8 https://" + "discordapp.com/channels/139565116151562240/139565116151562240/890813190433292298 HW8 reminder )" + ) + ) # Tests unpinning without a message with pytest.raises(commands.MissingRequiredArgument): await dpytest.message("$unpin") - assert dpytest.verify().message().contains().content( - 'To use the unpin command, do: $unpin TAGNAME \n ( For example: $unpin HW8 )') + assert ( + dpytest.verify() + .message() + .contains() + .content( + "To use the unpin command, do: $unpin TAGNAME \n ( For example: $unpin HW8 )" + ) + ) # Tests updating a pin with invalid input with pytest.raises(commands.MissingRequiredArgument): await dpytest.message("$updatepin") - assert dpytest.verify().message().contains().content( - "To use the updatepin command, do: $pin TAGNAME DESCRIPTION \n ( $updatepin HW8 https://discordapp" - ".com/channels/139565116151562240/139565116151562240/890814489480531969 HW8 reminder )") + assert ( + dpytest.verify() + .message() + .contains() + .content( + "To use the updatepin command, do: $pin TAGNAME DESCRIPTION \n ( $updatepin HW8 https://discordapp" + ".com/channels/139565116151562240/139565116151562240/890814489480531969 HW8 reminder )" + ) + ) # Tests using pinnedmessages with invalid input - #with pytest.raises(commands.CommandError): - #await dpytest.message("$pinnedmessages \" please fail omg") - #assert dpytest.verify().message().contains().content( - #"To use the pinnedmessages command, do: $pinnedmessages:" - #" TAGNAME \n ( For example: $pinnedmessages HW8 )") + # with pytest.raises(commands.CommandError): + # await dpytest.message("$pinnedmessages \" please fail omg") + # assert dpytest.verify().message().contains().content( + # "To use the pinnedmessages command, do: $pinnedmessages:" + # " TAGNAME \n ( For example: $pinnedmessages HW8 )") # The above test requires the else statement below to be included # in pinning.py's retrieveMessages_error function. - #@retrieveMessages.error - #async def retrieveMessages_error(self, ctx, error): - #if isinstance(error, commands.MissingRequiredArgument): - # ... - #else: - #await ctx.send( - #"To use the pinnedmessages command, do: $pinnedmessages:" - #" TAGNAME \n ( For example: $pinnedmessages HW8 )") - #print(error) + # @retrieveMessages.error + # async def retrieveMessages_error(self, ctx, error): + # if isinstance(error, commands.MissingRequiredArgument): + # ... + # else: + # await ctx.send( + # "To use the pinnedmessages command, do: $pinnedmessages:" + # " TAGNAME \n ( For example: $pinnedmessages HW8 )") + # print(error) # -------------------- # Tests cogs/newComer # -------------------- + @pytest.mark.asyncio async def test_verify(bot): user = dpytest.get_config().members[0] guild = dpytest.get_config().guilds[0] - channel = await guild.create_text_channel('general') + channel = await guild.create_text_channel("general") await dpytest.message("$verify Student Name", channel=channel) - assert dpytest.verify().message().contains().content( - 'Warning: Please make sure the verified and unverified roles exist in this server!') + assert ( + dpytest.verify() + .message() + .contains() + .content( + "Warning: Please make sure the verified and unverified roles exist in this server!" + ) + ) # Test self-verification - unverified role assigned await guild.create_role(name="unverified") @@ -478,8 +1217,12 @@ async def test_verify(bot): role = discord.utils.get(guild.roles, name="unverified") await dpytest.add_role(user, role) await dpytest.message("$verify Student Name", channel=channel) - assert dpytest.verify().message().contains().content( - f'Thank you for verifying! You can start using {guild.name}!') + assert ( + dpytest.verify() + .message() + .contains() + .content(f"Thank you for verifying! You can start using {guild.name}!") + ) dpytest.get_message() @@ -491,8 +1234,15 @@ async def test_verifyNoName(bot): # Test verification without proper argument given await dpytest.message("$verify") # print(dpytest.get_message().content) - assert dpytest.verify().message().contains().content( - 'To use the verify command, do: $verify \n ( For example: $verify Jane Doe )') + assert ( + dpytest.verify() + .message() + .contains() + .content( + "To use the verify command, do: $verify \n ( For example: $verify Jane Doe )" + ) + ) + # We cannot currently test newComer.py in a meaningful way due to not having a way to DM the test bot directly, # as well as inability to have dpytest add/remove roles to test specific cases @@ -505,28 +1255,35 @@ async def test_verifyNoName(bot): async def test_voting(bot): # Test voting await dpytest.message(content="$vote 1") - assert dpytest.verify().message().content( - "You are not in a group. You must join a group before voting on a project.") + assert ( + dpytest.verify() + .message() + .content( + "You are not in a group. You must join a group before voting on a project." + ) + ) await dpytest.message("$join 99") dpytest.get_message() await dpytest.message(content="$vote 1") - assert dpytest.verify().message().content( - "Group 99 has voted for Project 1!") + assert dpytest.verify().message().content("Group 99 has voted for Project 1!") await dpytest.message(content="$vote 2") - assert dpytest.verify().message().content( - "Group 99 removed vote for Project 1") - assert dpytest.verify().message().content( - "Group 99 has voted for Project 2!") + assert dpytest.verify().message().content("Group 99 removed vote for Project 1") + assert dpytest.verify().message().content("Group 99 has voted for Project 2!") await dpytest.message(content="$vote 2") - assert dpytest.verify().message().content( - "You already voted for Project 2") + assert dpytest.verify().message().content("You already voted for Project 2") with pytest.raises(commands.UserInputError): await dpytest.message(content="$vote") - assert dpytest.verify().message().contains().content( - "To join a project, use the join command, do: $vote \n( For example: $vote 0 )") + assert ( + dpytest.verify() + .message() + .contains() + .content( + "To join a project, use the join command, do: $vote \n( For example: $vote 0 )" + ) + ) await dpytest.message(content="$vote -1") - assert dpytest.verify().message().content( - "A valid project number is 1-99.") + assert dpytest.verify().message().content("A valid project number is 1-99.") + # ------------------- # Tests cogs/qanda @@ -537,74 +1294,110 @@ async def test_qanda(bot): # create channel and get user user = dpytest.get_config().members[0] guild = dpytest.get_config().guilds[0] - channel = await guild.create_text_channel('q-and-a') + channel = await guild.create_text_channel("q-and-a") irole = await guild.create_role(name="Instructor") await irole.edit(permissions=discord.Permissions(8)) role = discord.utils.get(guild.roles, name="Instructor") await dpytest.add_role(user, role) # Test asking a question anonymously - await dpytest.message("$ask \"What class is this?\" anonymous", channel=channel) - assert dpytest.verify().message().contains().content( - 'Q1: What class is this? by anonymous') + await dpytest.message('$ask "What class is this?" anonymous', channel=channel) + assert ( + dpytest.verify() + .message() + .contains() + .content("Q1: What class is this? by anonymous") + ) # Test asking a question with name - await dpytest.message("$ask \"When is the last day of classes?\"", channel=channel) - assert dpytest.verify().message().contains().content( - 'Q2: When is the last day of classes? by ' + user.name) + await dpytest.message('$ask "When is the last day of classes?"', channel=channel) + assert ( + dpytest.verify() + .message() + .contains() + .content("Q2: When is the last day of classes? by " + user.name) + ) # Tests getting answers: no answers await dpytest.message("$getAnswersFor 1", channel=channel) - assert dpytest.verify().message().contains().content( - 'No answers for Q1') + assert dpytest.verify().message().contains().content("No answers for Q1") # Test answering a question - await dpytest.message("$answer 2 \"TestA\"", channel=channel) + await dpytest.message('$answer 2 "TestA"', channel=channel) # Test answering a question anonymously - await dpytest.message("$answer 2 \"TestB\" anonymous", channel=channel) + await dpytest.message('$answer 2 "TestB" anonymous', channel=channel) # Tests getting answers: question has answers await dpytest.message("$getAnswersFor 2", channel=channel) - assert dpytest.verify().message().contains().content( - 'Q2: When is the last day of classes? by ' + user.name + '\n' - + user.name + ' (Instructor) Ans: TestA\n' - 'anonymous (Instructor) Ans: TestB\n') + assert ( + dpytest.verify() + .message() + .contains() + .content( + "Q2: When is the last day of classes? by " + + user.name + + "\n" + + user.name + + " (Instructor) Ans: TestA\n" + "anonymous (Instructor) Ans: TestB\n" + ) + ) # Tests channelGhost: not a ghost, has answers await dpytest.message("$channelGhost 2", channel=channel) - assert dpytest.verify().message().contains().content( - 'This question is not a ghost. Fetching anyway. . .') - assert dpytest.verify().message().contains().content( - 'Q2: When is the last day of classes? by ' + user.name + '\n' - + user.name + ' (Instructor) Ans: TestA\n' - 'anonymous (Instructor) Ans: TestB\n') + assert ( + dpytest.verify() + .message() + .contains() + .content("This question is not a ghost. Fetching anyway. . .") + ) + assert ( + dpytest.verify() + .message() + .contains() + .content( + "Q2: When is the last day of classes? by " + + user.name + + "\n" + + user.name + + " (Instructor) Ans: TestA\n" + "anonymous (Instructor) Ans: TestB\n" + ) + ) # test deleting all answers for a question with none await dpytest.message("$DALLAF 1", channel=channel) - assert dpytest.verify().message().contains().content( - 'No answers exist for Q1') + assert dpytest.verify().message().contains().content("No answers exist for Q1") # test deleting all answers await dpytest.message("$DALLAF 2", channel=channel) - assert dpytest.verify().message().contains().content( - 'deleted 2 answers for Q2') + assert dpytest.verify().message().contains().content("deleted 2 answers for Q2") # Test reviveGhost: non-existent question await dpytest.message("$reviveGhost 100", channel=channel) - assert dpytest.verify().message().contains().content( - "No such question with the number: 100") + assert ( + dpytest.verify() + .message() + .contains() + .content("No such question with the number: 100") + ) # Test channelGhost: non-existent question await dpytest.message("$channelGhost 100", channel=channel) - assert dpytest.verify().message().contains().content( - "No such question with the number: 100") + assert ( + dpytest.verify() + .message() + .contains() + .content("No such question with the number: 100") + ) # GHOST AND ZOMBIE TESTING # ask and dequeue - await dpytest.message("$ask \"Am I a zombie?\" anon", channel=channel) - assert dpytest.verify().message().contains().content( - 'Q3: Am I a zombie? by anonymous') + await dpytest.message('$ask "Am I a zombie?" anon', channel=channel) + assert ( + dpytest.verify().message().contains().content("Q3: Am I a zombie? by anonymous") + ) # hold on to q3 q3_id = channel.last_message_id @@ -612,129 +1405,165 @@ async def test_qanda(bot): # Tests channelGhost: not a ghost, no answers await dpytest.message("$channelGhost 3", channel=channel) - assert dpytest.verify().message().contains().content( - 'This question is not a ghost. Fetching anyway. . .') - assert dpytest.verify().message().contains().content( - 'Q3: Am I a zombie? by anonymous\n' - 'No answers for Q3\n') + assert ( + dpytest.verify() + .message() + .contains() + .content("This question is not a ghost. Fetching anyway. . .") + ) + assert ( + dpytest.verify() + .message() + .contains() + .content("Q3: Am I a zombie? by anonymous\n" "No answers for Q3\n") + ) # Test spooky: no ghosts or zombies await dpytest.message("$spooky", channel=channel) - assert dpytest.verify().message().contains().content( - "This channel isn't haunted.") + assert dpytest.verify().message().contains().content("This channel isn't haunted.") # Test unearthZombies: no zombies await dpytest.message("$unearthZombies", channel=channel) - assert dpytest.verify().message().contains().content( - "No zombies detected.") + assert dpytest.verify().message().contains().content("No zombies detected.") # zomb-ify Q3 await q3.delete() # test answering a zombie - await dpytest.message("$answer 3 \"zombie test\"", channel=channel) - assert dpytest.verify().message().contains().content( - "Question 3 not found. It's a zombie!" + await dpytest.message('$answer 3 "zombie test"', channel=channel) + assert ( + dpytest.verify() + .message() + .contains() + .content("Question 3 not found. It's a zombie!") ) # test getting answers for a zombie await dpytest.message("$getAnswersFor 3", channel=channel) - assert dpytest.verify().message().contains().content( - "Question 3 not found. It's a zombie!" + assert ( + dpytest.verify() + .message() + .contains() + .content("Question 3 not found. It's a zombie!") ) # ask and dequeue - await dpytest.message("$ask \"Am I a ghost?\" anonymous", channel=channel) - assert dpytest.verify().message().contains().content( - 'Q4: Am I a ghost? by anonymous') + await dpytest.message('$ask "Am I a ghost?" anonymous', channel=channel) + assert ( + dpytest.verify().message().contains().content("Q4: Am I a ghost? by anonymous") + ) # answer Q4 - await dpytest.message("$answer 4 \"Yes\" anon", channel=channel) + await dpytest.message('$answer 4 "Yes" anon', channel=channel) # ask and dequeue - await dpytest.message("$ask \"Zombie\" anonymous", channel=channel) - assert dpytest.verify().message().contains().content( - 'Q5: Zombie by anonymous') + await dpytest.message('$ask "Zombie" anonymous', channel=channel) + assert dpytest.verify().message().contains().content("Q5: Zombie by anonymous") # hold on to q5 q5_id = channel.last_message_id q5 = await channel.fetch_message(q5_id) # answer Q5; zombie with an answer - await dpytest.message("$answer 5 \"test\" anonymous", channel=channel) + await dpytest.message('$answer 5 "test" anonymous', channel=channel) # Test deleting a question await dpytest.message("$deleteQuestion 4", channel=channel) - assert dpytest.verify().message().contains().content( - 'Q4 is now a ghost. To restore it, use: $reviveGhost 4') + assert ( + dpytest.verify() + .message() + .contains() + .content("Q4 is now a ghost. To restore it, use: $reviveGhost 4") + ) # Test deleting a ghost question await dpytest.message("$deleteQuestion 4", channel=channel) - assert dpytest.verify().message().contains().content( - 'Q4 is already a ghost!') + assert dpytest.verify().message().contains().content("Q4 is already a ghost!") # test answering a ghost - await dpytest.message("$answer 4 \"Ghost Test\"", channel=channel) - assert dpytest.verify().message().contains().content( - "You can\'t answer a ghost!" - ) - + await dpytest.message('$answer 4 "Ghost Test"', channel=channel) + assert dpytest.verify().message().contains().content("You can't answer a ghost!") # Test channelGhost: answers await dpytest.message("$channelGhost 4", channel=channel) - assert dpytest.verify().message().contains().content( - 'Q4: Am I a ghost? by anonymous\nanonymous (Instructor) Ans: Yes\n' + assert ( + dpytest.verify() + .message() + .contains() + .content("Q4: Am I a ghost? by anonymous\nanonymous (Instructor) Ans: Yes\n") ) # Tests getting answers for a ghost await dpytest.message("$getAnswersFor 4", channel=channel) - assert dpytest.verify().message().contains().content( - 'Q4 is a ghost!') + assert dpytest.verify().message().contains().content("Q4 is a ghost!") # Test allChannelGhosts: answers await dpytest.message("$allChannelGhosts", channel=channel) - assert dpytest.verify().message().contains().content( - 'Q4: Am I a ghost? by anonymous\n' - 'anonymous (Instructor) Ans: Yes\n') + assert ( + dpytest.verify() + .message() + .contains() + .content("Q4: Am I a ghost? by anonymous\n" "anonymous (Instructor) Ans: Yes\n") + ) # test deleting all answers for ghost await dpytest.message("$DALLAF 4", channel=channel) - assert dpytest.verify().message().contains().content( - 'deleted 1 answers for Q4') - assert dpytest.verify().message().contains().content( - 'Q4 is a ghost!') + assert dpytest.verify().message().contains().content("deleted 1 answers for Q4") + assert dpytest.verify().message().contains().content("Q4 is a ghost!") # Test channelGhost: no answers await dpytest.message("$channelGhost 4", channel=channel) - assert dpytest.verify().message().contains().content( - 'Q4: Am I a ghost? by anonymous\n' - 'No answers for Q4\n') + assert ( + dpytest.verify() + .message() + .contains() + .content("Q4: Am I a ghost? by anonymous\n" "No answers for Q4\n") + ) # Test allChannelGhosts: no answers await dpytest.message("$allChannelGhosts", channel=channel) - assert dpytest.verify().message().contains().content( - 'Q4: Am I a ghost? by anonymous\n' - 'No answers for Q4\n') + assert ( + dpytest.verify() + .message() + .contains() + .content("Q4: Am I a ghost? by anonymous\n" "No answers for Q4\n") + ) # Test spooky: ghosts and zombies are present await dpytest.message("$spooky", channel=channel) - assert dpytest.verify().message().contains().content( - 'This channel is haunted by 1 ghosts and 1 zombies.') + assert ( + dpytest.verify() + .message() + .contains() + .content("This channel is haunted by 1 ghosts and 1 zombies.") + ) # Test archiveQA: zombie, ghost, questions with and without answers - await dpytest.message("$archiveQA",channel=channel) - assert dpytest.verify().message().contains().content( - 'Q1: What class is this? by anonymous\n' - 'No answers for Q1\n') - assert dpytest.verify().message().contains().content( - 'Q2: When is the last day of classes? by ' + user.name + '\n' - 'No answers for Q2\n') - assert dpytest.verify().message().contains().content( - "Q3 was deleted. It's a zombie!") - assert dpytest.verify().message().contains().content( - 'Q4 is a ghost!') - assert dpytest.verify().message().contains().content( - 'Q5: Zombie by anonymous\n' - 'anonymous (Instructor) Ans: test\n') + await dpytest.message("$archiveQA", channel=channel) + assert ( + dpytest.verify() + .message() + .contains() + .content("Q1: What class is this? by anonymous\n" "No answers for Q1\n") + ) + assert ( + dpytest.verify() + .message() + .contains() + .content( + "Q2: When is the last day of classes? by " + user.name + "\n" + "No answers for Q2\n" + ) + ) + assert ( + dpytest.verify().message().contains().content("Q3 was deleted. It's a zombie!") + ) + assert dpytest.verify().message().contains().content("Q4 is a ghost!") + assert ( + dpytest.verify() + .message() + .contains() + .content("Q5: Zombie by anonymous\n" "anonymous (Instructor) Ans: test\n") + ) # Test reviving a ghost (revive without answers) await dpytest.message("$reviveGhost 4", channel=channel) @@ -744,8 +1573,12 @@ async def test_qanda(bot): # Test deleting a zombie (ghosts + 1, zombies -1) await dpytest.message("$deleteQuestion 3", channel=channel) - assert dpytest.verify().message().contains().content( - 'Q3 was not found in channel. To restore it, use: $reviveGhost 3') + assert ( + dpytest.verify() + .message() + .contains() + .content("Q3 was not found in channel. To restore it, use: $reviveGhost 3") + ) # ghosts: 1, zombies: 0 @@ -757,16 +1590,18 @@ async def test_qanda(bot): # test reviving a zombie with answers await dpytest.message("$reviveGhost 5", channel=channel) # now we can assert because a message is actually posted this time. - assert dpytest.verify().message().contains().content( - 'Q5: Zombie by anonymous\n' - 'anonymous (Instructor) Ans: test\n') + assert ( + dpytest.verify() + .message() + .contains() + .content("Q5: Zombie by anonymous\n" "anonymous (Instructor) Ans: test\n") + ) # ghosts: 1, zombies: 0 # create another zombie - await dpytest.message("$ask \"Zombie2\" anonymous", channel=channel) - assert dpytest.verify().message().contains().content( - 'Q6: Zombie2 by anonymous') + await dpytest.message('$ask "Zombie2" anonymous', channel=channel) + assert dpytest.verify().message().contains().content("Q6: Zombie2 by anonymous") # hold on to q5 q6_id = channel.last_message_id @@ -775,22 +1610,27 @@ async def test_qanda(bot): # test unearthZombies: zombies found await dpytest.message("$unearthZombies", channel=channel) - assert dpytest.verify().message().contains().content( - "Found 1 zombies and assigned them ghost status.\n" - "To view them, use: $allChannelGhosts\n" - "To restore a question, use: $reviveGhost QUESTION_NUMBER") + assert ( + dpytest.verify() + .message() + .contains() + .content( + "Found 1 zombies and assigned them ghost status.\n" + "To view them, use: $allChannelGhosts\n" + "To restore a question, use: $reviveGhost QUESTION_NUMBER" + ) + ) # ghosts: 2, zombies: 0 # create final zombie - await dpytest.message("$ask \"Zombie3\" anonymous", channel=channel) - assert dpytest.verify().message().contains().content( - 'Q7: Zombie3 by anonymous') + await dpytest.message('$ask "Zombie3" anonymous', channel=channel) + assert dpytest.verify().message().contains().content("Q7: Zombie3 by anonymous") # hold on to q7 qz_id = channel.last_message_id qz = await channel.fetch_message(qz_id) - await dpytest.message("$answer 7 \"test\" anonymous", channel=channel) + await dpytest.message('$answer 7 "test" anonymous', channel=channel) # zomb-ify Q7 await qz.delete() @@ -799,15 +1639,19 @@ async def test_qanda(bot): # test deleting all answers for zombie await dpytest.message("$DALLAF 7", channel=channel) - assert dpytest.verify().message().contains().content( - 'deleted 1 answers for Q7') - assert dpytest.verify().message().contains().content( - 'Q7 is a zombie!') + assert dpytest.verify().message().contains().content("deleted 1 answers for Q7") + assert dpytest.verify().message().contains().content("Q7 is a zombie!") # test deleteAllQA: questions with and without answers, ghosts and zombies await dpytest.message("$deleteAllQA", channel=channel) - assert dpytest.verify().message().contains().content( - "Deleted 7 questions from the database, including 1 zombies and 2 ghosts.") + assert ( + dpytest.verify() + .message() + .contains() + .content( + "Deleted 7 questions from the database, including 1 zombies and 2 ghosts." + ) + ) # ------------------------- @@ -819,255 +1663,407 @@ async def test_qanda_errors(bot): # create channel and get user user = dpytest.get_config().members[0] guild = dpytest.get_config().guilds[0] - channel = await guild.create_text_channel('q-and-a') - gen_channel = await guild.create_text_channel('general') + channel = await guild.create_text_channel("q-and-a") + gen_channel = await guild.create_text_channel("general") irole = await guild.create_role(name="Instructor") await irole.edit(permissions=discord.Permissions(8)) role = discord.utils.get(guild.roles, name="Instructor") await dpytest.add_role(user, role) # Test asking a question in the wrong channel - msg = await dpytest.message("$ask \"Is this the right channel?\"", channel=gen_channel) + msg = await dpytest.message( + '$ask "Is this the right channel?"', channel=gen_channel + ) with pytest.raises(discord.NotFound): await gen_channel.fetch_message(msg.id) - assert dpytest.verify().message().contains().content( - 'Please send questions to the #q-and-a channel.') + assert ( + dpytest.verify() + .message() + .contains() + .content("Please send questions to the #q-and-a channel.") + ) # Tests unknown anonymous input (question) - await dpytest.message("$ask \"Who am I?\" wronganon", channel=channel) - assert dpytest.verify().message().contains().content( - 'Unknown input for *anonymous* option. Please type **anonymous**, **anon**, or leave blank.') + await dpytest.message('$ask "Who am I?" wronganon', channel=channel) + assert ( + dpytest.verify() + .message() + .contains() + .content( + "Unknown input for *anonymous* option. Please type **anonymous**, **anon**, or leave blank." + ) + ) # Tests incorrect use of ask command: missing args with pytest.raises(commands.MissingRequiredArgument): await dpytest.message("$ask", channel=channel) - assert dpytest.verify().message().contains().content( - 'To use the ask command, do: $ask \"QUESTION\" anonymous** \n ' - '(For example: $ask \"What class is this?\" anonymous)') + assert ( + dpytest.verify() + .message() + .contains() + .content( + 'To use the ask command, do: $ask "QUESTION" anonymous** \n ' + '(For example: $ask "What class is this?" anonymous)' + ) + ) # Test answering a question in the wrong channel - msga = await dpytest.message("$answer 1 \"Test\"", channel=gen_channel) + msga = await dpytest.message('$answer 1 "Test"', channel=gen_channel) with pytest.raises(discord.NotFound): await gen_channel.fetch_message(msga.id) - assert dpytest.verify().message().contains().content( - 'Please send answers to the #q-and-a channel.') + assert ( + dpytest.verify() + .message() + .contains() + .content("Please send answers to the #q-and-a channel.") + ) # Tests unknown anonymous input (answer) - await dpytest.message("$answer 1 \"A Thing\" wronganon", channel=channel) - assert dpytest.verify().message().contains().content( - 'Unknown input for *anonymous* option. Please type **anonymous**, **anon**, or leave blank.') + await dpytest.message('$answer 1 "A Thing" wronganon', channel=channel) + assert ( + dpytest.verify() + .message() + .contains() + .content( + "Unknown input for *anonymous* option. Please type **anonymous**, **anon**, or leave blank." + ) + ) # Tests answering a nonexistent question (answer) - await dpytest.message("$answer 100 \"nope\"", channel=channel) - assert dpytest.verify().message().contains().content( - 'No such question with the number: 100') + await dpytest.message('$answer 100 "nope"', channel=channel) + assert ( + dpytest.verify() + .message() + .contains() + .content("No such question with the number: 100") + ) # placeholder test for empty input (answer) - #await dpytest.message("$answer 100 \"\" ", channel=channel) - #assert dpytest.verify().message().contains().content('STRING GOES HERE') + # await dpytest.message("$answer 100 \"\" ", channel=channel) + # assert dpytest.verify().message().contains().content('STRING GOES HERE') # Tests incorrect use of answer command: no args with pytest.raises(commands.MissingRequiredArgument): await dpytest.message("$answer", channel=channel) - assert dpytest.verify().message().contains().content( - 'To use the answer command, do: $answer QUESTION_NUMBER \"ANSWER\" anonymous**\n ' - '(For example: $answer 2 \"Yes\")') + assert ( + dpytest.verify() + .message() + .contains() + .content( + 'To use the answer command, do: $answer QUESTION_NUMBER "ANSWER" anonymous**\n ' + '(For example: $answer 2 "Yes")' + ) + ) # Tests answering with bad input (answer) - await dpytest.message("$answer \"nope\" lol", channel=channel) - assert dpytest.verify().message().contains().content( - 'Please include a valid question number. EX: $answer 1 /"Oct 12/" anonymous') + await dpytest.message('$answer "nope" lol', channel=channel) + assert ( + dpytest.verify() + .message() + .contains() + .content( + 'Please include a valid question number. EX: $answer 1 /"Oct 12/" anonymous' + ) + ) # Test getAnswersFor in wrong channel msg = await dpytest.message("$getAnswersFor 1", channel=gen_channel) with pytest.raises(discord.NotFound): await gen_channel.fetch_message(msg.id) - assert dpytest.verify().message().contains().content( - 'Please use this command inside the #q-and-a channel.') + assert ( + dpytest.verify() + .message() + .contains() + .content("Please use this command inside the #q-and-a channel.") + ) - # Tests getting answers for nonexistent question + # Tests getting answers for nonexistent question await dpytest.message("$getAnswersFor 100", channel=channel) - assert dpytest.verify().message().contains().content( - 'No such question with the number: 100') + assert ( + dpytest.verify() + .message() + .contains() + .content("No such question with the number: 100") + ) # Tests getAnswersFor with bad input: no arg with pytest.raises(commands.MissingRequiredArgument): await dpytest.message("$getAnswersFor", channel=channel) - assert dpytest.verify().message().contains().content( - 'To use the getAnswersFor command, do: $getAnswersFor QUESTION_NUMBER\n ' - '(Example: $getAnswersFor 1)') + assert ( + dpytest.verify() + .message() + .contains() + .content( + "To use the getAnswersFor command, do: $getAnswersFor QUESTION_NUMBER\n " + "(Example: $getAnswersFor 1)" + ) + ) # Tests getting answers with bad input await dpytest.message("$getAnswersFor abc", channel=channel) - assert dpytest.verify().message().contains().content( - 'Please include a valid question number. EX: $getAnswersFor 1') + assert ( + dpytest.verify() + .message() + .contains() + .content("Please include a valid question number. EX: $getAnswersFor 1") + ) # Test that deleting answers does not work outside of QA msg = await dpytest.message("$DALLAF 1", channel=gen_channel) with pytest.raises(discord.NotFound): await gen_channel.fetch_message(msg.id) - assert dpytest.verify().message().contains().content( - 'Please use this command inside the #q-and-a channel.') + assert ( + dpytest.verify() + .message() + .contains() + .content("Please use this command inside the #q-and-a channel.") + ) # test deleting all answers with bad input: no args with pytest.raises(commands.MissingRequiredArgument): await dpytest.message("$DALLAF", channel=channel) - assert dpytest.verify().message().contains().content( - 'To use the deleteAllAnswersFor command, do: $DALLAF QUESTION_NUMBER\n ' - '(Example: $DALLAF 1)') + assert ( + dpytest.verify() + .message() + .contains() + .content( + "To use the deleteAllAnswersFor command, do: $DALLAF QUESTION_NUMBER\n " + "(Example: $DALLAF 1)" + ) + ) # test deleting all answers with bad input await dpytest.message("$DALLAF abc", channel=channel) - assert dpytest.verify().message().contains().content( - 'Please include a valid question number. EX: $DALLAF 1') + assert ( + dpytest.verify() + .message() + .contains() + .content("Please include a valid question number. EX: $DALLAF 1") + ) # test deleting all answers for a non-existent question await dpytest.message("$DALLAF 100", channel=channel) - assert dpytest.verify().message().contains().content( - 'No such question with the number: 100') + assert ( + dpytest.verify() + .message() + .contains() + .content("No such question with the number: 100") + ) # Test that deleting questions does not work outside of QA msg = await dpytest.message("$deleteQuestion 1", channel=gen_channel) with pytest.raises(discord.NotFound): await gen_channel.fetch_message(msg.id) - assert dpytest.verify().message().contains().content( - 'Please use this command inside the #q-and-a channel.') + assert ( + dpytest.verify() + .message() + .contains() + .content("Please use this command inside the #q-and-a channel.") + ) # test deleting questions with bad input: no args with pytest.raises(commands.MissingRequiredArgument): await dpytest.message("$deleteQuestion", channel=channel) - assert dpytest.verify().message().contains().content( - 'To use the deleteQuestion command, do: $deleteQuestion QUESTION_NUMBER\n ' - '(Example: $deleteQuestion 1') + assert ( + dpytest.verify() + .message() + .contains() + .content( + "To use the deleteQuestion command, do: $deleteQuestion QUESTION_NUMBER\n " + "(Example: $deleteQuestion 1" + ) + ) # test deleting question with bad input await dpytest.message("$deleteQuestion abc", channel=channel) - assert dpytest.verify().message().contains().content( - 'Please include a valid question number. EX: $deleteQuestion 1') + assert ( + dpytest.verify() + .message() + .contains() + .content("Please include a valid question number. EX: $deleteQuestion 1") + ) # test deleting a non-existent question await dpytest.message("$deleteQuestion 100", channel=channel) - assert dpytest.verify().message().contains().content( - 'Question number not in database: 100') + assert ( + dpytest.verify() + .message() + .contains() + .content("Question number not in database: 100") + ) # Test that deleting all QAs does not work outside of QA await dpytest.message("$deleteAllQA", channel=gen_channel) - assert dpytest.verify().message().contains().content( - 'Please use this command inside the #q-and-a channel.') + assert ( + dpytest.verify() + .message() + .contains() + .content("Please use this command inside the #q-and-a channel.") + ) # Test deleting all QAs without any questions await dpytest.message("$deleteAllQA", channel=channel) - assert dpytest.verify().message().contains().content( - 'No questions found in database.') + assert ( + dpytest.verify().message().contains().content("No questions found in database.") + ) # Test archiveQA: empty database - await dpytest.message("$archiveQA",channel=channel) - assert dpytest.verify().message().contains().content( - 'No questions found in database.') + await dpytest.message("$archiveQA", channel=channel) + assert ( + dpytest.verify().message().contains().content("No questions found in database.") + ) # Test that archiveQA does not work outside of QA await dpytest.message("$archiveQA", channel=gen_channel) - assert dpytest.verify().message().contains().content( - 'Please use this command inside the #q-and-a channel.') + assert ( + dpytest.verify() + .message() + .contains() + .content("Please use this command inside the #q-and-a channel.") + ) # Test channelGhost in the wrong channel await dpytest.message("$channelGhost 1", channel=gen_channel) - assert dpytest.verify().message().contains().content( - 'Please use this command inside the #q-and-a channel.') + assert ( + dpytest.verify() + .message() + .contains() + .content("Please use this command inside the #q-and-a channel.") + ) # Tests channelGhost: missing args with pytest.raises(commands.MissingRequiredArgument): await dpytest.message("$channelGhost", channel=channel) - assert dpytest.verify().message().contains().content( - 'To use the channelGhost command, do: $channelGhost QUESTION_NUMBER\n ' - '(Example: $channelGhost 1)') + assert ( + dpytest.verify() + .message() + .contains() + .content( + "To use the channelGhost command, do: $channelGhost QUESTION_NUMBER\n " + "(Example: $channelGhost 1)" + ) + ) # Tests channelGhost: invalid arg await dpytest.message("$channelGhost blah", channel=channel) - assert dpytest.verify().message().contains().content( - 'Please include a valid question number. EX: $channelGhost 1') + assert ( + dpytest.verify() + .message() + .contains() + .content("Please include a valid question number. EX: $channelGhost 1") + ) # Tests channelGhost: empty database await dpytest.message("$channelGhost 1", channel=channel) - assert dpytest.verify().message().contains().content( - 'No such question with the number: 1') + assert ( + dpytest.verify() + .message() + .contains() + .content("No such question with the number: 1") + ) # Test allChannelGhosts in the wrong channel await dpytest.message("$allChannelGhosts", channel=gen_channel) - assert dpytest.verify().message().contains().content( - 'Please use this command inside the #q-and-a channel.') + assert ( + dpytest.verify() + .message() + .contains() + .content("Please use this command inside the #q-and-a channel.") + ) # allChannelGhosts without any ghosts await dpytest.message("$allChannelGhosts", channel=channel) - assert dpytest.verify().message().contains().content( - 'No ghosts found in database.') + assert dpytest.verify().message().contains().content("No ghosts found in database.") # Test spooky in the wrong channel await dpytest.message("$spooky", channel=gen_channel) - assert dpytest.verify().message().contains().content( - 'Please use this command inside the #q-and-a channel.') + assert ( + dpytest.verify() + .message() + .contains() + .content("Please use this command inside the #q-and-a channel.") + ) # Test spooky: empty database await dpytest.message("$spooky", channel=channel) - assert dpytest.verify().message().contains().content( - "This channel isn't haunted.") + assert dpytest.verify().message().contains().content("This channel isn't haunted.") # Test unearthZombies in the wrong channel await dpytest.message("$unearthZombies", channel=gen_channel) - assert dpytest.verify().message().contains().content( - 'Please use this command inside the #q-and-a channel.') + assert ( + dpytest.verify() + .message() + .contains() + .content("Please use this command inside the #q-and-a channel.") + ) # Test unearthZombies: empty database await dpytest.message("$unearthZombies", channel=channel) - assert dpytest.verify().message().contains().content( - "No zombies detected.") + assert dpytest.verify().message().contains().content("No zombies detected.") # Test reviveGhost in the wrong channel await dpytest.message("$reviveGhost 1", channel=gen_channel) - assert dpytest.verify().message().contains().content( - 'Please use this command inside the #q-and-a channel.') + assert ( + dpytest.verify() + .message() + .contains() + .content("Please use this command inside the #q-and-a channel.") + ) # Tests reviveGhost: missing args with pytest.raises(commands.MissingRequiredArgument): await dpytest.message("$reviveGhost", channel=channel) - assert dpytest.verify().message().contains().content( - 'To use the reviveGhost command, do: $reviveGhost QUESTION_NUMBER\n ' - '(Example: $reviveGhost 1)') + assert ( + dpytest.verify() + .message() + .contains() + .content( + "To use the reviveGhost command, do: $reviveGhost QUESTION_NUMBER\n " + "(Example: $reviveGhost 1)" + ) + ) # Tests reviveGhost: invalid arg await dpytest.message("$reviveGhost blah", channel=channel) - assert dpytest.verify().message().contains().content( - 'Please include a valid question number. EX: $reviveGhost 1') + assert ( + dpytest.verify() + .message() + .contains() + .content("Please include a valid question number. EX: $reviveGhost 1") + ) # Test reviveGhost: empty database await dpytest.message("$reviveGhost 1", channel=channel) - assert dpytest.verify().message().contains().content( - "No such question with the number: 1") + assert ( + dpytest.verify() + .message() + .contains() + .content("No such question with the number: 1") + ) # Test ask: empty question - await dpytest.message("$ask \"\"", channel=channel) - assert dpytest.verify().message().contains().content( - "Please enter a valid question.") + await dpytest.message('$ask ""', channel=channel) + assert ( + dpytest.verify().message().contains().content("Please enter a valid question.") + ) # Test ask: whitepaces only - await dpytest.message("$ask \" \"", channel=channel) - assert dpytest.verify().message().contains().content( - "Please enter a valid question.") + await dpytest.message('$ask " "', channel=channel) + assert ( + dpytest.verify().message().contains().content("Please enter a valid question.") + ) # Test ask: one char question - await dpytest.message("$ask \"A\"", channel=channel) - assert dpytest.verify().message().contains().content( - "Question too short.") + await dpytest.message('$ask "A"', channel=channel) + assert dpytest.verify().message().contains().content("Question too short.") # Test answer: empty answer - await dpytest.message("$answer 1 \"\"", channel=channel) - assert dpytest.verify().message().contains().content( - "Please enter a valid answer.") + await dpytest.message('$answer 1 ""', channel=channel) + assert dpytest.verify().message().contains().content("Please enter a valid answer.") # Test answer: whitespaces only - await dpytest.message("$answer 1 \" \"", channel=channel) - assert dpytest.verify().message().contains().content( - "Please enter a valid answer.") + await dpytest.message('$answer 1 " "', channel=channel) + assert dpytest.verify().message().contains().content("Please enter a valid answer.") # -------------------- @@ -1084,21 +2080,38 @@ async def test_review_qs(bot): await dpytest.add_role(user, role) # Test adding a question - await dpytest.message("$addQuestion \"What class is this?\" \"CSC510\"", member=user) - assert dpytest.verify().message().contains().content( - 'A new review question has been added! Question: What class is this? and Answer: CSC510.') + await dpytest.message('$addQuestion "What class is this?" "CSC510"', member=user) + assert ( + dpytest.verify() + .message() + .contains() + .content( + "A new review question has been added! Question: What class is this? and Answer: CSC510." + ) + ) # Test getting a question await dpytest.message("$getQuestion", member=user) - assert dpytest.verify().message().contains().content( - "What class is this? \n ||CSC510||") + assert ( + dpytest.verify() + .message() + .contains() + .content("What class is this? \n ||CSC510||") + ) # Test error with pytest.raises(Exception): - await dpytest.message("$addQuestion \"Is this a test question?\"", member=user) - assert dpytest.verify().message().contains().content( - 'To use the addQuestion command, do: $addQuestion \"Question\" \"Answer\" \n' - '(For example: $addQuestion \"What class is this?\" "CSC510")') + await dpytest.message('$addQuestion "Is this a test question?"', member=user) + assert ( + dpytest.verify() + .message() + .contains() + .content( + 'To use the addQuestion command, do: $addQuestion "Question" "Answer" \n' + '(For example: $addQuestion "What class is this?" "CSC510")' + ) + ) + # -------------------------------- # Test polling: poll @@ -1107,27 +2120,38 @@ async def test_review_qs(bot): async def test_poll(bot): user = dpytest.get_config().members[0] guild = dpytest.get_config().guilds[0] - channel = await guild.create_text_channel('polls') + channel = await guild.create_text_channel("polls") # Test poll: no input await dpytest.message("$poll", channel=channel) - assert dpytest.verify().message().contains().content( - "Please enter a question for your poll.") + assert ( + dpytest.verify() + .message() + .contains() + .content("Please enter a question for your poll.") + ) # Test poll: whitespace await dpytest.message("$poll ", channel=channel) - assert dpytest.verify().message().contains().content( - "Please enter a question for your poll.") + assert ( + dpytest.verify() + .message() + .contains() + .content("Please enter a question for your poll.") + ) # Test poll: question too short await dpytest.message("$poll ab", channel=channel) - assert dpytest.verify().message().contains().content( - "Poll question too short.") + assert dpytest.verify().message().contains().content("Poll question too short.") # Test poll: student await dpytest.message("$poll abc", channel=channel) - assert dpytest.verify().message().contains().content( - "**POLL by Student**\n\nabc\n** **") + assert ( + dpytest.verify() + .message() + .contains() + .content("**POLL by Student**\n\nabc\n** **") + ) await guild.create_role(name="Instructor") role = discord.utils.get(guild.roles, name="Instructor") @@ -1135,8 +2159,12 @@ async def test_poll(bot): # Test poll: instructor await dpytest.message("$poll abc", channel=channel) - assert dpytest.verify().message().contains().content( - "**POLL by Instructor**\n\nabc\n** **") + assert ( + dpytest.verify() + .message() + .contains() + .content("**POLL by Instructor**\n\nabc\n** **") + ) # Test poll: reactions msgid = channel.last_message_id @@ -1148,53 +2176,168 @@ async def test_poll(bot): assert len(msg.reactions) == 1 - # -------------------------------- # Test polling: quizpoll # -------------------------------- @pytest.mark.asyncio async def test_quizpoll(bot): - #user = dpytest.get_config().members[0] - #guild = dpytest.get_config().guilds[0] - #channel = await guild.create_text_channel('polls') + # user = dpytest.get_config().members[0] + # guild = dpytest.get_config().guilds[0] + # channel = await guild.create_text_channel('polls') # Test quizpoll: no input with pytest.raises(commands.MissingRequiredArgument): await dpytest.message("$quizpoll") - assert dpytest.verify().message().contains().content( - 'To use the quizpoll command, do: $quizpoll "TITLE" [option1] [option2] ... [option6]\n ' - 'Be sure to enclose title with quotes and options with brackets!\n' - 'EX: $quizpoll "I am a poll" [Vote for me!] [I am option 2]') + assert ( + dpytest.verify() + .message() + .contains() + .content( + 'To use the quizpoll command, do: $quizpoll "TITLE" [option1] [option2] ... [option6]\n ' + "Be sure to enclose title with quotes and options with brackets!\n" + 'EX: $quizpoll "I am a poll" [Vote for me!] [I am option 2]' + ) + ) # Test quizpoll: title is whitespace - await dpytest.message("$quizpoll \" \" [a] [b] [c] [d] [e] [f]") - assert dpytest.verify().message().contains().content( - "Please enter a valid title.") + await dpytest.message('$quizpoll " " [a] [b] [c] [d] [e] [f]') + assert dpytest.verify().message().contains().content("Please enter a valid title.") # Test quizpoll: title is too short - await dpytest.message("$quizpoll \"a\" [a] [b] [c] [d] [e] [f]") - assert dpytest.verify().message().contains().content( - "Title too short.") + await dpytest.message('$quizpoll "a" [a] [b] [c] [d] [e] [f]') + assert dpytest.verify().message().contains().content("Title too short.") # Test quizpoll: too few options - await dpytest.message("$quizpoll \"TITLE\" [a]") - assert dpytest.verify().message().contains().content( - "Polls need at least two options.") + await dpytest.message('$quizpoll "TITLE" [a]') + assert ( + dpytest.verify() + .message() + .contains() + .content("Polls need at least two options.") + ) # Test quizpoll: too many options - await dpytest.message("$quizpoll \"TITLE\" [a] [b] [c] [d] [e] [f] [g]") - assert dpytest.verify().message().contains().content( - "Polls cannot have more than six options.") + await dpytest.message('$quizpoll "TITLE" [a] [b] [c] [d] [e] [f] [g]') + assert ( + dpytest.verify() + .message() + .contains() + .content("Polls cannot have more than six options.") + ) # Test quizpoll: option is empty - await dpytest.message("$quizpoll \"TITLE\" [] [b] [c]") - assert dpytest.verify().message().contains().content( - "Options cannot be blank or whitespace only.") + await dpytest.message('$quizpoll "TITLE" [] [b] [c]') + assert ( + dpytest.verify() + .message() + .contains() + .content("Options cannot be blank or whitespace only.") + ) - e = discord.Embed(title="**TITLE**", - description="\n\n🇦 a\n\n🇧 b\n\n🇨 c", - colour=0x83bae3) + e = discord.Embed( + title="**TITLE**", + description="\n\n🇦 a\n\n🇧 b\n\n🇨 c", + colour=0x83BAE3, + ) # Test quizpoll embed - await dpytest.message("$quizpoll \"TITLE\" [a] [b] [c]") + await dpytest.message('$quizpoll "TITLE" [a] [b] [c]') assert dpytest.verify().message().embed(e) + + +# -------------------------------- +# Test calendar: subscribe and remove +# -------------------------------- +@pytest.mark.asyncio +async def test_calendar(bot): + user = dpytest.get_config().members[0] + guild = dpytest.get_config().guilds[0] + irole = await guild.create_role(name="Instructor") + await irole.edit(permissions=discord.Permissions(8)) + role = discord.utils.get(guild.roles, name="Instructor") + await dpytest.add_role(user, role) + + # Test subscribeCalendar success + await dpytest.message("$subscribeCalendar johndoe@gmail.com") + assert ( + dpytest.verify() + .message() + .contains() + .content("Added johndoe@gmail.com to the calendar.") + ) + + # Test subscribeCalendar failure + await dpytest.message("$subscribeCalendar johndoe") + assert ( + dpytest.verify() + .message() + .contains() + .content("Error adding user: johndoe is not a valid email.") + ) + + # Test removeCalendar failure + await dpytest.message("$removeCalendar johndoe") + assert ( + dpytest.verify() + .message() + .contains() + .content("User johndoe was not found in the calendar's permissions.") + ) + + # Test removeCalendar failure + await dpytest.message("$removeCalendar johndoe@gmail.com") + assert ( + dpytest.verify() + .message() + .contains() + .content("User johndoe@gmail.com has been removed from the calendar.") + ) + + +@pytest.mark.asyncio +async def test_get_calendar_downloads(bot): + await dpytest.message("$clearCalendar") + assert dpytest.verify().message().contains().content("Calendar has been cleared") + await dpytest.message("$getPdfDownload") + assert dpytest.verify().message().contains().content("No upcoming events found.") + date = datetime.now(ZoneInfo("America/New_York")) + timedelta(days=1) + caldate = date.astimezone(ZoneInfo("UTC")) + dateiso = date.isoformat(timespec="seconds") + caldateiso = caldate.isoformat(timespec="seconds")[:-6] + await dpytest.message(f"$addCalendarEvent HW3 CSC510 {caldateiso}Z") + assert dpytest.verify().message().contains().content("Event HW3 added to calendar!") + + await dpytest.message("$getPdfDownload") + calendar_path = os.getenv("CALENDAR_PATH") + + reader = PdfReader(f"{calendar_path}calendar.pdf") + + page = reader.pages[0] + + text = page.extract_text() + text = text.split("\n") + + assert len(text) == 7 + assert text[0] == "Summary" + assert text[1] == "Start" + assert text[2] == "End" + assert text[3] == "0" + assert text[4] == "HW3" + assert text[5][:10] == dateiso[:10] + assert text[5][10:16] == dateiso[10:16] + + await dpytest.message("$getiCalDownload") + + # referenced https://stackoverflow.com/questions/3408097/parsing-files-ics-icalendar-using-python + + caldata = open(f"{calendar_path}ical.ics").read() + + for cal in vobject.readComponents(caldata): + for component in cal.components(): + if component.name == "VEVENT": + assert component.description.valueRepr() == "CSC510" + assert component.summary.valueRepr() == "HW3" + assert component.dtstart.valueRepr().month == caldate.month + assert component.dtstart.valueRepr().year == caldate.year + assert component.dtstart.valueRepr().day == caldate.day + assert component.dtstart.valueRepr().hour == caldate.hour