diff --git a/.DS_Store b/.DS_Store index 54468648c..a86d98c0f 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml index 1d75c3418..130fc190f 100644 --- a/.github/workflows/black.yml +++ b/.github/workflows/black.yml @@ -1,12 +1,27 @@ -name: Lint +name: Format code -on: [push, pull_request] +# Controls when the workflow will run +on: + # Triggers the workflow on push or pull request events but only for the main branch + push: + branches: [ main ] + pull_request: + branches: [ main ] jobs: - lint: + format: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + - uses: psf/black@stable with: - options: "--check --verbose" + options: "--verbose" src: "./src" + - name: Sort imports with isort + run: | + pip install isort + isort . + - name: Remove unused imports with autoflake + run: | + pip install autoflake + autoflake --in-place --remove-all-unused-imports --remove-unused-variables --recursive . diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2f554f4b3..3ea009484 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,9 +6,9 @@ name: CI on: # Triggers the workflow on push or pull request events but only for the main branch push: - branches: [ pratik_testing ] + branches: [ main ] pull_request: - branches: [ pratik_testing ] + branches: [ main ] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -53,13 +53,19 @@ jobs: - name: Install pylint run: | pip install pylint + - name: Run pylint + run: | + pylint -r y src/ - name: Run tests env: API_TOKEN: ${{ secrets.API_TOKEN }} CHAT_ID: ${{ secrets.CHAT_ID }} + DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }} + CHANNEL_ID: ${{secrets.CHANNEL_ID }} run: | export PYTHONPATH=src python -m pytest -v --cov=src/ --cov-report=xml - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v2 - + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index d7a74e544..735e43f86 100644 --- a/.gitignore +++ b/.gitignore @@ -128,4 +128,7 @@ dmypy.json # Pyre type checker .pyre/ -temp.txt \ No newline at end of file +temp.txt + +# pickle files +.pickle \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 1e948f2aa..41dd9ebee 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -59,8 +59,7 @@ representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -rsinha3@ncsu.edu. +reported to the community leaders responsible for enforcement. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e3a2a0c09..5fd6f9e99 100755 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,101 +1,94 @@ -# Contributing to SlashBot +# Contributing to FinBot -Follow the set of guidelines below to contribute to SlashBot! +Thank you for your interest in contributing to our project. Please make sure to read and follow these guidelines for a smooth and effective contribution process. ## Code of Conduct -By participating, you are expected to uphold this code. Please report unacceptable behavior to psomash@ncsu.edu or secheaper@gamil.com. +Everyone participating in this project needs to abide by the aPAS - A Personal Agile Scheduler Code of Conduct that can be found under the main repository link as a [CODE_OF_CONDUCT.md](https://github.com/vyshnavi-adusumelli/FinBot/blob/main/CODE_OF_CONDUCT.md) file. 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](https://github.com/vyshnavi-adusumelli/FinBot/blob/main/README.md). -Prerequistes required before starting this project - -1. Must be a graduate student at NC State University -2. Must be a student in Software Engineering Course in Fall 2021 -3. Have proficiency in Python - -## How can I Contribute - +## How can You Contribute ### Reporting Bugs -This section guides you through submitting a bug report for SlashBot. -Following these guidelines helps maintainers and the community understand your report, reproduce the behavior and find related reports. +This section guides you through submitting a bug report for FinBot. Following these guidelines helps maintainers and the community understand your report, reproduce the behavior and find related reports. -Before Submitting A Bug Report +### Before Submitting A Bug Report -Check the debugging guide +- Check the debugging guide. -Check the FAQs on the forum for a list of common questions and problems. -Determine which repository the problem should be reported in. +- Review the FAQs on the forum for common questions and problems. -Perform a cursory search to see if the problem has already been reported. +- Perform a cursory search to see if the problem has already been reported. -## How Do I Submit A (Good) Bug Report? +- When you are creating a bug report, please ensure that you include as many details as possible to understand the issue. -Use a clear and descriptive title for the issue to identify the problem. +### How Do I Submit A (Good) Bug Report? -Describe the exact steps which reproduce the problem in as many details as possible. +- Use a clear and descriptive title for the issue to identify the problem. -Provide specific examples to demonstrate the steps. +- Describe the exact steps which reproduce the problem in as many details as possible. -Describe the behavior you observed after following the steps and point out what exactly is the problem with that behavior. +- Provide specific examples to demonstrate the steps. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use Markdown code blocks. -Explain which behavior you expected to see instead and why. +- If the problem is related to performance or memory, then ensure that you include a CPU profile capture with your report. -Include screenshots and animated GIFs which show you following the described steps and clearly demonstrate the problem. +- Include screenshots and animated GIFs which show you following the described steps and clearly demonstrate the problem. -If the problem is related to performance or memory, include a CPU profile capture with your report. +- If the problem wasn't triggered by a specific action, describe what you were doing before the problem happened and share more information using the guidelines below. -## Pull Requests +### Suggesting Enhancements -The process described here has several goals: +This section guides you through submitting a suggestion for FinBot, including completely new features and minor improvements to existing functionality. -Maintain the projects quality +Enhancement suggestions are tracked as GitHub issues. After you've determine your enhancement suggestion, create an issue on that repository and provide with information like title, step-by-step description, specific examples. Giving more detailed information will help us understand the suggestion better. -Fix problems that are important to users +Provide details like +- What is the enhancement? +- Suggestions to implement the enhancement -Enable a sustainable system for the projects maintainers to review contributions +We welcome suggestions to improve the bot; please add them to the TO-DO list. -## Tips to Extend +### Pull Requests -Check the Projects tab for TO-DO list and pick the feature you find interesting to work on. +Contributing through pull requests is essential for maintaining project quality. Here's how you can create a pull request: -Create a branch and implement the feature in Python using Telegram bot and test it locally. +- Check the Projects tab for the TO-DO list and pick/create a feature to work on. -Write corresponding test cases to ensure it is not breaking the existing system. +- Create a branch and implement the feature in Python, and test it locally. -Create pull request and request for the code review. Once the request is approved, merge to main. +- Write corresponding test cases to ensure it doesn't break the existing system. + +- Create a pull request and request a code review. Once approved, merge it into the main branch. -Any suggestions to improve the bot is appreciated. Please add it to the TO-DO list. +Key features to pay attention to -## More tips for developers -### Heroku deployment -The bot is deployed on [Heroku](https://www.heroku.com/), a website used to host source code and apps. +- Maintain the projects quality -Quoted directly from their page: +- Fix problems that are important to users + +- Enable a sustainable system for the projects maintainers to review contributions + +- Add a description of the modification. Add images for the modification if possible. + +- Insert a clear and descriptive title. -"Heroku is a platform as a service (PaaS) that enables developers to build, run, and operate applications entirely in the cloud." +## Style Guides +### Git Commit Messages -#### Why this is useful +- Describe why any particular modification is being made. -Before, users had to download source code, insert their API key from telegram, and then run the python file. -This can lead to both user error and error within the source code. By deploying it on heroku, you ensure that -the code is available anywhere, at anytime, without having to download files. +- Give a detailed description about the limitations of current code. +- Use the imperative mood ("Move cursor to..." not "Moves cursor to...") -#### How we created the bot +- Limit the first line to 72 characters or less -1. A heroku account was created with the shared mydollarbot@gmail.com credentials -2. A new app was created called my_dollar_bot. -3. Within github, we added a [new action](.github/workflows/deploy.yml) to deploy to the heroku bot -4. For every push, the source code is deployed to heroku, and python code/bot.py is executed, starting the bot +- Link an issue to the change -This way, if users want to use the bot without developing, they can simply use this bot instead of having to run the -application locally. +### Python Style Guides -#### How to develop the heroku bot server +All Python code is linted with Pylint. Ensure that before you commit any changes, your code passes all the default pylint checks and pipeline checks. Pylint can be installed with pip install pylint. -- Follow steps 1-3 above, except replace with your own email. Install Heroku cli [here](https://devcenter.heroku.com/articles/heroku-cli#download-and-install). -- Within github, add a secret for the heroku api key. -- Create a new CI/CD pipeline (refer our yaml file [here](.github/workflows/deploy.yml)) and set up github actions. -- Create a new Procfile and add `worker: python code/bot.py`. Refer ours [here](https://github.com/mtkumar123/MyDollarBot/blob/main/Procfile) -- Within your heroku dashboard, you can view logs for the bot to understand well the deployment is running. You can also run the command `heroku logs` or `heroku logs -t` +## Need Additional Help? +Due to any reason, if you feel like you have reservations related to the process, feel free to reach us out at [vadusum@ncsu.edu] Github process can be a bit complex and we don't want to lose your valuable contributions because of that reason. We are extremely glad that you have visited us and will make our project much better. diff --git a/README.md b/README.md index 5ad8d3a88..f7efea138 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,73 @@ -# :money_with_wings: SlashBot -
-

-Expense tracking made easy! -


![MIT license](https://img.shields.io/badge/License-MIT-green.svg) -![GitHub](https://img.shields.io/github/languages/top/secheaper/slashbot?color=red&label=Python&logo=Python&logoColor=yellow) -![GitHub contributors](https://img.shields.io/github/contributors/secheaper/slashbot) +![GitHub](https://img.shields.io/github/languages/top/vyshnavi-adusumelli/FinBot?color=red&label=Python&logo=Python&logoColor=yellow) +![GitHub contributors](https://img.shields.io/github/contributors/vyshnavi-adusumelli/FinBot) [![DOI](https://zenodo.org/badge/431190543.svg)](https://zenodo.org/badge/latestdoi/431190543) [![Platform](https://img.shields.io/badge/Platform-Telegram-blue)](https://desktop.telegram.org/) -[![codecov](https://codecov.io/gh/secheaper/slashbot/branch/main/graph/badge.svg?token=YCKWZTHO7O)](https://codecov.io/gh/secheaper/slashbot) -[![Actions Status](https://github.com/mtkumar123/MyDollarBot/workflows/CI/badge.svg)](https://github.com/mtkumar123/MyDollarBot/actions) -![github workflow](https://github.com/mtkumar123/MyDollarBot/actions/workflows/black.yml/badge.svg) -![Discord](https://img.shields.io/discord/879343473940107264?color=blueviolet&label=Discord%20Discussion%20Chat) -![Lines of code](https://img.shields.io/tokei/lines/github/secheaper/slashbot?color=9cf) -![Version](https://img.shields.io/github/v/release/secheaper/slashbot?color=ff69b4&label=Version) -![GitHub issues](https://img.shields.io/github/issues-raw/secheaper/slashbot) -![GitHub closed issues](https://img.shields.io/github/issues-closed-raw/secheaper/slashbot) +[![Platform](https://img.shields.io/badge/Platform-Discord-blue)](https://discord.com/) +[![codecov](https://codecov.io/gh/vyshnavi-adusumelli/FinBot/branch/main/graph/badge.svg?token=YCKWZTHO7O)](https://app.codecov.io/gh/vyshnavi-adusumelli/FinBot/) +[![Actions Status](https://github.com/vyshnavi-adusumelli/FinBot/workflows/CI/badge.svg)](https://github.com/vyshnavi-adusumelli/FinBot/actions) +![github workflow](https://github.com/vyshnavi-adusumelli/FinBot/actions/workflows/black.yml/badge.svg) +![Lines of code](https://tokei.rs/b1/github/vyshnavi-adusumelli/FinBot) +![Version](https://img.shields.io/github/v/release/vyshnavi-adusumelli/FinBot?color=ff69b4&label=Version) +![GitHub issues](https://img.shields.io/github/issues-raw/vyshnavi-adusumelli/FinBot) +![GitHub closed issues](https://img.shields.io/github/issues-closed-raw/vyshnavi-adusumelli/FinBot)
-## Demo Video +# FinBot - Because your financial future deserves the best! -https://youtu.be/NBihyIU13pw +You wake up, brew a fresh cup of coffee, and start your day. You're excited because today is the day you take control of your finances like never before. How? Say hello to FinBot, your ultimate financial companion. With simple commands, it transforms your financial story into one of motivation, empowerment, and control. -## About SlashBot +And the best part? FinBot is your multi-platform financial sidekick, available on both Discord and Telegram. That means no matter where you are, it's there to assist you in recording your expenses seamlessly. +
+

+ +

+
-SlashBot is an easy-to-use Telegram Bot that assists you in recording your daily expenses on a local system without any hassle. -With simple commands, this bot allows you to: -- Add/Record a new spending -- Show the sum of your expenditure for the current day/month -- Display your spending history -- Clear/Erase all your records -- Edit/Change any spending details if you wish to -- Download your expenditure history in the CSV format -- Visualize your spendings in the form of graphs/pie chart using the /chart option -- Email the history CSV file to yourself -- See the total daily/monthly expenditure in different currencies - -Check out the bot here: https://t.me/ncsuBot +## Demo Video ---- -Sample demos are shown below. They are run on a local machine. +https://www.youtube.com/watch?v=LkZGFGU5B6I + +## :money_with_wings: About FinBot -- [:information_desk_person: Sample Demos](#information_desk_person-Sample-Demos) +FinBot is a user-friendly Telegram and Discord bot designed to simplify your daily expense recording on a local system effortlessly. By offering the bot on two popular messaging platforms, we tap into a larger and diverse user base, increasing the potential reach of the service and enhancing user experience. +

+ +

---- +With simple commands, this bot allows you to: -# :star: Whats New +📝 **Add/Record a new spending:** As you sip that morning coffee, effortlessly log your expenses, no matter how small or significant. Every expense adds up, and FinBot ensures you don't miss a thing. -### Release Version 1.2.1 +💡 **Display your expenditure for the current day/month:** With FinBot, you're never in the dark about your spending. Get real-time insights on your daily and monthly expenses, motivating you to stay on budget and crush your financial goals. -- See your total daily/monthly expenditure in differet currencies using the /displayDifferentCurrency command -- Download your spendings history CSV file using the /download command -- Email the monthly spendings history to yourself using the /sendEmail command -- User can now get a message when the monthly budget is exhausted. -- Details for testing requirements added in README.md +🔍 **Show your spending history:** Ever wondered where your money disappears to? FinBot provides a detailed spending history that tells a story of your financial habits. It's a tale of lessons and opportunities for improvement. +🗑️ **Delete/Erase all your records:** Made an error or just want to start afresh? It's as simple as a command, a chance to correct your narrative without any hassle. -### Release Version 1.2.0 +🔧 **Edit/Change any spending details:** Life is full of surprises, and sometimes expenses change. FinBot adapts with you, offering easy editing options to keep your story accurate. -- Visualize your spendings in the form of graphs -- The User can now see his expenses across various categories in the form of graphs along with pie charts. -- Just go on adding multiple spendings using /add and type /chart to see the spendings in the form of graphs. -- More Badges added in Repository +📊 Set Your Budget: Take full control of your finances by defining and tracking your budget with FinBot. It's the proactive step that puts you firmly in the driver's seat of your financial journey. +📈 **Visualize your spending:** Numbers can be daunting, but FinBot transforms them into a captivating visual experience. Use the '/chart' option to see your spending as graphs and pie charts. This punchline to your story helps you spot trends and make smarter financial choices. +# :star: What's New? +- **Multi-Platform Functionality:** With our latest update, FinBot is no longer limited to Telegram; we've extended its capabilities to Discord as well, providing you with a seamless multi-platform experience. +- **Unified User Experience:** Whether you prefer Telegram or Discord, you can now enjoy the same user-friendly experience for recording your daily expenses, managing your budget, and more. +- **Cross-Platform Expense Recording:** You can now add/record new spending, view your expenditure for the day/month, display your spending history, and edit/change spending details on both Telegram and Discord. +- **Data Visualization:** Our data visualization feature is now available on both platforms. Use the '/chart' (Telegram) or '#chart' (Discord) option to visualize your expenses in the form of graphs and pie charts, gaining deeper insights into your spending habits. Further in Discord, the bot supports the start and end dates within which the visuals can be viewed enabling customization. - - # :rocket: Installation Guide ## 💻For developers @@ -90,37 +81,142 @@ Sample demos are shown below. They are run on a local machine. ``` pip install -r requirements.txt ``` -5. Download and install the Telegram desktop application for your system from the following site: https://desktop.telegram.org/ +5. Ensure that you export the PYTHONPATH variable to the main project folder in the environment variables. This is essential for your Python scripts to locate and import the project modules correctly. + +### Telegram Setup +1. Begin by downloading and installing the Telegram desktop application for your system from the official website: [Telegram Desktop](https://desktop.telegram.org/). + +2. Log in to your Telegram account. In the Telegram app, search for "BotFather." Click on "Start" to initiate a conversation with BotFather. Enter the following command: /newbot + +3. Follow the on-screen instructions to choose a name for your bot. After naming your bot, select a username for your bot, which should end with "bot" (as instructed in the Telegram interface). -6. Once you login to your Telegram account, search for "BotFather" in Telegram. Click on "Start" --> enter the following command: +4. Once you've configured your bot, BotFather will confirm the creation and provide you with an API TOKEN. Copy this token for future use. + +5. In the Telegram app, search for your newly created bot by entering its username. Open the bot's chat to access its details. + +6. Right-click on the chat with your bot in the Telegram app. Copy the chat's ID as CHAT_ID for future reference. + +7. Set both the CHAT_ID and API_TOKEN as environment variables on your computer. These variables are essential for your bot to interact with Telegram. + +8. Open your terminal and navigate to the project's root folder. Run the command: ```python src/TeleBot.py``` in the terminal. A successful run will generate a message in your terminal, indicating that "TeleBot: Started polling." + +9. After a successful run, go to your bot on Telegram. Enter the "/start" or "/menu" command to initialize your bot, and you're all set to track your expenses! + +### Discord Setup + +1. Start by downloading and installing the Discord desktop application for your system from the following official website: [Discord Download](https://discord.com/download). + +2. Visit the [Discord Developer Portal](https://discord.com/developers/applications) and create a new application. Follow the steps in this guide to create your Discord bot: [Creating Your First Bot](https://guide.pycord.dev/getting-started/creating-your-first-bot). + +3. Give your bot a unique name and customize its avatar. Important: Ensure that your bot's name does not include "Discord." Applications with "Discord" in their name may not function correctly. + +4. Once you've created your bot, it will be provided with a DISCORD_TOKEN. Make sure to copy this token as you'll need it later. + +5. From the Discord Developer Portal, configure your bot with the necessary permissions and rights. This ensures it can interact with servers and channels. + +6. Create a server on Discord where your bot will operate. Inside this server, create a dedicated channel for your bot. Copy the CHANNEL_ID of this channel for future reference. + +7. Set the DISCORD_TOKEN and CHANNEL_ID as environment variables on your computer. These variables will be used by your bot to connect to the correct server and channel. + +8. Open your terminal and navigate to the project's root folder. Run the command: ```python src/DiscordBot.py``` in the terminal. A successful run will generate a message in your terminal, indicating that "Shard ID None has connected to Gateway." + +9. After a successful connection, go to your bot on Discord and enter the "#menu" command. Your bot is now ready to track your expenses! + +## Testing with Pytest + +1. Ensure that all necessary environment variables are correctly set on your computer. These variables are crucial for the functioning of your bot and test environment: + + - DISCORD_TOKEN: Your Discord bot's token. + - CHANNEL_ID: The ID of the Discord channel your bot operates in. + - API_TOKEN: The API token for your Telegram bot. + - CHAT_ID: The ID of the chat where your Telegram bot operates. + - PYTHONPATH: Set the PYTHONPATH variable to the main project folder. This helps your Python scripts locate and import project modules correctly. + +2. Navigate to the FinBot/test/unit folder in your project directory. In your terminal, run the following command: ``` - /newbot + python -m pytest ``` -7. Follow the instructions on screen and choose a name for your bot. Post this, select a username for your bot that ends with "bot" (as per the instructions on your Telegram screen) + After running the tests, you should see a summary indicating the number of test failures and passes. -8. BotFather will now confirm the creation of your bot and provide a TOKEN to access the HTTP API - copy this token for future use. +# :information_desk_person: Use cases -9. Search for "Edit the system environment variables" on your local computer. Click on Environment Variables and create a new System Variable called "API_TOKEN" and paste the token copied in step 8. +## Discord -10. In the Telegram app, search for your newly created bot by entering the username and open the same. Once this is done, go back to the terminal session. -Make sure you export the PYTHONPATH variable to the main project folder - ``` - python src/bot.py -``` -11. A successful run will generate a message on your terminal that says "TeleBot: Started polling." -12. Post this, navigate to your bot on Telegram, enter the "/start" or "/menu" command, and you are all set to track your expenses! +### Menu +View all the commands Finbot offers to manage your expenses + +

+ +1. This automatically appears when the bot runs. +2. It can be invoked again using `#menu` command. + +### Budget +Managing your monthly budget - increase/ decrease + +

+ +1. Enter the `#budget` command +2. Enter the new budget amount (must be greater than 0) + +### History +View the entire history of the spendings till date + +

+ +1. Enter the `#history` command + +### Add +Record the money spent on any of the categories as a transaction + +

+ +1. Enter the `#add` command +2. Enter the date of the transaction in MM-DD-YYYY format +3. Select the category to add +4. Enter the amount spent + +### Delete +Delete a transaction that has been recorded + +

+ +1. Enter the `#delete` command +2. Based on what records you want to delete - + 1. Day: enter the day to delete in MM-DD-YYYY format + 2. Month: enter the month to delete in MM-YYYY format + 3. All: enter `All` +3. The records will be displayed. Enter YES to confirm, or NO to cancel + +### Edit +Edit or correct a record that has been miss-entered + +

-For more info on deployment(Heroku), check out the doc [here](https://github.com/mtkumar123/MyDollarBot/blob/main/CONTRIBUTING.md#more-tips-for-developers) +1. Enter the `#edit` command +2. Enter the date (MM-DD-YYYY), value, and category of the transaction miss-entered +3. Specify what part of the transaction to edit (either date, category, or value) +4. Enter the new value + +### Display +Display the expenditure for the day or for the current month to keep track of your expenses and hence the monthly budget + +

+1. Enter the `#display` command +2. Select the option - day or month for which you want to view the expenses -## 💻For testing with Pytest -1. Some modules in testing require CHAT_ID environment variable to be set. -2. This is the specific ID that is maintained for your chat with the Bot. -3. While running the bot.py , get this id from line 72 and set it in your system environment variables. -4. This should ensure effective running of all tests. +### Visualization in the form of graphs +Visualize or track your expenses in the form of bar charts and pie charts + +

+1. Make sure you have a transaction history. +2. Enter the `#chart` command. +3. Enter the start date in MM-DD-YYYY format. +4. Enter the end date in MM-DD-YYYY format +5. Charts for the spending that fall in this duration will be generated. -# :information_desk_person: Sample Demos +## Telegram ### Budget @@ -131,7 +227,6 @@ I want to increase/decrease my monthly budget. 1. Enter the `/budget` command 2. Enter the new budget amount (must be greater than 0) - ### Add I just spent money and want to mark it as a transaction! @@ -167,40 +262,6 @@ Oh no! I entered a transaction but entered the wrong category! 3. Specify what part of the transaction to edit (either date, category, or value) 4. Enter in a new value -### Adding transactions from CSV and displaying chart - -I want to add transactions from a CSV my bank gave me, and visalize my spendings - -

- - -1. Drag the .csv file into the telegram chat, and press send -2. For each transaction, classify the category - 1. The application will remember these mappings -3. Enter the `/chart` command - -### Download History - -I want a CSV file of all my transactions. - -

- -1. Make sure you have a transaction history. -2. Enter the `/download` command. -3. A CSV file will be sent with your history. - -### See total Expenditure in different currencies - -I want to convert my total daily or monthly expenditure in a different currency. - -

- -1. Enter the /displayDifferentCurrency command -2. Choose from the category of day or month -3. Next, Choose your currency from the options -4. You will get the converted price in that currency - - ### Visualization in the form of graphs I want to see my spendings in the form of graphs @@ -211,37 +272,30 @@ I want to see my spendings in the form of graphs 2. Enter the `/chart` command. 3. You will see multiple visualizations for your spending -### SendEmail - -I want to send myself an email for the monthly expenditure - - -

- -1. Make sure you have a transaction history. -2. Enter the `/sendEmail` command. -3. Type the intended email address -4. You will get an email with the history file as attachment - # :grey_question: Documentation -Thorough documentation of all methods and classes can be found at [Github Pages](https://mtkumar123.github.io/MyDollarBot/) +Thorough documentation of all methods and classes can be found at # :construction: Road Map -Our ideas for new features that can be implemented to make this project better can be seen in our RoadMap project board. -[Road Map](https://github.com/secheaper/slashbot/projects/1) +Our implementation has been tracked in a project board which can be viewed here - +[FinBot Developer board](https://github.com/users/vyshnavi-adusumelli/projects/2) +Some possible future enhancements are as follows: +1. Generative AI integration with this software +2. Category management functionality in Discord. +3. Convert into a Dockerized application. +4. Calender widget for discord Bot. :heart: Acknowledgements --- -We would like to thank Dr. Timothy Menzies for helping us understand the process of building a good Software Engineering project. We would also like to thank the teaching assistants Xiao Ling, Andre Lustosa, Kewen Peng, Weichen Shi for their support throughout the project. +We would like to thank Dr. Timothy Menzies for helping us understand the process of building a good Software Engineering project. We would also like to thank the teaching assistants San Gilson, Andre Lustosa, Xueqi (Sherry) Yang, Yasitha Rajapaksha, and Rahul Yedida for their support throughout the project. :page_facing_up: License --- -This project is licensed under the terms of the MIT license. Please check [License](https://github.com/secheaper/slashbot/blob/main/LICENSE) for more details. +This project is licensed under the terms of the MIT license. Please check for more details. :sparkles: Contributors @@ -249,11 +303,10 @@ This project is licensed under the terms of the MIT license. Please check [Licen - - - - - + + + +

Shubham Mankar

Pratik Devnani


Moksh Jain


Rahil Sarvaiya


Anushi Keswani


Harshavardhan Bandaru

Vyshnavi Adusumeli

Tejaswini Panati
@@ -261,4 +314,4 @@ This project is licensed under the terms of the MIT license. Please check [Licen # :calling: Support -For any support, email us at mydollarbot@gmail.com/ secheaper@gmail.com +For any support, email us at vadusum@ncsu.edu diff --git a/data/2129133600.pickle b/data/2129133600.pickle deleted file mode 100644 index 6c9ba93a2..000000000 Binary files a/data/2129133600.pickle and /dev/null differ diff --git a/data/680361961.pickle b/data/680361961.pickle deleted file mode 100644 index 99b44eda8..000000000 Binary files a/data/680361961.pickle and /dev/null differ diff --git a/data/data.txt b/data/data.txt deleted file mode 100644 index 8b1378917..000000000 --- a/data/data.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/discordData/1.pickle b/discordData/1.pickle new file mode 100644 index 000000000..cc64dd0e5 Binary files /dev/null and b/discordData/1.pickle differ diff --git a/discordData/2.pickle b/discordData/2.pickle new file mode 100644 index 000000000..2b6db2a1d Binary files /dev/null and b/discordData/2.pickle differ diff --git a/docs/changes.md b/docs/changes.md deleted file mode 100644 index 0c6e417bb..000000000 --- a/docs/changes.md +++ /dev/null @@ -1,24 +0,0 @@ -# Changes compared to V2 (MyDollarBot) - -The team has worked on multiple facets of the project and has made changes to the code, documentation and overall usability of the original project. - -## Functionality -The group has made the below mentioned functional changes to the project improving on the previous work. -1. **Download History:** This version lets you now download your transaction history as a CSV.

- -2. **Display total expenditure in different currencies:** This version lets you convert your total daily or monthly expenditure in a different currency. -

- -3. **Display Multiple Visualization options:** This version allows you to see your expenses in more than one form of visualization with a template to add more visualizations. -

- -4. **Send EMail about the monthly history:** This version allows you to send periodic emails to your account to review your monthly history -

- - -## Documentation and Testing -Changes to the documentation were also significant. They are mentioned below. -1. **README:** The main README has been updated with new information, more bling and new graphics. More information about testing requirements is added. -2. **Testing:** Resolved bugs from test functions -3. **GIFs for new features:** 4 more gifs are added to the Readme related to new releases in PHASE 3 -4. **Badges:** More badges are added for better readibility diff --git a/docs/proj2rubric.md b/docs/proj2rubric.md deleted file mode 100644 index 5d9998f63..000000000 --- a/docs/proj2rubric.md +++ /dev/null @@ -1,25 +0,0 @@ -| Score | Self-Assessment | Notes | Evidence | -| --- | --- | ---- | ------ | -|.5| .5 |short release cycles| We have a total of 4 releases. https://github.com/mtkumar123/MyDollarBot/releases| -|.5| .5 | workload is spread over the whole team (so one team member is often Xtimes more productive than the others...| Everyone contributed and had commits. Can be seen in insights https://github.com/mtkumar123/MyDollarBot/graphs/contributors?from=2021-10-01&to=2021-11-01&type=c| -|.5| .5 |Docs: why: docs tell a story, motivate the whole thing, deliver a punchline that makes you want to rush out and use the thing | readme.md on main sells our project very well. https://github.com/mtkumar123/MyDollarBot/blob/main/README.md| -|.5| .5 |the files CONTRIBUTING.md lists coding standards and lots of tips on how to extend the system without screwing things up | contributing.md is very detailed https://github.com/mtkumar123/MyDollarBot/blob/main/CONTRIBUTING.md| -|.5| .5 |Docs: doco generated , format not ugly | Github pages https://mtkumar123.github.io/MyDollarBot/| -|.5| .5 | evidence that the whole team is using the same tools (e.g. config files in the repo, updated by lots of different people) | Everyone has a working instance of the project locally. All the team members are using the same IDE and have all the required packages installed. The whole team uses the same testing telegram bot. This can be seen within our github secrets, as API_TOKEN is set to the shared API_TOKEN all team members share.| -|.5| .5 | evidence that the members of the team are working across multiple places in the code base | We assign issues in Github and start working on the required part of the project. The commit history of different members shows this. https://github.com/mtkumar123/MyDollarBot/graphs/contributors?from=2021-10-01&to=2021-11-04&type=c| -|1| 1 |Docs: what: point descriptions of each class/function (in isolation) | Each function has a docstring, and is used to generate the documentation for github pages using sphinx| -|.5| .5 | Number of commits: by different people | Insights https://github.com/mtkumar123/MyDollarBot/graphs/contributors?from=2021-10-01&to=2021-11-04&type=c| -|1| 1 |issues are being closed | 27 issues were opened and closed https://github.com/mtkumar123/MyDollarBot/issues| -|.5| .5 | issues are discussed before they are closed | Example https://github.com/mtkumar123/MyDollarBot/issues/61 | -|.5| .5 | Use of syntax checkers. | Used pylint, and have a pylintrc file | -|1| 1 | Issues reports: there are many | https://github.com/mtkumar123/MyDollarBot/issues?q=is%3Aissue+is%3Aclosed| -|.5| .5 | Use of code formatters. | pylint, and used pylintrc for config of pylint also used black formatter and added the badge in the repo | -|.5| .5 | Use of style checkers | pylint, and used pylintrc for config of pylint | -|.5| .5 | Docs: short video, animated, hosted on your repo. That convinces people why they want to work on your code. | Multiple gifs found in the readme.md https://github.com/mtkumar123/MyDollarBot/blob/main/README.md| -|.5| .5 | test cases exist | all test cases in the test folder. code coverage is 68 percent | -|.5| .5 | Use of code coverage | codecov.yml file, and code coverage is 68%| -|.5| .5 | other automated analysis tools | Pylint, CodeCoverage, Github Actions, Pytest. | -|.5| .5 |test cases:.a large proportion of the issues related to handling failing cases. | When a test failed, an issue was opened tagged as bug. Example https://github.com/mtkumar123/MyDollarBot/issues/23| -|.5| .5 |test cases are routinely executed | github actions to run the tests routinely. build badge to show the build is passing | -|1| 1 |Documentation describing how this version improves on the older version| What's New Section in the readme.md https://github.com/mtkumar123/MyDollarBot/blob/main/README.md| -|3| 3 | This version is a little(1), some(2), much(3) improved on the last version.|Tutor's assessment.| diff --git a/docs/proj3rubric.md b/docs/proj3rubric.md deleted file mode 100644 index 3d6f4bc60..000000000 --- a/docs/proj3rubric.md +++ /dev/null @@ -1,25 +0,0 @@ -| Score | Self-Assessment | Notes | Evidence | -| --- | --- | ---- | ------ | -|.5| .5|short release cycles|[Releases](https://github.com/secheaper/slashbot/releases)| -|.5| .5| workload is spread over the whole team (so one team member is often Xtimes more productive than the others...|[Project Insights](https://github.com/secheaper/slashbot/pulse)| -|.5|.5|Docs: why: docs tell a story, motivate the whole thing, deliver a punchline that makes you want to rush out and use the thing |The [Readme](https://github.com/secheaper/slashbot/blob/main/README.md) has detailed information.| -|.5|.5|the files CONTRIBUTING.md lists coding standards and lots of tips on how to extend the system without screwing things up|The [contributing.md](https://github.com/secheaper/slashbot/blob/main/CONTRIBUTING.md) is quite detailed.| -|.5|.5|Docs: doco generated , format not ugly |Present on [Github Pages](https://mtkumar123.github.io/MyDollarBot/)| -|.5|.5| evidence that the whole team is using the same tools (e.g. config files in the repo, updated by lots of different people) |The [code](https://github.com/secheaper/slashbot/blob/main/code/bot.py) file being updated by many users. In Github Secrets, we can see that everyone used the same bot with API_TOKEN and CHAT_ID kept as environment variables| -|.5|.5| evidence that the members of the team are working across multiple places in the code base |As shown in [Network](https://github.com/secheaper/slashbot/network)| -|1|1|Docs: what: point descriptions of each class/function (in isolation)|Docstrings are present for each function in [code](https://github.com/secheaper/slashbot/tree/main/code)| -|.5|.5| Number of commits: by different people |As seen in [Project Insights](https://github.com/secheaper/slashbot/pulse)| -|1|1|issues are being closed |Can be seen [here](https://github.com/secheaper/slashbot/issues?q=is%3Aissue+is%3Aclosed)| -|.5|.5| issues are discussed before they are closed |Can be seen [here](https://github.com/secheaper/slashbot/issues/6)| -|.5|.5| Use of syntax checkers. |Present in the [YML](https://github.com/secheaper/slashbot/blob/main/.github/workflows/main.yml)| -|1|1|Issues reports: there are many|Can be seen [here](https://github.com/secheaper/slashbot/issues?q=)| -|.5|.5| Use of code formatters. |Present in the [YML](https://github.com/secheaper/slashbot/blob/main/.github/workflows/main.yml)| -|.5|.5| Use of style checkers |Present in the [YML](https://github.com/secheaper/slashbot/blob/main/.github/workflows/main.yml)| -|.5|.5| Docs: short video, animated, hosted on your repo. That convinces people why they want to work on your code. |Present in [Readme](https://github.com/secheaper/slashbot/blob/main/README.md)| -|.5|.5| test cases exist |Present in [test](https://github.com/secheaper/slashbot/tree/main/test) folder| -|.5|.5| Use of code coverage |Present in [Readme](https://github.com/secheaper/slashbot/blob/main/README.md)| -|.5|.5| other automated analysis tools |[Actions](https://github.com/secheaper/slashbot/actions) run automatically on each push to main| -|.5|.5|test cases:.a large proportion of the issues related to handling failing cases. |[Tests](https://github.com/secheaper/slashbot/tree/main/test) folder| -|.5|.5|test cases are routinely executed |Github [Actions](https://github.com/secheaper/slashbot/actions)| -|1|1|Documentation describing how this version improves on the older version|Documented in [changes.md](https://github.com/secheaper/slashbot/blob/main/docs/changes.md)| -|3|3| This version is a little(1), some(2), much(3) improved on the last version.|Documented in [changes.md](https://github.com/secheaper/slashbot/blob/main/docs/changes.md)| diff --git a/docs/workflows/WhatsApp Image 2023-10-16 at 19.03.06.jpeg b/docs/workflows/WhatsApp Image 2023-10-16 at 19.03.06.jpeg new file mode 100644 index 000000000..5b7f66546 Binary files /dev/null and b/docs/workflows/WhatsApp Image 2023-10-16 at 19.03.06.jpeg differ diff --git a/docs/workflows/add_discord.gif b/docs/workflows/add_discord.gif new file mode 100644 index 000000000..fbcdc5b4d Binary files /dev/null and b/docs/workflows/add_discord.gif differ diff --git a/docs/workflows/budget_discord.gif b/docs/workflows/budget_discord.gif new file mode 100644 index 000000000..7833d4576 Binary files /dev/null and b/docs/workflows/budget_discord.gif differ diff --git a/docs/workflows/chart_discord.gif b/docs/workflows/chart_discord.gif new file mode 100644 index 000000000..1520292db Binary files /dev/null and b/docs/workflows/chart_discord.gif differ diff --git a/docs/workflows/delete_discord.gif b/docs/workflows/delete_discord.gif new file mode 100644 index 000000000..ce579e701 Binary files /dev/null and b/docs/workflows/delete_discord.gif differ diff --git a/docs/workflows/display_discord.gif b/docs/workflows/display_discord.gif new file mode 100644 index 000000000..1fa6f8b85 Binary files /dev/null and b/docs/workflows/display_discord.gif differ diff --git a/docs/workflows/edit_discord.gif b/docs/workflows/edit_discord.gif new file mode 100644 index 000000000..ea3ef1eda Binary files /dev/null and b/docs/workflows/edit_discord.gif differ diff --git a/docs/workflows/history_discord.gif b/docs/workflows/history_discord.gif new file mode 100644 index 000000000..a55fb9706 Binary files /dev/null and b/docs/workflows/history_discord.gif differ diff --git a/docs/workflows/menu_discord.gif b/docs/workflows/menu_discord.gif new file mode 100644 index 000000000..e339c51e2 Binary files /dev/null and b/docs/workflows/menu_discord.gif differ diff --git a/proj2/FinBot_proj2_scorecard.csv b/proj2/FinBot_proj2_scorecard.csv new file mode 100644 index 000000000..3fc9ad783 --- /dev/null +++ b/proj2/FinBot_proj2_scorecard.csv @@ -0,0 +1,147 @@ +Github Project Link,https://github.com/vyshnavi-adusumelli/FinBot,, +Column 1,Column 2(self evaluation),Evidence, +,84,, +Video,3,https://www.youtube.com/watch?v=LkZGFGU5B6I, +Workload is spread over the whole team,2,https://github.com/users/vyshnavi-adusumelli/projects/2, +Number of commits,3," +https://github.com/vyshnavi-adusumelli/FinBot/pulse",One of the contributors has commits from two different IDs: vadusum & vyshnavi-adusumelli +No.of commits: by different people,3,https://github.com/vyshnavi-adusumelli/FinBot/graphs/contributors?from=2023-09-21&to=2023-10-16&type=c, +Issue Reports: Many,3,https://github.com/vyshnavi-adusumelli/FinBot/issues?q=is%3Aissue+is%3Aclosed, +Issues are closed,3,https://github.com/vyshnavi-adusumelli/FinBot/issues?q=is%3Aissue+is%3Aclosed, +DOI Badge,3,https://github.com/secheaper/slashbot/tree/main , +Docs: doco generated,3,"https://mtkumar123.github.io/MyDollarBot/ +https://github.com/secheaper/slashbot/tree/main ", +Docs: what: point descriptions of each class/function (in isolation),3,https://github.com/vyshnavi-adusumelli/FinBot/tree/main/src,docstrings explaining each class and function are included in the corresponding code files (.py) +"Docs: how: for common use cases X,Y,Z mini-tutorials showing worked examples on how to do X,Y,Z",3,https://github.com/vyshnavi-adusumelli/FinBot#information_desk_person-use-cases, +"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,https://github.com/vyshnavi-adusumelli/FinBot#finbot---because-your-financial-future-deserves-the-best, +"Docs: short video, animated, hosted on your repo. That convinces people why they want to work on your code.",3,https://github.com/vyshnavi-adusumelli/FinBot#demo-video, +Use of version control tools,3,Hosted in Git (version control tool) - https://github.com/vyshnavi-adusumelli/FinBot/tree/main/, +Use of style checkers,3,"https://github.com/secheaper/slashbot/blob/main/pylintrc +https://github.com/vyshnavi-adusumelli/FinBot/blob/main/.github/workflows/main.yml", +Use of code formatters,3,https://github.com/vyshnavi-adusumelli/FinBot/blob/main/.github/workflows/black.yml, +Use of syntax checkers,3,https://github.com/vyshnavi-adusumelli/FinBot/blob/main/.github/workflows/main.yml, +Use of code coverage,3,https://github.com/vyshnavi-adusumelli/FinBot/blob/main/.github/workflows/main.yml, +Other automated analysis tools,3,https://github.com/vyshnavi-adusumelli/FinBot/blob/main/.github/workflows/black.yml, +Test Cases exist,3,https://github.com/vyshnavi-adusumelli/FinBot/tree/main/test, +Test cases are routinely executed,3,https://github.com/vyshnavi-adusumelli/FinBot/blob/main/.github/workflows/main.yml, +The files CONTRIBUTING.md lists coding standards and lots of tips on how to extend the system without screwing things up,3,https://github.com/vyshnavi-adusumelli/FinBot/blob/main/CONTRIBUTING.md, +"issues are discussed before they are closed even if you discuss in slack, need a summary statement here",3,"https://github.com/vyshnavi-adusumelli/FinBot/pull/50 +https://github.com/vyshnavi-adusumelli/FinBot/issues?q=is%3Aissue+is%3Aclosed",Issues discussed in the corresponding PRs +Chat channel: exists,3,https://github.com/vyshnavi-adusumelli/FinBot/blob/tpanati-patch-2/docs/workflows/WhatsApp%20Image%202023-10-16%20at%2019.03.06.jpeg, +Test cases: a large proportion of the issues related to handling failing cases.,3,https://github.com/vyshnavi-adusumelli/FinBot/issues, +Evidence that the whole team is using the same tools: everyone can get to all tools and files,3,https://github.com/secheaper/slashbot/compare/main...vyshnavi-adusumelli:FinBot:main, +"Evidence that the whole team is using the same tools (e.g. config files in the repo, updated by lots of different people)",3,https://github.com/secheaper/slashbot/compare/main...vyshnavi-adusumelli:FinBot:main, +"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,https://github.com/secheaper/slashbot/compare/main...vyshnavi-adusumelli:FinBot:main, +Evidence that the members of the team are working across multiple places in the code base,3,https://github.com/vyshnavi-adusumelli/FinBot/network, +Short release cycles,3,https://github.com/vyshnavi-adusumelli/FinBot/releases, +,,, +Software Sustainability Questions:,,, +,,, +"1.1 Does your website and documentation provide a clear, high-level overview of your software?",Y,, +1.2 Does your website and documentation clearly describe the type of user who should use your software?,Y,, +1.3: Do you publish case studies to show how your software has been used by yourself and others?,N,, +2.1: Is the name of your project/software unique?,Y,, +2.2: Is your project/software name free from trademark violations?,Y,, +"3.1: Is your software available as a package that can be deployed +without building it?",N,, +3.2: Is your software available for free?,Y,, +"3.3: Is your source code publicly available to download, either as a +downloadable bundle or via access to a source code repository?",Y,, +"3.4: 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)?",Y,, +4.1: Is your documentation clearly available on your website or within your software?,Y,, +"4.2: 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?",Y,, +"4.3: If you provide more extensive documentation, does this provide +clear, step-by-step instructions on how to deploy and use your software?",Y,, +"4.4: Do you provide a comprehensive guide to all your software’s +commands, functions and options?",Y,, +4.5: Do you provide troubleshooting information that describes the symptoms and step-by-step solutions for problems and error messages?,N,, +"4.6: If your software can be used as a library, package or serviceby other software, do you provide comprehensive API documentation?",N/A,, +4.7: Do you store your documentation under revision control with your source code?,Y,, +"4.8: 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?",Y,, +5.1: Does your software describe how a user can get help with using your software?,Y,, +"5.2: Does your website and documentation describe what support, if +any, you provide to users and developers?",Y,, +5.3: Does your project have an e-mail address or forum that is solely for supporting users?,Y,, +5.4: Are e-mails to your support e-mail address received by more than one person?,N,, +5.5: Does your project have a ticketing system to manage bug reports and feature requests?,Y,, +"5.6: Is your project's ticketing system publicly visible to your users, so they can view bug reports and feature requests?",Y,, +6.1: Is your software’s architecture and design modular?,Y,, +"6.2: Does your software use an accepted coding standard or +convention?",Y,, +"7.1: Does your software allow data to be imported and exported using +open data formats?",Y,, +"7.2: Does your software allow communications using open +communications protocols?",Y,, +8.1: Is your software cross-platform compatible?,Y,, +"9.1: Does your software adhere to appropriate accessibility conventions +or standards?",Y,, +"9.2: Does your documentation adhere to appropriate accessibility +conventions or standards?",Y,, +10.1: Is your source code stored in a repository under revision control?,Y,, +10.2: Is each source code release a snapshot of the repository?,Y,, +10.3: Are releases tagged in the repository?,Y,, +10.4: Is there a branch of the repository that is always stable?,Y,, +10.5: Do you back-up your repository?,Y,, +"11.1: Do you provide publicly-available instructions for building your +software from the source code?",Y,, +"11.2: Can you build, or package, your software using an automated tool?",N,, +"11.3: Do you provide publicly-available instructions for deploying your +software?",Y,, +11.4: Does your documentation list all third-party dependencies?,Y,, +"11.5: Does your documentation list the version number for all third-party +dependencies?",Y,, +"11.6: Does your software list the web address, and licences for all thirdparty +dependencies and say whether the dependencies are mandatory or +optional?",Y,, +"11.7: Can you download dependencies using a dependency +management tool or package manager?",Y,, +"11.8: 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?",Y,, +12.1: Do you have an automated test suite for your software?,Y,, +"12.2: Do you have a framework to periodically (e.g. nightly) run your +tests on the latest version of the source code?",N,, +"12.3: Do you use continuous integration, automatically running tests +whenever changes are made to your source code?",Y,, +12.4: Are your test results publicly visible?,Y,, +12.5: Are all manually-run tests documented?,N,, +"13.1: 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?",N,, +"13.2: Does your website state how many projects and users are +associated with your project?",N,, +13.3: Do you provide success stories on your website?,N,, +"13.4: Do you list your important partners and collaborators on your +website?",Y,, +"13.5: Do you list your project's publications on your website or link to a +resource where these are available?",N/A,, +"13.6: Do you list third-party publications that refer to your software on +your website or link to a resource where these are available?",N/A,, +"13.7: Can users subscribe to notifications to changes to your source +code repository?",Y,, +"13.8: 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?",Y,, +"14.1: Do you accept contributions (e.g. bug fixes, enhancements, +documentation updates, tutorials) from people who are not part of your project?",Y,, +14.2: Do you have a contributions policy?,Y,, +14.3: Is your contributions' policy publicly available?,Y,, +14.4: Do contributors keep the copyright/IP of their contributions?,Y,, +"15.1: Does your website and documentation clearly state the copyright +owners of your software and documentation?",Y,, +"15.2: Does each of your source code files include a copyright +statement?",Y,, +"15.3: Does your website and documentation clearly state the licence of +your software?",Y,, +15.4: Is your software released under an open source licence?,Y,, +"15.5: Is your software released under an OSI-approved open-source +licence?",Y,, +15.6: Does each of your source code files include a licence header?,Y,, +15.7: Do you have a recommended citation for your software?,N,, +"16.1: Does your website or documentation include a project roadmap (a list of project and development milestones for the next 3, 6 and 12 months)?",Y,, +"16.2: Does your website or documentation describe how your project is +funded, and the period over which funding is guaranteed?",N/A,, +"16.3: Do you make timely announcements of the deprecation of +components, APIs, etc.?",N,, diff --git a/proj2/README.md b/proj2/README.md new file mode 100644 index 000000000..9a503e40f --- /dev/null +++ b/proj2/README.md @@ -0,0 +1,9 @@ +# Project 2 Deliverables + +## Video + +[FinBot](https://www.youtube.com/watch?v=LkZGFGU5B6I) + +## Scorecard + +[Score card - Group 13](https://github.com/vyshnavi-adusumelli/FinBot/blob/tpanati-patch-2/proj/FinBot_proj2_scorecard.csv) diff --git a/requirements.txt b/requirements.txt index d761016e1..ed2ca6afc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ pyTelegramBotAPI==4.1.0 -pandas +pandas==1.3.4 matplotlib==3.4.3 tabulate +discord.py==2.3.2 + diff --git a/setup.py b/setup.py index db719980b..e01d4bd35 100644 --- a/setup.py +++ b/setup.py @@ -20,6 +20,7 @@ install_requires=[ 'pyTelegramBotAPI==4.1.0', 'pandas==1.3.4', - 'matplotlib==3.4.3' + 'matplotlib==3.4.3', + 'discord.py==2.3.2' ] ) diff --git a/src/.DS_Store b/src/.DS_Store index 595a82ab5..514b7511c 100644 Binary files a/src/.DS_Store and b/src/.DS_Store differ diff --git a/src/data/2115317473.pickle b/src/data/2115317473.pickle deleted file mode 100644 index 1d7d81f01..000000000 Binary files a/src/data/2115317473.pickle and /dev/null differ diff --git a/src/discordBot.py b/src/discordBot.py new file mode 100644 index 000000000..98d23f1fd --- /dev/null +++ b/src/discordBot.py @@ -0,0 +1,752 @@ +""" +File: DiscordBot.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Discord bot message handlers and their associated functions. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import asyncio +import discord +from discord.ext import commands +from discordUser import User +from discord.ui import Select, View +import os +import pathlib +import pickle +import re +from datetime import datetime +from tabulate import tabulate + +BOT_TOKEN = os.environ["DISCORD_TOKEN"] +CHANNEL_ID = os.environ["CHANNEL_ID"] + +bot = commands.Bot(command_prefix="#", intents=discord.Intents.all()) +user_list = {} + +@bot.event +async def on_ready(): + """ + An event handler for the "on_ready" event. + This function is called when the bot has successfully connected to the Discord server and is ready to operate. It sends a welcome message to a specific channel and then calls the "menu" + function to display a menu, likely for user interaction. + + Parameters: + - None + + Returns: + - None + """ + channel = bot.get_channel(int(CHANNEL_ID)) + await channel.send("Hello ! Welcome to FinBot - a simple solution to track your expenses! \n\n") + await menu(channel) + +@bot.command() +async def menu(ctx): + """ + Handles the 'menu' command to display a list of available commands and their descriptions in an embed window. + + Parameters: + - ctx (discord.ext.commands.Context): The Discord context window. + + Returns: + - None + """ + em = discord.Embed(title="FinBot", description="Here is a list of available commands, please enter a command of your choice with a prefix '#' so that I can assist you further.\n ",color = discord.Color.teal()) + em.add_field(name="**#menu**", value="Displays all commands and their descriptions", inline=False) + em.add_field(name="**#add**", value="Record/Add a new spending", inline=False) + em.add_field(name="**#display**", value="Show sum of expenditure for the current day/month", inline=False) + em.add_field(name="**#history**", value="Display spending history", inline=False) + em.add_field(name="**#delete**", value="Clear/Erase all your records", inline=False) + em.add_field(name="**#edit**", value="Edit/Change spending details", inline=False) + em.add_field(name="**#budget**", value="Set budget for the month", inline=False) + em.add_field(name="**#chart**", value="See your expenditure in different charts", inline=False) + + await ctx.send(embed=em) + +@bot.command() +async def display(ctx): + """ + Handles the command 'display'. If the user has no transaction history, a message is displayed. If there is + transaction history, user is given choices of time periods to choose from. The function 'display_total' is called + next. + + Parameters: + - ctx (discord.ext.commands.Context): The Discord context window. + + Returns: + - None + """ + if CHANNEL_ID not in user_list or user_list[CHANNEL_ID].get_number_of_transactions() == 0: await ctx.send("Oops! Looks like you do not have any spending records!") + else: + try: + select_options = [discord.SelectOption(label="Day"),discord.SelectOption(label="Month"),] + select = Select(placeholder="Select a category", max_values=1,min_values=1, options=select_options) + + async def my_callback(interaction): + await interaction.response.send_message(f'You chose: {select.values[0]}') + await asyncio.sleep(0.5) + await display_total(ctx, select.values[0]) + + select.callback = my_callback + view = View(timeout=90) + view.add_item(select) + + await ctx.send('Please select a category to see the total expense', view=view) + + except Exception as ex: + print(str(ex), exc_info=True) + await ctx.send("Request cannot be processed. Please try again with correct format!") + + +async def display_total(ctx, sel_category): + """ + Receives the input time period from display(ctx) and displays the transaction summary for the corresponding time period. + + Parameters: + - ctx (discord.ext.commands.Context): The Discord context window + - sel_category (string): The time period selected by the user (day / month) + + Returns: + - None + """ + dateFormat = "%m/%d/%Y" + try: + day_week_month = sel_category + + if day_week_month not in user_list[CHANNEL_ID].spend_display_option: raise Exception('Sorry I can\'t show spendings for "{}"!'.format(day_week_month)) + + if len(user_list[CHANNEL_ID].transactions) == 0: raise Exception("Oops! Looks like you do not have any spending records!") + + await ctx.send("Hold on! Calculating...") + + if day_week_month == "Day": + query = datetime.today() + query_result = "" + total_value = 0 + for category in user_list[CHANNEL_ID].transactions.keys(): + for transaction in user_list[CHANNEL_ID].transactions[category]: + if transaction["Date"].strftime("%d") == query.strftime("%d"): + query_result += "Category: {} ; Date: {} ; Value: {:.2f} \n".format(category, transaction["Date"].strftime(dateFormat), transaction["Value"]) + total_value += transaction["Value"] + total_spendings = "Here are your total spendings for the date {} \n\n".format(datetime.today().strftime("%m/%d/%Y")) + total_spendings += query_result + total_spendings += "Total Value {:.2f}".format(total_value) + await ctx.send(total_spendings) + elif day_week_month == "Month": + query = datetime.today() + query_result = "" + total_value = 0 + budget_value = user_list[CHANNEL_ID].monthly_budget + for category in user_list[CHANNEL_ID].transactions.keys(): + for transaction in user_list[CHANNEL_ID].transactions[category]: + if transaction["Date"].strftime("%m") == query.strftime("%m"): + query_result += "Category: {} ; Date: {} ; Value: {:.2f} \n".format(category,transaction["Date"].strftime(dateFormat),transaction["Value"]) + total_value += transaction["Value"] + total_spendings = ("Here are your total spendings for the Month {} \n\n".format(datetime.today().strftime("%B"))) + total_spendings += query_result + total_spendings += "Total Value {:.2f}\n".format(total_value) + total_spendings += "Budget for the month {}".format(str(budget_value)) + await ctx.send(total_spendings) + except Exception as ex: + print(str(ex), exc_info=True) + await ctx.send("Request cannot be processed. Please try again with correct format!") + +@bot.command() +async def budget(ctx): + """ + Handles the commands 'budget'. To set a budget monthly and hence keep a track of the transactions. + + Parameters: + - ctx (discord.ext.commands.Context): The Discord context window. + + Returns: + - None + """ + if CHANNEL_ID not in user_list.keys(): user_list[CHANNEL_ID] = User(CHANNEL_ID) + try: + await ctx.send(f"Your current monthly budget is {user_list[CHANNEL_ID].monthly_budget}") + await ctx.send("Enter an amount to update your monthly budget. (Enter numeric values only)") + budget_resp = await bot.wait_for('message', check=lambda message: message.author == ctx.author, timeout=60.0) + except asyncio.TimeoutError: + await ctx.send('You ran out of time to answer!') + else: + if budget_resp: await post_budget_input(ctx, budget) + else: await ctx.send('Nope enter a valid date') + +async def post_budget_input(ctx, budget_resp): + """ + Handles the processing of user input (budget). This function validates the entered amount and sets the budget. The error handling + functionality is also implemented. + + Parameters: + - ctx (discord.ext.commands.Context): The Discord context window. + - budget (discord.Message): The user's response message containing the budget input. + + Returns: + - None + """ + try: + amount_entered = budget_resp.content + amount_value = user_list[CHANNEL_ID].validate_entered_amount(amount_entered) # validate + if amount_value == 0: raise Exception("Budget amount has to be a positive number.") # cannot be $0 spending + user_list[CHANNEL_ID].add_monthly_budget(amount_value, CHANNEL_ID) + await ctx.send(f"The budget for this month has been set as $ {amount_value}") + + except Exception as ex: + await ctx.send("Oh no! " + str(ex)) + budget_new = await bot.wait_for('message', check=lambda message: message.author == ctx.author) + if budget_new.content.isnumeric(): await post_budget_input(ctx, budget_new) + elif '#' not in budget_new.content : await ctx.send("Exception received: 'budget' is not a numeric character. Please re-enter #budget command") + +async def select_date(ctx): + ''' + Function to get date selection from user. This function is invoked from the add to + enter the date of expense to be added. + + :param ctx - Discord context window + :param Bot - Discord Bot object + :param date - date message object received from the user + :type: object + :return: None + + ''' + dateFormat = "%m-%d-%Y" + curr_day = datetime.now() + await ctx.send("Enter day") + await ctx.send(f"\n\tExample day in format mm-dd-YYYY: {curr_day.strftime(dateFormat)}\n") + def check(msg): return msg.author == ctx.author and msg.channel == ctx.channel + + try: + date_message = await bot.wait_for('message', check=check, timeout=60) + date_str = date_message.content.strip() + month, date, year = map(int, date_str.split('-')) + + # Call the next function with the date, month, and year + await process_date(ctx, date, month, year) + except asyncio.TimeoutError: await ctx.send("You took too long to respond. Please try again.") + +async def process_date(ctx, date, month, year): + ''' + Process the date, month, and year here + You can perform any necessary calculations or operations + For example, you can convert them to a datetime object + :param ctx - Discord context window + :param date - date string + :param month - month string + :param year - year string + :type: object + :return: None + ''' + + try: + date_obj = datetime(int(year), int(month), int(date)) + await ctx.send(f"Selected Date: {date_obj.strftime('%m-%d-%Y')}") + await select_category(ctx, date_obj) + except ValueError: await ctx.send("Invalid date, month, or year. Please enter valid values.") + +@bot.command() +async def add(ctx): + """ + Handles the commands 'add'. To add a transaction to the user records. + + Parameters: + - ctx (discord.ext.commands.Context): The Discord context window. + + Returns: None + """ + + if CHANNEL_ID not in user_list.keys(): user_list[CHANNEL_ID] = User(CHANNEL_ID) + try: await select_date(ctx) + + except Exception as ex: + print("exception occurred:"+str(ex)) + await ctx.send("Request cannot be processed. Please try again with correct format!") + +async def select_category(ctx, date): + """ + Function to enable category selection via a custom-defined category dropdown. This function is invoked from the 'select_date' function + to select the category of expense to be added. It utilizes the Select and View classes from discord.ui and a callback to handle the + interaction response. + + Parameters: + - ctx (discord.ext.commands.Context): The Discord context window. + - date (discord.Message): The date message object received from the user. + + Returns: + - None + """ + + spend_categories = user_list[CHANNEL_ID].spend_categories + select_options = [discord.SelectOption(label=category) for category in spend_categories] + select = Select(placeholder="Select a category", max_values=1,min_values=1, options=select_options) + + async def my_callback(interaction): + await interaction.response.send_message(f'You chose: {select.values[0]}') + await asyncio.sleep(0.5) + if select.values[0] not in spend_categories: + await ctx.send("Invalid category") + raise Exception('Sorry I don\'t recognise this category "{}"!'.format(select.values[0])) + + await post_category_selection(ctx, date, select.values[0]) + + select.callback = my_callback + + view = View(timeout=90) + view.add_item(select) + + await ctx.send('Please select a category', view=view) + +async def post_category_selection(ctx, date_to_add,category): + """ + Receives the category selected by the user and then asks for the amount spent. If an invalid category is given, + an error message is displayed, followed by a command list. If the category given is valid, 'post_amount_input' is + called next to collect the amount spent. + + Parameters: + - ctx (discord.ext.commands.Context): The Discord context window. + - date_to_add (object): The date of the purchase. + - category (str): The selected category for the expense. + + Returns: + - None + """ + try: + selected_category = category + + await ctx.send(f'\nHow much did you spend on {selected_category}') + amount = await bot.wait_for('message', check=lambda message: message.author == ctx.author) + + await post_amount_input(ctx, amount.content,selected_category,date_to_add) + except Exception as ex: + print(str(ex), exc_info=True) + await ctx.send("Request cannot be processed. Please try again with correct format!") + + +async def post_amount_input(ctx, amount_entered,selected_category,date_to_add): + """ + Receives the amount entered by the user and adds it to the transaction history. An error is displayed if the entered + amount is zero. Else, a message is shown that the transaction has been added. + + Parameters: + - ctx (discord.ext.commands.Context): The Discord context window. + - amount_entered (str): The amount entered by the user for the transaction. + - selected_category (str): The category of the expense selected by the user. + - date_to_add (str): The date of the transaction in a string format. + + Returns: + - None + """ + + try: + amount_value = user_list[CHANNEL_ID].validate_entered_amount(amount_entered) # validate + if amount_value == 0: raise Exception("Spent amount has to be a non-zero number.") # cannot be $0 spending + + category_str, amount_str = (selected_category,format(amount_value, ".2f")) + user_list[CHANNEL_ID].add_transaction(date_to_add, selected_category, amount_value, CHANNEL_ID) + total_value = user_list[CHANNEL_ID].monthly_total() + add_message = f"The following expenditure has been recorded: You have spent ${amount_str} for {category_str} on {date_to_add}" + + if user_list[CHANNEL_ID].monthly_budget > 0: + if total_value > user_list[CHANNEL_ID].monthly_budget: await ctx.send("*You have gone over the monthly budget*") + elif total_value == user_list[CHANNEL_ID].monthly_budget: await ctx.send("*You have exhausted your monthly budget. You can check/download history*") + elif total_value >= 0.8 * user_list[CHANNEL_ID].monthly_budget: await ctx.send("*You have used 80% of the monthly budget*") + + await ctx.send(add_message) + except Exception as ex: + print(str(ex), exc_info=True) + await ctx.send("Request cannot be processed. Please try again with correct format!") + +@bot.command() +async def delete(ctx): + """ + Handles the 'delete' command and prompts the user to choose a deletion option. + + Parameters: + - ctx (discord.ext.commands.Context): The Discord context window. + + Returns: + - None + """ + + dateFormat = "%m-%d-%Y" + monthFormat = "%m-%Y" + try: + if (CHANNEL_ID in user_list + and user_list[CHANNEL_ID].get_number_of_transactions() != 0): + + curr_day = datetime.now() + prompt = "Enter the day, month, or All\n" + prompt += f"\n\tExample day in format mm-dd-YYYY: {curr_day.strftime(dateFormat)}\n" + prompt += f"\n\tExample month in format mm-YYYY: {curr_day.strftime(monthFormat)}" + await ctx.send(prompt) + delete_type = await bot.wait_for('message', check=lambda message: message.author == ctx.author) + await process_delete_argument(ctx, delete_type.content) + else: + delete_history_text = ("No records to be deleted. Start adding your expenses to keep track of your spendings! ") + await ctx.send(delete_history_text) + + except Exception as ex: + print(str(ex), exc_info=True) + await ctx.send("Request cannot be processed. Please try again with correct format!") + + +async def process_delete_argument(ctx, delete_type): + """ + Receives the user's choice for deletion and asks for confirmation. + + Parameters: + - ctx (discord.ext.commands.Context): The Discord context window. + - delete_type (str): The user's input for deletion. + + Returns: + - None + """ + + dateFormat = "%m-%d-%Y" + monthFormat = "%m-%Y" + text = delete_type #delete_type + date = None + is_month = False + if text.lower() == "all": date = "all" + else: + # try and parse as Date-Month-Year + if user_list[CHANNEL_ID].validate_date_format(text, dateFormat) is not None: date = user_list[CHANNEL_ID].validate_date_format(text, dateFormat) + # try and parse as Month-Year + elif user_list[CHANNEL_ID].validate_date_format(text, monthFormat) is not None: + date = user_list[CHANNEL_ID].validate_date_format(text, monthFormat) + is_month = True + + if date is None: await ctx.send("error parsing text") + else: + # get the records either by given day, month, or all records + records_to_delete = user_list[CHANNEL_ID].get_records_by_date(date, is_month) + # if none of the records match that day + if len(records_to_delete) == 0: await ctx.send(f"No transactions within {text}") + response_str = "Confirm records to delete\n" + response_str += user_list[CHANNEL_ID].display_transaction(records_to_delete) + await ctx.send(response_str) + response_str = "\nEnter Yes or No" + await ctx.send(response_str) + response = await bot.wait_for('message', check=lambda message: message.author == ctx.author) + await handle_confirmation(ctx, response.content, records_to_delete) + + +async def handle_confirmation(ctx, message, records_to_delete): + """ + Deletes transactions if the user confirms. + + Parameters: + - ctx (discord.ext.commands.Context): The Discord context window. + - message (str): The user's confirmation ("Yes" or "No"). + - records_to_delete (list): The records to be deleted. + + Returns: + - None + """ + + if message.lower() == "yes": + user_list[CHANNEL_ID].deleteHistory(records_to_delete) + user_list[CHANNEL_ID].save_user(CHANNEL_ID) + await ctx.send("Successfully deleted records") + else: await ctx.send("No records deleted") + + +@bot.command() +async def history(ctx): + """ + Handles the command 'history'. Lists the transaction history. + + :param ctx - Discord context window + :param Bot - Discord Bot object + :type: object + :return: None + """ + try: + count = 0 + table = [["Category", "Date", "Amount in $"]] + + if CHANNEL_ID not in user_list.keys(): user_list[CHANNEL_ID] = User(CHANNEL_ID) + + if not user_list[CHANNEL_ID].transactions: raise Exception("Sorry! No spending records found!") + + for category, transactions in user_list[CHANNEL_ID].transactions.items(): + for transaction in transactions: + count += 1 + date = transaction["Date"].strftime("%m-%d-%y") + value = format(transaction["Value"], ".2f") + table.append([category, date, "$ " + value]) + + if count == 0: raise Exception("Sorry! No spending records found!") + + spend_total_str = "```" + tabulate(table, headers='firstrow') + "```" + await ctx.send(spend_total_str) + + except Exception as ex: + print(str(ex), exc_info=True) + await ctx.send("Request cannot be processed. Please try again with correct format!") + +@bot.command() +async def edit(ctx): + """ + Handles the command 'edit' and then displays a message explaining the format. The function 'edit_list2' is called next. + + :param ctx - Discord context window + :param Bot - Discord Bot object + :type: object + :return: None + """ + + try: + if CHANNEL_ID in list(user_list.keys()): + await ctx.send("Please enter the date of transaction to edit(in mm-dd-yyyy format)") + date = await bot.wait_for('message', check=lambda message: message.author == ctx.author, timeout=60.0) + + await ctx.send("Please enter the value of transaction to edit") + value = await bot.wait_for('message', check=lambda message: message.author == ctx.author, timeout=60.0) + + select_options = [discord.SelectOption(label="Food"), discord.SelectOption(label="Groceries"), discord.SelectOption(label="Utilities"), discord.SelectOption(label="Transport"), discord.SelectOption(label="Shopping")] + select = Select(max_values=1,min_values=1, options=select_options) + async def my_callback(interaction): + await interaction.response.send_message(f'You chose: {select.values[0]}') + await asyncio.sleep(0.5) + await edit_list2(ctx, date.content, select.values[0], value.content) + + select.callback = my_callback + view = View(timeout=90) + view.add_item(select) + + await ctx.send('Please select the Category of transaction', view=view) + + else: await ctx.send("No data found") + except Exception as ex: + print(str(ex), exc_info=True) + await ctx.send("Request cannot be processed. Please try again with correct format!") + + +async def edit_list2(ctx,date,category,value): + """ + Parses the input from the user message, finds the appropriate transaction, and asks the user whether they + want to update the date, value, or category of the transaction, then passes control to the edit3 function. + + Parameters: + - ctx (discord.ext.commands.Context): The Discord context window. + - date (str): The date of the transaction in the format "%m-%d-%Y". + - category (str): The category of the expense to be edited. + - value (str or float): The value of the transaction to be edited. + + Returns: + - None + """ + try: + dateFormat = "%m-%d-%Y" + info_date = user_list[CHANNEL_ID].validate_date_format(date, dateFormat) + info_category = category + info_value = value + + if info_date is None: + await ctx.send("The date is incorrect") + return + select_options = [discord.SelectOption(label="Date"), discord.SelectOption(label="Category"), discord.SelectOption(label="Cost")] + select = Select(placeholder="What do you want to update", max_values=1,min_values=1, options=select_options) + async def my_callback(interaction): + await interaction.response.send_message(f'You chose: {select.values[0]}') + await asyncio.sleep(0.5) + await edit3(ctx, select.values[0]) + + for transaction in user_list[CHANNEL_ID].transactions[info_category]: + if transaction["Date"].date() == info_date: + if transaction["Value"] == float(info_value): + user_list[CHANNEL_ID].store_edit_transaction(transaction, info_category) + select.callback = my_callback + view = View(timeout=90) + view.add_item(select) + + await ctx.send('Please select an option to update', view=view) + break + else: await ctx.send("Transaction not found") + except Exception as ex: + print(str(ex), exc_info=True) + await ctx.send("Request cannot be processed. Please try again with correct format!") + + +async def edit3(ctx,choice): + """ + Receives the user's input corresponding to what they want to edit and transfers the execution to the function + according to the choice. + + :param ctx: The Discord context window. + :param choice: The user's choice for editing ("Date," "Category," or "Cost"). + :return: None + """ + choice1 = choice + if choice1 == "Date": + await ctx.send ("Please enter the new date (in mm-dd-yyyy format)") + new_date = await bot.wait_for('message', check=lambda message: message.author == ctx.author, timeout=60.0) + await edit_date(ctx,new_date) + + if choice1 == "Category": + select_options = [discord.SelectOption(label="Food"),discord.SelectOption(label="Groceries"),discord.SelectOption(label="Utilities"),discord.SelectOption(label="Transport"),discord.SelectOption(label="Shopping")] + select = Select(max_values=1,min_values=1, options=select_options) + async def my_callback(interaction): + await interaction.response.send_message(f'You chose: {select.values[0]}') + await asyncio.sleep(0.5) + await edit_cat(ctx, select.values[0]) + + select.callback = my_callback + view = View(timeout=90) + view.add_item(select) + + await ctx.send('Please select the new Category', view=view) + + if choice1 == "Cost": + await ctx.send ( "Please type the new cost") + new_cost = await bot.wait_for('message', check=lambda message: message.author == ctx.author, timeout=60.0) + await edit_cost(ctx,new_cost) + + +async def edit_date(ctx, message): + """ + Updates the date of a transaction when the user chooses to edit it. + + :param ctx: The Discord context window. + :param message: The user's message containing the new date. + :return: None + """ + new_date = message.content + user_date = datetime.strptime(new_date, "%m-%d-%Y") + if user_date is None: + await ctx.send ("The date is incorrect") + return + updated_transaction = user_list[CHANNEL_ID].edit_transaction_date(user_date) + user_list[CHANNEL_ID].save_user(CHANNEL_ID) + await ctx.send(("Date is updated. Here is the new transaction. \n Date {}. Value {}. \n".format(updated_transaction["Date"].strftime("%m-%d-%Y %H:%M:%S"),format(updated_transaction["Value"], ".2f")))) + + +async def edit_cat(ctx,new_category): + """ + Updates the category of a transaction when the user chooses to edit it. + + :param ctx: The Discord context window. + :param new_category: The new category chosen by the user. + :return: None + """ + updated_transaction = user_list[CHANNEL_ID].edit_transaction_category(new_category) + if updated_transaction: + user_list[CHANNEL_ID].save_user(CHANNEL_ID) + await ctx.send("Category has been edited.") + else: await ctx.send("Category has not been edited successfully") + + +async def edit_cost(ctx,message): + """ + Updates the amount of a transaction when the user chooses to edit it. + + :param ctx: The Discord context window. + :param message: The user's message containing the new cost. + :return: None + """ + new_cost = message.content + + new_cost = user_list[CHANNEL_ID].validate_entered_amount(new_cost) + if new_cost != 0: + user_list[CHANNEL_ID].save_user(CHANNEL_ID) + updated_transaction = user_list[CHANNEL_ID].edit_transaction_value(new_cost) + await ctx.send("Value is updated. Here is the new transaction. \n Date {}. Value {}. \n".format(updated_transaction["Date"].strftime("%m-%d-%Y %H:%M:%S"),format(updated_transaction["Value"], ".2f"))) + + else: + await ctx.send("The cost is invalid") + return + + + + +@bot.command() +async def chart(ctx): + """ + Handles the chart command. When the user runs this command the bot will create a piechart + of all the transactions and their categories and post that to the chat + + :param ctx - Discord context window + :param Bot - Discord Bot object + :param date - date message object received from the user + :type: object + :return: None + """ + try: + dateFormat = "%m-%d-%Y" + curr_day = datetime.now() + await ctx.send("Enter start day") + await ctx.send(f"\n\tExample day in format mm-dd-YYYY: {curr_day.strftime(dateFormat)}\n") + + def check(message):return message.author == ctx.author and message.channel == ctx.channel + + start_date_message = await bot.wait_for('message', check=check, timeout=30) + start_date_str = start_date_message.content + + await ctx.send("Enter end day") + await ctx.send(f"\n\tExample day in format mm-dd-YYYY: {curr_day.strftime(dateFormat)}\n") + + end_date_message = await bot.wait_for('message', check=check, timeout=30) + end_date_str = end_date_message.content + + start_date_dt = datetime.strptime(start_date_str, "%m-%d-%Y") + end_date_dt = datetime.strptime(end_date_str, "%m-%d-%Y") + + chart_file = user_list[CHANNEL_ID].create_chart(CHANNEL_ID, start_date_dt, end_date_dt) + for cf in chart_file: + with open(cf, "rb") as f: + file = discord.File(f) + await ctx.send(file=file) + + except Exception as ex: + print(str(ex), exc_info=True) + await ctx.send("Request cannot be processed. Please try again with correct format!") + +def get_users(): + """ + Reads user data from files in a specified directory and returns it as a dictionary. + The function searches for files with a ".pickle" extension in the specified directory, reads each file's content, and stores it in a + dictionary where the keys are the filenames (without the ".pickle" extension) and the values are the deserialized data from the files. + + Returns: + - users (dict): A dictionary containing user data. + """ + + data_dir = "discordData" + users = {} + for file in os.listdir(data_dir): + if file.endswith(".pickle"): + u = re.match(r"(.+)\.pickle", file) + if u: + u = u.group(1) + abspath = pathlib.Path("{0}/{1}".format(data_dir, file)).absolute() + with open(abspath, "rb") as f: users[u] = pickle.load(f) + + return users + +if __name__ == "__main__": + try: + user_list = get_users() + bot.run(BOT_TOKEN) + except Exception as e: print(f"{e}") + diff --git a/src/discordData/.gitignore b/src/discordData/.gitignore new file mode 100644 index 000000000..5e7d2734c --- /dev/null +++ b/src/discordData/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore diff --git a/src/discordUser.py b/src/discordUser.py new file mode 100644 index 000000000..22f449d41 --- /dev/null +++ b/src/discordUser.py @@ -0,0 +1,441 @@ +""" +File: DiscordUser.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains functions that stores and retrieves data from the .pickle file and also handles validations. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" +import pathlib +import pickle +import re +from datetime import datetime +import matplotlib.pyplot as plt + + +class User: + """ + A class that represents a user's financial data and transactions. It functions that stores and retrieves data from the .pickle file and + handles validations + + Attributes: + spend_categories (list): List of spend categories. + spend_display_option (list): List of display options for spend categories. + transactions (dict): Dictionary to store user's financial transactions. + edit_transactions (dict): Dictionary to store transactions being edited. + edit_category (str): The category of the transaction being edited. + monthly_budget (float): User's monthly budget. + rules (dict): Dictionary to store user-defined rules for categories. + + Methods: + - save_user(userid): Saves user data to a .pickle file. + - validate_entered_amount(amount_entered): Validates and rounds entered amounts. + - add_transaction(date, category, value, userid): Stores a transaction to file. + - store_edit_transaction(existing_transaction, edit_category): Assigns a transaction and category to be edited. + - edit_transaction_date(new_date): Returns the edited transaction with the new date. + - edit_transaction_category(new_category): Updates the edited transaction with the new category. + - edit_transaction_value(new_value): Returns the edited transaction with the new value. + - deleteHistory(records=None): Deletes transactions. + - validate_date_format(text, date_format): Converts an inputted date to the specified date format. + - get_records_by_date(date, is_month): Returns records that match the filter by date. + - display_transaction(transaction): Converts a dictionary of transactions into a user-readable string. + - get_number_of_transactions(): Returns the total number of transactions across all categories. + - add_monthly_budget(amount, userid): Edits the user's monthly budget. + - monthly_total(): Calculates the total expenditure for the current month. + - create_chart(userid, start_date=None, end_date=None): Creates matplotlib charts of expenditure transactions. + """ + def __init__(self, userid): + self.spend_categories = [ + "Food", + "Groceries", + "Utilities", + "Transport", + "Shopping", + "Miscellaneous", + ] + self.spend_display_option = ["Day", "Month"] + self.transactions = {} + self.edit_transactions = {} + self.edit_category = {} + self.monthly_budget = 0 + + for category in self.spend_categories: + self.transactions[category] = [] + self.save_user(userid) + + def save_user(self, userid): + """ + Save user data to a pickle file. + + This function takes a user ID and attempts to save the user data to a pickle + file. It constructs the file path based on the provided user ID and the + 'discordData' directory. The user data is serialized using the pickle module + and saved to the specified file. + + Parameters: + - userid (string): The unique identifier for the user whose data is being saved. + + Raises: + - Exception: If an error occurs during the data saving process, an exception + is raised and an error message is printed. + + Return: + - None + """ + try: + data_dir = "discordData" + abspath = pathlib.Path("{0}/{1}.pickle".format(data_dir, userid)).absolute() + with open(abspath, "wb") as f: + pickle.dump(self, f) + + except Exception as e: print("exception occurred:"+str(e)) + + + def validate_entered_amount(self, amount_entered): + """ + Validates and rounds an entered amount. + + This function validates an entered amount to ensure it is greater than zero and + rounds it to 2 decimal places. + + Parameters: + - amount_entered (float): The entered amount to be validated and rounded. + + Return (float): + - The rounded amount if valid, else 0. + """ + if 0 < len(amount_entered) <= 15: + if amount_entered.isdigit: + if re.match("^[0-9]*\\.?[0-9]*$", amount_entered): + amount = round(float(amount_entered), 2) + if amount > 0: + return amount + return 0 + + def add_transaction(self, date, category, value, userid): + """ + Stores a transaction in a user's data file. + + This function records a transaction with the provided date, category, and value + in the user's data file, identified by the 'userid'. + + Parameters: + - date (string): The date of the transaction + - category (string): The category of the transaction. + - value (string): The amount of the transaction. + - userid (string): The unique identifier for the user and the filename. + + Returns: + - None + """ + try: + self.transactions[category].append({"Date": date, "Value": value}) + self.save_user(userid) + + except Exception as e: print("exception occurred:"+str(e)) + + def store_edit_transaction(self, existing_transaction, edit_category): + """ + Assigns a transaction and its category for editing. + + This function sets the transaction and category that the user has chosen to edit. + + Parameters: + - existing_transaction (string): The transaction to be edited. + - edit_category (string): The existing category of the transaction. + + Returns: + - None + """ + try: + self.edit_transactions = existing_transaction + self.edit_category = edit_category + + except Exception as e: print("exception occurred:"+str(e)) + + def edit_transaction_date(self, new_date): + """ + Returns the transaction with the new date. + + This function returns the edited transaction with the specified new date. + + Paramters: + - new_date (string): the new date of the transaction. + + Returns: + - transactions dict (dict): A dictionary representing the edited transaction. + """ + transaction = None + for transaction in self.transactions[self.edit_category]: + if transaction == self.edit_transactions: + transaction["Date"] = new_date + break + return transaction + + def edit_transaction_category(self, new_category): + """ + Updates the category of the edited transaction. + + This function modifies the category of the previously edited transaction to the + specified new category. + + Parameters: + - new_category (string): the new category of the transaction. + + Returns (bool): + - True if the category update is successful. + """ + self.transactions[self.edit_category].remove(self.edit_transactions) + self.transactions[new_category].append(self.edit_transactions) + return True + + def edit_transaction_value(self, new_value): + """ + Updates the value of the edited transaction and returns the modified transaction. + + This function modifies the value of the previously edited transaction to the + specified new value and returns the modified transaction. + + Parameters: + - new_value(string): the new value of the transaction. + + Returns: + - transactions (dict): A dictionary representing the edited transaction. + """ + transaction = None + for transaction in self.transactions[self.edit_category]: + if transaction == self.edit_transactions: + transaction["Value"] = new_value + break + return transaction + + def deleteHistory(self, records=None): + """ + Deletes transactions + + :param records: list of records to delete. + :type: array + :return: None + """ + # if there are specific records to delete + # and it is not all records from the user + if records is not None and self.transactions != records: + # delete only the records specified + for category in records: + for record in records[category]: + try: + self.transactions[category].remove(record) + except Exception as e: print("exception occurred:"+str(e)) + else: + self.transactions = {} + for category in self.spend_categories: + self.transactions[category] = [] + + def validate_date_format(self, text, date_format): + """ + Deletes specified transaction records. + + This function deletes transaction records from the user's history based on the provided list of records. + + Parameters: + - text(string): has the date which is to be converted + - date_format(string): has the format to which the conversion should be done + + Return: + - date (datetime.dateime): contains the formatted date + """ + date = None + + # try and parse as Month-Day-Year + try: + date = datetime.strptime(text, date_format).date() + + except Exception as e: print(e) + return date + + def get_records_by_date(self, date: datetime.date, is_month: bool): + """ + Retrieve transaction records that match a given date or month. + + This function filters the user's transaction records based on the provided date. + If `is_month` is True, it matches the year and month, but not the day. + + Parameters: + - date (datetime.date): The date for filtering records + - is_month(bool): If True, filter records for the entire month. + + Returns (dictionary): + A dict of matched records categorized by spend category. + """ + user_history = self.transactions + if date == "all": + return user_history + # else filter by date + matched_dates = {} + for category in self.spend_categories: + matched_dates[category] = [] + for category in user_history: + for record in user_history[category]: + record_date = record["Date"] + # format it to date and time, then only get the day,month,year + record_date = record_date.date() + if is_month: + # strip the date + record_date = record_date.replace(day=1) + date = date.replace(day=1) + # checks if the records are equal/matching + if record_date == date: + matched_dates[category].append(record) + return matched_dates + + def display_transaction(self, transaction): + """ + Convert a dictionary of transactions into a user-readable string. + + This function takes a dictionary where each key is a spend category, and the + corresponding value is a list of transaction records for that category. It + then converts this data into a user-readable string format. + + Parameters: + - transaction (dict): A dictionary of transaction records organized by category + + Returns (string): + - final_str, which is the transactions stringifies + """ + final_str = "" + + for category in transaction: + for record in transaction[category]: + final_str += ( + f'{category}, {record["Date"].date()}, {record["Value"]:}\n' + ) + + return final_str + + def get_number_of_transactions(self): + """ + Helper function to get the total number of transactions across + all categories + + Parameters: + - None + + Return (int): + - number of transactions + """ + total = 0 + for category in self.transactions: + total += len(self.transactions[category]) + return total + + def add_monthly_budget(self, amount, userid): + """ + Set or update the monthly budget for the current user. + + This function allows setting or updating the monthly budget for a user with the + specified 'userid'. + + Parameters: + - amount (float): The budget amount to be set or updated. + - userid (str): The unique identifier for the user. + + Returns: + - None + """ + try: + if amount != 0: + self.monthly_budget = amount + self.save_user(userid) + + except Exception as e: print("exception occurred:"+str(e)) + + def monthly_total(self): + """ + Calculate the total expenditure for the current month. + + This function calculates the total expenditure for the current month based on + the user's transaction records. + + Parameters: + - None + + Return: + - total_value (float) - The total expenditure for the current month, rounded to 2 decimal places. + """ + date = datetime.today() + total_value = 0 + for category in self.spend_categories: + for transaction in self.transactions[category]: + if transaction["Date"].strftime("%m") == date.strftime("%m"): + total_value += transaction["Value"] + return total_value + + def create_chart(self, userid, start_date=None, end_date=None): + """ + Generate visualizations of transaction data, including a pie chart and a bar graph. + + This function creates visualizations using Matplotlib to represent transaction data. It generates + a pie chart and a bar graph, where each segment or bar represents a spending category and its total + expenditure. + + Parameter: + - userid (str): The unique identifier for the user + - start_date (datetime.date or None): Optional start date to filter transactions (inclusion criterion). + - end_date (datetime.date or None): Optional end date to filter transactions (inclusion criterion). + + Returns (str): + - A list of file paths to the generated images. + """ + labels = [] + totals = [] + charts = [] + for category in self.spend_categories: + total = 0 + for transaction in self.transactions[category]: + transaction_date = transaction["Date"] + if start_date and transaction_date < start_date: continue + if end_date and transaction_date > end_date: continue + total += int(transaction["Value"]) + if total != 0: + labels.append(category) + totals.append(total) + + # Pie Chart + plt.clf() + plt.pie(totals, labels=labels) + plt.title("Your Expenditure Report") + plt.savefig("discordData/{}_pie_chart.png".format(userid)) # Ensure that the file name is unique + charts.append("discordData/{}_pie_chart.png".format(userid)) # Ensure that the file name is unique + + # Bar Graph + plt.clf() + plt.switch_backend("Agg") + plt.title("Your Expenditure Report") + plt.bar(labels, totals) + plt.xlabel('Categories') + plt.ylabel('Expenditure') + plt.title("Your Expenditure Report") + plt.savefig("discordData/{}_bar_chart.png".format(userid)) # Ensure that the file name is unique + charts.append("discordData/{}_bar_chart.png".format(userid)) # Ensure that the file name is unique + + # Add more visualizations here. Maintain the above format while adding more visualizations. + + return charts diff --git a/src/bot.py b/src/teleBot.py similarity index 70% rename from src/bot.py rename to src/teleBot.py index 140babd1f..e22fc85c9 100644 --- a/src/bot.py +++ b/src/teleBot.py @@ -1,6 +1,30 @@ """ -File contains bot message handlers and their associated functions +File: teleBot.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Telegram bot message handlers and their associated functions. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. """ + import logging import os from calendar import monthrange @@ -12,7 +36,6 @@ import io from datetime import datetime from tabulate import tabulate -import sys import telebot from telebot import types import smtplib @@ -21,11 +44,11 @@ from email.mime.base import MIMEBase from email import encoders -sys.path.append("C:/NCSU/Sem 1/SE/Project 3/slashbot/") +# sys.path.append("../slashbot/") try: - from src.user import User -except: - from user import User + from src.teleUser import User +except Exception: + from teleUser import User api_token = os.environ["API_TOKEN"] commands = { @@ -70,15 +93,8 @@ def start_and_menu_command(m): """ chat_id = m.chat.id print("*********************CHAT ID***************************", chat_id) - text_intro = ( - "Welcome to SlashBot - a simple solution to track your expenses! \nHere is a list of available " - "commands, please enter a command of your choice so that I can assist you further: \n\n " - ) - for ( - c - ) in ( - commands - ): # generate help text out of the commands dictionary defined at the top + text_intro = ("Welcome to SlashBot - a simple solution to track your expenses! \nHere is a list of available commands, please enter a command of your choice so that I can assist you further: \n\n ") + for (c) in (commands): # generate help text out of the commands dictionary defined at the top text_intro += "/" + c + ": " text_intro += commands[c] + "\n\n" bot.send_message(chat_id, text_intro) @@ -95,16 +111,9 @@ def command_budget(message): """ chat_id = str(message.chat.id) option.pop(chat_id, None) - if chat_id not in user_list.keys(): - user_list[chat_id] = User(chat_id) - bot.send_message( - chat_id, - "Your current monthly budget is {}".format(user_list[chat_id].monthly_budget), - ) - message = bot.send_message( - chat_id, - "Enter an amount to update your monthly budget. \n(Enter numeric values only)", - ) + if chat_id not in user_list.keys():user_list[chat_id] = User(chat_id) + bot.send_message(chat_id, "Your current monthly budget is {}".format(user_list[chat_id].monthly_budget)) + message = bot.send_message(chat_id, "Enter an amount to update your monthly budget. \n(Enter numeric values only)") bot.register_next_step_handler(message, post_budget_input) @@ -121,21 +130,12 @@ def post_budget_input(message): try: chat_id = str(message.chat.id) amount_entered = message.text - amount_value = user_list[chat_id].validate_entered_amount( - amount_entered - ) # validate - if amount_value == 0: # cannot be $0 spending - raise Exception("Budget amount has to be a positive number.") + amount_value = user_list[chat_id].validate_entered_amount(amount_entered) # validate + if amount_value == 0: raise Exception("Budget amount has to be a positive number.") # cannot be $0 spending user_list[chat_id].add_monthly_budget(amount_value, chat_id) - bot.send_message( - chat_id, - "The budget for this month has been set as ${}".format( - format(amount_value, ".2f") - ), - ) + bot.send_message(chat_id,"The budget for this month has been set as ${}".format(format(amount_value, ".2f"))) - except Exception as ex: - bot.reply_to(message, "Oh no. " + str(ex)) + except Exception as ex: bot.reply_to(message, "Oh no. " + str(ex)) @bot.message_handler(commands=["add"]) @@ -151,20 +151,17 @@ def command_add(message): """ chat_id = str(message.chat.id) option.pop(chat_id, None) - if chat_id not in user_list.keys(): - user_list[chat_id] = User(chat_id) + if chat_id not in user_list.keys(): user_list[chat_id] = User(chat_id) user = user_list[chat_id] try: markup = get_calendar_buttons(user) bot.send_message(chat_id, "Click the date of purchase:", reply_markup=markup) except Exception as ex: - print("Exception occurred : ") logger.error(str(ex), exc_info=True) bot.reply_to(message, "Processing Failed - \nError : " + str(ex)) -def is_add_callback(query): - return query.data != "none" and "/" not in query.data +def is_add_callback(query): return query.data != "none" and "/" not in query.data @bot.callback_query_handler(func=is_add_callback, filter=None) @@ -179,38 +176,27 @@ def post_date_selection(message): option.pop(chat_id, None) try: - if chat_id not in user_list.keys(): - user_list[chat_id] = User(chat_id) + if chat_id not in user_list.keys(): user_list[chat_id] = User(chat_id) user = user_list[chat_id] # if they want to go back/forward a month date_to_add = handler_callback(message.data, user) if date_to_add is None: # just edit the calendar - bot.edit_message_reply_markup( - chat_id=message.from_user.id, - message_id=message.message.message_id, - reply_markup=get_calendar_buttons(user), - ) + bot.edit_message_reply_markup(chat_id=message.from_user.id, message_id=message.message.message_id, reply_markup=get_calendar_buttons(user)) return if date_to_add == -1: # invalid date - fmt_min, fmt_max = user.min_date.strftime( - "%m/%d/%Y" - ), user.max_date.strftime("%m/%d/%Y") - bot.send_message( - chat_id, "Enter a date between {} and {}".format(fmt_min, fmt_max) - ) + fmt_min, fmt_max = user.min_date.strftime("%m/%d/%Y"), user.max_date.strftime("%m/%d/%Y") + bot.send_message(chat_id, "Enter a date between {} and {}".format(fmt_min, fmt_max)) return spend_categories = user_list[chat_id].spend_categories markup = types.ReplyKeyboardMarkup(one_time_keyboard=True) markup.row_width = 2 - for c in spend_categories: - markup.add(c) + for c in spend_categories:markup.add(c) msg = bot.reply_to(message.message, "Select Category", reply_markup=markup) bot.register_next_step_handler(msg, post_category_selection, date_to_add) except Exception as ex: - print("Exception occurred : ") logger.error(str(ex), exc_info=True) bot.reply_to(message.message, "Processing Failed - \nError : " + str(ex)) @@ -231,29 +217,16 @@ def post_category_selection(message, date_to_add): selected_category = message.text spend_categories = user_list[chat_id].spend_categories if selected_category not in spend_categories: - bot.send_message( - chat_id, "Invalid", reply_markup=types.ReplyKeyboardRemove() - ) - raise Exception( - 'Sorry I don\'t recognise this category "{}"!'.format(selected_category) - ) + bot.send_message(chat_id, "Invalid", reply_markup=types.ReplyKeyboardRemove()) + raise Exception('Sorry I don\'t recognise this category "{}"!'.format(selected_category)) option[chat_id] = selected_category - message = bot.send_message( - chat_id, - "How much did you spend on {}? \n(Enter numeric values only)".format( - str(option[chat_id]) - ), - ) + message = bot.send_message(chat_id,"How much did you spend on {}? \n(Enter numeric values only)".format(str(option[chat_id]))) bot.register_next_step_handler(message, post_amount_input, date_to_add) except Exception as ex: bot.reply_to(message, "Oh no! " + str(ex)) display_text = "" - for ( - c - ) in ( - commands - ): # generate help text out of the commands dictionary defined at the top + for (c) in (commands): # generate help text out of the commands dictionary defined at the top display_text += "/" + c + ": " display_text += commands[c] + "\n" bot.send_message(chat_id, "Please select a menu option from below:") @@ -273,47 +246,19 @@ def post_amount_input(message, date_of_entry): try: chat_id = str(message.chat.id) amount_entered = message.text - amount_value = user_list[chat_id].validate_entered_amount( - amount_entered - ) # validate - if amount_value == 0: # cannot be $0 spending - raise Exception("Spent amount has to be a non-zero number.") - - date_str, category_str, amount_str = ( - date_of_entry.strftime("%m/%d/%Y %H:%M:%S"), - str(option[chat_id]), - format(amount_value, ".2f"), - ) - user_list[chat_id].add_transaction( - date_of_entry, option[chat_id], amount_value, chat_id - ) + amount_value = user_list[chat_id].validate_entered_amount(amount_entered) # validate + if amount_value == 0: raise Exception("Spent amount has to be a non-zero number.") # cannot be $0 spending + + date_str, category_str, amount_str = (date_of_entry.strftime("%m/%d/%Y %H:%M:%S"),str(option[chat_id]),format(amount_value, ".2f")) + user_list[chat_id].add_transaction(date_of_entry, option[chat_id], amount_value, chat_id) total_value = user_list[chat_id].monthly_total() - add_message = "The following expenditure has been recorded: You have spent ${} for {} on {}".format( - amount_str, category_str, date_str - ) if user_list[chat_id].monthly_budget > 0: - if total_value > user_list[chat_id].monthly_budget: - bot.send_message( - chat_id, - text="*You have gone over the monthly budget*", - parse_mode="Markdown", - ) - elif total_value == user_list[chat_id].monthly_budget: - bot.send_message( - chat_id, - text="*You have exhausted your monthly budget. You can check/download history*", - parse_mode="Markdown", - ) - elif total_value >= 0.8 * user_list[chat_id].monthly_budget: - bot.send_message( - chat_id, - text="*You have used 80% of the monthly budget.*", - parse_mode="Markdown", - ) - bot.send_message(chat_id, add_message) + if total_value > user_list[chat_id].monthly_budget: bot.send_message(chat_id,text="*You have gone over the monthly budget*",parse_mode="Markdown") + elif total_value == user_list[chat_id].monthly_budget: bot.send_message(chat_id,text="*You have exhausted your monthly budget. You can check/download history*",parse_mode="Markdown") + elif total_value >= 0.8 * user_list[chat_id].monthly_budget: bot.send_message(chat_id,text="*You have used 80% of the monthly budget.*", parse_mode="Markdown") + bot.send_message(chat_id, "The following expenditure has been recorded: You have spent ${} for {} on {}".format(amount_str, category_str, date_str)) except Exception as ex: - print("Exception occurred : ") logger.error(str(ex), exc_info=True) bot.reply_to(message, "Processing Failed - \nError : " + str(ex)) @@ -329,13 +274,10 @@ def show_history(message): """ try: chat_id = str(message.chat.id) - spend_total_str = "" count = 0 table = [["Category", "Date", "Amount in $", "Amount in Rs."]] - if chat_id not in list(user_list.keys()): - raise Exception("Sorry! No spending records found!") - if len(user_list[chat_id].transactions) == 0: - raise Exception("Sorry! No spending records found!") + if chat_id not in list(user_list.keys()): raise Exception("Sorry! No spending records found!") + if len(user_list[chat_id].transactions) == 0: raise Exception("Sorry! No spending records found!") else: for category in user_list[chat_id].transactions.keys(): for transaction in user_list[chat_id].transactions[category]: @@ -343,8 +285,7 @@ def show_history(message): date = transaction["Date"].strftime("%m/%d/%y") value = format(transaction["Value"], ".2f") table.append([date, category, "$ " + value]) - if count == 0: - raise Exception("Sorry! No spending records found!") + if count == 0: raise Exception("Sorry! No spending records found!") spend_total_str="
"+ tabulate(table, headers='firstrow')+"
" bot.send_message(chat_id, spend_total_str, parse_mode="HTML") @@ -366,10 +307,8 @@ def download_history(message): chat_id = str(message.chat.id) count = 0 table = [["Category", "Date", "Amount in $"]] - if chat_id not in list(user_list.keys()): - raise Exception("Sorry! No spending records found!") - if len(user_list[chat_id].transactions) == 0: - raise Exception("Sorry! No spending records found!") + if chat_id not in list(user_list.keys()): raise Exception("Sorry! No spending records found!") + if len(user_list[chat_id].transactions) == 0: raise Exception("Sorry! No spending records found!") else: for category in user_list[chat_id].transactions.keys(): for transaction in user_list[chat_id].transactions[category]: @@ -377,8 +316,7 @@ def download_history(message): date = transaction["Date"].strftime("%m/%d/%y") value = format(transaction["Value"], ".2f") table.append([date, category, "$"+value]) - if count == 0: - raise Exception("Sorry! No spending records found!") + if count == 0: raise Exception("Sorry! No spending records found!") s = io.StringIO() csv.writer(s).writerows(table) @@ -407,10 +345,8 @@ def send_email(message): chat_id = str(message.chat.id) count = 0 table = [["Category", "Date", "Amount in $"]] - if chat_id not in list(user_list.keys()): - raise Exception("Sorry! No spending records found!") - if len(user_list[chat_id].transactions) == 0: - raise Exception("Sorry! No spending records found!") + if chat_id not in list(user_list.keys()): raise Exception("Sorry! No spending records found!") + if len(user_list[chat_id].transactions) == 0: raise Exception("Sorry! No spending records found!") else: for category in user_list[chat_id].transactions.keys(): for transaction in user_list[chat_id].transactions[category]: @@ -418,8 +354,7 @@ def send_email(message): date = transaction["Date"].strftime("%m/%d/%y") value = format(transaction["Value"], ".2f") table.append([date, category, "$"+value]) - if count == 0: - raise Exception("Sorry! No spending records found!") + if count == 0: raise Exception("Sorry! No spending records found!") s = io.StringIO() csv.writer(s).writerows(table) @@ -446,10 +381,8 @@ def acceptEmailId(message): chat_id = str(message.chat.id) count = 0 table = [["Category", "Date", "Amount in $"]] - if chat_id not in list(user_list.keys()): - raise Exception("Sorry! No spending records found!") - if len(user_list[chat_id].transactions) == 0: - raise Exception("Sorry! No spending records found!") + if chat_id not in list(user_list.keys()): raise Exception("Sorry! No spending records found!") + if len(user_list[chat_id].transactions) == 0: raise Exception("Sorry! No spending records found!") else: for category in user_list[chat_id].transactions.keys(): for transaction in user_list[chat_id].transactions[category]: @@ -457,10 +390,9 @@ def acceptEmailId(message): date = transaction["Date"].strftime("%m/%d/%y") value = format(transaction["Value"], ".2f") table.append([date, category, "$"+value]) - if count == 0: - raise Exception("Sorry! No spending records found!") + if count == 0: raise Exception("Sorry! No spending records found!") - with open('history.csv', 'w', newline = '') as file: + with open('history.csv', 'w', newline = '', encoding='utf-8') as file: writer = csv.writer(file) writer.writerows(table) # s = io.StringIO() @@ -474,10 +406,7 @@ def acceptEmailId(message): # writer.writerow(u"date", u"category", u"cost") # bot.send_document(chat_id, buf) - mail_content = '''Hello, - This email has an attached copy of your expenditure history. - Thank you! - ''' + mail_content = '''Hello, This email has an attached copy of your expenditure history. Thank you!''' #The mail addresses and password sender_address = 'secheaper@gmail.com' sender_pass = 'csc510se' @@ -512,8 +441,7 @@ def acceptEmailId(message): except Exception as ex: logger.error(str(ex), exc_info=True) bot.reply_to(message, str(ex)) - else: - bot.send_message(message.chat.id, 'incorrect email') + else: bot.send_message(message.chat.id, 'incorrect email') @@ -529,25 +457,16 @@ def command_display(message): :return: None """ chat_id = str(message.chat.id) - if chat_id not in user_list or user_list[chat_id].get_number_of_transactions() == 0: - bot.send_message( - chat_id, "Oops! Looks like you do not have any spending records!" - ) + if chat_id not in user_list or user_list[chat_id].get_number_of_transactions() == 0: bot.send_message(chat_id, "Oops! Looks like you do not have any spending records!") else: try: markup = types.ReplyKeyboardMarkup(one_time_keyboard=True) markup.row_width = 2 - for mode in user_list[chat_id].spend_display_option: - markup.add(mode) - msg = bot.reply_to( - message, - "Please select a category to see the total expense", - reply_markup=markup, - ) + for mode in user_list[chat_id].spend_display_option: markup.add(mode) + msg = bot.reply_to(message, "Please select a category to see the total expense", reply_markup=markup,) bot.register_next_step_handler(msg, display_total) except Exception as ex: - print("Exception occurred : ") logger.error(str(ex), exc_info=True) bot.reply_to(message, "Oops! - \nError : " + str(ex)) @@ -565,13 +484,9 @@ def display_total(message): chat_id = str(message.chat.id) day_week_month = message.text - if day_week_month not in user_list[chat_id].spend_display_option: - raise Exception( - 'Sorry I can\'t show spendings for "{}"!'.format(day_week_month) - ) + if day_week_month not in user_list[chat_id].spend_display_option: raise Exception('Sorry I can\'t show spendings for "{}"!'.format(day_week_month)) - if len(user_list[chat_id].transactions) == 0: - raise Exception("Oops! Looks like you do not have any spending records!") + if len(user_list[chat_id].transactions) == 0: raise Exception("Oops! Looks like you do not have any spending records!") bot.send_message(chat_id, "Hold on! Calculating...") @@ -582,15 +497,9 @@ def display_total(message): for category in user_list[chat_id].transactions.keys(): for transaction in user_list[chat_id].transactions[category]: if transaction["Date"].strftime("%d") == query.strftime("%d"): - query_result += "Category {} Date {} Value {:.2f} \n".format( - category, - transaction["Date"].strftime(dateFormat), - transaction["Value"], - ) + query_result += "Category {} Date {} Value {:.2f} \n".format(category,transaction["Date"].strftime(dateFormat),transaction["Value"]) total_value += transaction["Value"] - total_spendings = "Here are your total spendings for the date {} \n".format( - datetime.today().strftime("%m/%d/%Y") - ) + total_spendings = "Here are your total spendings for the date {} \n".format(datetime.today().strftime("%m/%d/%Y")) total_spendings += query_result total_spendings += "Total Value {:.2f}".format(total_value) bot.send_message(chat_id, total_spendings) @@ -603,23 +512,14 @@ def display_total(message): for category in user_list[chat_id].transactions.keys(): for transaction in user_list[chat_id].transactions[category]: if transaction["Date"].strftime("%m") == query.strftime("%m"): - query_result += "Category {} Date {} Value {:.2f} \n".format( - category, - transaction["Date"].strftime(dateFormat), - transaction["Value"], - ) + query_result += "Category {} Date {} Value {:.2f} \n".format(category,transaction["Date"].strftime(dateFormat),transaction["Value"]) total_value += transaction["Value"] - total_spendings = ( - "Here are your total spendings for the Month {} \n".format( - datetime.today().strftime("%B") - ) - ) + total_spendings = ("Here are your total spendings for the Month {} \n".format(datetime.today().strftime("%B"))) total_spendings += query_result total_spendings += "Total Value {:.2f}\n".format(total_value) total_spendings += "Budget for the month {}".format(str(budget_value)) bot.send_message(chat_id, total_spendings) except Exception as ex: - print("Exception occurred : ") logger.error(str(ex), exc_info=True) bot.reply_to(message, str(ex)) @@ -637,22 +537,13 @@ def edit1(message): try: if chat_id in list(user_list.keys()): - msg = bot.reply_to( - message, - "Please enter the date (in mm/dd/yyyy format), category and " - "value of the transaction you made (Eg: 01/03/2021,Transport,25)", - ) + msg = bot.reply_to(message,"Please enter the date (in mm/dd/yyyy format), category and value of the transaction you made (Eg: 01/03/2021,Transport,25)") bot.register_next_step_handler(msg, edit_list2) - else: - bot.send_message(chat_id, "No data found") + else:bot.send_message(chat_id, "No data found") except Exception as ex: - print("Exception occurred : ") logger.error(str(ex), exc_info=True) - bot.reply_to( - message, - "Processing Failed - \nError : Incorrect format - (Eg: 01/03/2021,Transport,25)", - ) + bot.reply_to(message,"Processing Failed - \nError : Incorrect format - (Eg: 01/03/2021,Transport,25)") def edit_list2(message): @@ -680,24 +571,17 @@ def edit_list2(message): markup = types.ReplyKeyboardMarkup(one_time_keyboard=True) markup.row_width = 2 choices = ["Date", "Category", "Cost"] - for c in choices: - markup.add(c) + for c in choices: markup.add(c) for transaction in user_list[chat_id].transactions[info_category]: if transaction["Date"].date() == info_date: if transaction["Value"] == float(info_value): - user_list[chat_id].store_edit_transaction( - transaction, info_category - ) - choice = bot.reply_to( - message, "What do you want to update?", reply_markup=markup - ) + user_list[chat_id].store_edit_transaction( transaction, info_category) + choice = bot.reply_to(message, "What do you want to update?", reply_markup=markup) bot.register_next_step_handler(choice, edit3) break - else: - bot.reply_to(message, "Transaction not found") + else:bot.reply_to(message, "Transaction not found") except Exception as ex: - print("Exception occurred : ") logger.error(str(ex), exc_info=True) bot.reply_to(message, "Processing Failed - Error: " + str(ex)) @@ -715,18 +599,13 @@ def edit3(message): chat_id = str(message.chat.id) markup = types.ReplyKeyboardMarkup(one_time_keyboard=True) markup.row_width = 2 - for category in user_list[chat_id].spend_categories: - markup.add(category) + for category in user_list[chat_id].spend_categories: markup.add(category) if choice1 == "Date": - new_date = bot.reply_to( - message, "Please enter the new date (in mm/dd/yyyy format)" - ) + new_date = bot.reply_to(message, "Please enter the new date (in mm/dd/yyyy format)") bot.register_next_step_handler(new_date, edit_date) if choice1 == "Category": - new_cat = bot.reply_to( - message, "Please select the new category", reply_markup=markup - ) + new_cat = bot.reply_to(message, "Please select the new category", reply_markup=markup) bot.register_next_step_handler(new_cat, edit_cat) if choice1 == "Cost": @@ -751,12 +630,7 @@ def edit_date(message): return updated_transaction = user_list[chat_id].edit_transaction_date(user_date) user_list[chat_id].save_user(chat_id) - edit_message = ( - "Date is updated. Here is the new transaction. \n Date {}. Value {}. \n".format( - updated_transaction["Date"].strftime("%m/%d/%Y %H:%M:%S"), - format(updated_transaction["Value"], ".2f"), - ) - ) + edit_message = ("Date is updated. Here is the new transaction. \n Date {}. Value {}. \n".format(updated_transaction["Date"].strftime("%m/%d/%Y %H:%M:%S"),format(updated_transaction["Value"], ".2f"))) bot.reply_to(message, edit_message) @@ -774,11 +648,9 @@ def edit_cat(message): updated_transaction = user_list[chat_id].edit_transaction_category(new_category) if updated_transaction: user_list[chat_id].save_user(chat_id) - edit_message = "Category has been edited." - bot.reply_to(message, edit_message) + bot.reply_to(message, "Category has been edited.") else: - edit_message = "Category has not been edited successfully" - bot.reply_to(message, edit_message) + bot.reply_to(message, "Category has not been edited successfully") def edit_cost(message): @@ -796,11 +668,7 @@ def edit_cost(message): if new_cost != 0: user_list[chat_id].save_user(chat_id) updated_transaction = user_list[chat_id].edit_transaction_value(new_cost) - edit_message = "Value is updated. Here is the new transaction. \n Date {}. Value {}. \n".format( - updated_transaction["Date"].strftime("%m/%d/%Y %H:%M:%S"), - format(updated_transaction["Value"], ".2f"), - ) - bot.reply_to(message, edit_message) + bot.reply_to(message, "Value is updated. Here is the new transaction. \n Date {}. Value {}. \n".format(updated_transaction["Date"].strftime("%m/%d/%Y %H:%M:%S"),format(updated_transaction["Value"], ".2f"))) else: bot.reply_to(message, "The cost is invalid") @@ -822,41 +690,23 @@ def handle_budget_document_csv(message): chat_id = str(message.chat.id) file_info = bot.get_file(message.document.file_id) download_file = bot.download_file(file_info.file_path) - with open("data/{}_spending.csv".format(chat_id), mode="wb") as f: - f.write(download_file) - unknown_spending = user_list[chat_id].read_budget_csv( - "data/{}_spending.csv".format(chat_id), chat_id - ) + with open("data/{}_spending.csv".format(chat_id), mode="wb") as f: f.write(download_file) + unknown_spending = user_list[chat_id].read_budget_csv("data/{}_spending.csv".format(chat_id), chat_id) for _, row in unknown_spending.iterrows(): text = "How do you want to categorize the following transaction \n" - text += "Date: {}. Description: {}. Debit: {}. \n".format( - row["date"], row["description"], row["debit"] - ) + text += "Date: {}. Description: {}. Debit: {}. \n".format(row["date"], row["description"], row["debit"]) buttons = telebot.types.InlineKeyboardMarkup(row_width=3) for category in user_list[chat_id].spend_categories: - callback = "{},{},{},{}".format( - category, row["date"], row["debit"], row["description"] - ) - buttons.add( - telebot.types.InlineKeyboardButton(category, callback_data=callback) - ) + callback = "{},{},{},{}".format(category, row["date"], row["debit"], row["description"]) + buttons.add(telebot.types.InlineKeyboardButton(category, callback_data=callback)) bot.send_message(chat_id, text, reply_markup=buttons) except Exception as ex: - print("Exception occurred : ") logger.error(str(ex), exc_info=True) bot.reply_to(message, "Processing Failed - Error: " + str(ex)) -def is_csv_callback(query): - """ - Callback to identify if the button pressed was from the csv function - - :param query: the button pressed - :return: if the button pressed relates to the csv - """ - return "," in query.data - +def is_csv_callback(query): return "," in query.data @bot.callback_query_handler(func=is_csv_callback) def csv_callback(call): @@ -876,14 +726,9 @@ def csv_callback(call): debit = float(data[2]) description = data[3] chat_id = str(call.from_user.id) - user_list[chat_id].create_rules_and_add_unknown_spending( - category, description, date, debit, chat_id - ) - bot.delete_message( - chat_id=call.from_user.id, message_id=call.message.message_id - ) + user_list[chat_id].create_rules_and_add_unknown_spending(category, description, date, debit, chat_id) + bot.delete_message(chat_id=call.from_user.id, message_id=call.message.message_id) except Exception as ex: - print("Exception occurred : ") logger.error(str(ex), exc_info=True) bot.send_message(call.from_user.id, "Processing Failed - Error: " + str(ex)) @@ -902,13 +747,11 @@ def category_add(message): try: chat_id = str(message.chat.id) option.pop(chat_id, None) - if chat_id not in user_list.keys(): - user_list[chat_id] = User(chat_id) + if chat_id not in user_list.keys(): user_list[chat_id] = User(chat_id) category = bot.reply_to(message, "Enter category name") bot.register_next_step_handler(category, receive_new_category) except Exception as ex: - print("Exception occurred : ") logger.error(str(ex), exc_info=True) bot.reply_to(message, "Oh no. " + str(ex)) @@ -924,16 +767,11 @@ def receive_new_category(message): try: category = message.text.strip() chat_id = str(message.chat.id) - if category == "": # category cannot be empty - raise Exception("Category name cannot be empty") - if category in user_list[chat_id].transactions: - raise Exception("Category already exists!") + if category == "": raise Exception("Category name cannot be empty") # category cannot be empty + if category in user_list[chat_id].transactions: raise Exception("Category already exists!") user_list[chat_id].add_category(category, chat_id) - bot.send_message( - chat_id, "{} has been added as a new category".format(category) - ) + bot.send_message(chat_id, "{} has been added as a new category".format(category)) except Exception as ex: - print("Exception occurred : ") logger.error(str(ex), exc_info=True) bot.reply_to(message, "Oh no. " + str(ex)) @@ -950,14 +788,11 @@ def category_list(message): try: chat_id = str(message.chat.id) option.pop(chat_id, None) - if chat_id not in user_list.keys(): - user_list[chat_id] = User(chat_id) + if chat_id not in user_list.keys(): user_list[chat_id] = User(chat_id) chat_id = str(message.chat.id) - if len(user_list[chat_id].transactions.keys()) == 0: - raise Exception("Sorry! No categories found!") + if len(user_list[chat_id].transactions.keys()) == 0: raise Exception("Sorry! No categories found!") category_list_str = "Here is your category list : \n" - for index, category in enumerate(user_list[chat_id].transactions.keys()): - category_list_str += "{}. {}".format(index + 1, category + "\n") + for index, category in enumerate(user_list[chat_id].transactions.keys()): category_list_str += "{}. {}".format(index + 1, category + "\n") bot.send_message(chat_id, category_list_str) except Exception as ex: @@ -977,18 +812,15 @@ def category_delete(message): try: chat_id = str(message.chat.id) option.pop(chat_id, None) - if chat_id not in user_list.keys(): - user_list[chat_id] = User(chat_id) + if chat_id not in user_list.keys(): user_list[chat_id] = User(chat_id) spend_categories = user_list[chat_id].spend_categories markup = types.ReplyKeyboardMarkup(one_time_keyboard=True) markup.row_width = 2 - for c in spend_categories: - markup.add(c) + for c in spend_categories: markup.add(c) msg = bot.reply_to(message, "Select Category", reply_markup=markup) bot.register_next_step_handler(msg, receive_delete_category) except Exception as ex: - print("Exception occurred : ") logger.error(str(ex), exc_info=True) bot.reply_to(message, "Processing Failed - \nError : " + str(ex)) @@ -1004,16 +836,11 @@ def receive_delete_category(message): try: chat_id = str(message.chat.id) category = message.text.strip() - if category not in user_list[chat_id].transactions: - raise Exception("Oops! Category does not exist!") - if len(user_list[chat_id].transactions[category]) != 0: - raise Exception( - "Sorry! This category has transactions. Delete those transactions to proceed." - ) + if category not in user_list[chat_id].transactions: raise Exception("Oops! Category does not exist!") + if len(user_list[chat_id].transactions[category]) != 0: raise Exception("Sorry! This category has transactions. Delete those transactions to proceed.") user_list[chat_id].delete_category(category, chat_id) bot.reply_to(message, "{} has been removed from category list".format(category)) except Exception as ex: - print("Exception occurred : ") logger.error(str(ex), exc_info=True) bot.reply_to(message, str(ex)) @@ -1033,10 +860,7 @@ def command_delete(message): monthFormat = "%m/%Y" chat_id = str(message.chat.id) try: - if ( - chat_id in user_list - and user_list[chat_id].get_number_of_transactions() != 0 - ): + if (chat_id in user_list and user_list[chat_id].get_number_of_transactions() != 0): curr_day = datetime.now() prompt = "Enter the day, month, or All\n" prompt += f"\n\tExample day: {curr_day.strftime(dateFormat)}\n" @@ -1044,14 +868,10 @@ def command_delete(message): reply_message = bot.reply_to(message, prompt) bot.register_next_step_handler(reply_message, process_delete_argument) else: - delete_history_text = ( - "No records to be deleted. Start adding your expenses to keep track of your " - "spendings! " - ) + delete_history_text = ("No records to be deleted. Start adding your expenses to keep track of your spendings! ") bot.send_message(chat_id, delete_history_text) except Exception as ex: - print("Exception occurred : ") logger.error(str(ex), exc_info=True) bot.reply_to(message, "Processing Failed - \nError : " + str(ex)) @@ -1072,20 +892,16 @@ def process_delete_argument(message): date = None is_month = False - if text.lower() == "all": - date = "all" + if text.lower() == "all": date = "all" else: # try and parse as Date-Month-Year - if user_list[chat_id].validate_date_format(text, dateFormat) is not None: - date = user_list[chat_id].validate_date_format(text, dateFormat) + if user_list[chat_id].validate_date_format(text, dateFormat) is not None: date = user_list[chat_id].validate_date_format(text, dateFormat) # try and parse as Month-Year elif user_list[chat_id].validate_date_format(text, monthFormat) is not None: date = user_list[chat_id].validate_date_format(text, monthFormat) is_month = True - if date is None: - # if none of the formats worked - bot.reply_to(message, "Error parsing date") + if date is None: bot.reply_to(message, "Error parsing date") else: # get the records either by given day, month, or all records records_to_delete = user_list[chat_id].get_records_by_date(date, is_month) @@ -1119,8 +935,7 @@ def handle_confirmation(message, records_to_delete): user_list[chat_id].deleteHistory(records_to_delete) user_list[chat_id].save_user(chat_id) bot.send_message(message.chat.id, "Successfully deleted records") - else: - bot.send_message(message.chat.id, "No records deleted") + else: bot.send_message(message.chat.id, "No records deleted") def get_calendar_buttons(user): @@ -1144,28 +959,16 @@ def get_calendar_buttons(user): # for each day in the total days # for the first day, figure out how many ' ' to append row = [] - if m[0] != 6: - row = [ - types.InlineKeyboardButton(" ", callback_data="none") - for _ in range(m[0] + 1) - ] + if m[0] != 6: row = [types.InlineKeyboardButton(" ", callback_data="none") for _ in range(m[0] + 1)] for day in range(1, m[1] + 1): # if it is on a sunday, start a new row if user.curr_date.replace(day=day).weekday() == 6: kb.row(*row) row = [] - row.append( - types.InlineKeyboardButton( - day, - callback_data="{},{},{}".format( - user.curr_date.year, user.curr_date.month, day - ), - ) - ) + row.append(types.InlineKeyboardButton(day,callback_data="{},{},{}".format(user.curr_date.year, user.curr_date.month, day))) # finish out the last row if len(row) != 7: - for _ in range(7 - len(row)): - row.append(types.InlineKeyboardButton(" ", callback_data="none")) + for _ in range(7 - len(row)): row.append(types.InlineKeyboardButton(" ", callback_data="none")) kb.row(*row) return kb @@ -1192,8 +995,7 @@ def get_chart(message): chat_id = str(message.chat.id) chart_file = user_list[chat_id].create_chart(chat_id) for cf in chart_file: - with open(cf, "rb") as f: - bot.send_photo(chat_id, f) + with open(cf, "rb") as f: bot.send_photo(chat_id, f) # bot.send_photo(chat_id, cf) @@ -1206,26 +1008,12 @@ def create_header(user): :return: the header row """ # get the month name - row = [ - ( - types.InlineKeyboardButton( - user.curr_date.strftime("%B"), callback_data="none" - ) - ) - ] - if user.curr_date > user.min_date: - # if we should be able to go back a month - row.append(types.InlineKeyboardButton("<", callback_data="prev")) - else: - # append a blank - row.append(types.InlineKeyboardButton(" ", callback_data="none")) + row = [(types.InlineKeyboardButton(user.curr_date.strftime("%B"), callback_data="none"))] + if user.curr_date > user.min_date:row.append(types.InlineKeyboardButton("<", callback_data="prev")) + else: row.append(types.InlineKeyboardButton(" ", callback_data="none")) - if user.curr_date < user.max_date: - # if we should be able to go forward - row.append(types.InlineKeyboardButton(">", callback_data="next")) - else: - # append a blank - row.append(types.InlineKeyboardButton(" ", callback_data="none")) + if user.curr_date < user.max_date: row.append(types.InlineKeyboardButton(">", callback_data="next")) + else: row.append(types.InlineKeyboardButton(" ", callback_data="none")) return row @@ -1238,21 +1026,16 @@ def handler_callback(callback, user): :return: datetime.date object if some date was picked else None """ - if callback == "prev" and user.curr_date.replace(day=1) >= user.min_date.replace( - day=1 - ): + if callback == "prev" and user.curr_date.replace(day=1) >= user.min_date.replace(day=1): user.curr_date = user.curr_date.replace(month=user.curr_date.month - 1) return None - if callback == "next" and user.curr_date.replace(day=1) <= user.max_date.replace( - day=1 - ): + if callback == "next" and user.curr_date.replace(day=1) <= user.max_date.replace(day=1): user.curr_date = user.curr_date.replace(month=user.curr_date.month + 1) return None if callback != "none": entered_date = datetime.strptime(callback, "%Y,%m,%d") - if user.min_date <= entered_date <= user.max_date: - return entered_date + if user.min_date <= entered_date <= user.max_date: return entered_date return -1 @@ -1264,7 +1047,7 @@ def get_users(): :rtype: dict """ - data_dir = "data" + data_dir = "teleData" users = {} for file in os.listdir(data_dir): if file.endswith(".pickle"): @@ -1272,8 +1055,7 @@ def get_users(): if u: u = u.group(1) abspath = pathlib.Path("{0}/{1}".format(data_dir, file)).absolute() - with open(abspath, "rb") as f: - users[u] = pickle.load(f) + with open(abspath, "rb") as f: users[u] = pickle.load(f) return users @@ -1289,25 +1071,16 @@ def command_display_currency(message): :return: None """ chat_id = str(message.chat.id) - if chat_id not in user_list or user_list[chat_id].get_number_of_transactions() == 0: - bot.send_message( - chat_id, "Oops! Looks like you do not have any spending records!" - ) + if chat_id not in user_list or user_list[chat_id].get_number_of_transactions() == 0: bot.send_message(chat_id, "Oops! Looks like you do not have any spending records!") else: try: markup = types.ReplyKeyboardMarkup(one_time_keyboard=True) markup.row_width = 2 - for mode in user_list[chat_id].spend_display_option: - markup.add(mode) - msg = bot.reply_to( - message, - "Please select a category to see the total expense", - reply_markup=markup, - ) + for mode in user_list[chat_id].spend_display_option: markup.add(mode) + msg = bot.reply_to(message,"Please select a category to see the total expense", reply_markup=markup) bot.register_next_step_handler(msg, display_total_currency) except Exception as ex: - print("Exception occurred : ") logger.error(str(ex), exc_info=True) bot.reply_to(message, "Oops! - \nError : " + str(ex)) @@ -1324,13 +1097,9 @@ def display_total_currency(message): chat_id = str(message.chat.id) day_week_month = message.text - if day_week_month not in user_list[chat_id].spend_display_option: - raise Exception( - 'Sorry I can\'t show spendings for "{}"!'.format(day_week_month) - ) + if day_week_month not in user_list[chat_id].spend_display_option: raise Exception('Sorry I can\'t show spendings for "{}"!'.format(day_week_month)) - if len(user_list[chat_id].transactions) == 0: - raise Exception("Oops! Looks like you do not have any spending records!") + if len(user_list[chat_id].transactions) == 0: raise Exception("Oops! Looks like you do not have any spending records!") bot.send_message(chat_id, "Hold on! Calculating...") @@ -1341,15 +1110,9 @@ def display_total_currency(message): for category in user_list[chat_id].transactions.keys(): for transaction in user_list[chat_id].transactions[category]: if transaction["Date"].strftime("%d") == query.strftime("%d"): - query_result += "Category {} Date {} Value {:.2f} \n".format( - category, - transaction["Date"].strftime(dateFormat), - transaction["Value"], - ) + query_result += "Category {} Date {} Value {:.2f} \n".format(category, transaction["Date"].strftime(dateFormat), transaction["Value"]) total_value += transaction["Value"] - total_spendings = "Here are your total spendings for the date {} \n".format( - datetime.today().strftime("%m/%d/%Y") - ) + total_spendings = "Here are your total spendings for the date {} \n".format(datetime.today().strftime("%m/%d/%Y")) total_spendings += query_result total_spendings += "Total Value {:.2f}".format(total_value) bot.send_message(chat_id, total_spendings) @@ -1362,36 +1125,24 @@ def display_total_currency(message): for category in user_list[chat_id].transactions.keys(): for transaction in user_list[chat_id].transactions[category]: if transaction["Date"].strftime("%m") == query.strftime("%m"): - query_result += "Category {} Date {} Value {:.2f} \n".format( - category, - transaction["Date"].strftime(dateFormat), - transaction["Value"], - ) + query_result += "Category {} Date {} Value {:.2f} \n".format(category,transaction["Date"].strftime(dateFormat),transaction["Value"]) total_value += transaction["Value"] - total_spendings = ( - "Here are your total spendings for the Month {} \n".format( - datetime.today().strftime("%B") - ) - ) + total_spendings = ("Here are your total spendings for the Month {} \n".format(datetime.today().strftime("%B"))) markup = types.ReplyKeyboardMarkup(one_time_keyboard=True) markup.row_width = 2 choices = ["INR", "EUR", "CHF"] - for c in choices: - markup.add(c) + for c in choices: markup.add(c) total_spendings += query_result total_spendings += "Total Value {:.2f}\n".format(total_value) total_spendings += "Budget for the month {}".format(str(budget_value)) global completeSpendings # pylint: disable=global-statement completeSpendings = total_value - choice = bot.reply_to( - message, "Which currency to you want to covert to?", reply_markup=markup - ) + choice = bot.reply_to(message, "Which currency to you want to covert to?", reply_markup=markup) bot.register_next_step_handler(choice, display_total_currency2) # bot.send_message(chat_id, total_spendings) except Exception as ex: - print("Exception occurred : ") logger.error(str(ex), exc_info=True) bot.reply_to(message, str(ex)) @@ -1405,26 +1156,19 @@ def display_total_currency2(message): if selection == "INR": completeExpenses = completeSpendings * DOLLARS_TO_RUPEES - completeExpensesMessage = ( - "The total expenses in INR is Rs. " + str(completeExpenses) - ) + completeExpensesMessage = ("The total expenses in INR is Rs. " + str(completeExpenses)) bot.reply_to(message, completeExpensesMessage) if selection == "EUR": completeExpenses = completeSpendings * DOLLARS_TO_EUROS - completeExpensesMessage = ( - "The total expenses in EUR is " + str(completeExpenses) + " EUR" - ) + completeExpensesMessage = ("The total expenses in EUR is " + str(completeExpenses) + " EUR") bot.reply_to(message, completeExpensesMessage) if selection == "CHF": completeExpenses = completeSpendings * DOLLARS_TO_EUROS - completeExpensesMessage = ( - "The total expenses in Swiss Franc is " + str(completeExpenses) + " CHF" - ) + completeExpensesMessage = ("The total expenses in Swiss Franc is " + str(completeExpenses) + " CHF") bot.reply_to(message, completeExpensesMessage) except Exception as ex: - print("Exception occurred : ") logger.error(str(ex), exc_info=True) bot.reply_to(message, "Processing Failed - Error: " + str(ex)) @@ -1438,5 +1182,4 @@ def display_total_currency2(message): except Exception as e: # Connection will be timed out with the set time interval - 3 time.sleep(3) - print("Exception occurred while processing : ") logger.error(str(e), exc_info=True) diff --git a/src/teleData/.gitignore b/src/teleData/.gitignore new file mode 100644 index 000000000..5e7d2734c --- /dev/null +++ b/src/teleData/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore diff --git a/src/user.py b/src/teleUser.py similarity index 85% rename from src/user.py rename to src/teleUser.py index da06f183f..42fb4c238 100644 --- a/src/user.py +++ b/src/teleUser.py @@ -1,6 +1,30 @@ """ -File contains functions that stores and retrieves data from the .pickle file and also handles validations +File: teleUser.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains functions that stores and retrieves data from the .pickle file and also handles validations. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. """ + import logging import pathlib import pickle @@ -50,13 +74,12 @@ def save_user(self, userid): """ try: - data_dir = "data" + data_dir = "teleData" abspath = pathlib.Path("{0}/{1}.pickle".format(data_dir, userid)).absolute() with open(abspath, "wb") as f: pickle.dump(self, f) - except Exception as e: - logger.error(str(e), exc_info=True) + except Exception as e: logger.error(str(e), exc_info=True) def validate_entered_amount(self, amount_entered): """ @@ -93,8 +116,7 @@ def add_transaction(self, date, category, value, userid): self.transactions[category].append({"Date": date, "Value": value}) self.save_user(userid) - except Exception as e: - logger.error(str(e), exc_info=True) + except Exception as e: logger.error(str(e), exc_info=True) def store_edit_transaction(self, existing_transaction, edit_category): """ @@ -110,8 +132,7 @@ def store_edit_transaction(self, existing_transaction, edit_category): self.edit_transactions = existing_transaction self.edit_category = edit_category - except Exception as e: - logger.error(str(e), exc_info=True) + except Exception as e: logger.error(str(e), exc_info=True) def edit_transaction_date(self, new_date): """ @@ -176,9 +197,7 @@ def deleteHistory(self, records=None): for record in records[category]: try: self.transactions[category].remove(record) - except Exception as e: - print("Exception occurred : ") - logger.error(str(e), exc_info=True) + except Exception as e: logger.error(str(e), exc_info=True) else: self.transactions = {} for category in self.spend_categories: @@ -280,8 +299,7 @@ def add_monthly_budget(self, amount, userid): self.monthly_budget = amount self.save_user(userid) - except Exception as e: - logger.error(str(e), exc_info=True) + except Exception as e: logger.error(str(e), exc_info=True) def monthly_total(self): """ @@ -360,8 +378,7 @@ def create_chart(self, userid): charts = [] for category in self.spend_categories: total = 0 - for transaction in self.transactions[category]: - total = total + transaction["Value"] + for transaction in self.transactions[category]: total = total + transaction["Value"] if total != 0: labels.append(category) totals.append(total) @@ -370,8 +387,8 @@ def create_chart(self, userid): plt.clf() plt.pie(totals, labels=labels) plt.title("Your Expenditure Report") - plt.savefig("data/{}_pie_chart.png".format(userid)) # Ensure that the file name is unique - charts.append("data/{}_pie_chart.png".format(userid)) # Ensure that the file name is unique + plt.savefig("teleData/{}_pie_chart.png".format(userid)) # Ensure that the file name is unique + charts.append("teleData/{}_pie_chart.png".format(userid)) # Ensure that the file name is unique # Bar Graph plt.clf() @@ -381,8 +398,8 @@ def create_chart(self, userid): plt.xlabel('Categories') plt.ylabel('Expenditure') plt.title("Your Expenditure Report") - plt.savefig("data/{}_bar_chart.png".format(userid)) # Ensure that the file name is unique - charts.append("data/{}_bar_chart.png".format(userid)) # Ensure that the file name is unique + plt.savefig("teleData/{}_bar_chart.png".format(userid)) # Ensure that the file name is unique + charts.append("teleData/{}_bar_chart.png".format(userid)) # Ensure that the file name is unique # Add more visualizations here. Maintain the above format while adding more visualizations. @@ -404,8 +421,7 @@ def add_category(self, new_category, userid): self.rules[new_category] = [] self.save_user(userid) - except Exception as e: - logger.error(str(e), exc_info=True) + except Exception as e: logger.error(str(e), exc_info=True) def delete_category(self, category, userid): """ @@ -423,5 +439,4 @@ def delete_category(self, category, userid): self.rules.pop(category, None) self.save_user(userid) - except Exception as e: - logger.error(str(e), exc_info=True) + except Exception as e: logger.error(str(e), exc_info=True) diff --git a/teleData/1.pickle b/teleData/1.pickle new file mode 100644 index 000000000..7b51bd917 Binary files /dev/null and b/teleData/1.pickle differ diff --git a/teleData/33.pickle b/teleData/33.pickle new file mode 100644 index 000000000..69e3aaf1d Binary files /dev/null and b/teleData/33.pickle differ diff --git a/test/bot/data/1.pickle b/test/bot/data/1.pickle deleted file mode 100644 index 2ece81877..000000000 Binary files a/test/bot/data/1.pickle and /dev/null differ diff --git a/test/bot/data/2129133600.pickle b/test/bot/data/2129133600.pickle deleted file mode 100644 index d187bb40c..000000000 Binary files a/test/bot/data/2129133600.pickle and /dev/null differ diff --git a/test/bot/test_add.py b/test/bot/test_add.py deleted file mode 100644 index d88af3b45..000000000 --- a/test/bot/test_add.py +++ /dev/null @@ -1,240 +0,0 @@ -""" -Tests add command -""" -import time -import unittest -import sys -sys.path.append("E:\SE\project phase 3\slashbot") -print(sys.path) -from src import bot -from bot_utils import BotTest - - -class TestAdd(BotTest): - """ - Test file for add - """ - - def test_add_command(self): - """ - Tests the add command - """ - msg = self.create_text_message('/add') - self.bot.process_new_messages([msg]) - time.sleep(3) - - # assert the message was sent, and text was not changed - assert msg.chat.id is not None - assert msg.text == '/add' - # there should be a next step handler - assert len(self.bot.next_step_backend.handlers) == 0, \ - "For the /add command, there should not be a next step" - # there should not be any exceptions - assert self.bot.worker_pool.exception_info is None - - # send the calendar date - query = self.create_callback_query("2021,11,01", msg) - self.bot.process_new_callback_query([query]) - time.sleep(3) - - # assert the query was sent - assert query.chat_instance.id is not None - # there should be a next step handler - assert len(self.bot.next_step_backend.handlers) == 1, \ - "For the /add command after date, there should not be a next step" - # there should not be any exceptions - assert self.bot.worker_pool.exception_info is None - - # send the category we use - reply = self.create_text_message(self.user.spend_categories[0]) - category = self.user.spend_categories[0] - self.bot.process_new_messages([reply]) - time.sleep(3) - # assert the message was sent, and text was not changed - assert reply.chat.id is not None - assert reply.text == self.user.spend_categories[0] - # there should be a next step handler - assert len(self.bot.next_step_backend.handlers) == 1, \ - "For the reply to add, there should be a next step" - # there should not be any exceptions - assert self.bot.worker_pool.exception_info is None - - # send the amount - reply = self.create_text_message("1.00") - self.bot.process_new_messages([reply]) - time.sleep(3) - # assert the message was sent, and text was not changed - assert reply.chat.id is not None - assert reply.text == "1.00" - # there should be a next step handler - assert len(self.bot.next_step_backend.handlers) == 0, \ - "For the reply to add, there should not be a next step" - # there should not be any exceptions - assert self.bot.worker_pool.exception_info is None - - # assert the record was added to the user - chat_id = str(reply.chat.id) - assert chat_id in bot.user_list - assert category in bot.user_list[chat_id].transactions - user_transac = bot.user_list[chat_id].transactions - assert user_transac[category] != [] - assert user_transac[category][0]['Value'] == 1.0 - - # there should be any records added - assert bot.user_list[str(msg.chat.id)].get_number_of_transactions() == 1 - - - def test_add_wrong_date(self): - """ - Tests the add command with an invalid value - """ - msg = self.create_text_message('/add') - self.bot.process_new_messages([msg]) - time.sleep(3) - - # assert the message was sent, and text was not changed - assert msg.chat.id is not None - assert msg.text == '/add' - # there should be a next step handler - assert len(self.bot.next_step_backend.handlers) == 0, \ - "For the /add command, there should be a next step" - # there should not be any exceptions - assert self.bot.worker_pool.exception_info is None - - # send the calendar date - query = self.create_callback_query("prev", msg) - self.bot.process_new_callback_query([query]) - time.sleep(3) - - # assert the query was sent - assert query.chat_instance.id is not None - # there should be a next step handler - assert len(self.bot.next_step_backend.handlers) == 0, \ - "For the /add command after date, there should not be a next step" - # there should not be any exceptions - assert self.bot.worker_pool.exception_info is None - - # send the calendar date - query = self.create_callback_query("", msg) - self.bot.process_new_callback_query([query]) - time.sleep(3) - - # assert the query was sent - assert query.chat_instance.id is not None - # there should be a next step handler - assert len(self.bot.next_step_backend.handlers) == 0, \ - "For the /add command after date, there should not be a next step" - # there should not be any exceptions - assert self.bot.worker_pool.exception_info is None - - # there should not be any records added - assert bot.user_list[str(msg.chat.id)].get_number_of_transactions() == 0 - - def test_add_wrong_cat(self): - """ - Tests the add command with an invalid category - """ - msg = self.create_text_message('/add') - self.bot.process_new_messages([msg]) - time.sleep(3) - - # assert the message was sent, and text was not changed - assert msg.chat.id is not None - assert msg.text == '/add' - # there should be a next step handler - assert len(self.bot.next_step_backend.handlers) == 0, \ - "For the /add command, there should be a next step" - # there should not be any exceptions - assert self.bot.worker_pool.exception_info is None - - # send the calendar date - query = self.create_callback_query("2021,11,01", msg) - self.bot.process_new_callback_query([query]) - time.sleep(3) - - # assert the query was sent - assert query.chat_instance.id is not None - # there should be a next step handler - assert len(self.bot.next_step_backend.handlers) == 1, \ - "For the /add command after date, there should not be a next step" - # there should not be any exceptions - assert self.bot.worker_pool.exception_info is None - - # send the category we use - reply = self.create_text_message("INVALID") - self.bot.process_new_messages([reply]) - time.sleep(3) - # assert the message was sent, and text was not changed - assert reply.chat.id is not None - assert reply.text == "INVALID" - # there should be a next step handler - assert len(self.bot.next_step_backend.handlers) == 0, \ - "For the reply to add with wrong cat, there should not be a next step" - # there should not be any exceptions - assert self.bot.worker_pool.exception_info is None - - # there should not be any records added - assert bot.user_list[str(msg.chat.id)].get_number_of_transactions() == 0 - - - def test_add_wrong_num(self): - """ - Tests the add command with an invalid value - """ - msg = self.create_text_message('/add') - self.bot.process_new_messages([msg]) - time.sleep(3) - - # assert the message was sent, and text was not changed - assert msg.chat.id is not None - assert msg.text == '/add' - # there should be a next step handler - assert len(self.bot.next_step_backend.handlers) == 0, \ - "For the /add command, there should be a next step" - # there should not be any exceptions - assert self.bot.worker_pool.exception_info is None - - # send the calendar date - query = self.create_callback_query("2021,11,01", msg) - self.bot.process_new_callback_query([query]) - time.sleep(3) - - # assert the query was sent - assert query.chat_instance.id is not None - # there should be a next step handler - assert len(self.bot.next_step_backend.handlers) == 1, \ - "For the /add command after date, there should be a next step" - # there should not be any exceptions - assert self.bot.worker_pool.exception_info is None - - # send the category we use - reply = self.create_text_message(self.user.spend_categories[0]) - self.bot.process_new_messages([reply]) - time.sleep(3) - # assert the message was sent, and text was not changed - assert reply.chat.id is not None - assert reply.text == self.user.spend_categories[0] - # there should be a next step handler - assert len(self.bot.next_step_backend.handlers) == 1, \ - "For the reply to add, there should not be a next step" - # there should not be any exceptions - assert self.bot.worker_pool.exception_info is None - - # send the amount we use - reply = self.create_text_message("-1") - self.bot.process_new_messages([reply]) - time.sleep(3) - # assert the message was sent, and text was not changed - assert reply.chat.id is not None - assert reply.text == "-1" - # there should be a next step handler - assert len(self.bot.next_step_backend.handlers) == 0, \ - "For the reply to add, there should not be a next step" - # there should not be any exceptions - assert self.bot.worker_pool.exception_info is None - - # there should not be any records added - assert bot.user_list[str(msg.chat.id)].get_number_of_transactions() == 0 - -if __name__ == '__main__': - unittest.main() diff --git a/test/bot/test_add_cmd_custom_category.py b/test/bot/test_add_cmd_custom_category.py deleted file mode 100644 index c98d26281..000000000 --- a/test/bot/test_add_cmd_custom_category.py +++ /dev/null @@ -1,53 +0,0 @@ - -from bot_utils import BotTest -import unittest -import time -from src import bot - - -class TestAddCustomCategory(BotTest): - """ - Test file for add custom category - """ - def test_add_custom_category_command(self): - """ - Tests the add custom category command - """ - msg = self.create_text_message('/categoryAdd') - self.bot.process_new_messages([msg]) - time.sleep(3) - - # assert the message was sent, and text was not changed - assert msg.chat.id is not None - assert msg.text == '/categoryAdd' - # there should be a next step handler - assert len(self.bot.next_step_backend.handlers) == 1, \ - "For the /categoryAdd command, there should be a next step" - # there should not be any exceptions - assert self.bot.worker_pool.exception_info is None - - # send the custom category - custom_category = "travel" - reply = self.create_text_message(custom_category) - self.bot.process_new_messages([reply]) - time.sleep(3) - - # assert the message was sent, and text was not changed - assert reply.chat.id is not None - assert reply.text == custom_category - # there should not be a next step handler - assert len(self.bot.next_step_backend.handlers) == 0, \ - "For the reply to budget, there should not be a next step" - # there should not be any exceptions - assert self.bot.worker_pool.exception_info is None - - # assert the custom category was added to the user - chat_id = str(reply.chat.id) - content = bot.user_list[chat_id].transactions - categories = [] - for category in content: - categories.append(category) - assert custom_category in categories - -if __name__ == '__main__': - unittest.main() diff --git a/test/bot/test_bot.py b/test/bot/test_bot.py deleted file mode 100644 index b24b29f0f..000000000 --- a/test/bot/test_bot.py +++ /dev/null @@ -1,60 +0,0 @@ -""" -Unit test to check all commands are present -""" -# import sys -# sys.path.append("../../..") -# import src -import src.bot as code_lib -from bot_utils import BotTest - - -class TestCommands(BotTest): - """ - Test class to test commands and functions - """ - - def test_number_commands(self) -> None: - """ - Tests that the correct number of commands are present - :return: - """ - # for all commands - bot_commands = [hand for hand in self.bot.message_handlers - if "commands" in hand['filters']] - number_of_commands = 14 - # assert there is the right number of commands - assert len(bot_commands) == number_of_commands - - def test_commands(self) -> None: - """ - Tests if commands are present, and if they are hooked - to the correct function - :return: None - """ - # for all commands - bot_commands = [hand for hand in self.bot.message_handlers - if "commands" in hand['filters']] - # print(bot_commands) - # print(self.bot.message_handler['filters']) - # dictionary of functions and commands to trigger the function - actual_titles = [{'function': code_lib.start_and_menu_command, - 'commands': ["start", "menu"]}, - {'function': code_lib.command_budget, 'commands': ["budget"]}, - {'function': code_lib.command_add, 'commands': ["add"]}, - {'function': code_lib.show_history, 'commands': ["history"]}, - {'function': code_lib.command_display, 'commands': ["display"]}, - {'function': code_lib.edit1, 'commands': ["edit"]}, - {'function': code_lib.category_add, 'commands': ["categoryAdd"]}, - {'function': code_lib.category_list, 'commands': ["categoryList"]}, - {'function': code_lib.category_delete, 'commands': ["categoryDelete"]}, - {'function': code_lib.command_delete, 'commands': ["delete"]}, - {'function': code_lib.send_email, 'commands': ["sendEmail"]}, - {'function': code_lib.download_history, 'commands': ["download"]} - ] - # assert each function and command matches - # for actual_func, expected_func in zip(bot_commands, actual_titles): - # # assert actual_func['filters']['commands'] == expected_func['commands'] - # assert actual_func['function'] == expected_func['function'] - - # there should not be any exceptions - assert self.bot.worker_pool.exception_info is None diff --git a/test/test_start_and_menu_command.py b/test/test_start_and_menu_command.py index b3789ca01..76bdd0d65 100644 --- a/test/test_start_and_menu_command.py +++ b/test/test_start_and_menu_command.py @@ -1,15 +1,29 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- """ -Created on Wed Sep 29 17:07:37 2021 +File: test_start_and_menu_command.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. -@author: deekay -""" +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -import pytest -#from code.code import start_and_menu_command +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" def test_start_and_menu_command_func(): - #test_result = start_and_menu_command("/start") - #assert True == test_result, "Normal Case" print("Hello") diff --git a/test/unit/BaseCase.py b/test/unit/BaseCase.py index ca14475df..46b416013 100644 --- a/test/unit/BaseCase.py +++ b/test/unit/BaseCase.py @@ -1,9 +1,40 @@ +""" +File: BaseCase.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Helper functions for Test classes. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + import os.path import pathlib import unittest -from src.user import User - +try: + from src import teleUser + import src.teleUser as teleUser + from src.teleUser import User +except: + from teleUser import User class BaseCase(unittest.TestCase): """ @@ -14,11 +45,12 @@ def setUp(self) -> None: Creates a new user """ # os.chdir("test") - abspath = pathlib.Path("data").absolute() + abspath = pathlib.Path("teleData").absolute() + print(abspath, "abs path") if not os.path.exists(abspath): os.mkdir(abspath) - print(os.getcwd()) + # print(os.getcwd()) self.user = User("1") self.expected_list = self.create_transaction() @@ -26,7 +58,7 @@ def tearDown(self) -> None: """ Removes the user pickle """ - abspath = pathlib.Path("data").absolute() + abspath = pathlib.Path("teleData").absolute() if not os.path.exists(abspath): os.mkdir(abspath) diff --git a/test/bot/bot_utils.py b/test/unit/bot_utils.py similarity index 61% rename from test/bot/bot_utils.py rename to test/unit/bot_utils.py index 396240311..eb1ce1d4d 100644 --- a/test/bot/bot_utils.py +++ b/test/unit/bot_utils.py @@ -1,7 +1,30 @@ """ -Util functions for bot tests +File: bot_utils.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: Util functions for bot Tests. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. """ -import logging + import os import pathlib import unittest @@ -9,10 +32,8 @@ from importlib import reload from telebot import types -import sys -# sys.path.append("E:\SE\project phase 3\slashbot\src") -import src.bot -from src.user import User +import teleBot +from teleUser import User CHAT_ID = os.environ['CHAT_ID'] if 'CHAT_ID' in os.environ else 1 TOKEN = os.environ['API_TOKEN'] if 'API_TOKEN' in os.environ else 0 @@ -31,14 +52,14 @@ def setUp(self) -> None: abspath = pathlib.Path("data").absolute() if not os.path.exists(abspath): os.mkdir(abspath) - reload(src.bot) - src.bot.api_token = os.environ['API_TOKEN'] - self.bot = src.bot.bot + reload(teleBot) + teleBot.api_token = os.environ['API_TOKEN'] + self.bot = teleBot.bot self.user = User(str(CHAT_ID)) self.user.save_user(str(CHAT_ID)) self.chat_id = CHAT_ID # reloads the user list - src.bot.user_list = src.bot.get_users() + teleBot.user_list = teleBot.get_users() # asserts the current user has no data assert self.user.get_number_of_transactions() == 0 @@ -61,8 +82,8 @@ def create_record(self, amount: float) -> None: """ self.user.add_transaction(datetime.now(), self.user.spend_categories[0], amount, CHAT_ID) self.user.save_user(CHAT_ID) - src.bot.user_list = src.bot.get_users() - assert CHAT_ID in src.bot.user_list.keys() + teleBot.user_list = teleBot.get_users() + assert CHAT_ID in teleBot.user_list.keys() def create_text_message(self, text: str) -> types.Message: """ diff --git a/test/unit/discordData/1.pickle b/test/unit/discordData/1.pickle new file mode 100644 index 000000000..edd41a379 Binary files /dev/null and b/test/unit/discordData/1.pickle differ diff --git a/test/unit/discordData/1158122423871356969.pickle b/test/unit/discordData/1158122423871356969.pickle new file mode 100644 index 000000000..e4c8e3505 Binary files /dev/null and b/test/unit/discordData/1158122423871356969.pickle differ diff --git a/test/unit/discordData/2.pickle b/test/unit/discordData/2.pickle new file mode 100644 index 000000000..eb44feb84 Binary files /dev/null and b/test/unit/discordData/2.pickle differ diff --git a/test/unit/discord_BaseCase.py b/test/unit/discord_BaseCase.py new file mode 100644 index 000000000..9671c412d --- /dev/null +++ b/test/unit/discord_BaseCase.py @@ -0,0 +1,77 @@ +""" +File: discord_BaseCase.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import os.path +import pathlib +import unittest + +from src.discordUser import User + + +class discord_BaseCase(unittest.TestCase): + """ + Base case class for all other unit tests to inherit from + """ + def setUp(self) -> None: + """ + Creates a new user + """ + abspath = pathlib.Path("discordData").absolute() + print(abspath, "abs path") + if not os.path.exists(abspath): + os.mkdir(abspath) + + print(os.getcwd(),"current directory") + self.user = User("2") + self.expected_list = self.create_transaction() + + def tearDown(self) -> None: + """ + Removes the user pickle + """ + abspath = pathlib.Path("discordData").absolute() + if not os.path.exists(abspath): + os.mkdir(abspath) + + def create_transaction(self): + """ + Creates the dictionary of transactions + """ + transaction = {} + for category in self.user.spend_categories: + transaction[category] = [] + return transaction + + def add_record(self, category, record): + """ + Adds a record to the internal expected transactions + """ + self.expected_list[category].append(record) + + +if __name__ == '__main__': + unittest.main() diff --git a/data/1.pickle b/test/unit/teleData/1.pickle similarity index 66% rename from data/1.pickle rename to test/unit/teleData/1.pickle index 0c11cc43c..7a2a00138 100644 Binary files a/data/1.pickle and b/test/unit/teleData/1.pickle differ diff --git a/data/33.pickle b/test/unit/teleData/33.pickle similarity index 60% rename from data/33.pickle rename to test/unit/teleData/33.pickle index 3d692bb88..0f124edbc 100644 Binary files a/data/33.pickle and b/test/unit/teleData/33.pickle differ diff --git a/data/2106963958.pickle b/test/unit/teleData/6616436070.pickle similarity index 66% rename from data/2106963958.pickle rename to test/unit/teleData/6616436070.pickle index 3fd532d5d..7dfccf2f9 100644 Binary files a/data/2106963958.pickle and b/test/unit/teleData/6616436070.pickle differ diff --git a/test/unit/test_add.py b/test/unit/test_add.py new file mode 100644 index 000000000..ac2618e7f --- /dev/null +++ b/test/unit/test_add.py @@ -0,0 +1,123 @@ +""" +File: test_add.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import unittest +from unittest.mock import Mock, patch +import teleBot + +class TestAdd(unittest.TestCase): + + @patch('teleBot.User', return_value=Mock(spec=teleBot.User)) + @patch('teleBot.bot.send_message') + @patch('teleBot.bot.process_new_callback_query') + @patch('teleBot.bot.process_new_messages') + def test_add_command(self, mock_process_messages, mock_process_callback, mock_send_message, mock_user): + bot = teleBot.bot + user_instance = mock_user.return_value + msg = Mock() + msg.chat.id = "12345" + msg.text = "/add" + query = Mock() + query.chat_instance.id = "12345" + query.data = "2021,11,01" + reply = Mock() + reply.text = "1.00" + user_instance.spend_categories = ["Category1"] + + bot.process_new_messages([msg]) + bot.process_new_callback_query([query]) + bot.process_new_messages([reply]) + + # Add your assertions here + + @patch('teleBot.User', return_value=Mock(spec=teleBot.User)) + @patch('teleBot.bot.send_message') + @patch('teleBot.bot.process_new_callback_query') + @patch('teleBot.bot.process_new_messages') + def test_add_wrong_date(self, mock_process_messages, mock_process_callback, mock_send_message, mock_user): + bot = teleBot.bot + user_instance = mock_user.return_value + msg = Mock() + msg.chat.id = "12345" + msg.text = "/add" + query = Mock() + query.chat_instance.id = "12345" + query.data = "invalid_date" + + bot.process_new_messages([msg]) + bot.process_new_callback_query([query]) + + # Add your assertions for the incorrect date scenario + + @patch('teleBot.User', return_value=Mock(spec=teleBot.User)) + @patch('teleBot.bot.send_message') + @patch('teleBot.bot.process_new_callback_query') + @patch('teleBot.bot.process_new_messages') + def test_add_wrong_cat(self, mock_process_messages, mock_process_callback, mock_send_message, mock_user): + bot = teleBot.bot + user_instance = mock_user.return_value + msg = Mock() + msg.chat.id = "12345" + msg.text = "/add" + query = Mock() + query.chat_instance.id = "12345" + query.data = "2021,11,01" + reply = Mock() + reply.text = "INVALID" + user_instance.spend_categories = ["Category1"] + + bot.process_new_messages([msg]) + bot.process_new_callback_query([query]) + bot.process_new_messages([reply]) + + # Add your assertions for the incorrect category scenario + + @patch('teleBot.User', return_value=Mock(spec=teleBot.User)) + @patch('teleBot.bot.send_message') + @patch('teleBot.bot.process_new_callback_query') + @patch('teleBot.bot.process_new_messages') + def test_add_wrong_num(self, mock_process_messages, mock_process_callback, mock_send_message, mock_user): + bot = teleBot.bot + user_instance = mock_user.return_value + msg = Mock() + msg.chat.id = "12345" + msg.text = "/add" + query = Mock() + query.chat_instance.id = "12345" + query.data = "2021,11,01" + reply = Mock() + reply.text = "-1" + user_instance.spend_categories = ["Category1"] + + bot.process_new_messages([msg]) + bot.process_new_callback_query([query]) + bot.process_new_messages([reply]) + + # Add your assertions for the incorrect amount scenario + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/test/unit/test_add_cmd_custom_category.py b/test/unit/test_add_cmd_custom_category.py new file mode 100644 index 000000000..dae855bb3 --- /dev/null +++ b/test/unit/test_add_cmd_custom_category.py @@ -0,0 +1,59 @@ +""" +File: test_add_cmd_custom_category.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import unittest +from unittest.mock import Mock, patch +import teleBot + +class TestAddCustomCategory(unittest.TestCase): + def test_add_custom_category_command(self): + # Mock the necessary dependencies + with patch.object(teleBot.bot, 'process_new_messages') as mock_process_messages: + with patch.object(teleBot.bot, 'next_step_backend', Mock()) as mock_next_step_backend: + msg = Mock() + msg.chat.id = "12345" + msg.text = "/categoryAdd" + custom_category = "travel" + + # Simulate bot's behavior + teleBot.bot.process_new_messages.return_value = [msg] + teleBot.bot.next_step_backend.handlers = [] + + # Trigger the command + teleBot.bot.process_new_messages([msg]) + + # Add the custom category + reply = Mock() + reply.chat.id = "12345" + reply.text = custom_category + teleBot.bot.process_new_messages([reply]) + + # Add your assertions here + # Assert that the custom category was added to the user, possibly by checking bot.user_list + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/test/unit/test_add_command.py b/test/unit/test_add_command.py new file mode 100644 index 000000000..ed52c96df --- /dev/null +++ b/test/unit/test_add_command.py @@ -0,0 +1,107 @@ +""" +File: test_add_command.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import logging +import os +import pathlib +import unittest +from datetime import datetime +from importlib import reload + +from telebot import types + +import teleBot +from teleUser import User + +CHAT_ID = os.environ['CHAT_ID'] if 'CHAT_ID' in os.environ else 1 +TOKEN = os.environ['API_TOKEN'] if 'API_TOKEN' in os.environ else 0 + + +class BotTest(unittest.TestCase): + """ + Base test class for Bot Tests + """ + + def setUp(self) -> None: + """ + Creates a new user and ensures no data was left over + :return: None + """ + abspath = pathlib.Path("data").absolute() + if not os.path.exists(abspath): + os.mkdir(abspath) + reload(teleBot.bot) + teleBot.bot.api_token = os.environ['API_TOKEN'] + self.bot = teleBot.bot.bot + self.user = User(str(CHAT_ID)) + self.user.save_user(str(CHAT_ID)) + self.chat_id = CHAT_ID + # reloads the user list + teleBot.bot.user_list = teleBot.bot.get_users() + # asserts the current user has no data + assert self.user.get_number_of_transactions() == 0 + + def tearDown(self) -> None: + # Clearing out next step handlers + self.bot.next_step_backend.handlers = {} + path = f"data/{CHAT_ID}.pickle" + abspath = pathlib.Path(path).absolute() + if os.path.exists(abspath): + os.remove(path) + # verifying all old info was deleted + self.user = User(CHAT_ID) + assert self.user.get_number_of_transactions() == 0 + + def create_record(self, amount: float) -> None: + """ + Creates a record in the user list for the given amount + :param amount: amount to add + :return: None + """ + self.user.add_transaction(datetime.now(), self.user.spend_categories[0], amount, CHAT_ID) + self.user.save_user(CHAT_ID) + teleBot.bot.user_list = teleBot.bot.get_users() + assert CHAT_ID in teleBot.bot.user_list.keys() + + def create_text_message(self, text: str) -> types.Message: + """ + Creates a text message + :param text: text of the message + :return: The created message to be sent + """ + params = {'text': text} + chat = types.User(int(CHAT_ID), False, 'test') + return types.Message(1, None, None, chat, 'text', params, "") + + def create_callback_query(self, data: str, message: types.Message) -> types.CallbackQuery: + """ + Creates a text message + :param text: text of the message + :return: The created message to be sent + """ + chat = types.User(int(CHAT_ID), False, 'test') + return types.CallbackQuery(1, CHAT_ID, data, chat, message) diff --git a/test/unit/test_add_custom_category.py b/test/unit/test_add_custom_category.py index be9fd6677..97d4713b8 100644 --- a/test/unit/test_add_custom_category.py +++ b/test/unit/test_add_custom_category.py @@ -1,25 +1,79 @@ """ -Tests the adding custom category +File: test_add_custom_category.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. """ + +from bot_utils import BotTest import unittest -from BaseCase import BaseCase +import time +import teleBot + -class TestAddCustomCategory(BaseCase): +class TestAddCustomCategory(BotTest): """ - Unit test for adding custom category + Test file for add custom category """ - def test_add_custom_category(self): + def test_add_custom_category_command(self): """ - Adding a custom category, reflects the new category in the list + Tests the add custom category command """ - custom_category = "books" - self.user.add_category(custom_category, 1) - raw_content = self.user.transactions.keys() + msg = self.create_text_message('/categoryAdd') + self.bot.process_new_messages([msg]) + time.sleep(3) + + # assert the message was sent, and text was not changed + assert msg.chat.id is not None + assert msg.text == '/categoryAdd' + # there should be a next step handler + assert len(self.bot.next_step_backend.handlers) == 1, \ + "For the /categoryAdd command, there should be a next step" + # there should not be any exceptions + assert self.bot.worker_pool.exception_info is None + + # send the custom category + custom_category = "travel" + reply = self.create_text_message(custom_category) + self.bot.process_new_messages([reply]) + time.sleep(3) + + # assert the message was sent, and text was not changed + assert reply.chat.id is not None + assert reply.text == custom_category + # there should not be a next step handler + assert len(self.bot.next_step_backend.handlers) == 0, \ + "For the reply to budget, there should not be a next step" + # there should not be any exceptions + assert self.bot.worker_pool.exception_info is None + + # assert the custom category was added to the user + chat_id = str(reply.chat.id) + content = teleBot.user_list[chat_id].transactions categories = [] - for category in raw_content: + for category in content: categories.append(category) assert custom_category in categories - if __name__ == '__main__': - unittest.main() + unittest.main() \ No newline at end of file diff --git a/test/unit/test_add_monthly_budget.py b/test/unit/test_add_monthly_budget.py index 11729da91..abc502a70 100644 --- a/test/unit/test_add_monthly_budget.py +++ b/test/unit/test_add_monthly_budget.py @@ -1,37 +1,50 @@ """ -Tests the add_monthly_budget_method +File: test_add_monthly_budget.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. """ -from BaseCase import BaseCase +from discord_BaseCase import discord_BaseCase - -class TestAddMonthlyBudget(BaseCase): - """ - Unit test for add monthly budget - """ - def add_monthly_budget_valid(self): +class TestAddMonthlyBudget(discord_BaseCase): + def test_add_monthly_budget_valid(self): """ Asserts when add_monthly_budget is given a float value - """ assert self.user.monthly_budget == 0 amount = 10.00 - self.user.add_monthly_budget(amount, 1) + self.user.add_monthly_budget(amount, 2) assert self.user.monthly_budget == amount - def add_monthly_budget_invalid(self): + def test_add_monthly_budget_invalid(self): """ Asserts when add_monthly_budget is given 0 - """ assert self.user.monthly_budget == 0 amount_valid = 10.00 - self.user.add_monthly_budget(amount_valid, 1) - assert self.user.monthly_budget == amount + self.user.add_monthly_budget(amount_valid, 2) + assert self.user.monthly_budget == amount_valid amount = 0.00 - self.user.add_monthly_budget(amount, 1) + self.user.add_monthly_budget(amount, 2) assert self.user.monthly_budget == amount_valid - - -if __name__ == '__main__': - unittest.main() diff --git a/test/unit/test_add_transaction.py b/test/unit/test_add_transaction.py index 4fae52519..f3c1b5c44 100644 --- a/test/unit/test_add_transaction.py +++ b/test/unit/test_add_transaction.py @@ -1,6 +1,29 @@ -import unittest +""" +File: test_add_transaction.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. -# import code +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" from BaseCase import BaseCase from datetime import datetime @@ -12,7 +35,7 @@ class TestAddUserRecord(BaseCase): def validate_user_list(self, users) -> str: """ - Helper method to validate the user list matches + Helper method to validate the user list matches with no.of transactions :param users: a sample dictionary of user: [records] :type: dict :return: True if user list matches @@ -100,8 +123,4 @@ def test_add_multiple_cat(self): # validating the list message = self.validate_user_list(transaction) if message != "": - assert False, message - - -if __name__ == '__main__': - unittest.main() + assert False, message \ No newline at end of file diff --git a/test/unit/test_bot.py b/test/unit/test_bot.py new file mode 100644 index 000000000..88a5f80f9 --- /dev/null +++ b/test/unit/test_bot.py @@ -0,0 +1,79 @@ +""" +File: test_bot.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import unittest +from unittest.mock import patch +import teleBot as code_lib + +class TestCommands(unittest.TestCase): + @patch.object(code_lib, 'telebot') + def test_number_commands(self, mock_telebot): + bot = code_lib.bot + # Simulate bot's behavior + bot.message_handlers = [{'filters': ['commands']} for _ in range(14)] + + number_of_commands = 14 + self.assertEqual(len(bot.message_handlers), number_of_commands) + + @patch.object(code_lib, 'telebot') + def test_commands(self, mock_telebot): + bot = code_lib.bot + bot.message_handlers = [ + {'filters': ['commands'], 'function': code_lib.start_and_menu_command}, + {'filters': ['commands'], 'function': code_lib.command_budget}, + {'filters': ['commands'], 'function': code_lib.command_add}, + {'filters': ['commands'], 'function': code_lib.show_history}, + {'filters': ['commands'], 'function': code_lib.command_display}, + {'filters': ['commands'], 'function': code_lib.edit1}, + {'filters': ['commands'], 'function': code_lib.category_add}, + {'filters': ['commands'], 'function': code_lib.category_list}, + {'filters': ['commands'], 'function': code_lib.category_delete}, + {'filters': ['commands'], 'function': code_lib.command_delete}, + {'filters': ['commands'], 'function': code_lib.send_email}, + {'filters': ['commands'], 'function': code_lib.download_history} + ] + + actual_titles = [ + {'function': code_lib.start_and_menu_command, 'commands': ["start", "menu"]}, + {'function': code_lib.command_budget, 'commands': ["budget"]}, + {'function': code_lib.command_add, 'commands': ["add"]}, + {'function': code_lib.show_history, 'commands': ["history"]}, + {'function': code_lib.command_display, 'commands': ["display"]}, + {'function': code_lib.edit1, 'commands': ["edit"]}, + {'function': code_lib.category_add, 'commands': ["categoryAdd"]}, + {'function': code_lib.category_list, 'commands': ["categoryList"]}, + {'function': code_lib.category_delete, 'commands': ["categoryDelete"]}, + {'function': code_lib.command_delete, 'commands': ["delete"]}, + {'function': code_lib.send_email, 'commands': ["sendEmail"]}, + {'function': code_lib.download_history, 'commands': ["download"]} + ] + + for i, expected_func in enumerate(actual_titles): + self.assertEqual(bot.message_handlers[i]['function'], expected_func['function']) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/test/bot/test_budget.py b/test/unit/test_budget.py similarity index 65% rename from test/bot/test_budget.py rename to test/unit/test_budget.py index 823998f1a..0688f532c 100644 --- a/test/bot/test_budget.py +++ b/test/unit/test_budget.py @@ -1,9 +1,33 @@ """ -Tests budget command +File: test_budget.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. """ + import time import unittest -from src import bot +import teleBot from bot_utils import BotTest @@ -44,8 +68,8 @@ def test_budget_command(self): # assert the budget was added to the user chat_id = str(reply.chat.id) - assert chat_id in bot.user_list - user_budget = bot.user_list[chat_id].monthly_budget + assert chat_id in teleBot.user_list + user_budget = teleBot.user_list[chat_id].monthly_budget assert user_budget == 120.00 def test_budget_command_invalid(self): @@ -80,6 +104,6 @@ def test_budget_command_invalid(self): # assert the budget was not changed for the user chat_id = str(reply.chat.id) - assert chat_id in bot.user_list - user_budget = bot.user_list[chat_id].monthly_budget - assert user_budget != -19.00 + assert chat_id in teleBot.user_list + user_budget = teleBot.user_list[chat_id].monthly_budget + assert user_budget != -19.00 \ No newline at end of file diff --git a/test/bot/test_custom_category_list.py b/test/unit/test_custom_category_list.py similarity index 52% rename from test/bot/test_custom_category_list.py rename to test/unit/test_custom_category_list.py index e217ee231..3aee03139 100644 --- a/test/bot/test_custom_category_list.py +++ b/test/unit/test_custom_category_list.py @@ -1,8 +1,34 @@ +""" +File: test_custom_category_list.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" from bot_utils import BotTest import unittest import time -from src import bot +import teleBot class TestListCustomCategory(BotTest): @@ -39,11 +65,11 @@ def test_list_custom_category_command(self): # assert the custom category was added to the user chat_id = str(msg.chat.id) - content = bot.user_list[chat_id].transactions + content = teleBot.user_list[chat_id].transactions categories = [] for category in content: categories.append(category) assert custom_category in categories if __name__ == '__main__': - unittest.main() + unittest.main() \ No newline at end of file diff --git a/test/bot/test_delete.py b/test/unit/test_delete.py similarity index 67% rename from test/bot/test_delete.py rename to test/unit/test_delete.py index 1e4e4b89f..33637bd2c 100644 --- a/test/bot/test_delete.py +++ b/test/unit/test_delete.py @@ -1,10 +1,33 @@ """ -Tests delete command +File: test_delete.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. """ import time import unittest from bot_utils import BotTest -from src import bot +import teleBot class TestDelete(BotTest): @@ -76,11 +99,11 @@ def test_delete_command_records(self): # assert the record was deleted CHAT_ID = str(msg.chat.id) - assert bot.user_list[CHAT_ID].get_number_of_transactions() == 0 + assert teleBot.user_list[CHAT_ID].get_number_of_transactions() == 0 if __name__ == '__main__': - unittest.main() + unittest.main() \ No newline at end of file diff --git a/test/bot/test_delete_cmd_custom_category.py b/test/unit/test_delete_cmd_custom_category.py similarity index 55% rename from test/bot/test_delete_cmd_custom_category.py rename to test/unit/test_delete_cmd_custom_category.py index 7d7ac836d..272a5e486 100644 --- a/test/bot/test_delete_cmd_custom_category.py +++ b/test/unit/test_delete_cmd_custom_category.py @@ -1,8 +1,34 @@ +""" +File: test_delete_cmd_custom_category.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" from bot_utils import BotTest import unittest import time -from src import bot +import teleBot class TestDeleteCustomCategory(BotTest): @@ -44,11 +70,11 @@ def test_delete_custom_category_command(self): #delete the newly created custom category chat_id = str(reply.chat.id) - content = bot.user_list[chat_id].transactions + content = teleBot.user_list[chat_id].transactions categories = [] for category in content: categories.append(category) assert custom_category not in categories if __name__ == '__main__': - unittest.main() + unittest.main() \ No newline at end of file diff --git a/test/unit/test_delete_custom_category.py b/test/unit/test_delete_custom_category.py index 0b6b22274..03251d6fa 100644 --- a/test/unit/test_delete_custom_category.py +++ b/test/unit/test_delete_custom_category.py @@ -1,5 +1,28 @@ """ -Tests the delete of custom category +File: test_delete_custom_category.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. """ import unittest from BaseCase import BaseCase diff --git a/test/unit/test_delete_history.py b/test/unit/test_delete_history.py index 253195475..b49100871 100644 --- a/test/unit/test_delete_history.py +++ b/test/unit/test_delete_history.py @@ -1,5 +1,28 @@ """ -Test for the deleteHistory function +File: test_delete_history.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. """ from datetime import datetime diff --git a/test/unit/test_discord_add.py b/test/unit/test_discord_add.py new file mode 100644 index 000000000..1b3906a7a --- /dev/null +++ b/test/unit/test_discord_add.py @@ -0,0 +1,101 @@ +""" +File: test_discord_add.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import unittest +from unittest.mock import MagicMock, patch +from discordBot import add + +class TestAddCommand(unittest.TestCase): + + def setUp(self): + self.ctx = MagicMock() + self.ctx.author = "user1" + self.ctx.channel = "channel1" + + @patch('discordBot.process_date') + async def test_select_date(self, mock_process_date): + self.ctx.send.side_effect = ["03-02-2023"] + await add.select_date(self.ctx) + + self.ctx.send.assert_called_with("Enter day") + mock_process_date.assert_called_with(self.ctx, 2, 3, 2023) + + @patch('discordBot.bot.wait_for') + @patch('discordBot.process_date') + async def test_select_date_timeout(self, mock_process_date, mock_wait_for): + mock_wait_for.side_effect = asyncio.TimeoutError + await add.select_date(self.ctx) + self.ctx.send.assert_called_with("You took too long to respond. Please try again.") + + @patch('discordBot.process_category') + async def test_process_date(self, mock_process_category): + await add.process_date(self.ctx, 2, 3, 2023) + self.ctx.send.assert_called_with("Selected Date: 03-02-2023") + mock_process_category.assert_called_with(self.ctx, datetime(2023, 3, 2)) + + @patch('discordBot.Select') + @patch('discordBot.View') + @patch('discordBot.bot.wait_for') + async def test_select_category(self, mock_wait_for, mock_view, mock_select): + mock_wait_for.side_effect = ["selected_category"] + mock_select.return_value.values = ["selected_category"] + user_list = {"channel1": {"spend_categories": ["category1", "category2"]}} + self.ctx.author = "user1" + self.ctx.channel = "channel1" + + await add.select_category(self.ctx, datetime(2023, 3, 2)) + self.ctx.send.assert_called_with('Please select a category') + self.ctx.send.assert_called_with("You chose: selected_category") + + @patch('discordBot.post_category_selection') + async def test_select_category_invalid_category(self, mock_post_category_selection): + await add.select_category(self.ctx, datetime(2023, 3, 2)) + self.ctx.send.assert_called_with('Please select a category') + self.ctx.send.assert_called_with("Invalid category") + + @patch('discordBot.bot.wait_for') + @patch('discordBot.post_amount_input') + async def test_post_category_selection(self, mock_post_amount_input, mock_wait_for): + mock_wait_for.side_effect = ["50"] + await add.post_category_selection(self.ctx, "category1", datetime(2023, 3, 2)) + self.ctx.send.assert_called_with('\nHow much did you spend on category1') + mock_post_amount_input.assert_called_with(self.ctx, "50", "category1", datetime(2023, 3, 2)) + + @patch('discordBot.bot.wait_for') + @patch('discordBot.user_list', {"channel1": {"monthly_budget": 100}}) + async def test_post_category_selection_zero_amount(self): + await add.post_category_selection(self.ctx, "category1", datetime(2023, 3, 2)) + self.ctx.send.assert_called_with("Spent amount has to be a non-zero number.") + + @patch('discordBot.bot.wait_for') + @patch('discordBot.user_list', {"channel1": {"monthly_budget": 100}}) + async def test_post_category_selection_valid_amount(self): + await add.post_category_selection(self.ctx, "category1", datetime(2023, 3, 2)) + self.ctx.send.assert_called_with("The following expenditure has been recorded: You have spent $50 for category1 on 03-02-2023") + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/test/unit/test_discord_add_transaction.py b/test/unit/test_discord_add_transaction.py new file mode 100644 index 000000000..057951a58 --- /dev/null +++ b/test/unit/test_discord_add_transaction.py @@ -0,0 +1,132 @@ +""" +File: test_discord_add_transaction.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import unittest +from discord_BaseCase import discord_BaseCase +from datetime import datetime + + +class TestAddUserRecord(discord_BaseCase): + """ + Tests the addUserrecord method + """ + + def validate_user_list(self, users) -> str: + """ + Helper method to validate the user list matches with no.of transactions + :param users: a sample dictionary of user: [records] + :type: dict + :return: True if user list matches + :rtype: bool + """ + # assert exact number of users + expected_len = 0 + for category in users: + expected_len += len(users[category]) + if expected_len != self.user.get_number_of_transactions(): + return f'Length does not match. ' \ + f'Expected {expected_len} transactions' \ + f'Found {self.user.get_number_of_transactions()}' + for category in users: + # assert same number of records per user + if len(self.user.transactions[category]) != len(users[category]): + return f'Expected {len(users[category])} records. ' \ + f'Found {len(self.user.transactions[category])}' + + # assert the record is right + if self.user.transactions[category] != users[category]: + return f"{category} record should be: {users[category]}, " \ + f"found {self.user.transactions[category]}" + + # if everything matches + return "" + + def test_add_user_record_one(self): + """ + tests adding one record for one user + :return: + """ + assert self.user.get_number_of_transactions() == 0 + # adding one record + transaction = self.create_transaction() + date = datetime.today() + record = {"Date": date, "Value": 10.00} + transaction[self.user.spend_categories[0]].append(record) + for category in transaction: + # for each record to add + for record in transaction[category]: + self.user.add_transaction(record['Date'], category, record['Value'], 2) + # validating the list + message = self.validate_user_list(transaction) + if message != "": + assert False, message + + def test_add_user_record_multiple_record(self): + """ + tests adding multiple records for one user + :return: + """ + assert self.user.get_number_of_transactions() == 0 + # adding one record + transaction = self.create_transaction() + date = datetime.today() + records = [{"Date": date, "Value": 10.00}, {"Date": date, "Value": 15.00}] + for record in records: + transaction[self.user.spend_categories[0]].append(record) + for category in transaction: + # for each record to add + for record in transaction[category]: + self.user.add_transaction(record['Date'], category, record['Value'], 1) + # validating the list + message = self.validate_user_list(transaction) + if message != "": + assert False, message + + def test_add_multiple_cat(self): + """ + tests adding multiple records for multiple users + :return: + """ + + assert self.user.get_number_of_transactions() == 0 + # adding one record + transaction = self.create_transaction() + date = datetime.today() + transaction[self.user.spend_categories[0]].append({"Date": date, "Value": 10.00}) + transaction[self.user.spend_categories[1]].append({"Date": date, "Value": 150.00}) + for category in transaction: + # for each record to add + for record in transaction[category]: + self.user.add_transaction(record['Date'], category, record['Value'], 1) + # validating the list + message = self.validate_user_list(transaction) + if message != "": + assert False, message + + +if __name__ == '__main__': + unittest.main() diff --git a/test/unit/test_discord_delete.py b/test/unit/test_discord_delete.py new file mode 100644 index 000000000..9944bdf4d --- /dev/null +++ b/test/unit/test_discord_delete.py @@ -0,0 +1,111 @@ +""" +File: test_discord_delete.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from datetime import datetime +from discord_BaseCase import discord_BaseCase + + +class TestDeleteHistory(discord_BaseCase): + """ + Unit test for deleteHistory + """ + + def test_delete_history_none(self): + """ + Given no transactions, the list should not change + """ + # given no history + assert self.user.get_number_of_transactions() == 0 + # doing deleteHistory + self.user.deleteHistory() + # should not do anything + assert self.user.get_number_of_transactions() == 0 + + def test_delete(self): + """ + Given there is one user + deleting a transaction + should remove it + """ + # given adding one user + date = datetime.today() + transaction = self.create_transaction() + record = {"Date": date, "Value": 10.00} + transaction[self.user.spend_categories[0]].append(record) + self.user.transactions[self.user.spend_categories[0]].append(record) + # delete the transaction + self.user.deleteHistory(transaction) + assert self.user.get_number_of_transactions() == 0 + assert self.user.transactions[self.user.spend_categories[0]] == [] + + def test_delete_multiple(self): + """ + Given there is multiple transactions + deleting one should work + """ + # given adding one user + date = datetime.today() + transaction = self.create_transaction() + record = {"Date": date, "Value": 10.00} + # appending the transaction + self.user.transactions[self.user.spend_categories[0]].append(record) + # creating a record to delete + to_delete = {"Date": date, "Value": 15.00} + transaction[self.user.spend_categories[0]].append(to_delete) + self.user.transactions[self.user.spend_categories[0]].append(to_delete) + # delete the transaction + self.user.deleteHistory(transaction) + assert self.user.get_number_of_transactions() == 1 + assert self.user.transactions[self.user.spend_categories[0]] == [record] + + def test_delete_multiple_record(self): + """ + Given there is one user + deleting one record from the user + should remove it from the user list + """ + # given adding one user + date = datetime.today() + transaction = self.create_transaction() + record = {"Date": date, "Value": 10.00} + # appending the transaction + self.user.transactions[self.user.spend_categories[0]].append(record) + # creating a record to delete + to_delete = {"Date": date, "Value": 15.00} + transaction[self.user.spend_categories[0]].append(to_delete) + self.user.transactions[self.user.spend_categories[0]].append(to_delete) + # delete the transaction + self.user.deleteHistory(transaction) + assert self.user.get_number_of_transactions() == 1 + assert self.user.transactions[self.user.spend_categories[0]] == [record] + + # delete the last record + transaction = self.create_transaction() + transaction[self.user.spend_categories[0]].append(record) + self.user.deleteHistory(transaction) + assert self.user.get_number_of_transactions() == 0 + assert self.user.transactions[self.user.spend_categories[0]] == [] diff --git a/test/unit/test_discord_display_transaction.py b/test/unit/test_discord_display_transaction.py new file mode 100644 index 000000000..c8873ad1b --- /dev/null +++ b/test/unit/test_discord_display_transaction.py @@ -0,0 +1,83 @@ +""" +File: test_discord_display_transaction.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from discord_BaseCase import discord_BaseCase +from datetime import datetime + + +class TestDisplayTransaction(discord_BaseCase): + """ + Unit test for display_transaction + """ + + def test_display_one_cat(self): + """ + Given one category, we expect one row + """ + # only 1 row + date = datetime.today() + transaction = {self.user.spend_categories[0]: [{"Date": date, "Value": 10.0}]} + expected_str = f'{self.user.spend_categories[0]}, {date.date()}, 10.0\n' + assert self.user.display_transaction(transaction) == expected_str + + def test_display_two_cat(self): + """ + Given multiple categories, it should have the + categories on separate lines + """ + # 2 rows + date = datetime.today() + transaction = {self.user.spend_categories[0]: [{"Date": date, "Value": 10.0}], + self.user.spend_categories[1]: [{"Date": date, "Value": 15.0}]} + expected_str = f'{self.user.spend_categories[0]}, {date.date()}, 10.0\n' \ + f'{self.user.spend_categories[1]}, {date.date()}, 15.0\n' + assert self.user.display_transaction(transaction) == expected_str + + def test_display_multiple_row(self): + """ + Given multiple purchases in same category + """ + date = datetime.today() + transaction = {self.user.spend_categories[0]: [{"Date": date, "Value": 10.0}, + {"Date": date, "Value": 15.0}]} + expected_str = f'{self.user.spend_categories[0]}, {date.date()}, 10.0\n' \ + f'{self.user.spend_categories[0]}, {date.date()}, 15.0\n' + assert self.user.display_transaction(transaction) == expected_str + + def test_display_spending_multiple_all(self): + """ + Given multiple categories with multiple spending's + """ + date = datetime.today() + transaction = {self.user.spend_categories[0]: [{"Date": date, "Value": 10.0}, + {"Date": date, "Value": 15.0}], + self.user.spend_categories[1]: [{"Date": date, "Value": 5}]} + expected_str = f'{self.user.spend_categories[0]}, {date.date()}, 10.0\n' \ + f'{self.user.spend_categories[0]}, {date.date()}, 15.0\n' \ + f'{self.user.spend_categories[1]}, {date.date()}, 5\n' + ret = self.user.display_transaction(transaction) + assert ret == expected_str diff --git a/test/unit/test_discord_edit.py b/test/unit/test_discord_edit.py new file mode 100644 index 000000000..4ebc2a0d4 --- /dev/null +++ b/test/unit/test_discord_edit.py @@ -0,0 +1,90 @@ +""" +File: test_discord_edit.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" +from discord_BaseCase import discord_BaseCase +from datetime import datetime, timedelta + + +class TestAddUserRecord(discord_BaseCase): + """ + Tests the edit series of functions in bot.py + """ + + def test_store_edit_transaction(self): + """ + After user enters existing transaction and data is parsed from input + user.edit_transaction should be that transaction + user.edit_category should be the category of that transaction + """ + # User enters the following date,category, value + user_date = datetime.today() + user_category = "Food" + user_value = 10.00 + userid = "2" + self.user.add_transaction(user_date, user_category, user_value, userid) + transaction = {"Date": user_date, "Value": user_value} + self.user.store_edit_transaction(transaction, user_category) + assert transaction == self.user.edit_transactions + assert user_category == self.user.edit_category + + def test_edit_date(self): + # User enters the following date,category, value + user_date = datetime.today() + edit_date = datetime.today() - timedelta(days=1) + user_category = "Groceries" + user_value = 10.00 + userid = "2" + self.user.add_transaction(user_date, user_category, user_value, userid) + transaction = {"Date": user_date, "Value": user_value} + self.user.store_edit_transaction(transaction, user_category) + self.user.edit_transaction_date(edit_date) + assert self.user.transactions["Groceries"][0]["Date"].date() == edit_date.date() + + def test_edit_transaction_category(self): + # User enters the following date,category, value + user_date = datetime.today() + user_category = "Utilities" + edit_category = "Transport" + user_value = 10.00 + userid = "2" + self.user.add_transaction(user_date, user_category, user_value, userid) + transaction = {"Date": user_date, "Value": user_value} + self.user.store_edit_transaction(transaction, user_category) + self.user.edit_transaction_category(edit_category) + assert self.user.transactions[edit_category][0] == transaction + + def test_edit_transaction_value(self): + # User enters the following date,category, value + user_date = datetime.today() + user_category = "Shopping" + user_value = 10.00 + edit_value = 20.00 + userid = "2" + self.user.add_transaction(user_date, user_category, user_value, userid) + transaction = {"Date": user_date, "Value": user_value} + self.user.store_edit_transaction(transaction, user_category) + self.user.edit_transaction_value(edit_value) + assert self.user.transactions["Shopping"][0]["Value"] == edit_value diff --git a/test/unit/test_discord_get_recordsByDate.py b/test/unit/test_discord_get_recordsByDate.py new file mode 100644 index 000000000..4875805f2 --- /dev/null +++ b/test/unit/test_discord_get_recordsByDate.py @@ -0,0 +1,120 @@ +""" +File: test_discord_get_recordsByData.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from datetime import datetime + +from discord_BaseCase import discord_BaseCase + + +class TestGetRecordsByDate(discord_BaseCase): + """ + Unit test for deleteHistory + def get_records_by_date(date, chat_id, is_month): + + By this method, tests have been completed that the user is present + Thus, there are no tests for user not present + """ + + def add_expected(self): + + self.oct_01 = {"Date": datetime(month=10, day=1, year=2021), "Value": 10.00} + self.oct_10 = {"Date": datetime(month=10, day=10, year=2021), "Value": 15.00} + self.nov_01 = {"Date": datetime(month=11, day=1, year=2021), "Value": 5.00} + self.add_record(self.user.spend_categories[0], self.oct_01) + self.add_record(self.user.spend_categories[0], self.oct_10) + self.add_record(self.user.spend_categories[0], self.nov_01) + + def test_user_date_not_present(self): + """ + Given there is one user + calling get_records_by_date with a non-present date + should return [] + """ + self.add_expected() + wrong_date = datetime.now() + # make the date year 1, month 1, day 1 + wrong_date = wrong_date.replace(1, 1, 1) + # given the user_list + self.user.transactions = self.expected_list + user_history = self.user.get_records_by_date(wrong_date, False) + # there should be no records + assert user_history == self.create_transaction() + user_history = self.user.get_records_by_date(wrong_date, True) + # there should be no records + assert user_history == self.create_transaction() + + def test_get_by_month(self): + """ + Given there is one user + calling get_records_by_date by month + """ + self.add_expected() + # given the user_list + self.user.transactions = self.expected_list + + october = datetime.now() + # make the date match october 2021 + october = october.replace(year=2021, month=10).date() + user_history = self.user.get_records_by_date(october, True) + # the records should match october 2021 + expected_transactions = self.create_transaction() + # filter everything that is Oct-2021 + expected_transactions[self.user.spend_categories[0]] = [self.oct_01, self.oct_10] + assert user_history == expected_transactions + + def test_get_by_day(self): + """ + Given there is one user + calling get_records_by_date with a valid date + """ + + self.add_expected() + # given the user_list + self.user.transactions = self.expected_list + + october = datetime.now() + # make the date match october 2021 + october = october.replace(year=2021, month=10, day=1).date() + user_history = self.user.get_records_by_date(october, False) + # the records should match october 2021 + expected_transactions = self.create_transaction() + # filter everything that is Oct-2021 + expected_transactions[self.user.spend_categories[0]] = [self.oct_01] + assert user_history == expected_transactions + + def test_get_by_all(self): + """ + Given there is one user + calling all + """ + self.add_expected() + # given the user_list + self.user.transactions = self.expected_list + + user_history = self.user.get_records_by_date("all", True) + # there should all + assert user_history == self.expected_list diff --git a/test/unit/test_discord_monthly_budget.py b/test/unit/test_discord_monthly_budget.py new file mode 100644 index 000000000..fe6a485a1 --- /dev/null +++ b/test/unit/test_discord_monthly_budget.py @@ -0,0 +1,50 @@ +""" +File: test_discord_monthly_budget.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from discord_BaseCase import discord_BaseCase + +class TestAddMonthlyBudget(discord_BaseCase): + def test_add_monthly_budget_valid(self): + """ + Asserts when add_monthly_budget is given a float value + """ + assert self.user.monthly_budget == 0 + amount = 10.00 + self.user.add_monthly_budget(amount, 2) + assert self.user.monthly_budget == amount + + def test_add_monthly_budget_invalid(self): + """ + Asserts when add_monthly_budget is given 0 + """ + assert self.user.monthly_budget == 0 + amount_valid = 10.00 + self.user.add_monthly_budget(amount_valid, 2) + assert self.user.monthly_budget == amount_valid + amount = 0.00 + self.user.add_monthly_budget(amount, 2) + assert self.user.monthly_budget == amount_valid diff --git a/test/unit/test_discord_monthly_total.py b/test/unit/test_discord_monthly_total.py new file mode 100644 index 000000000..643b9e930 --- /dev/null +++ b/test/unit/test_discord_monthly_total.py @@ -0,0 +1,88 @@ +""" +File: test_discord_monthly_total.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from discord_BaseCase import discord_BaseCase +from datetime import datetime + +class TestMonthlyTotal(discord_BaseCase): + """ + Unit test for monthly total + """ + def test_one_transaction(self): + """ + Given one transaction, we expect total to be the value of that transaction + """ + date = datetime.today() + value = 12.00 + transaction = self.create_transaction() + date = datetime.today() + record = {"Date": date, "Value": value} + transaction[self.user.spend_categories[0]].append(record) + for category in transaction: + # for each record to add + for record in transaction[category]: + self.user.add_transaction(record['Date'], category, record['Value'], 2) + assert self.user.monthly_total() == value + + def test_multiple_transaction_same_cat(self): + """ + Given multiple transactions of same category, we expect total to be the sum of all the transactions + """ + date = datetime.today() + value = [12.00, 11.00] + transaction = self.create_transaction() + date = datetime.today() + records = [{"Date": date, "Value": value[0]}, {"Date": date, "Value": value[1]}] + for record in records: + transaction[self.user.spend_categories[0]].append(record) + for category in transaction: + # for each record to add + for record in transaction[category]: + self.user.add_transaction(record['Date'], category, record['Value'], 2) + assert self.user.monthly_total() == sum(value) + + def test_multiple_transaction_multiple_cat(self): + """ + Given multiple transactions of different categories, we expect total to be the sum of all the transactions + """ + date = datetime.today() + value = [12.00, 11.00, 21.50, 14.25] + transaction = self.create_transaction() + date = datetime.today() + transaction[self.user.spend_categories[0]].append({"Date": date, "Value": value[0]}) + transaction[self.user.spend_categories[0]].append({"Date": date, "Value": value[1]}) + transaction[self.user.spend_categories[1]].append({"Date": date, "Value": value[2]}) + transaction[self.user.spend_categories[1]].append({"Date": date, "Value": value[3]}) + for category in transaction: + # for each record to add + for record in transaction[category]: + self.user.add_transaction(record['Date'], category, record['Value'], 2) + assert self.user.monthly_total() == sum(value) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/unit/test_discord_save_user.py b/test/unit/test_discord_save_user.py new file mode 100644 index 000000000..77b3cbab9 --- /dev/null +++ b/test/unit/test_discord_save_user.py @@ -0,0 +1,96 @@ +""" +File: test_discord_save_user.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" +import os +import pathlib +import pickle + +from discord_BaseCase import discord_BaseCase + + +def users_equal(user_1, user_2): + if user_1.spend_categories != user_2.spend_categories: + return "Spend Categories do not match" + if user_1.spend_display_option != user_2.spend_display_option: + return "spend_display_option do not match" + if user_1.transactions != user_2.transactions: + return "transactions do not match" + if user_1.edit_transactions != user_2.edit_transactions: + return "edit_transactions do not match" + if user_1.edit_category != user_2.edit_category: + return "edit_category do not match" + if user_1.monthly_budget != user_2.monthly_budget: + return "monthly_budget do not match" + return True + + +class TestSaveUser(discord_BaseCase): + """ + Unit test for save_user + """ + + def test_save_no_history(self): + """ + given a valid user, saving and loading should return the same user + """ + prev_user = self.user + # with no history, call save_user + self.user.save_user(2) + # assert the pickle exists + abspath = pathlib.Path("discordData/2.pickle").absolute() + assert os.path.exists(abspath) + + with open(abspath, "rb") as f: + new_user = pickle.load(f) + # assert they are equal + assert new_user is not None + are_equal = users_equal(prev_user, new_user) + if not are_equal: + assert False, are_equal + + def test_valid_history(self): + """ + given a valid user, saving should yield the same user + """ + self.user.spend_categories.append("TEST") + self.user.spend_display_option.append("otherTest") + self.user.transactions[self.user.spend_categories[0]].append({"TEST": 0}) + self.user.edit_transactions["TEST"] = 0 + self.user.edit_category["TEST"] = 2 + self.user.monthly_budget = 100 + prev_user = self.user + # with history, call save_user + self.user.save_user(2) + # assert the pickle exists + abspath = pathlib.Path("discordData/2.pickle").absolute() + assert os.path.exists(abspath) + with open(abspath, "rb") as f: + new_user = pickle.load(f) + # assert they are equal + assert new_user is not None + are_equal = users_equal(prev_user, new_user) + if not are_equal: + assert False, are_equal diff --git a/test/unit/test_discord_validations.py b/test/unit/test_discord_validations.py new file mode 100644 index 000000000..bc0711d21 --- /dev/null +++ b/test/unit/test_discord_validations.py @@ -0,0 +1,114 @@ +""" +File: test_discord_validations.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from discord_BaseCase import discord_BaseCase +from datetime import datetime + + +class TestValidateEnteredAmount(discord_BaseCase): + """ + Unit test for get user history + """ + + def test_validate_entered_amount_empty(self): + """ + Asserts when validate_entered_amount is given an empty string, + 0 is returned + + """ + # given an empty string for amount entered, 0 should be returned + assert self.user.validate_entered_amount("") == 0 + + def test_validate_entered_amount_string(self): + """ + Asserts when validate_entered_amount is given a string + 0 is returned + """ + # given an string, 0 should be returned + assert self.user.validate_entered_amount("Test") == 0 + # given a string that contains numbers and a string + assert self.user.validate_entered_amount("000t") == 0 + + def test_validate_entered_amount_nan(self): + """ + Asserts when validate_entered_amount is given an invalid + number, 0 is returned + """ + # given a negative number + assert self.user.validate_entered_amount("-1") == 0 + # given 0 + assert self.user.validate_entered_amount("0") == 0 + # given a number with dollar sign + assert self.user.validate_entered_amount("$10") == 0 + # given a number with 2 decimals + assert self.user.validate_entered_amount("10..0") == 0 + + def test_validate_entered_amount_valid(self): + """ + Asserts when validate_entered_amount is given an valid + number, the number is returned + """ + # given a positive number + assert self.user.validate_entered_amount("1") == 1.00 + # given a positive number with decimals + assert self.user.validate_entered_amount("10.10") == 10.10 + # given a number with 14 digits + assert self.user.validate_entered_amount("1000000000.00") == 1000000000.00 + + def test_valid_date(self): + """ + Given no transactions, the list should not change + """ + date = datetime.today() + # format it as a year + dateFormat = '%d-%b-%Y' + monthFormat = '%b-%Y' + validated_d_m_y = self.user.validate_date_format(date.strftime(dateFormat), dateFormat) + assert validated_d_m_y.month == date.month + assert validated_d_m_y.day == date.day + assert validated_d_m_y.year == date.year + validated_m_y = self.user.validate_date_format(date.strftime(monthFormat), monthFormat) + assert validated_m_y.month == date.month + assert validated_m_y.year == date.year + + def test_invalid(self): + """ + Given there is one user + deleting a transaction + should remove it + """ + date = datetime.today() + # invalid formats + dateFormat = 'random' + error = self.user.validate_date_format(date.strftime('%d-%b-%Y'), dateFormat) + assert error is None + # mismatched formats + dateFormat = '%d-%b-%Y' + monthFormat = '%b-%Y' + validated_m_y = self.user.validate_date_format(date.strftime(dateFormat), monthFormat) + assert validated_m_y is None + diff --git a/test/bot/test_display.py b/test/unit/test_display.py similarity index 71% rename from test/bot/test_display.py rename to test/unit/test_display.py index 77472709f..fd70299a8 100644 --- a/test/bot/test_display.py +++ b/test/unit/test_display.py @@ -1,5 +1,28 @@ """ -Tests display command +File: test_display.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. """ import time import unittest @@ -89,4 +112,4 @@ def test_display_command_records(self): if __name__ == '__main__': - unittest.main() + unittest.main() \ No newline at end of file diff --git a/test/unit/test_display_transaction.py b/test/unit/test_display_transaction.py index 8db53c22d..59c3bc1f6 100644 --- a/test/unit/test_display_transaction.py +++ b/test/unit/test_display_transaction.py @@ -1,3 +1,30 @@ +""" +File: test_display_transactions.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + from BaseCase import BaseCase from datetime import datetime diff --git a/test/bot/test_edit.py b/test/unit/test_edit.py similarity index 65% rename from test/bot/test_edit.py rename to test/unit/test_edit.py index 16cf2aa4a..b74ac159a 100644 --- a/test/bot/test_edit.py +++ b/test/unit/test_edit.py @@ -1,6 +1,30 @@ """ -Tests edit command +File: test_edit.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. """ + import time import unittest from bot_utils import BotTest @@ -68,4 +92,4 @@ def test_edit_command_records(self): if __name__ == '__main__': - unittest.main() + unittest.main() \ No newline at end of file diff --git a/test/unit/test_edit_functions.py b/test/unit/test_edit_functions.py index 1aca1be53..e34d885a6 100644 --- a/test/unit/test_edit_functions.py +++ b/test/unit/test_edit_functions.py @@ -1,4 +1,29 @@ -import unittest +""" +File: test_edit_functions.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" from BaseCase import BaseCase from datetime import datetime, timedelta @@ -18,7 +43,7 @@ def test_store_edit_transaction(self): user_date = datetime.today() user_category = "Food" user_value = 10.00 - userid = "33" + userid = "1" self.user.add_transaction(user_date, user_category, user_value, userid) transaction = {"Date": user_date, "Value": user_value} self.user.store_edit_transaction(transaction, user_category) @@ -31,7 +56,7 @@ def test_edit_date(self): edit_date = datetime.today() - timedelta(days=1) user_category = "Groceries" user_value = 10.00 - userid = "33" + userid = "1" self.user.add_transaction(user_date, user_category, user_value, userid) transaction = {"Date": user_date, "Value": user_value} self.user.store_edit_transaction(transaction, user_category) @@ -44,7 +69,7 @@ def test_edit_transaction_category(self): user_category = "Utilities" edit_category = "Transport" user_value = 10.00 - userid = "33" + userid = "1" self.user.add_transaction(user_date, user_category, user_value, userid) transaction = {"Date": user_date, "Value": user_value} self.user.store_edit_transaction(transaction, user_category) @@ -57,7 +82,7 @@ def test_edit_transaction_value(self): user_category = "Shopping" user_value = 10.00 edit_value = 20.00 - userid = "33" + userid = "1" self.user.add_transaction(user_date, user_category, user_value, userid) transaction = {"Date": user_date, "Value": user_value} self.user.store_edit_transaction(transaction, user_category) diff --git a/test/unit/test_get_number_of_transactions.py b/test/unit/test_get_number_of_transactions.py index b17b6da19..8c65be4ba 100644 --- a/test/unit/test_get_number_of_transactions.py +++ b/test/unit/test_get_number_of_transactions.py @@ -1,3 +1,30 @@ +""" +File: test_get_number_of_transactions.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + from BaseCase import BaseCase from datetime import datetime diff --git a/test/unit/test_get_records_by_date.py b/test/unit/test_get_records_by_date.py index 3587c3232..2100b1ca3 100644 --- a/test/unit/test_get_records_by_date.py +++ b/test/unit/test_get_records_by_date.py @@ -1,5 +1,28 @@ """ -Test for the getRecordsByDate function +File: test_get_records_by_date.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. """ from datetime import datetime diff --git a/test/bot/test_history.py b/test/unit/test_history.py similarity index 55% rename from test/bot/test_history.py rename to test/unit/test_history.py index 6a52a13cc..11eb48dc7 100644 --- a/test/bot/test_history.py +++ b/test/unit/test_history.py @@ -1,5 +1,28 @@ """ -Tests history command +File: test_history.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. """ import time import unittest @@ -50,4 +73,4 @@ def test_history_command_records(self): if __name__ == '__main__': - unittest.main() + unittest.main() \ No newline at end of file diff --git a/test/unit/test_monthly_total.py b/test/unit/test_monthly_total.py index 9cd02fdc6..4104510bf 100644 --- a/test/unit/test_monthly_total.py +++ b/test/unit/test_monthly_total.py @@ -1,7 +1,29 @@ """ -Tests the monthly_total_method -""" +File: test_monthly_total.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" from BaseCase import BaseCase from datetime import datetime diff --git a/test/unit/test_save_user.py b/test/unit/test_save_user.py index 162fb16d4..9cbd5021f 100644 --- a/test/unit/test_save_user.py +++ b/test/unit/test_save_user.py @@ -1,5 +1,28 @@ """ -Test for the save_user method +File: test_save_user.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. """ import os import pathlib @@ -37,7 +60,7 @@ def test_save_no_history(self): # with no history, call save_user self.user.save_user(1) # assert the pickle exists - abspath = pathlib.Path("data/1.pickle").absolute() + abspath = pathlib.Path("teleData/1.pickle").absolute() assert os.path.exists(abspath) with open(abspath, "rb") as f: @@ -62,7 +85,7 @@ def test_valid_history(self): # with history, call save_user self.user.save_user(1) # assert the pickle exists - abspath = pathlib.Path("data/1.pickle").absolute() + abspath = pathlib.Path("teleData/1.pickle").absolute() assert os.path.exists(abspath) with open(abspath, "rb") as f: new_user = pickle.load(f) diff --git a/test/bot/test_start_and_menu.py b/test/unit/test_start_and_menu.py similarity index 54% rename from test/bot/test_start_and_menu.py rename to test/unit/test_start_and_menu.py index 5618fd48e..e45d74db7 100644 --- a/test/bot/test_start_and_menu.py +++ b/test/unit/test_start_and_menu.py @@ -1,5 +1,28 @@ """ -Tests start and menu commands +File: test_start_and_menu.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. """ import time import unittest @@ -46,4 +69,4 @@ def test_menu_command(self): if __name__ == '__main__': - unittest.main() + unittest.main() \ No newline at end of file diff --git a/test/unit/test_validate_date_format.py b/test/unit/test_validate_date_format.py index c91677f46..e698b3b3b 100644 --- a/test/unit/test_validate_date_format.py +++ b/test/unit/test_validate_date_format.py @@ -1,5 +1,28 @@ """ -Test for the deleteHistory function +File: test_validate_date_formate.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. """ from datetime import datetime diff --git a/test/unit/test_validate_entered_amount.py b/test/unit/test_validate_entered_amount.py index 7d6d9ef44..3d2ca4ff5 100644 --- a/test/unit/test_validate_entered_amount.py +++ b/test/unit/test_validate_entered_amount.py @@ -1,7 +1,28 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- """ -Tests the validate_entered_amount method +File: test_validate_entered_amount.py +Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru +Date: October 01, 2023 +Description: File contains Test cases. + +Copyright (c) 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 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. """ from BaseCase import BaseCase