diff --git a/.gitignore b/.gitignore
index 5573ac5..5f79988 100644
--- a/.gitignore
+++ b/.gitignore
@@ -140,7 +140,6 @@ __pycache__/
# Distribution / packaging
.Python
-build/
develop-eggs/
dist/
downloads/
@@ -439,3 +438,6 @@ x509.genkey
# Clang's compilation database file
/compile_commands.json
+code/codetime_server/docker-compose.yml
+code/codetime_server/Dockerfile
+code/codetime_server/run.sh
diff --git a/.travis.yml b/.travis.yml
index 040ac29..64f4efb 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,21 +1,30 @@
-language: python
-python:
- - "3.6" # current default Python on Travis CI
- - "3.7"
+matrix:
+ include:
+ - os: linux
+ language: python
+
services:
- xvfb
+ - mysql
+
+env:
+ - MYSQL_CODE_TIME_DB_NAME=codetime_db MYSQL_CODE_TIME_USER=travis MYSQL_CODE_TIME_PASSWORD='' MYSQL_CODE_TIME_HOST='127.0.0.1' MYSQL_CODE_TIME_CONNECTION_PORT=3306 CODE_TIME_SECRET_KEY=Sample
+
before_install:
- curl -OL https://raw.githubusercontent.com/SublimeText/UnitTesting/master/sbin/travis.sh
-# command to install dependencies
+
install:
- - pip install -r requirements.txt
- - python setup.py install
- pip install flake8==3.5
- - sh sbin/travis.sh bootstrap
- - sh sbin/travis.sh install_color_scheme_unit
- - if ["$PCINSTALL" == true]; then sh sbin/travis.sh install_package_control; fi
-# command to run tests
+ - pip install coverage
+ - sh travis.sh bootstrap
+ - pip install -r code/codetime_server/requirements.txt
+
+before_script:
+ - mysql -e 'create database codetime_db;'
script:
- - flake8 --statistics
- - sh sbin/travis.sh run_tests --coverage
- - python -m pytes
\ No newline at end of file
+ - flake8 --max-line-length=200
+ - sh travis.sh run_tests
+ - cd code/codetime_server/
+ - coverage run --source='.' --omit=*sgi*,*apps* manage.py test codetime
+ - coverage report
+
diff --git a/CITATION.md b/CITATION.md
index f8ccd36..4eb7849 100644
--- a/CITATION.md
+++ b/CITATION.md
@@ -1,19 +1,25 @@
# Cite as
-Nirav Shah,Omkar Kulkarni,Chintan Gandhi, Suraj Patel, Jay Modi
-SE_Fall20_Project-1:
-Group 23,
-August, 2020
+Adarsh Trivedi, oaaky, Jay Modi, Chintan Gandhi, NIRAV SHAH, Suraj Patel, Ayushi Rajendra Kumar, Prithvi Patel, Prithviraj Chaudhuri and Rashi0911. (2020, October 27). adarshtri/CodeTime: 1.3-pre-release (Version 1.3). Zenodo. http://doi.org/10.5281/zenodo.4136964
```bibtex
-@article{oaaky:SE_Fall20_Project-1,
- title = {sj23patel: Homework 1},
- DOI = {10.5281/zenodo.4041230},
- author = {Group 23},
- publisher = {Zenodo},
- year = {2020},
- month = {September}
- version = {v1.1},
- url = {https://doi.org/10.5281/zenodo.4041230}
+@software{adarsh_trivedi_2020_4136964,
+ author = {Adarsh Trivedi and
+ oaaky and
+ Jay Modi and
+ Chintan Gandhi and
+ NIRAV SHAH and
+ Suraj Patel and
+ Ayushi Rajendra Kumar and
+ Prithvi Patel and
+ Prithviraj Chaudhuri and
+ Rashi0911},
+ title = {adarshtri/CodeTime: 1.3-pre-release},
+ month = oct,
+ year = 2020,
+ publisher = {Zenodo},
+ version = {1.3},
+ doi = {10.5281/zenodo.4136964},
+ url = {https://doi.org/10.5281/zenodo.4136964}
}
```
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ae22cbc..baa8d1d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,14 +1,11 @@
-# Contributing to SE-PROJECT-1
+# Contributing to CodeTime
:+1::tada: First off, thanks for taking the time to contribute! :tada::+1:
-The following is a set of guidelines for contributing to CSC 510 Project 1 and its packages, which are hosted on the [GitHub project page](https://github.com/oaaky/SE_Fall20_Project-1.git). These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
+Thank you so much for taking an interest in contributing! We are lookng forward to contributions that will enable lesser human intervention!! There are many ways to contribute to this porject!
#### Table Of Contents
-
-[Code of Conduct](#code-of-conduct)
-
-[What should I know before I get started?](#what-should-i-know-before-i-get-started)
+[Code of Conduct](CODE_OF_CONDUCT.md)
[How Can I Contribute?](#how-can-i-contribute)
* [Reporting Bugs](#reporting-bugs)
@@ -16,88 +13,104 @@ The following is a set of guidelines for contributing to CSC 510 Project 1 and i
* [Your First Code Contribution](#your-first-code-contribution)
* [Pull Requests](#pull-requests)
-[Styleguides](#styleguides)
- * [Coding Style](#coding-style)
- * [Git Commit Messages](#git-commit-messages)
+[Styleguide](#styleguide)
+
+[Attribution](#attribution)
-## Code of Conduct
+## How Can I Contribute?
-This project and everyone participating in it is governed by the [Code of Conduct](CODE-OF-CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [cagandhi97@gmail.com](mailto:cagandhi97@gmail.com).
+Each contribution counts for our project. So make sure to classify which is yours.
-## What should I know before I get started?
+### Obvious Fixes
-This is a sample project that sets up some base project files for future work. Please look through the [README](README.md) file.
+The Obvious Fixes comprise of:
-* For feature requests, please fork the repository, create a new branch with your feature changes and make a pull request. The maintainers will review the code change and merge it into the master branch once checks have successfully passed.
-* Provide documentation for the modifications in the code. It will be easier and quicker for the maintainers to review and approve your code if you have added meaningful comments to your code change.
+* Spelling / grammar fixes and Typo correction
+* Formatting changes
+* Comment and code clean up
+* Bug fixes that change default return values or error codes stored in constants
+* Adding logging messages or debugging output
+* Updating documentation
-## How Can I Contribute?
+One can go ahead and follow the [3-step process](#required-3-steps-for-contributing)
### Reporting Bugs
-This section guides you through submitting a bug report. Following these guidelines helps maintainers and the community understand your report :pencil:, reproduce the behavior :computer: :computer:, and find related reports :mag_right:.
+This section guides you through submitting a bug report for this repository. Following these guidelines helps maintainers and the community understand your report :pencil:, reproduce the behavior :computer: :computer:, and find related reports :mag_right:.
-Before creating bug reports, please check [this list](#before-submitting-a-bug-report) as you might find out that you don't need to create one. When you are creating a bug report, please include detailed information about the environment, package version numbers, OS and other information maintainers may find useful in reproducing and resolving issues quickly.
+Before creating bug reports, please perform a cursory search to see if the problem has already been reported. If it has **and the issue is still open**, add a comment to the existing issue instead of opening a new one.
> **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one.
-#### Before Submitting A Bug Report
-
-Check that the bug does not exists because of any issue in your local environment. You might be able to find the cause of the problem and fix things yourself. If the problem has been reported **and the issue is still open**, add a comment to the existing issue instead of opening a new one.
-
#### How Do I Submit A (Good) Bug Report?
-Explain the problem and include details to help maintainers reproduce the problem:
+Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/). After you've determined [which repository](#atom-and-packages) your bug is related to, create an issue on that repository and provide the following information by filling in [the template](https://github.com/atom/.github/blob/master/.github/ISSUE_TEMPLATE/bug_report.md).
+
+Explain the problem and include additional details to help a developer reproduce the problem:
* **Use a clear and descriptive title** for the issue to identify the problem.
-* **Describe the exact steps which reproduce the problem** in as many details as possible. When listing steps, **don't just say what you did, but explain how you did it**.
+* **Describe the exact steps which reproduce the problem** in as many details as possible. Alongwith it, provide the details regarding the name and version of OS, Python version, configuration of the environment, if used any.
+* **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](https://help.github.com/articles/markdown-basics/#multiple-lines).
* **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior.
* **Explain which behavior you expected to see instead and why.**
-* **Include screenshots** that show the described steps and clearly demonstrate the problem.
+* **If the problem is related to performance or memory**, include details of the errors encountered with your report.
+* **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens.
+
+
+### Suggesting Enhancements and new features
+
+This section guides you through submitting an enhancement suggestion for this project:
-Include details about your configuration and environment:
-* **What's the name and version of the OS you're using**?
-* **Which packages do you have installed?** Check that issue is not present because of a local package.
+#### How Do I Submit A (Good) Enhancement Suggestion?
+
+Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/). Create an issue on that repository with an enhancement or feature tag and provide the following information:
+
+* **Use a clear and descriptive title** for the issue to identify the suggestion.
+* **Provide a step-by-step description of the suggested enhancement** in as many details as possible.
+* **Provide specific examples to demonstrate the steps**. Include copy/pasteable snippets which you use in those examples, as [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines).
+* **Describe the current behavior** and **explain which behavior you expected to see instead** and why.
+
+### Your First Code Contribution
+
+#### Required 3 Steps for contributing:
+* Commit changes to a new git branch.
+* Create a Pull-Request for the changes. Make sure to follow the [Pull Request Template](#pull-requests).
+* Request a Code-Review from the project maintainers.
+
### Pull Requests
The process described here has several goals:
-- Maintain the project's quality
+- Maintain code quality by following some basic [PEP 8 standards](https://www.python.org/dev/peps/pep-0008/)
- Fix problems that are important to users
-- Engage the community in working toward the best possible Atom
-- Enable a sustainable system for project maintainers to review contributions
+- Enable a sustainable system for this project's maintainers to review contributions
Please follow these steps to have your contribution considered by the maintainers:
1. Follow the [styleguides](#styleguides)
-2. After you submit your pull request, verify that the build is passing and the tests are successful.
+2. After you submit your pull request, verify that all [status checks](https://help.github.com/articles/about-status-checks/) are passing What if the status checks are failing?
If a status check is failing, and you believe that the failure is unrelated to your change, please leave a comment on the pull request explaining why you believe the failure is unrelated. A maintainer will re-run the status check for you. If we conclude that the failure was a false positive, then we will open an issue to track that problem with our status check suite.
+
+| Name | Stmts | Miss | Cover |
+| -------------------------------------------------|---------|------|------------|
+| codetime\__init__.py | 0 | 0 | 100% |
+| codetime\admin.py | 0 | 0 | 100% |
+| codetime\apps.py | 3 | 3 | 0% |
+| codetime\migrations\0001_initial.py | 6 | 0 | 100% |
+| codetime\migrations\0002_auto_20201016_1851.py | 4 | 0 | 100% |
+| codetime\migrations\0003_auto_20201017_1840.py | 4 | 0 | 100% |
+| codetime\migrations\0004_auto_20201026_0331.py | 4 | 0 | 100% |
+| codetime\migrations\__init__.py | 0 | 0 | 100% |
+| codetime\models.py | 122 | 26 | 79% |
+| codetime\request_handlers.py | 96 | 28 | 71% |
+| codetime\serializers.py | 20 | 4 | 80% |
+| codetime\tests\__init__.py | 0 | 0 | 100% |
+| codetime\tests\test_views.py | 96 | 0 | 100% |
+| codetime\tests\tests_url.py | 13 | 0 | 100% |
+| codetime\urls.py | 3 | 0 | 100% |
+| codetime\views.py | 21 | 4 | 81% |
+| codetime_server\__init__.py | 0 | 0 | 100% |
+| codetime_server\asgi.py | 4 | 4 | 0% |
+| codetime_server\settings.py | 26 | 0 | 100% |
+| codetime_server\urls.py | 4 | 0 | 100% |
+| codetime_server\wsgi.py | 4 | 4 | 0% |
+| manage.py | 12 | 2 | 83% |
+| tests.py | 0 | 0 | 100% |
+| TOTAL | 442 | 75 | 83% |
+
diff --git a/PROJ1-selfAssessment.md b/PROJ1-selfAssessment.md
deleted file mode 100644
index a9cdf71..0000000
--- a/PROJ1-selfAssessment.md
+++ /dev/null
@@ -1,52 +0,0 @@
-
-|What | Notes|score 0..4
(0=no, 2=ok, 4=wow!)|
-|-----|------|------|
-|Misc | Group members attended tutorial sessions|4|
-|Distrbuted dev model: | decisions made by unanmyous vote|4|
-|| group meetings had a round robin speaking order|4|
-|| group meetings had a moderator that managed the round robin|4|
-|| group meeting moderator rotated among the group|4|
-|| code conforms to some packaging standard|4|
-|| code has can be downloaded from some standard package manager|0|
-| |workload is spread over the whole team (one team member is often Xtimes more productive than the others... but nevertheless, here is a track record that everyone is contributing a lot)|4|
-|| Number of commits|4|
-|| Number of commits: by different people|4|
-|| Issues reports: there are many|4|
-|| issues are being closed|4|
-|| License: exists|4|
-|| DOI badge: exists |4|
-||Docs: doco generated , format not ugly |4|
-||Docs: what: point descriptions of each class/function (in isolation) |2|
-||Docs: how: for common use cases X,Y,Z mini-tutorials showing worked examples on how to do X,Y,Z|4|
-||Docs: why: docs tell a story, motivate the whole thing, deliver a punchline that makes you want to rush out and use the thing|4|
-||Docs: 3 minute video, posted to YouTube. That convinces people why they want to work on your code.|4|
-|| (hard) code conforms to some known patterns |2|
-|Tools Matter| Use of version control tools|4|
-|| Extensive use of version control tools |4|
-|| Repo has an up-to-date requirements.txt file|4|
-|| Repo does not have "ignore" files.|4|
-||Use of style checkers |4|
-||Extensive Use of style checkers |4|
-|| Use of code formatters. |2|
-|| Extensive Use of code formatters. |0|
-|| Use of syntax checkers. |4|
-|| Extensive use of syntax checkers. |4|
-|| Use of code coverage |0|
-|| Extensive use of code coverage |0|
-|| other automated analysis tools|2|
-|| Extensive use of other automated analysis tools|0|
-|| test cases exist|4|
-|| test cases are routinely executed|2|
-| consensus-oriented model| the files CONTRIBUTING.md and CODEOFCONDUCT.md has have multiple edits by multiple people|4|
-| | the files CONTRIBUTING.md lists coding standards and lots of tips on how to extend the system without screwing things up|4|
-| | multiple people contribute to discussions|4|
-|| issues are discussed before they are closed|4|
-|| Chat channel: exists|4|
-|| Chat channel: is active |4|
-|| test cases:.a large proportion of the issues related to handling failing cases.|2|
-| zero internal boundaries | evidence that the whole team is using the same tools: everyone can get to all tools and files|4|
-| | evidence that the whole team is using the same tools (e.g. config files in the repo, updated by lots of different people)||
-| | 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)|4|
-| | evidence that the members of the team are working across multiple places in the code base|4|
-| low-regressions rule | (hard to judge) features released are not subsequently removed|4|
-|short release cycles | (hard to see in short projects) project members are committing often enough so that everyone can get your work|4|
diff --git a/PROJ2-selfAssessment.md b/PROJ2-selfAssessment.md
new file mode 100644
index 0000000..03e5f5f
--- /dev/null
+++ b/PROJ2-selfAssessment.md
@@ -0,0 +1,61 @@
+# Group 18 Self Assessment
+
+## Total score
+
+- write down "4" into every right-hand-side cell
+- Look for evidence that any "4" should be something else
+- sum the right-hand-column, divide by number of rows
+
+
+
+| What | Notes | score 0..4
(0=no, 2=ok, 4=wow!) |
+| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
+| Misc | Group members attended tutorial sessions | 4 |
+| Distrbuted dev model: | decisions made by unanmyous vote | 4 |
+| | group meetings had a round robin speaking order | 4 |
+| | group meetings had a moderator that managed the round robin | 4 |
+| | group meeting moderator rotated among the group | 4 |
+| | code conforms to some packaging standard | 4 |
+| | code has can be downloaded from some standard package manager | 4 |
+| | workload is spread over the whole team (one team member is often Xtimes more productive than the others... but nevertheless, here is a track record that everyone is contributing a lot) | 4 |
+| | Number of commits | 4 |
+| | Number of commits: by different people | 4 |
+| | Issues reports: there are many | 4 |
+| | issues are being closed | 4 |
+| | License: exists | 4 |
+| | DOI badge: exists | 4 |
+| | Docs: doco generated , format not ugly | 4 |
+| | Docs: what: point descriptions of each class/function (in isolation) | 4 |
+| | Docs: how: for common use cases X,Y,Z mini-tutorials showing worked examples on how to do X,Y,Z | 4 |
+| | Docs: why: docs tell a story, motivate the whole thing, deliver a punchline that makes you want to rush out and use the thing | 4 |
+| | Docs: 3 minute video, posted to YouTube. That convinces people why they want to work on your code. | 4 |
+| | (hard) code conforms to some known patterns | 4 |
+| Tools Matter | Use of version control tools | 4 |
+| | Extensive use of version control tools | 4 |
+| | Repo has an up-to-date requirements.txt file | 4 |
+| | Repo does not have "ignore" files. | 4 |
+| | Use of style checkers | 4 |
+| | Extensive Use of style checkers | 4 |
+| | Use of code formatters. | 4 |
+| | Extensive Use of code formatters. | 4 |
+| | Use of syntax checkers. | 4 |
+| | Extensive use of syntax checkers. | 4 |
+| | Use of code coverage | 4 |
+| | Extensive use of code coverage | 2 |
+| | other automated analysis tools | 4 |
+| | Extensive use of other automated analysis tools | 4 |
+| | test cases exist | 4 |
+| | test cases are routinely executed | 4 |
+| consensus-oriented model | the files CONTRIBUTING.md and CODEOFCONDUCT.md has have multiple edits by multiple people | 4 |
+| | the files CONTRIBUTING.md lists coding standards and lots of tips on how to extend the system without screwing things up | 4 |
+| | multiple people contribute to discussions | 4 |
+| | issues are discussed before they are closed | 4 |
+| | Chat channel: exists | 4 |
+| | Chat channel: is active | 4 |
+| | test cases:.a large proportion of the issues related to handling failing cases. | 4 |
+| zero internal boundaries | evidence that the whole team is using the same tools: everyone can get to all tools and files | 4 |
+| | evidence that the whole team is using the same tools (e.g. config files in the repo, updated by lots of different people) | 4 |
+| | 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) | 4 |
+| | evidence that the members of the team are working across multiple places in the code base | 4 |
+| low-regressions rule | (hard to judge) features released are not subsequently removed | 4 |
+| short release cycles | (hard to see in short projects) project members are committing often enough so that everyone can get your work | 4 |
diff --git a/Project_Requirements.md b/Project_Requirements.md
deleted file mode 100644
index 7840327..0000000
--- a/Project_Requirements.md
+++ /dev/null
@@ -1,13 +0,0 @@
-## Project Requirements
-
-1. The plugin should record the following data:
-* Time spent on a specific file
-* Time spent on a specific programming language.
-* Time spent on specific project.
-
-2. The plugin should produce the following results:
-* Rank the time spent on different programming languages
-* Display the time spent on coding per hour on a daily basis.
-* Display the time spent on different files. Rank the files based on time spent and show the amount of time spent in percentage.
-* Display the time spent on a project in a number of days/hr. Rank the project based on time spent on them.
-* Show the leaderboard of multiple developers at one single place where performance of all developers can be tracked.
diff --git a/README.md b/README.md
index ee77d73..bbcbc31 100644
--- a/README.md
+++ b/README.md
@@ -1,90 +1,49 @@
-# CodeTime - A time tracking plugin for text editors [G23 Project 1]
-[](https://zenodo.org/badge/latestdoi/295515546)
-[](https://travis-ci.org/oaaky/SE_Fall20_Project-1)
+# CodeTime - A time tracking plugin for text editors
+
+[](https://zenodo.org/badge/latestdoi/299711213)
+[](https://travis-ci.org/github/adarshtri/CodeTime)
[](https://github.com/oaaky/SE_Fall20_Project-1/blob/master/LICENSE)

-
-
-
+
+
+## Test plan for Phase 3
+- [Phase 3 Test Plan](test-plan.md)
## About CodeTime
- CodeTime is a plugin for Sublime Text editor which will help developers to track the amount of time spent on multiple files, programming languages and projects. The user will be able to perform the analysis of the time spent to improve the productivity by analysing the most frequently used programming language, most productive time of the day, files in project which took maximum time for development and projects which took maximum time for completion.
-- The developer can add the project deadlines to the plugin and the plugin can help developers stick to their goal by predicting the finish time of the project based on the data gathered from the user. Multiple developers can compete with each other through a leaderboard where a leaderboard will display the ranking of most productive developer and admin/manager can easily monitor the productivity of each and every developer with the help of a common interface.
-
-[Project Requirement](https://github.com/oaaky/SE_Fall20_Project-1/blob/master/project_requiremnt.md) | [Architecture Diagram](https://github.com/oaaky/SE_Fall20_Project-1/blob/master/architecure.png) | [UI MockUps](https://github.com/oaaky/SE_Fall20_Project-1/blob/master/Capture.PNG) | [Current Working Dashboard](https://github.com/oaaky/SE_Fall20_Project-1/blob/master/CodeTimeDashboard.png)
-
-[](http://tiny.cc/codeTimePromo)
-
-
-## Installation
-
-1. Open Sublime Text.
-2. Go to Preferences -> Browse packages.
-3. A new window containing Sublime packages directory will open up. Let's call this folder `SublimePackagesFolder`.
-4. Open your terminal and navigate to `SublimePackagesFolder`.
-5. Clone this repository inside `SublimePackagesFolder` (This makes sure that Sublime recognizes our plugin package to execute).
-6. Copy the [Context.sublime-menu](code/SublimePlugin/Config/Context.sublime-menu) file to your User Packages directory. To go to User Packages directory, navigate to `SublimePackagesFolder/User` folder.
-7. You are all set. The plugin is now active and is running in the background.
+- The online dashboard can be used to track the developers' productivity and view charts displaying how much time the user spent on each kind of file they worked on.
+[Project Requirement](docs/Project_Requirements.md) | [Architecture](docs/architecure.png) | [Web Dashboard TBD](docs/CodeTimeDashboard.png) | [Installation and Developers Guide](docs/guide.md) | [Phase 3 Test Plan](test-plan.md)
-## Usage
+## Advertisement Video
-1. Open Sublime Text.
-2. Open a file that you wish to work on.
-3. In the file pane, right click and select the option `View CodeTime Dashboard`.
+[](https://youtu.be/lnOyBFZFu7g)
+## Working Video
-## Setup (For contributors)
+[](https://youtu.be/E7EuaExx8Ww)
-> Note: Please install and use Sublime Text 3 only for development.
+## How to Contribute
-1. Perform the steps in the [Installation](https://github.com/oaaky/SE_Fall20_Project-1#installation-for-non-contributors) section described above.
-2. Install Package Control by pressing `ctrl+shift+p (Win/Linux)` or `cmd+shift+p (Mac)`.
-3. Run `python setup.py install` to install all the dependencies.
-4. Back in Sublime Text, Open Package Control by pressing `ctrl+shift+p (Win/Linux)` or `cmd+shift+p (Mac)`. Navigate to option `Package Control: Install Package`. Install packages: `SublimeLinter`, `SublimeLinter-flake8`, `sublack`, `UnitTesting`.
-5. Navigate to `Package Settings` option under `Preferences` in Menu bar. For `Mac` users, the `Preferences` option will be found under `Sublime Text` in Menu bar.
-6. Once under Package Settings, move to `SublimeLinter > Settings`. You will see that a file with the name `SublimeLinter.sublime-settings - User` opens up. Copy the following code snippet to ignore a linting error related to Tabs vs Spaces war :)
-```
-// SublimeLinter Settings - User
-{
- "linters": {
- "flake8": {
- "args": ["--ignore=W191"],
- }
- }
-}
-
-```
-
-## How to Run Tests? (For contributors)
-
-1. For local execution of the tests, make sure that the Sublime package `UnitTesting` is installed.
-2. Navigate to a test file in `tests` folder that you want to run your tests for.
-3. Open Package Control and type in `UnitTesting: Test Current Package`.
-4. The tests will run and a small output panel pops up showing that the tests are running.
-
-For more information and guide on how to run tests, take a look at this [README.md by randy3k](https://github.com/randy3k/UnitTesting/blob/master/README.md). For examples on how to write tests for sublime plugin, take a look at this [Repo by randy3k](https://github.com/randy3k/UnitTesting-example).
+Please take a look at our CONTRIBUTING.md where we provide instructions on contributing to the repo and taking the plugin development further.
-## How to Contribute?
+[Installation and Developers Guide](docs/guide.md)
-Please take a look at our CONTRIBUTING.md where we provide instructions on contributing to the repo and taking the plugin development further.
+## What things have been done for Phase 1 (By previous project team)
-## What things have been done for Phase 1 ?
- Created the design and architecture of the project
- Implemented the logic to collect the data in background
-- Implemented the code to generate the graphs to analyse the time spent. ([Current Working Dashboard](https://github.com/oaaky/SE_Fall20_Project-1/blob/master/CodeTimeDashboard.png))
+- Implemented the code to generate the graphs to analyse the time spent. ([Current Working Dashboard](docs/CodeTimeDashboard.png))
- Integrated the code with sublime text editor
- Unit tests
- Build and Packaging of the plugin
-## What things are planned for Phase 2?
+## What things have been done for Phase 2
+
+- The data generated is being sent to Django based server.
+- Local data analysis will be shifted to user dashboard on server.
+- Realtime analysis of the user's file.
-- The data generated will be sent to server.
-- Local data analysis will be shifted to server based dashboard.
-- Slack notification of weekly report to user will be sent through server.
-- Leaderboard based on user's usage of files.
-- Adding support for other editors such as visual code.
-- Possible realtime analysis of the user's file.
diff --git a/code/SublimePlugin/Config/CustomPreferences.sublime-settings b/code/SublimePlugin/Config/CustomPreferences.sublime-settings
new file mode 100644
index 0000000..28a17d1
--- /dev/null
+++ b/code/SublimePlugin/Config/CustomPreferences.sublime-settings
@@ -0,0 +1,6 @@
+{
+ "timeout": 30,
+ "api_token": "74815790-d740-4344-b9c3-a505514edf88VHSda13oJOr5Iba4",
+ "python-env": "/Users/prithvirajchaudhuri/Desktop/CSC510/Project/venv/bin/python3",
+ "request-url": "http://localhost:8000/codetime/timelog/"
+}
diff --git a/code/SublimePlugin/codeTime.py b/code/SublimePlugin/codeTime.py
index 191a114..edec4c0 100644
--- a/code/SublimePlugin/codeTime.py
+++ b/code/SublimePlugin/codeTime.py
@@ -1,4 +1,5 @@
import sublime_plugin
+import sublime
import time
import platform
import os
@@ -9,131 +10,192 @@
from .periodicLogSaver import PeriodicLogSaver
# create data folder based on OS
-if platform.system() == 'Windows':
- DATA_FOLDER_PATH = os.path.join(os.getenv('APPDATA'), '.codeTime')
+if platform.system() == "Windows":
+ DATA_FOLDER_PATH = os.path.join(os.getenv("APPDATA"), ".codeTime")
else:
- DATA_FOLDER_PATH = os.path.join(os.path.expanduser('~'), '.codeTime')
+ DATA_FOLDER_PATH = os.path.join(os.path.expanduser("~"), ".codeTime")
if not os.path.exists(DATA_FOLDER_PATH):
- os.makedirs(DATA_FOLDER_PATH)
+ os.makedirs(DATA_FOLDER_PATH)
# define log file path
-LOG_FILE_PATH = os.path.join(DATA_FOLDER_PATH, '.sublime_logs')
+LOG_FILE_PATH = os.path.join(DATA_FOLDER_PATH, ".sublime_logs")
# define local variables
file_times_dict = {}
periodic_log_save_timeout = 300 # seconds
periodic_log_save_on = True
+api_token = ""
+configs = None
def when_activated(view):
- try:
- window = view.window()
- if window is not None:
- file_name = view.file_name()
+ try:
+ window = view.window()
+ if window is not None:
+ file_name = view.file_name()
- if file_name is not None:
- start_time = time.time()
- end_time = None
+ if file_name is not None:
+ start_time = time.time()
+ end_time = None
- curr_date = dt.now().strftime('%Y-%m-%d')
+ curr_date = dt.now().strftime("%Y-%m-%d")
- if curr_date not in file_times_dict:
- file_times_dict[curr_date] = {}
+ if curr_date not in file_times_dict:
+ file_times_dict[curr_date] = {}
- if file_name not in file_times_dict[curr_date]:
- file_times_dict[curr_date][file_name] = [[start_time, end_time]] # noqa: E501
- else:
- file_times_dict[curr_date][file_name].append([start_time, end_time]) # noqa: E501
+ if file_name not in file_times_dict[curr_date]:
- print('File_name: ', file_name)
- print('\n ----- \n')
- except Exception as e:
- exc_type, exc_obj, exc_tb = sys.exc_info()
- print("codeTime:when_activated(): {error} on line number: {lno}".format(error=str(e), lno=str(exc_tb.tb_lineno))) # noqa: E501
+ file_times_dict[curr_date][file_name] = [
+ [start_time, end_time]
+ ] # noqa: E501
+ else:
+ file_times_dict[curr_date][file_name].append(
+ [start_time, end_time]
+ ) # noqa: E501
+
+ print("File_name: ", file_name)
+ print("\n ----- \n")
+ except Exception as e:
+ exc_type, exc_obj, exc_tb = sys.exc_info()
+ print(
+ "codeTime:when_activated(): {error} on line number: {lno}".format(
+ error=str(e), lno=str(exc_tb.tb_lineno)
+ )
+ ) # noqa: E501
def when_deactivated(view):
- try:
- window = view.window()
- if window is not None:
- file_name = view.file_name()
+ try:
+ window = view.window()
+ if window is not None:
+ file_name = view.file_name()
- if file_name is not None:
- end_time = time.time()
+ if file_name is not None:
+ end_time = time.time()
- curr_date = dt.now().strftime('%Y-%m-%d')
+ curr_date = dt.now().strftime("%Y-%m-%d")
- file_times_dict[curr_date][file_name][-1][1] = end_time
+ file_times_dict[curr_date][file_name][-1][1] = end_time
- print('File_name: ', file_name)
- print('\n ----- \n')
- except Exception as e:
- exc_type, exc_obj, exc_tb = sys.exc_info()
- print("codeTime:when_deactivated(): {error} on line number: {lno}".format(error=str(e), lno=str(exc_tb.tb_lineno))) # noqa: E501
+ print("File_name: ", file_name)
+ print("\n ----- \n")
+ except Exception as e:
+ exc_type, exc_obj, exc_tb = sys.exc_info()
+ print(
+ "codeTime:when_deactivated(): {error} on line number: {lno}".format(
+ error=str(e), lno=str(exc_tb.tb_lineno)
+ )
+ ) # noqa: E501
class CustomEventListener(sublime_plugin.EventListener):
- def on_post_save(self, view):
- print(view.file_name(), 'just got saved')
-
- def on_activated(self, view):
- try:
- print(view.file_name(), 'is now the active view')
- when_activated(view)
- except Exception as e:
- exc_type, exc_obj, exc_tb = sys.exc_info()
- print("codeTime:CustomEventListener():on_activated() {error} on line number: {lno}".format(error=str(e), lno=str(exc_tb.tb_lineno))) # noqa: E501
-
- def on_deactivated(self, view):
- try:
- print(view.file_name(), 'is deactivated view')
- when_deactivated(view)
- except Exception as e:
- exc_type, exc_obj, exc_tb = sys.exc_info()
- print("codeTime:CustomEventListener():on_deactivated() {error} on line number: {lno}".format(error=str(e), lno=str(exc_tb.tb_lineno))) # noqa: E501
-
- def on_close(self, view):
- try:
- print(view.file_name(), 'is no more')
-
- file_name = view.file_name()
- curr_date = dt.now().strftime('%Y-%m-%d')
- if file_name is not None and file_name in file_times_dict[curr_date]:
- end_time = time.time()
-
- last_time_list = file_times_dict[curr_date][file_name][-1]
-
- if last_time_list[1] is None:
- last_time_list[1] = end_time
-
- with open(LOG_FILE_PATH, 'a') as f:
- for _time in file_times_dict[curr_date][file_name]:
- f.write(curr_date + ',' + file_name + ',' + str(_time[0]) + ',' + str(_time[1]) + '\n') # noqa: E501
- file_times_dict[curr_date].pop(file_name, None)
- except Exception as e:
- exc_type, exc_obj, exc_tb = sys.exc_info()
- print("codeTime:CustomEventListener():on_close() {error} on line number: {lno}".format(error=str(e), lno=str(exc_tb.tb_lineno))) # noqa: E501
+ def on_post_save(self, view):
+ print(view.file_name(), "just got saved")
+
+ def on_activated(self, view):
+ try:
+ print(view.file_name(), "is now the active view")
+ when_activated(view)
+ except Exception as e:
+ exc_type, exc_obj, exc_tb = sys.exc_info()
+ print(
+ "codeTime:CustomEventListener():on_activated() {error} on line number: {lno}".format(
+ error=str(e), lno=str(exc_tb.tb_lineno)
+ )
+ ) # noqa: E501
+
+ def on_deactivated(self, view):
+ try:
+ print(view.file_name(), "is deactivated view")
+ when_deactivated(view)
+ except Exception as e:
+ exc_type, exc_obj, exc_tb = sys.exc_info()
+ print(
+ "codeTime:CustomEventListener():on_deactivated() {error} on line number: {lno}".format(
+ error=str(e), lno=str(exc_tb.tb_lineno)
+ )
+ ) # noqa: E501
+
+ def on_close(self, view):
+ try:
+ print(view.file_name(), "is no more")
+
+ file_name = view.file_name()
+ curr_date = dt.now().strftime("%Y-%m-%d")
+ if (
+ file_name is not None
+ and file_name in file_times_dict[curr_date]
+ ):
+
+ end_time = time.time()
+
+ last_time_list = file_times_dict[curr_date][file_name][-1]
+
+ if last_time_list[1] is None:
+ last_time_list[1] = end_time
+
+ with open(LOG_FILE_PATH, "a") as f:
+ for _time in file_times_dict[curr_date][file_name]:
+ f.write(
+ curr_date
+ + ","
+ + file_name
+ + ","
+ + str(_time[0])
+ + ","
+ + str(_time[1])
+ + "\n"
+ ) # noqa: E501
+ file_times_dict[curr_date].pop(file_name, None)
+ except Exception as e:
+ exc_type, exc_obj, exc_tb = sys.exc_info()
+ print(
+ "codeTime:CustomEventListener():on_close() {error} on line number: {lno}".format(
+ error=str(e), lno=str(exc_tb.tb_lineno)
+ )
+ ) # noqa: E501
# view.run_command('dashboard')
class DashboardCommand(sublime_plugin.TextCommand):
- def run(self, edit):
- try:
- print("Showing Graphs")
- dir_path = os.path.dirname(os.path.realpath(__file__))
- process = subprocess.Popen("python3 '" + dir_path + "/output.py'", shell=True, stdout=subprocess.PIPE) # noqa: E501, F841
- # if the above line doesn't work, replace 'python3' with actual executable path of your python3 (for contributors)
- except Exception as e:
- exc_type, exc_obj, exc_tb = sys.exc_info()
- print("codeTime:DashboardCommand():run() {error} on line number: {lno}".format(error=str(e), lno=str(exc_tb.tb_lineno))) # noqa: E501
+ def run(self, edit):
+ # Load config file
+ configs = sublime.load_settings("CustomPreferences.sublime-settings")
+ try:
+ print("Showing Graphs")
+ dir_path = os.path.dirname(os.path.realpath(__file__))
+ subprocess.Popen(
+ configs.get("python-env") + "'" + dir_path + "/output.py'",
+ shell=True,
+ stdout=subprocess.PIPE,
+ ) # noqa: E501, F841
+ except Exception as e:
+ exc_type, exc_obj, exc_tb = sys.exc_info()
+ print("codeTime:DashboardCommand():run() {error} on line number: {lno}".format(error=str(e), lno=str(exc_tb.tb_lineno)))
def plugin_loaded():
- try:
- if periodic_log_save_on:
- periodcLogSaver = PeriodicLogSaver(kwargs={'inMemoryLog': file_times_dict, 'timeout': periodic_log_save_timeout, 'LOG_FILE_PATH': LOG_FILE_PATH}) # noqa: E501
- periodcLogSaver.start()
- except Exception as e:
- exc_type, exc_obj, exc_tb = sys.exc_info()
- print("codeTime:plugin_loaded() {error} on line number: {lno}".format(error=str(e), lno=str(exc_tb.tb_lineno))) # noqa: E501
+ try:
+ configs = sublime.load_settings("CustomPreferences.sublime-settings")
+ periodic_log_save_timeout = configs.get("timeout")
+ api_token = configs.get("api_token")
+ request_url = configs.get("request-url")
+ if periodic_log_save_on:
+ periodcLogSaver = PeriodicLogSaver(
+ kwargs={
+ "inMemoryLog": file_times_dict,
+ "timeout": periodic_log_save_timeout,
+ "LOG_FILE_PATH": LOG_FILE_PATH,
+ "API_TOKEN": api_token,
+ "REQUEST_URL": request_url,
+ }
+ ) # noqa: E501
+ periodcLogSaver.start()
+ except Exception as e:
+ exc_type, exc_obj, exc_tb = sys.exc_info()
+ print(
+ "codeTime:plugin_loaded() {error} on line number: {lno}".format(
+ error=str(e), lno=str(exc_tb.tb_lineno)
+ )
+ ) # noqa: E501
diff --git a/code/SublimePlugin/output.py b/code/SublimePlugin/output.py
index 4accfd3..9415c7f 100644
--- a/code/SublimePlugin/output.py
+++ b/code/SublimePlugin/output.py
@@ -16,89 +16,118 @@
def show_graphs():
- if platform.system() == 'Windows':
- DATA_FOLDER_PATH = os.path.join(os.getenv('APPDATA'), '.codeTime')
- else:
- DATA_FOLDER_PATH = os.path.join(os.path.expanduser('~'), '.codeTime')
- LOG_FILE_PATH = os.path.join(DATA_FOLDER_PATH, '.sublime_logs')
-
- sample_data = open(LOG_FILE_PATH, "r").read()
- logs = sample_data.split("\n")
-
- logs_dict = {}
- for log in logs[:-1]:
- try:
- log_date, log_file_path, start_time, end_time = log.split(",")
-
- end_time, start_time = float(end_time), float(start_time)
- file_type = mimetypes.guess_type(log_file_path)
-
- if file_type[0] is not None:
- file_type = file_type[0].split("/")[-1].split("-")[-1]
- else:
- file_type = "other"
-
- logs_dict[log_file_path] = {"st": start_time, "dt": datetime.strptime(log_date, "%Y-%m-%d"), "et": end_time, "duration": end_time - start_time, "type": file_type} # noqa: E501
- except Exception:
- continue
-
- fig = tools.make_subplots(rows=3, cols=2, shared_xaxes=True, specs=[[{"colspan": 2}, None], # noqa: E501
- [{"type": "pie"}, {"type": "pie"}], # noqa: E128
- [{"colspan": 2, "type": "table"}, None]])
-
- # ###############################- Pie Chart-
- durations = defaultdict(int)
- for file_path in logs_dict.keys():
- duration = logs_dict[file_path]["duration"]
- file_type = logs_dict[file_path]["type"]
- durations[file_type] += duration
-
- trace_timespan = go.Pie(labels=list(durations.keys()), values=list(durations.values())) # noqa: E501
- fig.append_trace(trace_timespan, 2, 1)
- fig.append_trace(trace_timespan, 2, 2)
- # ##############################
-
- # ##############################-Time fill graph per filetype
-
- durations = defaultdict(dict)
- for file_path in logs_dict.keys():
- duration = logs_dict[file_path]["duration"]
- file_type = logs_dict[file_path]["type"]
- date = logs_dict[file_path]["dt"]
-
- if date in durations[file_type]:
- durations[file_type][date] += duration
- else:
- durations[file_type][date] = duration
-
- data = []
- for file_type in durations.keys():
- trace_timespan = go.Scatter(x=list(durations[file_type].keys()), y=list(durations[file_type].values()), name=file_type, fill='tozeroy') # noqa: E501
- # data.append(trace_timespan)
- fig.append_trace(trace_timespan, 1, 1)
- # fig = go.Figure(data=data)
- # plot(fig,filename="weekly_duration.html")
- # ##############################
-
- # ##############################-Time fill graph per filetype
- file_wise_durations = defaultdict(int)
- for filepath in logs_dict.keys():
- file_wise_durations[filepath] += logs_dict[filepath]["duration"]
-
- filenames = list(file_wise_durations.keys())
- durations = list(file_wise_durations.values())
-
- data = [go.Table(header=dict(values=['FileName', 'Total Time Spent(seconds)']), # noqa: E501
- cells=dict(values=[filenames, durations]))] # noqa: E128
- # fig = go.Figure(data=data)
- # plot(fig,filename="table.html")
-
- fig.append_trace(data[0], 3, 1)
- ###############################
- plot(fig, filename=os.path.join(DATA_FOLDER_PATH, "analysis.html"))
-
- # my_dboard = dashboard.Dashboard()
- # my_dboard.get_preview()
+ if platform.system() == "Windows":
+ DATA_FOLDER_PATH = os.path.join(os.getenv("APPDATA"), ".codeTime")
+ else:
+ DATA_FOLDER_PATH = os.path.join(os.path.expanduser("~"), ".codeTime")
+ LOG_FILE_PATH = os.path.join(DATA_FOLDER_PATH, ".sublime_logs")
+
+ sample_data = open(LOG_FILE_PATH, "r").read()
+ logs = sample_data.split("\n")
+
+ logs_dict = {}
+ for log in logs[:-1]:
+ try:
+ log_date, log_file_path, start_time, end_time = log.split(",")
+
+ end_time, start_time = float(end_time), float(start_time)
+ file_type = mimetypes.guess_type(log_file_path)
+
+ if file_type[0] is not None:
+ file_type = file_type[0].split("/")[-1].split("-")[-1]
+ else:
+ file_type = "other"
+
+ logs_dict[log_file_path] = {
+ "st": start_time,
+ "dt": datetime.strptime(log_date, "%Y-%m-%d"),
+ "et": end_time,
+ "duration": end_time - start_time,
+ "type": file_type,
+ } # noqa: E501
+ except Exception:
+ continue
+
+ fig = tools.make_subplots(
+ rows=3,
+ cols=2,
+ shared_xaxes=True,
+ specs=[
+ [{"colspan": 2}, None], # noqa: E501
+ [{"type": "pie"}, {"type": "pie"}], # noqa: E128
+ [{"colspan": 2, "type": "table"}, None],
+ ],
+ )
+
+ # ###############################- Pie Chart-
+ durations = defaultdict(int)
+ for file_path in logs_dict.keys():
+ duration = logs_dict[file_path]["duration"]
+ file_type = logs_dict[file_path]["type"]
+ durations[file_type] += duration
+
+ trace_timespan = go.Pie(
+ labels=list(durations.keys()), values=list(durations.values())
+ ) # noqa: E501
+
+ fig.append_trace(trace_timespan, 2, 1)
+ fig.append_trace(trace_timespan, 2, 2)
+ # ##############################
+
+ # ##############################-Time fill graph per filetype
+
+ durations = defaultdict(dict)
+ for file_path in logs_dict.keys():
+ duration = logs_dict[file_path]["duration"]
+ file_type = logs_dict[file_path]["type"]
+ date = logs_dict[file_path]["dt"]
+
+ if date in durations[file_type]:
+ durations[file_type][date] += duration
+ else:
+ durations[file_type][date] = duration
+
+ data = []
+ for file_type in durations.keys():
+ trace_timespan = go.Scatter(
+ x=list(durations[file_type].keys()),
+ y=list(durations[file_type].values()),
+ name=file_type,
+ fill="tozeroy",
+ ) # noqa: E501
+
+ # data.append(trace_timespan)
+ fig.append_trace(trace_timespan, 1, 1)
+ # fig = go.Figure(data=data)
+ # plot(fig,filename="weekly_duration.html")
+ # ##############################
+
+ # ##############################-Time fill graph per filetype
+ file_wise_durations = defaultdict(int)
+ for filepath in logs_dict.keys():
+ file_wise_durations[filepath] += logs_dict[filepath]["duration"]
+
+ filenames = list(file_wise_durations.keys())
+ durations = list(file_wise_durations.values())
+
+ data = [
+ go.Table(
+ header=dict(
+ values=["FileName", "Total Time Spent(seconds)"]
+ ), # noqa: E501
+ cells=dict(values=[filenames, durations]),
+ )
+ ] # noqa: E128
+
+ # fig = go.Figure(data=data)
+ # plot(fig,filename="table.html")
+
+ fig.append_trace(data[0], 3, 1)
+ ###############################
+ plot(fig, filename=os.path.join(DATA_FOLDER_PATH, "analysis.html"))
+
+ # my_dboard = dashboard.Dashboard()
+ # my_dboard.get_preview()
show_graphs()
diff --git a/code/SublimePlugin/periodicLogSaver.py b/code/SublimePlugin/periodicLogSaver.py
index aa5dc11..08d10d6 100644
--- a/code/SublimePlugin/periodicLogSaver.py
+++ b/code/SublimePlugin/periodicLogSaver.py
@@ -2,60 +2,120 @@
import time
import copy
import sublime
+import json
+import urllib.request
from datetime import datetime as dt
import sys
class PeriodicLogSaver(threading.Thread):
+ def __init__(
+ self,
+ group=None,
+ target=None,
+ name=None,
+ args=(),
+ kwargs=None,
+ verbose=None,
+ ):
+ super(PeriodicLogSaver, self).__init__(
+ group=group, target=target, name=name
+ )
+ self.args = args
+ self.kwargs = kwargs
+ return
- def __init__(self, group=None, target=None, name=None,
- args=(), kwargs=None, verbose=None): # noqa: E128
- super(PeriodicLogSaver, self).__init__(group=group, target=target, name=name)
- self.args = args
- self.kwargs = kwargs
- return
-
- def run(self):
- while True:
- try:
- curr_file = sublime.active_window().active_view().file_name()
- curr_date = dt.now().strftime('%Y-%m-%d')
-
- if curr_file is not None:
- inMemoryLogDeepCopy = copy.deepcopy(self.kwargs['inMemoryLog'])
- inMemoryLog = self.kwargs['inMemoryLog']
- inMemoryLog.clear()
-
- if curr_date in inMemoryLogDeepCopy and curr_file in inMemoryLogDeepCopy[curr_date]: # noqa: E501
- end_time = time.time()
- inMemoryLogDeepCopy[curr_date][curr_file][-1][1] = end_time
-
- if curr_date not in inMemoryLog:
- inMemoryLog[curr_date] = {}
-
- if curr_file not in inMemoryLog[curr_date]:
- inMemoryLog[curr_date][curr_file] = [[end_time, None]]
- # else: # do we need this?
- # inMemoryLog[curr_date][curr_file].append([start_time, end_time])
-
- self.write_log_file(inMemoryLogDeepCopy)
- time.sleep(self.kwargs['timeout'])
- except Exception as e:
- exc_type, exc_obj, exc_tb = sys.exc_info()
- print("periodicLogSaver:PeriodicLogSaver:run(): {error} on line number: {lno}".format(error=str(e), lno=str(exc_tb.tb_lineno))) # noqa: E501
-
- def write_log_file(self, file_times_dict):
- try:
-
- with open(self.kwargs['LOG_FILE_PATH'], 'a') as f:
- for key, val in file_times_dict.items():
- curr_date = key
- file_dict = val
-
- for file_name, times_list in file_dict.items():
- for time_start_end in times_list:
- f.write(curr_date + ',' + file_name + ',' + str(time_start_end[0]) + ',' + str(time_start_end[1]) + '\n') # noqa: E501
-
- except Exception as e:
- exc_type, exc_obj, exc_tb = sys.exc_info()
- print("periodicLogSaver:PeriodicLogSaver():write_log_file(): {error} on line number: {lno}".format(error=str(e), lno=str(exc_tb.tb_lineno))) # noqa: E501
+ def run(self):
+ while True:
+ try:
+ curr_file = sublime.active_window().active_view().file_name()
+ curr_date = dt.now().strftime("%Y-%m-%d")
+
+ if curr_file is not None:
+ inMemLog = self.kwargs["inMemoryLog"]
+ inMemoryLogDeepCopy = copy.deepcopy(inMemLog)
+ inMemoryLog = inMemLog
+ inMemoryLog.clear()
+
+ # Writing the current date and file inMemoryLogDeepCopy by calling write_log_file
+
+ if (
+ curr_date in inMemoryLogDeepCopy
+ and curr_file in inMemoryLogDeepCopy[curr_date]
+ ):
+ end_time = time.time()
+ cd = curr_date
+ cf = curr_file
+ inMemoryLogDeepCopy[cd][cf][-1][1] = end_time
+
+ if curr_date not in inMemoryLog:
+ inMemoryLog[curr_date] = {}
+
+ if curr_file not in inMemoryLog[curr_date]:
+ temp = [[end_time, None]]
+ inMemoryLog[curr_date][curr_file] = temp
+
+ self.write_log_file(inMemoryLogDeepCopy)
+ time.sleep(self.kwargs["timeout"])
+ except Exception as e:
+ exc_type, exc_obj, exc_tb = sys.exc_info()
+ print(
+ "periodicLogSaver:PeriodicLogSaver:run(): {error} on line \
+ number: {lno}".format(
+ error=str(e), lno=str(exc_tb.tb_lineno)
+ )
+ )
+
+ def write_log_file(self, file_times_dict):
+ try:
+
+ obj = []
+ api_token = self.kwargs["API_TOKEN"]
+ file_type = (
+ sublime.active_window().active_view().settings().get("syntax")
+ )
+ with open(self.kwargs["LOG_FILE_PATH"], "a") as f:
+
+ for key, val in file_times_dict.items():
+ curr_date = key
+ file_dict = val
+
+ for file_name, times_list in file_dict.items():
+ for time_start_end in times_list:
+ f.write(
+ curr_date
+ + ","
+ + file_name
+ + ","
+ + str(time_start_end[0])
+ + ","
+ + str(time_start_end[1])
+ + "\n"
+ ) # noqa: E501
+ row = {}
+ row["file_name"] = file_name.split("/")[-1]
+ row["file_extension"] = file_name.split(".")[-1]
+ row["detected_language"] = file_type.split("/")[-1].split(".")[-2]
+ row["log_date"] = curr_date
+ row["start_timestamp"] = str(time_start_end[0])
+ row["end_timestamp"] = str(time_start_end[1])
+ row["api_token"] = api_token
+ obj.append(row)
+
+ req = urllib.request.Request(self.kwargs["REQUEST_URL"])
+ req.add_header(
+ "Content-Type", "application/json; charset=utf-8"
+ )
+ jsondata = json.dumps(obj)
+ jsondataasbytes = jsondata.encode("utf-8")
+ req.add_header("Content-Length", len(jsondataasbytes))
+ response = (urllib.request.urlopen(req, jsondataasbytes).read().decode()) # noqa F841
+
+ except Exception as e:
+ exc_type, exc_obj, exc_tb = sys.exc_info()
+ print(
+ "periodicLogSaver:PeriodicLogSaver():write_log_file(): {error} \
+ on line number: {lno}".format(
+ error=str(e), lno=str(exc_tb.tb_lineno)
+ )
+ ) # noqa: E501
diff --git a/code/codetime_server/CodeTime.postman_collection.json b/code/codetime_server/CodeTime.postman_collection.json
new file mode 100644
index 0000000..24fac83
--- /dev/null
+++ b/code/codetime_server/CodeTime.postman_collection.json
@@ -0,0 +1,197 @@
+{
+ "info": {
+ "_postman_id": "c2783dee-42e3-4261-a65d-5743df35e914",
+ "name": "CodeTime",
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
+ },
+ "item": [
+ {
+ "name": "Signup",
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"username\": \"adarsh1\",\n \"password\": \"adarsh\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8000/codetime/user/?type=signup",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "codetime",
+ "user",
+ ""
+ ],
+ "query": [
+ {
+ "key": "type",
+ "value": "signup"
+ }
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Login",
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"username\": \"adarsh1\",\n \"password\": \"adarsh\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8000/codetime/user/?type=login",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "codetime",
+ "user",
+ ""
+ ],
+ "query": [
+ {
+ "key": "type",
+ "value": "login"
+ }
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Create Timelogs",
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "[{\n \"file_name\": \"test.java\",\n \"file_extension\": \"java\",\n \"detected_language\": \"java\",\n \"log_date\": \"2020-10-01\",\n \"log_timestamp\": \"160000290\",\n \"api_token\": \"74815790-d740-4344-b9c3-a505514edf88VHSda13oJOr5Iba4\"\n},{\n \"file_name\": \"test.py\",\n \"file_extension\": \"py\",\n \"detected_language\": \"python\",\n \"log_date\": \"2020-10-01\",\n \"log_timestamp\": \"160000290\",\n \"api_token\": \"74815790-d740-4344-b9c3-a505514edf88VHSda13oJOr5Iba4\"\n}]",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8000/codetime/timelog/",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "codetime",
+ "timelog",
+ ""
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get timelogs for particular api_token",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8000/codetime/timelog/?api_token=74815790-d740-4344-b9c3-a505514edf88VHSda13oJOr5Iba4",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "codetime",
+ "timelog",
+ ""
+ ],
+ "query": [
+ {
+ "key": "api_token",
+ "value": "74815790-d740-4344-b9c3-a505514edf88VHSda13oJOr5Iba4"
+ }
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get user details from api_token",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8000/codetime/user/?api_token=74815790-d740-4344-b9c3-a505514edf88VHSda13oJOr5Iba4",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "codetime",
+ "user",
+ ""
+ ],
+ "query": [
+ {
+ "key": "api_token",
+ "value": "74815790-d740-4344-b9c3-a505514edf88VHSda13oJOr5Iba4"
+ }
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "File extension wise summary for particular api_token/user",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8000/codetime/summary/?api_token=74815790-d740-4344-b9c3-a505514edf88VHSda13oJOr5Iba4&type=extension",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "codetime",
+ "summary",
+ ""
+ ],
+ "query": [
+ {
+ "key": "api_token",
+ "value": "74815790-d740-4344-b9c3-a505514edf88VHSda13oJOr5Iba4"
+ },
+ {
+ "key": "type",
+ "value": "extension"
+ }
+ ]
+ }
+ },
+ "response": []
+ }
+ ],
+ "protocolProfileBehavior": {}
+}
\ No newline at end of file
diff --git a/code/codetime_server/codetime/__init__.py b/code/codetime_server/codetime/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/code/codetime_server/codetime/admin.py b/code/codetime_server/codetime/admin.py
new file mode 100644
index 0000000..4185d36
--- /dev/null
+++ b/code/codetime_server/codetime/admin.py
@@ -0,0 +1,3 @@
+# from django.contrib import admin
+
+# Register your models here.
diff --git a/code/codetime_server/codetime/apps.py b/code/codetime_server/codetime/apps.py
new file mode 100644
index 0000000..0edf553
--- /dev/null
+++ b/code/codetime_server/codetime/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class CodetimeConfig(AppConfig):
+ name = 'codetime'
diff --git a/code/codetime_server/codetime/migrations/0001_initial.py b/code/codetime_server/codetime/migrations/0001_initial.py
new file mode 100644
index 0000000..fd14ed5
--- /dev/null
+++ b/code/codetime_server/codetime/migrations/0001_initial.py
@@ -0,0 +1,44 @@
+# Generated by Django 3.1 on 2020-10-16 16:26
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='User',
+ fields=[
+ ('log_user_id', models.AutoField(primary_key=True, serialize=False)),
+ ('username', models.CharField(max_length=100, unique=True)),
+ ('password', models.CharField(max_length=100)),
+ ('api_token', models.CharField(default='858c825a-f11a-41a2-9f83-ffbf1dbe0ec7ZeIcH0OcFn9lh2Cl', max_length=200)),
+ ],
+ options={
+ 'db_table': 'log_user',
+ },
+ ),
+ migrations.CreateModel(
+ name='TimeLog',
+ fields=[
+ ('log_file_time_id', models.AutoField(primary_key=True, serialize=False)),
+ ('file_name', models.CharField(max_length=1000)),
+ ('file_extension', models.CharField(blank=True, max_length=20, null=True)),
+ ('detected_language', models.CharField(blank=True, max_length=50, null=True)),
+ ('log_date', models.DateField()),
+ ('log_timestamp', models.FloatField()),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('modified_at', models.DateTimeField(auto_now=True)),
+ ('log_user_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_id', to='codetime.user')),
+ ],
+ options={
+ 'db_table': 'log_file_time',
+ },
+ ),
+ ]
diff --git a/code/codetime_server/codetime/migrations/0002_auto_20201016_1851.py b/code/codetime_server/codetime/migrations/0002_auto_20201016_1851.py
new file mode 100644
index 0000000..a052060
--- /dev/null
+++ b/code/codetime_server/codetime/migrations/0002_auto_20201016_1851.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.1 on 2020-10-16 18:51
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('codetime', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='user',
+ name='api_token',
+ field=models.CharField(max_length=200),
+ ),
+ ]
diff --git a/code/codetime_server/codetime/migrations/0003_auto_20201017_1840.py b/code/codetime_server/codetime/migrations/0003_auto_20201017_1840.py
new file mode 100644
index 0000000..a332993
--- /dev/null
+++ b/code/codetime_server/codetime/migrations/0003_auto_20201017_1840.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.1 on 2020-10-17 18:40
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('codetime', '0002_auto_20201016_1851'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='timelog',
+ name='log_user_id',
+ ),
+ migrations.AddField(
+ model_name='timelog',
+ name='api_token',
+ field=models.CharField(default='', max_length=200),
+ preserve_default=False,
+ ),
+ ]
diff --git a/code/codetime_server/codetime/migrations/0004_auto_20201026_0331.py b/code/codetime_server/codetime/migrations/0004_auto_20201026_0331.py
new file mode 100644
index 0000000..6c6c043
--- /dev/null
+++ b/code/codetime_server/codetime/migrations/0004_auto_20201026_0331.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.1 on 2020-10-26 03:31
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('codetime', '0003_auto_20201017_1840'),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name='timelog',
+ old_name='log_timestamp',
+ new_name='start_timestamp',
+ ),
+ migrations.AddField(
+ model_name='timelog',
+ name='end_timestamp',
+ field=models.FloatField(default=0.0),
+ preserve_default=False,
+ ),
+ ]
diff --git a/code/codetime_server/codetime/migrations/__init__.py b/code/codetime_server/codetime/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/code/codetime_server/codetime/models.py b/code/codetime_server/codetime/models.py
new file mode 100644
index 0000000..284ca33
--- /dev/null
+++ b/code/codetime_server/codetime/models.py
@@ -0,0 +1,302 @@
+from django.db import models
+from django.utils.crypto import get_random_string
+from django.core import serializers
+import uuid
+import json
+from django.db.utils import IntegrityError
+from collections import defaultdict
+
+# Create your models here.
+'''
+ ---models.py---
+ Structure
+ Models: User, TimeLog
+ Managers: UserManager, TimeLogManager
+'''
+
+
+class UserManager(models.Manager):
+ '''
+
+ APIs for the User
+ Performs User actions (CRUD operations)
+ for signup, login and updating user.
+
+ '''
+
+ def get_user_from_username(self, username, password):
+ """
+ Returns the user details using the login credentails (basic auth) of the user.
+
+ :param str username: username of the user
+ :param str password: password of user
+ :return: dictionary response
+ :rtype: object
+ """
+ user = self.filter(username=username, password=password).first()
+ if user:
+ return {
+ "username": user.username,
+ "password": user.password,
+ "api_token": user.api_token,
+ }
+ return None
+
+ def get_user_from_api_token(self, api_token):
+ """
+ Returns the user details using the unique API token of the user.
+
+ :param str api_token: unique apitoken of user
+ :return: dictionary response
+ :rtype: object
+ """
+ user = self.filter(api_token=api_token).first()
+ if user:
+ return {
+ "username": user.username,
+ "password": user.password,
+ "api_token": user.api_token,
+ }
+ return None
+
+ @staticmethod
+ def create_user(user):
+ """
+ Create a new user.
+
+ :param dict user: validated user details from post request
+ :return: response status (0 for failure, 1 for success and 2 for DB error)
+ :rtype: int
+ """
+ user["api_token"] = str(uuid.uuid4()) + get_random_string(length=16)
+
+ user_instance = User(**user)
+ try:
+ if not user_instance.save():
+ return 0
+ return 1
+ except IntegrityError as e:
+ print(e)
+ return 2
+
+ def update_user(self, user, api_token):
+ """
+ Update user information.
+
+ :param dict user: validated user details from post request
+ :param str api_token: unique apitoken of user
+ :return: response status (0 for success, 1 for no such user with api_token)
+ :rtype: int
+ """
+ user_instance = self.filter(api_token=api_token)
+
+ if user_instance:
+ user_instance.update(**user)
+ return 0
+ return 1
+
+ def login(self, username, password):
+ """
+ Login a user using basic auth
+
+ :param str username: username of the user
+ :param str password: password of user
+ :return: api_token is such user exists
+ :rtype: str
+ """
+ user_info = self.filter(username=username, password=password).first()
+
+ if user_info:
+ return user_info["api_token"]
+ return -1
+
+
+class User(models.Model):
+ '''
+ Description for User DB Model
+
+ :ivar log_user_id: Primary key indexing each user
+ :ivar username: unique username of the user
+ :ivar password: password of the user
+ :ivar api_token1: unique token for each user stored for sublime activity
+ '''
+
+ class Meta:
+ db_table = "log_user"
+
+ log_user_id = models.AutoField(primary_key=True)
+ username = models.CharField(unique=True, blank=False, null=False, max_length=100)
+ password = models.CharField(blank=False, null=False, max_length=100)
+ api_token = models.CharField(max_length=200)
+ objects = UserManager()
+
+
+class TimeLogManager(models.Manager):
+ @staticmethod
+ def create_log(time_log):
+ """
+ Create a new log for a file for a particular user
+
+ :param dict time_log: validated time log details from post request
+ :return: return status
+ :rtype: int
+ """
+ time_log_instance = TimeLog(**time_log)
+
+ try:
+
+ if not time_log_instance.save():
+ return 0
+ return 1
+ except Exception:
+ return 1
+
+ def get_time_logs(self, api_token):
+ """
+ Get all the filelogs for a particular user
+
+ :param str api_token: unique token for each user
+ :return: list of all the file logs of the user
+ :rtype: list
+ """
+ try:
+ logs = self.filter(api_token=api_token).all()
+ return json.loads(serializers.serialize("json", [log for log in logs]))
+ except Exception as e:
+ print("error in getting logs for user ", e)
+ return e
+
+ def get_file_name_extension_wise_summary(self, api_token):
+ """
+ Get fileextension wise log summary for a particular user
+
+ :param str api_token: unique token for each user
+ :return: list of all the file logs of the user
+ :rtype: list
+ """
+ summary = self.raw(
+ f"select 1 as log_file_time_id, file_name, file_extension, count(*) from log_file_time where api_token=\"{api_token}\" group by 1, 2, 3")
+
+ response = defaultdict(int)
+
+ for entry in summary:
+ response[entry.file_extension] += 1
+
+ ans = []
+ for key in dict(response):
+ val = {"language": key, "count": response[key]}
+ ans.append(val)
+
+ return ans
+
+ def get_weekday_count_summary(self, api_token):
+ """
+ Get weekday's summary for a particular user
+
+ :param str api_token: unique token for each user
+ :return: list of count of logs per day of the user
+ :rtype: list
+ """
+ summary = self.raw(
+ f"select 1 as log_file_time_id, dayname(log_date) day, count(distinct detected_language) count from log_file_time where api_token=\"{api_token}\" group by 1,2")
+
+ ans = []
+ for entry in summary:
+ val = {"day": entry.day, "count": entry.count}
+ ans.append(val)
+
+ return ans
+
+ def get_time_spent_per_coding_language(self, api_token):
+ """
+ Get the record for time spent on various codeing languages
+ for a particular user
+
+ :param str api_token: unique token for each user
+ :return: list of time spent per coding language for the user
+ :rtype: list
+ """
+ summary = self.raw(
+ f'select 1 as log_file_time_id, detected_language, sum(end_timestamp - start_timestamp) total_time from log_file_time where api_token=\"{api_token}\" group by 1,2')
+
+ ans = []
+ for entry in summary:
+ val = {"detected_language": entry.detected_language, "total_time": entry.total_time}
+ ans.append(val)
+
+ return ans
+
+ def get_time_spent_per_file(self, api_token):
+ """
+ Get the record for time spent on on each of the file
+ for a particular user
+
+ :param str api_token: unique token for each user
+ :return: list of time spent per file for the user
+ :rtype: list
+ """
+ summary = self.raw(
+ f'select 1 as log_file_time_id, file_name, sum(end_timestamp - start_timestamp) total_time from log_file_time where api_token=\"{api_token}\" group by 1,2')
+
+ ans = []
+ for entry in summary:
+ val = {"file_name": entry.file_name, "total_time": entry.total_time}
+ ans.append(val)
+
+ return ans
+
+ def get_user_overall_stats(self, api_token):
+
+ summary = self.raw(f'select 1 as log_file_time_id, api_token, count(distinct detected_language) total_languages, count(distinct file_name) total_files, sum(end_timestamp - start_timestamp) total_time from log_file_time where api_token = "{api_token}" group by 1,2') # noqa E501
+
+ ans = []
+
+ for entry in summary:
+ val = {"total_languages": entry.total_languages, "total_files": entry.total_files, "total_time": entry.total_time, "api_token": api_token}
+ ans.append(val)
+
+ return ans
+
+ def get_user_recent_stats(self, api_token):
+
+ summary = self.raw(f'select 1 as log_file_time_id, api_token, log_date, count(distinct file_name) file_count, count(distinct detected_language ) language_count, sum(end_timestamp - start_timestamp) total_time from log_file_time where api_token = "{api_token}" group by 1, 2, 3 order by 3 limit 30') # noqa E501
+ ans = []
+
+ for entry in summary:
+ val = {"file_count": entry.file_count, "language_count": entry.language_count,
+ "total_time": entry.total_time, "api_token": api_token, "log_date": entry.log_date}
+ ans.append(val)
+
+ return ans
+
+
+class TimeLog(models.Model):
+ '''
+ Description for Time Logging DB Model
+
+ :ivar log_file_time_id: Primary key indexing each time log of a file of a user
+ :ivar api_token: unique token for each user
+ :ivar file_name: file being edited by user
+ :ivar file_extension: extension of the file (or file type)
+ :ivar detected_language: language in the file
+ :ivar log_date: date of when was the file logged
+ :ivar start_timestamp: start time recorded for activity on file
+ :ivar end_timestamp: end time recorded for activity on file
+ :ivar created_at: timestamp of when file was created
+ :ivar modified_at: timestamp of when file was modified last
+ '''
+
+ class Meta:
+ db_table = "log_file_time"
+
+ log_file_time_id = models.AutoField(primary_key=True)
+ api_token = models.CharField(max_length=200, null=False, blank=False)
+ file_name = models.CharField(max_length=1000, null=False, blank=False)
+ file_extension = models.CharField(max_length=20, null=True, blank=True)
+ detected_language = models.CharField(max_length=50, null=True, blank=True)
+ log_date = models.DateField(blank=False, null=False)
+ start_timestamp = models.FloatField(blank=False, null=False)
+ end_timestamp = models.FloatField(blank=False, null=False)
+ created_at = models.DateTimeField(auto_now_add=True)
+ modified_at = models.DateTimeField(auto_now=True)
+ objects = TimeLogManager()
diff --git a/code/codetime_server/codetime/request_handlers.py b/code/codetime_server/codetime/request_handlers.py
new file mode 100644
index 0000000..bc5da7c
--- /dev/null
+++ b/code/codetime_server/codetime/request_handlers.py
@@ -0,0 +1,256 @@
+"""
+This modules has functions to handle all the supported commands for the
+codetime server's APIs.
+"""
+from .models import User, TimeLog
+from .serializers import UserSerializer, TimeLogSerializer
+
+
+def get_missing_param_response(data=None):
+ """
+ Returns error response for missing parameters
+
+ :param str data: message
+ :return: response
+ :rtype: object
+ """
+ return {"status": 400, "message": "Missing query parameter.", "data": data}
+
+
+def get_serializer_error_response(error):
+ """
+ Returns error response from serializer
+
+ :param str error: message
+ :return: error_response
+ :rtype: object
+ """
+ error_response = {"status": 422, "data": [], "message": error}
+ return error_response
+
+
+def get_invalid_request_param(message):
+ """
+ Returns error response for invalid request parameters
+
+ :param str error: message
+ :return: error_response
+ :rtype: object
+ """
+ error_response = {"status": 400, "data": [], "message": message}
+ return error_response
+
+
+def get_valid_output_response(data):
+ """
+ Returns success message correct processing of post/get request
+
+ :param str data: message
+ :return: response
+ :rtype: object
+ """
+ response = {"status": 200, "message": "Success", "data": data}
+
+ return response
+
+
+def get_valid_post_response(data):
+ """
+ Returns success message correct processing of post/get request
+
+ :param str data: message
+ :return: response
+ :rtype: object
+ """
+ response = {"status": 201, "message": "Created", "data": data}
+
+ return response
+
+
+def get_something_went_wrong_response(data=None):
+ """
+ Returns error response for server related error
+
+ :param str data: message
+ :return: response
+ :rtype: object
+ """
+ response = {"status": 500, "message": "Something went wrong", "data": data}
+
+ return response
+
+
+def get_invalid_user_credentials(data=None):
+ """
+ Returns error response for invalid user credentials
+
+ :param str data: message
+ :return: response
+ :rtype: object
+ """
+ response = {"status": 401, "message": "Invalid user credentials.", "data": data}
+
+ return response
+
+
+def handle_user_get(request):
+ api_token = request.query_params.get("api_token", None)
+
+ if api_token:
+ response = User.objects.get_user_from_api_token(api_token=api_token)
+ return get_valid_output_response(data=response)
+
+
+def handle_user_post(request):
+ """
+ Handler for post request made by a user to signup and login
+
+ :param HTTP POST request: message
+ :return: response
+ :rtype: object
+ """
+ request_type = request.query_params.get("type", None)
+
+ if request_type is None:
+ return get_missing_param_response()
+
+ if request_type == "login" or request_type == "signup":
+
+ serializer = UserSerializer(data=request.data)
+
+ if serializer.is_valid():
+ if request_type == "signup":
+ return_status = User.objects.create_user(request.data)
+
+ if return_status == 0:
+ data = User.objects.get_user_from_username(
+ request.data["username"], request.data["password"]
+ )
+ return get_valid_post_response(data)
+ elif return_status == 1:
+ return get_something_went_wrong_response(request.data)
+ elif return_status == 2:
+ return {"status": 400, "message": "Username already taken.", "data": request.data}
+ elif request_type == "login":
+
+ return_status = User.objects.get_user_from_username(
+ request.data["username"], request.data["password"]
+ )
+
+ if return_status:
+ return get_valid_post_response(return_status)
+ else:
+ return get_invalid_user_credentials(request.data)
+ else:
+ return get_serializer_error_response(serializer.errors)
+
+ else:
+ return get_invalid_request_param('Invalid value for url param "type".')
+
+
+def handle_log_file_post(request):
+ """
+ Handler for post request made by logging files
+
+ :param HTTP POST request: request
+ :return: response
+ :rtype: object
+ """
+ return_data = dict()
+ return_data["created"] = []
+ return_data["failed"] = []
+ return_status = 201
+
+ for data_point in request.data:
+
+ serializer = TimeLogSerializer(data=data_point)
+
+ if serializer.is_valid():
+ creation_status = TimeLog.objects.create_log(serializer.data)
+ if not creation_status:
+ return_data["created"].append(serializer.data)
+ else:
+ return_status = 500
+ return_data["failed"].append(data_point)
+ else:
+ return_status = 422
+ return_data["failed"].append(data_point)
+
+ return {"status": return_status, "message": "", "data": return_data}
+
+
+def handle_get_file_logs(request):
+ """
+ Handler for get request to obtain all records for a user.
+
+ :param HTTP GET request: request
+ :return: response
+ :rtype: object
+ """
+ user_api_token = request.query_params.get("api_token", None)
+ if user_api_token is not None:
+ response = TimeLog.objects.get_time_logs(user_api_token)
+ if isinstance(response, list):
+ return get_valid_output_response(response)
+ else:
+ return get_invalid_request_param(response)
+ else:
+ return get_missing_param_response("api_token")
+
+
+def handle_summary_request(request):
+ """
+ Handler for get request to summary records for a user.
+
+ :param HTTP GET request: request
+ :return: response
+ :rtype: object
+ """
+ api_token = request.query_params.get("api_token", None)
+ summary_type = request.query_params.get("type", None)
+
+ if api_token is not None and summary_type is not None:
+
+ if summary_type == "extension":
+ response = TimeLog.objects.get_file_name_extension_wise_summary(api_token=api_token)
+ return {
+ "status": 200,
+ "data": response
+ }
+
+ elif summary_type == "weekday":
+ response = TimeLog.objects.get_weekday_count_summary(api_token=api_token)
+ return {
+ "status": 200,
+ "data": response
+ }
+
+ elif summary_type == "language_total_time":
+ response = TimeLog.objects.get_time_spent_per_coding_language(api_token=api_token)
+ return {
+ "status": 200,
+ "data": response
+ }
+
+ elif summary_type == "file_name_total_time":
+ response = TimeLog.objects.get_time_spent_per_file(api_token=api_token)
+ return {
+ "status": 200,
+ "data": response
+ }
+
+ elif summary_type == "user_overall_stats":
+
+ response = TimeLog.objects.get_user_overall_stats(api_token=api_token)
+ return {
+ "status": 200,
+ "data": response
+ }
+
+ elif summary_type == "recent_stats":
+
+ response = TimeLog.objects.get_user_recent_stats(api_token=api_token)
+ return {
+ "status": 200,
+ "data": response
+ }
diff --git a/code/codetime_server/codetime/serializers.py b/code/codetime_server/codetime/serializers.py
new file mode 100644
index 0000000..68169e8
--- /dev/null
+++ b/code/codetime_server/codetime/serializers.py
@@ -0,0 +1,34 @@
+from rest_framework import serializers
+
+
+class UserSerializer(serializers.Serializer):
+ """
+ User Serializer
+ """
+ username = serializers.CharField(max_length=100, required=True)
+ password = serializers.CharField(max_length=100, required=True)
+
+ def update(self, instance, validated_data):
+ pass
+
+ def create(self, validated_data):
+ pass
+
+
+class TimeLogSerializer(serializers.Serializer):
+ """
+ TimeLog Serializer
+ """
+ file_name = serializers.CharField(max_length=1000, required=True)
+ file_extension = serializers.CharField(max_length=20, required=True)
+ detected_language = serializers.CharField(max_length=50, required=True)
+ log_date = serializers.DateField(required=True)
+ start_timestamp = serializers.FloatField(required=True)
+ end_timestamp = serializers.FloatField(required=True)
+ api_token = serializers.CharField(max_length=200)
+
+ def create(self, validated_data):
+ pass
+
+ def update(self, instance, validated_data):
+ pass
diff --git a/code/codetime_server/codetime/tests/__init__.py b/code/codetime_server/codetime/tests/__init__.py
new file mode 100644
index 0000000..fab776d
--- /dev/null
+++ b/code/codetime_server/codetime/tests/__init__.py
@@ -0,0 +1,7 @@
+# Created by Ayushi Rajendra Kumar at 10/25/2020
+
+# Feature: #Enter feature name here
+# Enter feature description here
+
+# Scenario: # Enter scenario name here
+# Enter steps here
diff --git a/code/codetime_server/codetime/tests/test_views.py b/code/codetime_server/codetime/tests/test_views.py
new file mode 100644
index 0000000..b8c5322
--- /dev/null
+++ b/code/codetime_server/codetime/tests/test_views.py
@@ -0,0 +1,184 @@
+# Created by Ayushi Rajendra Kumar at 10/25/2020
+
+# Feature: #Enter feature name here
+# Enter feature description here
+
+# Scenario: # Enter scenario name here
+# Enter steps here
+
+import copy
+import time
+from django.test import TestCase, Client
+from django.urls import reverse
+from rest_framework import status
+from rest_framework.utils import json
+from ..models import User
+
+users_data = {
+ "log_user_id": 1223,
+ "username": "ayushi2",
+ "password": "123ayushi1",
+ "api_token": "1234abcd"
+
+}
+
+users_data1 = {
+ "username": "ayushi2",
+ "password": "123ayushi1"
+
+}
+
+timeLog_data = [{
+ "file_name": "test",
+ "file_extension": "py",
+ "detected_language": "python",
+ "log_date": "2020-10-27",
+ "end_timestamp": time.time() + 500,
+ "start_timestamp": time.time(),
+ "api_token": "sample"
+}]
+
+
+class TestPostViews(TestCase):
+ """
+ Tests to verify the functioning of all the POST requests in codetime_server/codetime
+ """
+
+ def setUp(self):
+ """
+ Test setup for each test in this class. It is done for each of the tests
+ """
+ self.client = Client()
+ self.user_url = reverse('user_endpoint')
+ self.timelog_url = reverse('timelog_url')
+ self.summary_url = reverse('timelog_summary_url')
+ # User.objects.create_user(users_data)
+
+ def test_user(self):
+ """
+ Test behaviour of correct POST request for creating a user
+ """
+ user_url = f"{self.user_url}?type=signup"
+ response = self.client.post(user_url, data=json.dumps(users_data1), content_type='application/json')
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+ user_url = f"{self.user_url}?type=login"
+ response = self.client.post(user_url, data=json.dumps(users_data1), content_type='application/json')
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+ user_Details = User.objects.get_user_from_username(username='ayushi2', password='123ayushi1')
+ self.assertEqual(user_Details.get('username'), users_data['username'])
+ self.assertEqual(user_Details.get('password'), users_data['password'])
+ self.assertEqual(response.data["data"]["api_token"], user_Details.get('api_token'))
+ self.assertNotEquals(user_Details.get('api_token'), None)
+
+ def test_logtime(self):
+ """
+ Test behaviour of correct POST request for creating a user
+ """
+ user_url = f"{self.user_url}?type=signup"
+ response = self.client.post(user_url, data=json.dumps(users_data1), content_type='application/json')
+ user_Details = User.objects.get_user_from_username(username='ayushi2', password='123ayushi1')
+ self.assertEqual(response.data["data"]["api_token"], user_Details.get('api_token'))
+ timeLog_data[0]["api_token"] = user_Details.get('api_token')
+ response = self.client.post(self.timelog_url, data=json.dumps(timeLog_data), content_type='application/json')
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
+ def test_incorrect_get_signedup_user(self):
+ """
+ Test behaviour of incorrect POST request for logging in a user
+ """
+ user_url = f"{self.user_url}?type=login"
+ response = self.client.post(f"{self.user_url}?type=signup", data=json.dumps(users_data1), content_type='application/json')
+ incorrect_user = copy.deepcopy(users_data1)
+ incorrect_user["password"] = "ayushi21"
+ response = self.client.post(user_url, data=json.dumps(incorrect_user), content_type='application/json')
+ self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+
+ def test_invalid_user_signup_request(self):
+ """
+ Test behaviour of invalid POST request for logging in a user
+ """
+ user_url = f"{self.user_url}?type=signin"
+ response = self.client.post(user_url, data=json.dumps(users_data1), content_type='application/json')
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+
+class TestGetViews(TestCase):
+ """
+ Tests to verify the functioning of all the GET requests in codetime_server/codetime
+ """
+
+ def setUp(self):
+ """
+ Test setup for each test in this class. It is done for each of the tests
+ """
+ self.client = Client()
+ self.user_url = reverse('user_endpoint')
+ self.timelog_url = reverse('timelog_url')
+ self.summary_url = reverse('timelog_summary_url')
+ user_url = f"{self.user_url}?type=signup"
+ self.client.post(user_url, data=json.dumps(users_data1), content_type='application/json')
+ user_Details = User.objects.get_user_from_username(username='ayushi2', password='123ayushi1')
+ timeLog_data[0]["api_token"] = user_Details.get('api_token')
+ self.client.post(self.timelog_url, data=json.dumps(timeLog_data), content_type='application/json')
+
+ def test_get_summary_extension_api(self):
+ """
+ Test behaviour of GET request for getting summary by file extension
+ """
+ summary_url = f"{self.summary_url}?api_token={timeLog_data[0]['api_token']}&type=extension"
+ response = self.client.get(summary_url, content_type='application/json')
+ self.assertEqual(response.data['data'][0]['language'], timeLog_data[0]['file_extension'])
+ self.assertEqual(response.data['data'][0]['count'], 1)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_get_summary_weekday_api(self):
+ """
+ Test behaviour of GET request for getting summary by weekdays
+ """
+ summary_url = f"{self.summary_url}?api_token={timeLog_data[0]['api_token']}&type=weekday"
+ response = self.client.get(summary_url, content_type='application/json')
+ self.assertEqual(response.data['data'][0]['day'], 'Tuesday')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_get_summary_language_time_api(self):
+ """
+ Test behaviour of GET request for getting summary by time taken in languages
+ """
+ summary_url = f"{self.summary_url}?api_token={timeLog_data[0]['api_token']}&type=language_total_time"
+ response = self.client.get(summary_url, content_type='application/json')
+ self.assertEqual(response.data['data'][0]['detected_language'], timeLog_data[0]['detected_language'])
+ self.assertEqual(response.data['data'][0]['total_time'], timeLog_data[0]['end_timestamp']-timeLog_data[0]['start_timestamp'])
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_get_summary_file_names_api(self):
+ """
+ Test behaviour of GET request for getting summary by file names
+ """
+ summary_url = f"{self.summary_url}?api_token={timeLog_data[0]['api_token']}&type=file_name_total_time"
+ response = self.client.get(summary_url, content_type='application/json')
+ self.assertEqual(response.data['data'][0]['file_name'], timeLog_data[0]['file_name'])
+ self.assertEqual(response.data['data'][0]['total_time'], timeLog_data[0]['end_timestamp']-timeLog_data[0]['start_timestamp'])
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_get_summary_user_stats_api(self):
+ """
+ Test behaviour of GET request for getting summary by overall user statistics
+ """
+ summary_url = f"{self.summary_url}?api_token={timeLog_data[0]['api_token']}&type=user_overall_stats"
+ response = self.client.get(summary_url, content_type='application/json')
+ self.assertEqual(response.data['data'][0]['total_languages'], 1)
+ self.assertEqual(response.data['data'][0]['total_time'], timeLog_data[0]['end_timestamp']-timeLog_data[0]['start_timestamp'])
+ self.assertEqual(response.data['data'][0]['total_files'], 1)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_get_summary_recent_stats_api(self):
+ """
+ Test behaviour of GET request for getting summary by recent user statistics
+ """
+ summary_url = f"{self.summary_url}?api_token={timeLog_data[0]['api_token']}&type=recent_stats"
+ response = self.client.get(summary_url, content_type='application/json')
+ self.assertEqual(response.data['data'][0]['file_count'], 1)
+ self.assertEqual(response.data['data'][0]['language_count'], 1)
+ self.assertEqual(response.data['data'][0]['total_time'], timeLog_data[0]['end_timestamp']-timeLog_data[0]['start_timestamp'])
+ self.assertEqual(response.data['data'][0]['log_date'].isoformat(), timeLog_data[0]['log_date'])
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
diff --git a/code/codetime_server/codetime/tests/tests_url.py b/code/codetime_server/codetime/tests/tests_url.py
new file mode 100644
index 0000000..af7e5f9
--- /dev/null
+++ b/code/codetime_server/codetime/tests/tests_url.py
@@ -0,0 +1,22 @@
+# Create your tests here.
+from django.test import SimpleTestCase
+from django.urls import resolve, reverse
+from ..views import TimeLogView, UserView, TimeLogSummaryView
+
+
+# Create your tests here.
+
+
+class TestURLs(SimpleTestCase):
+
+ def test_time_log_resolution(self):
+ url = reverse('timelog_url')
+ self.assertEquals(resolve(url).func.view_class, TimeLogView)
+
+ def test_user_url_resolution(self):
+ url = reverse('user_endpoint')
+ self.assertEquals(resolve(url).func.view_class, UserView)
+
+ def test_summary_url_resolution(self):
+ url = reverse('timelog_summary_url')
+ self.assertEquals(resolve(url).func.view_class, TimeLogSummaryView)
diff --git a/code/codetime_server/codetime/urls.py b/code/codetime_server/codetime/urls.py
new file mode 100644
index 0000000..583fb72
--- /dev/null
+++ b/code/codetime_server/codetime/urls.py
@@ -0,0 +1,9 @@
+from django.conf.urls import url
+from .views import UserView, TimeLogView, TimeLogSummaryView
+
+
+urlpatterns = [
+ url('user/', UserView.as_view(), name='user_endpoint'),
+ url('timelog/', TimeLogView.as_view(), name='timelog_url'),
+ url('summary/', TimeLogSummaryView.as_view(), name='timelog_summary_url'),
+]
diff --git a/code/codetime_server/codetime/views.py b/code/codetime_server/codetime/views.py
new file mode 100644
index 0000000..5e9ec7b
--- /dev/null
+++ b/code/codetime_server/codetime/views.py
@@ -0,0 +1,57 @@
+from rest_framework import generics
+from .request_handlers import handle_user_get, handle_user_post, handle_log_file_post, handle_summary_request, \
+ handle_get_file_logs
+from rest_framework.response import Response
+
+
+class UserView(generics.ListCreateAPIView, generics.RetrieveUpdateDestroyAPIView):
+ """
+ User View
+ """
+
+ def get(self, request, *args, **kwargs):
+ """
+ User get request
+ """
+ response = handle_user_get(request)
+ return Response(data=response, status=response.get('status', 200))
+
+ def post(self, request, *args, **kwargs):
+ """
+ User post/signup and login request
+ """
+ response = handle_user_post(request)
+ return Response(data=response, status=response.get('status', 201))
+
+
+class TimeLogView(generics.ListAPIView, generics.CreateAPIView):
+ """
+ TimeLog View
+ """
+
+ def post(self, request, *args, **kwargs):
+ """
+ TimeLog post request
+ """
+ response = handle_log_file_post(request)
+ return Response(data=response, status=response.get('status', 201))
+
+ def get(self, request, *args, **kwargs):
+ """
+ TimeLog get request
+ """
+ response = handle_get_file_logs(request)
+ return Response(data=response, status=response.get('status', 200))
+
+
+class TimeLogSummaryView(generics.ListAPIView):
+ """
+ TimeLog Summary View
+ """
+
+ def get(self, request, *args, **kwargs):
+ """
+ TimeLogSummary get request
+ """
+ response = handle_summary_request(request)
+ return Response(data=response, status=response.get('status', 200))
diff --git a/code/codetime_server/codetime_server/__init__.py b/code/codetime_server/codetime_server/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/code/codetime_server/codetime_server/asgi.py b/code/codetime_server/codetime_server/asgi.py
new file mode 100644
index 0000000..188163e
--- /dev/null
+++ b/code/codetime_server/codetime_server/asgi.py
@@ -0,0 +1,16 @@
+"""
+ASGI config for codetime_server project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'codetime_server.settings')
+
+application = get_asgi_application()
diff --git a/code/codetime_server/codetime_server/settings.py b/code/codetime_server/codetime_server/settings.py
new file mode 100644
index 0000000..e3e4824
--- /dev/null
+++ b/code/codetime_server/codetime_server/settings.py
@@ -0,0 +1,141 @@
+"""
+Django settings for codetime_server project.
+Generated by 'django-admin startproject' using Django 3.1.
+For more information on this file, see
+https://docs.djangoproject.com/en/3.1/topics/settings/
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/3.1/ref/settings/
+"""
+
+from pathlib import Path
+import os
+import environ
+
+env = environ.Env()
+
+environ.Env.read_env()
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve(strict=True).parent.parent
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+
+SECRET_KEY = env("CODE_TIME_SECRET_KEY")
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
+
+# Application definition
+
+INSTALLED_APPS = [
+ 'corsheaders',
+ "codetime",
+ "django.contrib.admin",
+ "django.contrib.auth",
+ "django.contrib.contenttypes",
+ "django.contrib.sessions",
+ "django.contrib.messages",
+ "django.contrib.staticfiles",
+ "django.contrib.admindocs",
+]
+
+MIDDLEWARE = [
+ 'corsheaders.middleware.CorsMiddleware',
+ "django.middleware.security.SecurityMiddleware",
+ "django.contrib.sessions.middleware.SessionMiddleware",
+ "django.middleware.common.CommonMiddleware",
+ "django.middleware.csrf.CsrfViewMiddleware",
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
+ "django.contrib.messages.middleware.MessageMiddleware",
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
+]
+
+ROOT_URLCONF = "codetime_server.urls"
+
+TEMPLATES = [
+ {
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
+ "DIRS": [],
+ "APP_DIRS": True,
+ "OPTIONS": {
+ "context_processors": [
+ "django.template.context_processors.debug",
+ "django.template.context_processors.request",
+ "django.contrib.auth.context_processors.auth",
+ "django.contrib.messages.context_processors.messages",
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = "codetime_server.wsgi.application"
+
+REST_FRAMEWORK = {
+ # Use Django's standard `django.contrib.auth` permissions,
+ # or allow read-only access for unauthenticated users.
+ 'DEFAULT_PERMISSION_CLASSES': [],
+ 'TEST_REQUEST_DEFAULT_FORMAT': 'json'
+}
+
+# Database
+# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
+
+DATABASES = {
+
+ 'default': {
+ 'ENGINE': 'django.db.backends.mysql',
+ 'NAME': env("MYSQL_CODE_TIME_DB_NAME"),
+ 'USER': env("MYSQL_CODE_TIME_USER"),
+ 'PASSWORD': env("MYSQL_CODE_TIME_PASSWORD"),
+ 'HOST': env("MYSQL_CODE_TIME_HOST"),
+ 'PORT': int(env("MYSQL_CODE_TIME_CONNECTION_PORT")),
+ }
+}
+
+# Password validation
+# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
+ },
+]
+
+# Internationalization
+# https://docs.djangoproject.com/en/3.1/topics/i18n/
+
+LANGUAGE_CODE = "en-us"
+
+TIME_ZONE = "UTC"
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/3.1/howto/static-files/
+
+
+STATIC_URL = '/static/'
+STATICFILES_DIRS = (
+ os.path.join(BASE_DIR, 'static'),
+)
+STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
+
+CORS_ORIGIN_ALLOW_ALL = True
diff --git a/code/codetime_server/codetime_server/urls.py b/code/codetime_server/codetime_server/urls.py
new file mode 100644
index 0000000..7fa3624
--- /dev/null
+++ b/code/codetime_server/codetime_server/urls.py
@@ -0,0 +1,24 @@
+"""codetime_server URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/3.1/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: path('', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.urls import include, path
+ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
+"""
+from django.contrib import admin
+from django.urls import path
+from django.conf.urls import url, include
+
+urlpatterns = [
+ path('admin/', admin.site.urls),
+ path('admin/doc/', include('django.contrib.admindocs.urls')),
+ url('codetime/', include('codetime.urls'), name='codetime')
+]
diff --git a/code/codetime_server/codetime_server/wsgi.py b/code/codetime_server/codetime_server/wsgi.py
new file mode 100644
index 0000000..c04c55c
--- /dev/null
+++ b/code/codetime_server/codetime_server/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for codetime_server project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'codetime_server.settings')
+
+application = get_wsgi_application()
diff --git a/code/codetime_server/docs/Makefile b/code/codetime_server/docs/Makefile
new file mode 100644
index 0000000..d0c3cbf
--- /dev/null
+++ b/code/codetime_server/docs/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS ?=
+SPHINXBUILD ?= sphinx-build
+SOURCEDIR = source
+BUILDDIR = build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/code/codetime_server/docs/build/doctrees/codetime.doctree b/code/codetime_server/docs/build/doctrees/codetime.doctree
new file mode 100644
index 0000000..79b0610
Binary files /dev/null and b/code/codetime_server/docs/build/doctrees/codetime.doctree differ
diff --git a/code/codetime_server/docs/build/doctrees/codetime.tests.doctree b/code/codetime_server/docs/build/doctrees/codetime.tests.doctree
new file mode 100644
index 0000000..2f595fa
Binary files /dev/null and b/code/codetime_server/docs/build/doctrees/codetime.tests.doctree differ
diff --git a/code/codetime_server/docs/build/doctrees/environment.pickle b/code/codetime_server/docs/build/doctrees/environment.pickle
new file mode 100644
index 0000000..6f2d19c
Binary files /dev/null and b/code/codetime_server/docs/build/doctrees/environment.pickle differ
diff --git a/code/codetime_server/docs/build/doctrees/index.doctree b/code/codetime_server/docs/build/doctrees/index.doctree
new file mode 100644
index 0000000..b34de3d
Binary files /dev/null and b/code/codetime_server/docs/build/doctrees/index.doctree differ
diff --git a/code/codetime_server/docs/build/doctrees/manage.doctree b/code/codetime_server/docs/build/doctrees/manage.doctree
new file mode 100644
index 0000000..fe0a99b
Binary files /dev/null and b/code/codetime_server/docs/build/doctrees/manage.doctree differ
diff --git a/code/codetime_server/docs/build/html/_sources/codetime.rst.txt b/code/codetime_server/docs/build/html/_sources/codetime.rst.txt
new file mode 100644
index 0000000..17a3a5d
--- /dev/null
+++ b/code/codetime_server/docs/build/html/_sources/codetime.rst.txt
@@ -0,0 +1,77 @@
+codetime package
+=================
+
+Subpackages
+--------------------
+
+.. toctree::
+ :maxdepth: 4
+
+ codetime.tests
+
+Submodules
+----------
+
+codetime.admin module
+-------------------------
+
+.. automodule:: codetime.admin
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+codetime.apps module
+------------------------
+
+.. automodule:: codetime.apps
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+codetime.models module
+--------------------------
+
+.. automodule:: codetime.models
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+codetime.request\_handlers module
+---------------------------------------------
+
+.. automodule:: codetime.request_handlers
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+codetime.serializers module
+------------------------------
+
+.. automodule:: codetime.serializers
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+codetime.urls module
+------------------------
+
+.. automodule:: codetime.urls
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+codetime.views module
+-------------------------
+
+.. automodule:: codetime.views
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+Module contents
+------------------------
+
+.. automodule:: codetime
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/code/codetime_server/docs/build/html/_sources/codetime.tests.rst.txt b/code/codetime_server/docs/build/html/_sources/codetime.tests.rst.txt
new file mode 100644
index 0000000..28ebb62
--- /dev/null
+++ b/code/codetime_server/docs/build/html/_sources/codetime.tests.rst.txt
@@ -0,0 +1,29 @@
+codetime.tests package
+=======================
+
+Submodules
+----------
+
+codetime.tests.tests\_url module
+----------------------------------
+
+.. automodule:: codetime.tests.tests_url
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+codetime.tests.test\_views module
+-------------------------------------
+
+.. automodule:: codetime.tests.test_views
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+Module contents
+---------------
+
+.. automodule:: codetime.tests
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/code/codetime_server/docs/build/html/_sources/index.rst.txt b/code/codetime_server/docs/build/html/_sources/index.rst.txt
new file mode 100644
index 0000000..20b4b22
--- /dev/null
+++ b/code/codetime_server/docs/build/html/_sources/index.rst.txt
@@ -0,0 +1,25 @@
+.. codetime documentation master file, created by
+ sphinx-quickstart on Mon Oct 26 21:19:39 2020.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+Welcome to codetime's documentation!
+====================================
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Contents:
+
+ manage
+ codetime
+ codetime.tests
+
+
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/code/codetime_server/docs/build/html/_sources/manage.rst.txt b/code/codetime_server/docs/build/html/_sources/manage.rst.txt
new file mode 100644
index 0000000..776b9e3
--- /dev/null
+++ b/code/codetime_server/docs/build/html/_sources/manage.rst.txt
@@ -0,0 +1,7 @@
+manage module
+=============
+
+.. automodule:: manage
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/code/codetime_server/docs/build/html/_static/alabaster.css b/code/codetime_server/docs/build/html/_static/alabaster.css
new file mode 100644
index 0000000..0eddaeb
--- /dev/null
+++ b/code/codetime_server/docs/build/html/_static/alabaster.css
@@ -0,0 +1,701 @@
+@import url("basic.css");
+
+/* -- page layout ----------------------------------------------------------- */
+
+body {
+ font-family: Georgia, serif;
+ font-size: 17px;
+ background-color: #fff;
+ color: #000;
+ margin: 0;
+ padding: 0;
+}
+
+
+div.document {
+ width: 940px;
+ margin: 30px auto 0 auto;
+}
+
+div.documentwrapper {
+ float: left;
+ width: 100%;
+}
+
+div.bodywrapper {
+ margin: 0 0 0 220px;
+}
+
+div.sphinxsidebar {
+ width: 220px;
+ font-size: 14px;
+ line-height: 1.5;
+}
+
+hr {
+ border: 1px solid #B1B4B6;
+}
+
+div.body {
+ background-color: #fff;
+ color: #3E4349;
+ padding: 0 30px 0 30px;
+}
+
+div.body > .section {
+ text-align: left;
+}
+
+div.footer {
+ width: 940px;
+ margin: 20px auto 30px auto;
+ font-size: 14px;
+ color: #888;
+ text-align: right;
+}
+
+div.footer a {
+ color: #888;
+}
+
+p.caption {
+ font-family: inherit;
+ font-size: inherit;
+}
+
+
+div.relations {
+ display: none;
+}
+
+
+div.sphinxsidebar a {
+ color: #444;
+ text-decoration: none;
+ border-bottom: 1px dotted #999;
+}
+
+div.sphinxsidebar a:hover {
+ border-bottom: 1px solid #999;
+}
+
+div.sphinxsidebarwrapper {
+ padding: 18px 10px;
+}
+
+div.sphinxsidebarwrapper p.logo {
+ padding: 0;
+ margin: -10px 0 0 0px;
+ text-align: center;
+}
+
+div.sphinxsidebarwrapper h1.logo {
+ margin-top: -10px;
+ text-align: center;
+ margin-bottom: 5px;
+ text-align: left;
+}
+
+div.sphinxsidebarwrapper h1.logo-name {
+ margin-top: 0px;
+}
+
+div.sphinxsidebarwrapper p.blurb {
+ margin-top: 0;
+ font-style: normal;
+}
+
+div.sphinxsidebar h3,
+div.sphinxsidebar h4 {
+ font-family: Georgia, serif;
+ color: #444;
+ font-size: 24px;
+ font-weight: normal;
+ margin: 0 0 5px 0;
+ padding: 0;
+}
+
+div.sphinxsidebar h4 {
+ font-size: 20px;
+}
+
+div.sphinxsidebar h3 a {
+ color: #444;
+}
+
+div.sphinxsidebar p.logo a,
+div.sphinxsidebar h3 a,
+div.sphinxsidebar p.logo a:hover,
+div.sphinxsidebar h3 a:hover {
+ border: none;
+}
+
+div.sphinxsidebar p {
+ color: #555;
+ margin: 10px 0;
+}
+
+div.sphinxsidebar ul {
+ margin: 10px 0;
+ padding: 0;
+ color: #000;
+}
+
+div.sphinxsidebar ul li.toctree-l1 > a {
+ font-size: 120%;
+}
+
+div.sphinxsidebar ul li.toctree-l2 > a {
+ font-size: 110%;
+}
+
+div.sphinxsidebar input {
+ border: 1px solid #CCC;
+ font-family: Georgia, serif;
+ font-size: 1em;
+}
+
+div.sphinxsidebar hr {
+ border: none;
+ height: 1px;
+ color: #AAA;
+ background: #AAA;
+
+ text-align: left;
+ margin-left: 0;
+ width: 50%;
+}
+
+div.sphinxsidebar .badge {
+ border-bottom: none;
+}
+
+div.sphinxsidebar .badge:hover {
+ border-bottom: none;
+}
+
+/* To address an issue with donation coming after search */
+div.sphinxsidebar h3.donation {
+ margin-top: 10px;
+}
+
+/* -- body styles ----------------------------------------------------------- */
+
+a {
+ color: #004B6B;
+ text-decoration: underline;
+}
+
+a:hover {
+ color: #6D4100;
+ text-decoration: underline;
+}
+
+div.body h1,
+div.body h2,
+div.body h3,
+div.body h4,
+div.body h5,
+div.body h6 {
+ font-family: Georgia, serif;
+ font-weight: normal;
+ margin: 30px 0px 10px 0px;
+ padding: 0;
+}
+
+div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; }
+div.body h2 { font-size: 180%; }
+div.body h3 { font-size: 150%; }
+div.body h4 { font-size: 130%; }
+div.body h5 { font-size: 100%; }
+div.body h6 { font-size: 100%; }
+
+a.headerlink {
+ color: #DDD;
+ padding: 0 4px;
+ text-decoration: none;
+}
+
+a.headerlink:hover {
+ color: #444;
+ background: #EAEAEA;
+}
+
+div.body p, div.body dd, div.body li {
+ line-height: 1.4em;
+}
+
+div.admonition {
+ margin: 20px 0px;
+ padding: 10px 30px;
+ background-color: #EEE;
+ border: 1px solid #CCC;
+}
+
+div.admonition tt.xref, div.admonition code.xref, div.admonition a tt {
+ background-color: #FBFBFB;
+ border-bottom: 1px solid #fafafa;
+}
+
+div.admonition p.admonition-title {
+ font-family: Georgia, serif;
+ font-weight: normal;
+ font-size: 24px;
+ margin: 0 0 10px 0;
+ padding: 0;
+ line-height: 1;
+}
+
+div.admonition p.last {
+ margin-bottom: 0;
+}
+
+div.highlight {
+ background-color: #fff;
+}
+
+dt:target, .highlight {
+ background: #FAF3E8;
+}
+
+div.warning {
+ background-color: #FCC;
+ border: 1px solid #FAA;
+}
+
+div.danger {
+ background-color: #FCC;
+ border: 1px solid #FAA;
+ -moz-box-shadow: 2px 2px 4px #D52C2C;
+ -webkit-box-shadow: 2px 2px 4px #D52C2C;
+ box-shadow: 2px 2px 4px #D52C2C;
+}
+
+div.error {
+ background-color: #FCC;
+ border: 1px solid #FAA;
+ -moz-box-shadow: 2px 2px 4px #D52C2C;
+ -webkit-box-shadow: 2px 2px 4px #D52C2C;
+ box-shadow: 2px 2px 4px #D52C2C;
+}
+
+div.caution {
+ background-color: #FCC;
+ border: 1px solid #FAA;
+}
+
+div.attention {
+ background-color: #FCC;
+ border: 1px solid #FAA;
+}
+
+div.important {
+ background-color: #EEE;
+ border: 1px solid #CCC;
+}
+
+div.note {
+ background-color: #EEE;
+ border: 1px solid #CCC;
+}
+
+div.tip {
+ background-color: #EEE;
+ border: 1px solid #CCC;
+}
+
+div.hint {
+ background-color: #EEE;
+ border: 1px solid #CCC;
+}
+
+div.seealso {
+ background-color: #EEE;
+ border: 1px solid #CCC;
+}
+
+div.topic {
+ background-color: #EEE;
+}
+
+p.admonition-title {
+ display: inline;
+}
+
+p.admonition-title:after {
+ content: ":";
+}
+
+pre, tt, code {
+ font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
+ font-size: 0.9em;
+}
+
+.hll {
+ background-color: #FFC;
+ margin: 0 -12px;
+ padding: 0 12px;
+ display: block;
+}
+
+img.screenshot {
+}
+
+tt.descname, tt.descclassname, code.descname, code.descclassname {
+ font-size: 0.95em;
+}
+
+tt.descname, code.descname {
+ padding-right: 0.08em;
+}
+
+img.screenshot {
+ -moz-box-shadow: 2px 2px 4px #EEE;
+ -webkit-box-shadow: 2px 2px 4px #EEE;
+ box-shadow: 2px 2px 4px #EEE;
+}
+
+table.docutils {
+ border: 1px solid #888;
+ -moz-box-shadow: 2px 2px 4px #EEE;
+ -webkit-box-shadow: 2px 2px 4px #EEE;
+ box-shadow: 2px 2px 4px #EEE;
+}
+
+table.docutils td, table.docutils th {
+ border: 1px solid #888;
+ padding: 0.25em 0.7em;
+}
+
+table.field-list, table.footnote {
+ border: none;
+ -moz-box-shadow: none;
+ -webkit-box-shadow: none;
+ box-shadow: none;
+}
+
+table.footnote {
+ margin: 15px 0;
+ width: 100%;
+ border: 1px solid #EEE;
+ background: #FDFDFD;
+ font-size: 0.9em;
+}
+
+table.footnote + table.footnote {
+ margin-top: -15px;
+ border-top: none;
+}
+
+table.field-list th {
+ padding: 0 0.8em 0 0;
+}
+
+table.field-list td {
+ padding: 0;
+}
+
+table.field-list p {
+ margin-bottom: 0.8em;
+}
+
+/* Cloned from
+ * https://github.com/sphinx-doc/sphinx/commit/ef60dbfce09286b20b7385333d63a60321784e68
+ */
+.field-name {
+ -moz-hyphens: manual;
+ -ms-hyphens: manual;
+ -webkit-hyphens: manual;
+ hyphens: manual;
+}
+
+table.footnote td.label {
+ width: .1px;
+ padding: 0.3em 0 0.3em 0.5em;
+}
+
+table.footnote td {
+ padding: 0.3em 0.5em;
+}
+
+dl {
+ margin: 0;
+ padding: 0;
+}
+
+dl dd {
+ margin-left: 30px;
+}
+
+blockquote {
+ margin: 0 0 0 30px;
+ padding: 0;
+}
+
+ul, ol {
+ /* Matches the 30px from the narrow-screen "li > ul" selector below */
+ margin: 10px 0 10px 30px;
+ padding: 0;
+}
+
+pre {
+ background: #EEE;
+ padding: 7px 30px;
+ margin: 15px 0px;
+ line-height: 1.3em;
+}
+
+div.viewcode-block:target {
+ background: #ffd;
+}
+
+dl pre, blockquote pre, li pre {
+ margin-left: 0;
+ padding-left: 30px;
+}
+
+tt, code {
+ background-color: #ecf0f3;
+ color: #222;
+ /* padding: 1px 2px; */
+}
+
+tt.xref, code.xref, a tt {
+ background-color: #FBFBFB;
+ border-bottom: 1px solid #fff;
+}
+
+a.reference {
+ text-decoration: none;
+ border-bottom: 1px dotted #004B6B;
+}
+
+/* Don't put an underline on images */
+a.image-reference, a.image-reference:hover {
+ border-bottom: none;
+}
+
+a.reference:hover {
+ border-bottom: 1px solid #6D4100;
+}
+
+a.footnote-reference {
+ text-decoration: none;
+ font-size: 0.7em;
+ vertical-align: top;
+ border-bottom: 1px dotted #004B6B;
+}
+
+a.footnote-reference:hover {
+ border-bottom: 1px solid #6D4100;
+}
+
+a:hover tt, a:hover code {
+ background: #EEE;
+}
+
+
+@media screen and (max-width: 870px) {
+
+ div.sphinxsidebar {
+ display: none;
+ }
+
+ div.document {
+ width: 100%;
+
+ }
+
+ div.documentwrapper {
+ margin-left: 0;
+ margin-top: 0;
+ margin-right: 0;
+ margin-bottom: 0;
+ }
+
+ div.bodywrapper {
+ margin-top: 0;
+ margin-right: 0;
+ margin-bottom: 0;
+ margin-left: 0;
+ }
+
+ ul {
+ margin-left: 0;
+ }
+
+ li > ul {
+ /* Matches the 30px from the "ul, ol" selector above */
+ margin-left: 30px;
+ }
+
+ .document {
+ width: auto;
+ }
+
+ .footer {
+ width: auto;
+ }
+
+ .bodywrapper {
+ margin: 0;
+ }
+
+ .footer {
+ width: auto;
+ }
+
+ .github {
+ display: none;
+ }
+
+
+
+}
+
+
+
+@media screen and (max-width: 875px) {
+
+ body {
+ margin: 0;
+ padding: 20px 30px;
+ }
+
+ div.documentwrapper {
+ float: none;
+ background: #fff;
+ }
+
+ div.sphinxsidebar {
+ display: block;
+ float: none;
+ width: 102.5%;
+ margin: 50px -30px -20px -30px;
+ padding: 10px 20px;
+ background: #333;
+ color: #FFF;
+ }
+
+ div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p,
+ div.sphinxsidebar h3 a {
+ color: #fff;
+ }
+
+ div.sphinxsidebar a {
+ color: #AAA;
+ }
+
+ div.sphinxsidebar p.logo {
+ display: none;
+ }
+
+ div.document {
+ width: 100%;
+ margin: 0;
+ }
+
+ div.footer {
+ display: none;
+ }
+
+ div.bodywrapper {
+ margin: 0;
+ }
+
+ div.body {
+ min-height: 0;
+ padding: 0;
+ }
+
+ .rtd_doc_footer {
+ display: none;
+ }
+
+ .document {
+ width: auto;
+ }
+
+ .footer {
+ width: auto;
+ }
+
+ .footer {
+ width: auto;
+ }
+
+ .github {
+ display: none;
+ }
+}
+
+
+/* misc. */
+
+.revsys-inline {
+ display: none!important;
+}
+
+/* Make nested-list/multi-paragraph items look better in Releases changelog
+ * pages. Without this, docutils' magical list fuckery causes inconsistent
+ * formatting between different release sub-lists.
+ */
+div#changelog > div.section > ul > li > p:only-child {
+ margin-bottom: 0;
+}
+
+/* Hide fugly table cell borders in ..bibliography:: directive output */
+table.docutils.citation, table.docutils.citation td, table.docutils.citation th {
+ border: none;
+ /* Below needed in some edge cases; if not applied, bottom shadows appear */
+ -moz-box-shadow: none;
+ -webkit-box-shadow: none;
+ box-shadow: none;
+}
+
+
+/* relbar */
+
+.related {
+ line-height: 30px;
+ width: 100%;
+ font-size: 0.9rem;
+}
+
+.related.top {
+ border-bottom: 1px solid #EEE;
+ margin-bottom: 20px;
+}
+
+.related.bottom {
+ border-top: 1px solid #EEE;
+}
+
+.related ul {
+ padding: 0;
+ margin: 0;
+ list-style: none;
+}
+
+.related li {
+ display: inline;
+}
+
+nav#rellinks {
+ float: right;
+}
+
+nav#rellinks li+li:before {
+ content: "|";
+}
+
+nav#breadcrumbs li+li:before {
+ content: "\00BB";
+}
+
+/* Hide certain items when printing */
+@media print {
+ div.related {
+ display: none;
+ }
+}
\ No newline at end of file
diff --git a/code/codetime_server/docs/build/html/_static/basic.css b/code/codetime_server/docs/build/html/_static/basic.css
new file mode 100644
index 0000000..0119285
--- /dev/null
+++ b/code/codetime_server/docs/build/html/_static/basic.css
@@ -0,0 +1,768 @@
+/*
+ * basic.css
+ * ~~~~~~~~~
+ *
+ * Sphinx stylesheet -- basic theme.
+ *
+ * :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS.
+ * :license: BSD, see LICENSE for details.
+ *
+ */
+
+/* -- main layout ----------------------------------------------------------- */
+
+div.clearer {
+ clear: both;
+}
+
+/* -- relbar ---------------------------------------------------------------- */
+
+div.related {
+ width: 100%;
+ font-size: 90%;
+}
+
+div.related h3 {
+ display: none;
+}
+
+div.related ul {
+ margin: 0;
+ padding: 0 0 0 10px;
+ list-style: none;
+}
+
+div.related li {
+ display: inline;
+}
+
+div.related li.right {
+ float: right;
+ margin-right: 5px;
+}
+
+/* -- sidebar --------------------------------------------------------------- */
+
+div.sphinxsidebarwrapper {
+ padding: 10px 5px 0 10px;
+}
+
+div.sphinxsidebar {
+ float: left;
+ width: 230px;
+ margin-left: -100%;
+ font-size: 90%;
+ word-wrap: break-word;
+ overflow-wrap : break-word;
+}
+
+div.sphinxsidebar ul {
+ list-style: none;
+}
+
+div.sphinxsidebar ul ul,
+div.sphinxsidebar ul.want-points {
+ margin-left: 20px;
+ list-style: square;
+}
+
+div.sphinxsidebar ul ul {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+div.sphinxsidebar form {
+ margin-top: 10px;
+}
+
+div.sphinxsidebar input {
+ border: 1px solid #98dbcc;
+ font-family: sans-serif;
+ font-size: 1em;
+}
+
+div.sphinxsidebar #searchbox form.search {
+ overflow: hidden;
+}
+
+div.sphinxsidebar #searchbox input[type="text"] {
+ float: left;
+ width: 80%;
+ padding: 0.25em;
+ box-sizing: border-box;
+}
+
+div.sphinxsidebar #searchbox input[type="submit"] {
+ float: left;
+ width: 20%;
+ border-left: none;
+ padding: 0.25em;
+ box-sizing: border-box;
+}
+
+
+img {
+ border: 0;
+ max-width: 100%;
+}
+
+/* -- search page ----------------------------------------------------------- */
+
+ul.search {
+ margin: 10px 0 0 20px;
+ padding: 0;
+}
+
+ul.search li {
+ padding: 5px 0 5px 20px;
+ background-image: url(file.png);
+ background-repeat: no-repeat;
+ background-position: 0 7px;
+}
+
+ul.search li a {
+ font-weight: bold;
+}
+
+ul.search li div.context {
+ color: #888;
+ margin: 2px 0 0 30px;
+ text-align: left;
+}
+
+ul.keywordmatches li.goodmatch a {
+ font-weight: bold;
+}
+
+/* -- index page ------------------------------------------------------------ */
+
+table.contentstable {
+ width: 90%;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+table.contentstable p.biglink {
+ line-height: 150%;
+}
+
+a.biglink {
+ font-size: 1.3em;
+}
+
+span.linkdescr {
+ font-style: italic;
+ padding-top: 5px;
+ font-size: 90%;
+}
+
+/* -- general index --------------------------------------------------------- */
+
+table.indextable {
+ width: 100%;
+}
+
+table.indextable td {
+ text-align: left;
+ vertical-align: top;
+}
+
+table.indextable ul {
+ margin-top: 0;
+ margin-bottom: 0;
+ list-style-type: none;
+}
+
+table.indextable > tbody > tr > td > ul {
+ padding-left: 0em;
+}
+
+table.indextable tr.pcap {
+ height: 10px;
+}
+
+table.indextable tr.cap {
+ margin-top: 10px;
+ background-color: #f2f2f2;
+}
+
+img.toggler {
+ margin-right: 3px;
+ margin-top: 3px;
+ cursor: pointer;
+}
+
+div.modindex-jumpbox {
+ border-top: 1px solid #ddd;
+ border-bottom: 1px solid #ddd;
+ margin: 1em 0 1em 0;
+ padding: 0.4em;
+}
+
+div.genindex-jumpbox {
+ border-top: 1px solid #ddd;
+ border-bottom: 1px solid #ddd;
+ margin: 1em 0 1em 0;
+ padding: 0.4em;
+}
+
+/* -- domain module index --------------------------------------------------- */
+
+table.modindextable td {
+ padding: 2px;
+ border-collapse: collapse;
+}
+
+/* -- general body styles --------------------------------------------------- */
+
+div.body {
+ min-width: 450px;
+ max-width: 800px;
+}
+
+div.body p, div.body dd, div.body li, div.body blockquote {
+ -moz-hyphens: auto;
+ -ms-hyphens: auto;
+ -webkit-hyphens: auto;
+ hyphens: auto;
+}
+
+a.headerlink {
+ visibility: hidden;
+}
+
+a.brackets:before,
+span.brackets > a:before{
+ content: "[";
+}
+
+a.brackets:after,
+span.brackets > a:after {
+ content: "]";
+}
+
+h1:hover > a.headerlink,
+h2:hover > a.headerlink,
+h3:hover > a.headerlink,
+h4:hover > a.headerlink,
+h5:hover > a.headerlink,
+h6:hover > a.headerlink,
+dt:hover > a.headerlink,
+caption:hover > a.headerlink,
+p.caption:hover > a.headerlink,
+div.code-block-caption:hover > a.headerlink {
+ visibility: visible;
+}
+
+div.body p.caption {
+ text-align: inherit;
+}
+
+div.body td {
+ text-align: left;
+}
+
+.first {
+ margin-top: 0 !important;
+}
+
+p.rubric {
+ margin-top: 30px;
+ font-weight: bold;
+}
+
+img.align-left, .figure.align-left, object.align-left {
+ clear: left;
+ float: left;
+ margin-right: 1em;
+}
+
+img.align-right, .figure.align-right, object.align-right {
+ clear: right;
+ float: right;
+ margin-left: 1em;
+}
+
+img.align-center, .figure.align-center, object.align-center {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+img.align-default, .figure.align-default {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.align-left {
+ text-align: left;
+}
+
+.align-center {
+ text-align: center;
+}
+
+.align-default {
+ text-align: center;
+}
+
+.align-right {
+ text-align: right;
+}
+
+/* -- sidebars -------------------------------------------------------------- */
+
+div.sidebar {
+ margin: 0 0 0.5em 1em;
+ border: 1px solid #ddb;
+ padding: 7px 7px 0 7px;
+ background-color: #ffe;
+ width: 40%;
+ float: right;
+}
+
+p.sidebar-title {
+ font-weight: bold;
+}
+
+/* -- topics ---------------------------------------------------------------- */
+
+div.topic {
+ border: 1px solid #ccc;
+ padding: 7px 7px 0 7px;
+ margin: 10px 0 10px 0;
+}
+
+p.topic-title {
+ font-size: 1.1em;
+ font-weight: bold;
+ margin-top: 10px;
+}
+
+/* -- admonitions ----------------------------------------------------------- */
+
+div.admonition {
+ margin-top: 10px;
+ margin-bottom: 10px;
+ padding: 7px;
+}
+
+div.admonition dt {
+ font-weight: bold;
+}
+
+div.admonition dl {
+ margin-bottom: 0;
+}
+
+p.admonition-title {
+ margin: 0px 10px 5px 0px;
+ font-weight: bold;
+}
+
+div.body p.centered {
+ text-align: center;
+ margin-top: 25px;
+}
+
+/* -- tables ---------------------------------------------------------------- */
+
+table.docutils {
+ border: 0;
+ border-collapse: collapse;
+}
+
+table.align-center {
+ margin-left: auto;
+ margin-right: auto;
+}
+
+table.align-default {
+ margin-left: auto;
+ margin-right: auto;
+}
+
+table caption span.caption-number {
+ font-style: italic;
+}
+
+table caption span.caption-text {
+}
+
+table.docutils td, table.docutils th {
+ padding: 1px 8px 1px 5px;
+ border-top: 0;
+ border-left: 0;
+ border-right: 0;
+ border-bottom: 1px solid #aaa;
+}
+
+table.footnote td, table.footnote th {
+ border: 0 !important;
+}
+
+th {
+ text-align: left;
+ padding-right: 5px;
+}
+
+table.citation {
+ border-left: solid 1px gray;
+ margin-left: 1px;
+}
+
+table.citation td {
+ border-bottom: none;
+}
+
+th > p:first-child,
+td > p:first-child {
+ margin-top: 0px;
+}
+
+th > p:last-child,
+td > p:last-child {
+ margin-bottom: 0px;
+}
+
+/* -- figures --------------------------------------------------------------- */
+
+div.figure {
+ margin: 0.5em;
+ padding: 0.5em;
+}
+
+div.figure p.caption {
+ padding: 0.3em;
+}
+
+div.figure p.caption span.caption-number {
+ font-style: italic;
+}
+
+div.figure p.caption span.caption-text {
+}
+
+/* -- field list styles ----------------------------------------------------- */
+
+table.field-list td, table.field-list th {
+ border: 0 !important;
+}
+
+.field-list ul {
+ margin: 0;
+ padding-left: 1em;
+}
+
+.field-list p {
+ margin: 0;
+}
+
+.field-name {
+ -moz-hyphens: manual;
+ -ms-hyphens: manual;
+ -webkit-hyphens: manual;
+ hyphens: manual;
+}
+
+/* -- hlist styles ---------------------------------------------------------- */
+
+table.hlist td {
+ vertical-align: top;
+}
+
+
+/* -- other body styles ----------------------------------------------------- */
+
+ol.arabic {
+ list-style: decimal;
+}
+
+ol.loweralpha {
+ list-style: lower-alpha;
+}
+
+ol.upperalpha {
+ list-style: upper-alpha;
+}
+
+ol.lowerroman {
+ list-style: lower-roman;
+}
+
+ol.upperroman {
+ list-style: upper-roman;
+}
+
+li > p:first-child {
+ margin-top: 0px;
+}
+
+li > p:last-child {
+ margin-bottom: 0px;
+}
+
+dl.footnote > dt,
+dl.citation > dt {
+ float: left;
+}
+
+dl.footnote > dd,
+dl.citation > dd {
+ margin-bottom: 0em;
+}
+
+dl.footnote > dd:after,
+dl.citation > dd:after {
+ content: "";
+ clear: both;
+}
+
+dl.field-list {
+ display: grid;
+ grid-template-columns: fit-content(30%) auto;
+}
+
+dl.field-list > dt {
+ font-weight: bold;
+ word-break: break-word;
+ padding-left: 0.5em;
+ padding-right: 5px;
+}
+
+dl.field-list > dt:after {
+ content: ":";
+}
+
+dl.field-list > dd {
+ padding-left: 0.5em;
+ margin-top: 0em;
+ margin-left: 0em;
+ margin-bottom: 0em;
+}
+
+dl {
+ margin-bottom: 15px;
+}
+
+dd > p:first-child {
+ margin-top: 0px;
+}
+
+dd ul, dd table {
+ margin-bottom: 10px;
+}
+
+dd {
+ margin-top: 3px;
+ margin-bottom: 10px;
+ margin-left: 30px;
+}
+
+dt:target, span.highlighted {
+ background-color: #fbe54e;
+}
+
+rect.highlighted {
+ fill: #fbe54e;
+}
+
+dl.glossary dt {
+ font-weight: bold;
+ font-size: 1.1em;
+}
+
+.optional {
+ font-size: 1.3em;
+}
+
+.sig-paren {
+ font-size: larger;
+}
+
+.versionmodified {
+ font-style: italic;
+}
+
+.system-message {
+ background-color: #fda;
+ padding: 5px;
+ border: 3px solid red;
+}
+
+.footnote:target {
+ background-color: #ffa;
+}
+
+.line-block {
+ display: block;
+ margin-top: 1em;
+ margin-bottom: 1em;
+}
+
+.line-block .line-block {
+ margin-top: 0;
+ margin-bottom: 0;
+ margin-left: 1.5em;
+}
+
+.guilabel, .menuselection {
+ font-family: sans-serif;
+}
+
+.accelerator {
+ text-decoration: underline;
+}
+
+.classifier {
+ font-style: oblique;
+}
+
+.classifier:before {
+ font-style: normal;
+ margin: 0.5em;
+ content: ":";
+}
+
+abbr, acronym {
+ border-bottom: dotted 1px;
+ cursor: help;
+}
+
+/* -- code displays --------------------------------------------------------- */
+
+pre {
+ overflow: auto;
+ overflow-y: hidden; /* fixes display issues on Chrome browsers */
+}
+
+span.pre {
+ -moz-hyphens: none;
+ -ms-hyphens: none;
+ -webkit-hyphens: none;
+ hyphens: none;
+}
+
+td.linenos pre {
+ padding: 5px 0px;
+ border: 0;
+ background-color: transparent;
+ color: #aaa;
+}
+
+table.highlighttable {
+ margin-left: 0.5em;
+}
+
+table.highlighttable td {
+ padding: 0 0.5em 0 0.5em;
+}
+
+div.code-block-caption {
+ padding: 2px 5px;
+ font-size: small;
+}
+
+div.code-block-caption code {
+ background-color: transparent;
+}
+
+div.code-block-caption + div > div.highlight > pre {
+ margin-top: 0;
+}
+
+div.doctest > div.highlight span.gp { /* gp: Generic.Prompt */
+ user-select: none;
+}
+
+div.code-block-caption span.caption-number {
+ padding: 0.1em 0.3em;
+ font-style: italic;
+}
+
+div.code-block-caption span.caption-text {
+}
+
+div.literal-block-wrapper {
+ padding: 1em 1em 0;
+}
+
+div.literal-block-wrapper div.highlight {
+ margin: 0;
+}
+
+code.descname {
+ background-color: transparent;
+ font-weight: bold;
+ font-size: 1.2em;
+}
+
+code.descclassname {
+ background-color: transparent;
+}
+
+code.xref, a code {
+ background-color: transparent;
+ font-weight: bold;
+}
+
+h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
+ background-color: transparent;
+}
+
+.viewcode-link {
+ float: right;
+}
+
+.viewcode-back {
+ float: right;
+ font-family: sans-serif;
+}
+
+div.viewcode-block:target {
+ margin: -1px -10px;
+ padding: 0 10px;
+}
+
+/* -- math display ---------------------------------------------------------- */
+
+img.math {
+ vertical-align: middle;
+}
+
+div.body div.math p {
+ text-align: center;
+}
+
+span.eqno {
+ float: right;
+}
+
+span.eqno a.headerlink {
+ position: relative;
+ left: 0px;
+ z-index: 1;
+}
+
+div.math:hover a.headerlink {
+ visibility: visible;
+}
+
+/* -- printout stylesheet --------------------------------------------------- */
+
+@media print {
+ div.document,
+ div.documentwrapper,
+ div.bodywrapper {
+ margin: 0 !important;
+ width: 100%;
+ }
+
+ div.sphinxsidebar,
+ div.related,
+ div.footer,
+ #top-link {
+ display: none;
+ }
+}
\ No newline at end of file
diff --git a/code/codetime_server/docs/build/html/_static/custom.css b/code/codetime_server/docs/build/html/_static/custom.css
new file mode 100644
index 0000000..2a924f1
--- /dev/null
+++ b/code/codetime_server/docs/build/html/_static/custom.css
@@ -0,0 +1 @@
+/* This file intentionally left blank. */
diff --git a/code/codetime_server/docs/build/html/_static/doctools.js b/code/codetime_server/docs/build/html/_static/doctools.js
new file mode 100644
index 0000000..daccd20
--- /dev/null
+++ b/code/codetime_server/docs/build/html/_static/doctools.js
@@ -0,0 +1,315 @@
+/*
+ * doctools.js
+ * ~~~~~~~~~~~
+ *
+ * Sphinx JavaScript utilities for all documentation.
+ *
+ * :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS.
+ * :license: BSD, see LICENSE for details.
+ *
+ */
+
+/**
+ * select a different prefix for underscore
+ */
+$u = _.noConflict();
+
+/**
+ * make the code below compatible with browsers without
+ * an installed firebug like debugger
+if (!window.console || !console.firebug) {
+ var names = ["log", "debug", "info", "warn", "error", "assert", "dir",
+ "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace",
+ "profile", "profileEnd"];
+ window.console = {};
+ for (var i = 0; i < names.length; ++i)
+ window.console[names[i]] = function() {};
+}
+ */
+
+/**
+ * small helper function to urldecode strings
+ */
+jQuery.urldecode = function(x) {
+ return decodeURIComponent(x).replace(/\+/g, ' ');
+};
+
+/**
+ * small helper function to urlencode strings
+ */
+jQuery.urlencode = encodeURIComponent;
+
+/**
+ * This function returns the parsed url parameters of the
+ * current request. Multiple values per key are supported,
+ * it will always return arrays of strings for the value parts.
+ */
+jQuery.getQueryParameters = function(s) {
+ if (typeof s === 'undefined')
+ s = document.location.search;
+ var parts = s.substr(s.indexOf('?') + 1).split('&');
+ var result = {};
+ for (var i = 0; i < parts.length; i++) {
+ var tmp = parts[i].split('=', 2);
+ var key = jQuery.urldecode(tmp[0]);
+ var value = jQuery.urldecode(tmp[1]);
+ if (key in result)
+ result[key].push(value);
+ else
+ result[key] = [value];
+ }
+ return result;
+};
+
+/**
+ * highlight a given string on a jquery object by wrapping it in
+ * span elements with the given class name.
+ */
+jQuery.fn.highlightText = function(text, className) {
+ function highlight(node, addItems) {
+ if (node.nodeType === 3) {
+ var val = node.nodeValue;
+ var pos = val.toLowerCase().indexOf(text);
+ if (pos >= 0 &&
+ !jQuery(node.parentNode).hasClass(className) &&
+ !jQuery(node.parentNode).hasClass("nohighlight")) {
+ var span;
+ var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg");
+ if (isInSVG) {
+ span = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
+ } else {
+ span = document.createElement("span");
+ span.className = className;
+ }
+ span.appendChild(document.createTextNode(val.substr(pos, text.length)));
+ node.parentNode.insertBefore(span, node.parentNode.insertBefore(
+ document.createTextNode(val.substr(pos + text.length)),
+ node.nextSibling));
+ node.nodeValue = val.substr(0, pos);
+ if (isInSVG) {
+ var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ var bbox = node.parentElement.getBBox();
+ rect.x.baseVal.value = bbox.x;
+ rect.y.baseVal.value = bbox.y;
+ rect.width.baseVal.value = bbox.width;
+ rect.height.baseVal.value = bbox.height;
+ rect.setAttribute('class', className);
+ addItems.push({
+ "parent": node.parentNode,
+ "target": rect});
+ }
+ }
+ }
+ else if (!jQuery(node).is("button, select, textarea")) {
+ jQuery.each(node.childNodes, function() {
+ highlight(this, addItems);
+ });
+ }
+ }
+ var addItems = [];
+ var result = this.each(function() {
+ highlight(this, addItems);
+ });
+ for (var i = 0; i < addItems.length; ++i) {
+ jQuery(addItems[i].parent).before(addItems[i].target);
+ }
+ return result;
+};
+
+/*
+ * backward compatibility for jQuery.browser
+ * This will be supported until firefox bug is fixed.
+ */
+if (!jQuery.browser) {
+ jQuery.uaMatch = function(ua) {
+ ua = ua.toLowerCase();
+
+ var match = /(chrome)[ \/]([\w.]+)/.exec(ua) ||
+ /(webkit)[ \/]([\w.]+)/.exec(ua) ||
+ /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) ||
+ /(msie) ([\w.]+)/.exec(ua) ||
+ ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) ||
+ [];
+
+ return {
+ browser: match[ 1 ] || "",
+ version: match[ 2 ] || "0"
+ };
+ };
+ jQuery.browser = {};
+ jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true;
+}
+
+/**
+ * Small JavaScript module for the documentation.
+ */
+var Documentation = {
+
+ init : function() {
+ this.fixFirefoxAnchorBug();
+ this.highlightSearchWords();
+ this.initIndexTable();
+ if (DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) {
+ this.initOnKeyListeners();
+ }
+ },
+
+ /**
+ * i18n support
+ */
+ TRANSLATIONS : {},
+ PLURAL_EXPR : function(n) { return n === 1 ? 0 : 1; },
+ LOCALE : 'unknown',
+
+ // gettext and ngettext don't access this so that the functions
+ // can safely bound to a different name (_ = Documentation.gettext)
+ gettext : function(string) {
+ var translated = Documentation.TRANSLATIONS[string];
+ if (typeof translated === 'undefined')
+ return string;
+ return (typeof translated === 'string') ? translated : translated[0];
+ },
+
+ ngettext : function(singular, plural, n) {
+ var translated = Documentation.TRANSLATIONS[singular];
+ if (typeof translated === 'undefined')
+ return (n == 1) ? singular : plural;
+ return translated[Documentation.PLURALEXPR(n)];
+ },
+
+ addTranslations : function(catalog) {
+ for (var key in catalog.messages)
+ this.TRANSLATIONS[key] = catalog.messages[key];
+ this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')');
+ this.LOCALE = catalog.locale;
+ },
+
+ /**
+ * add context elements like header anchor links
+ */
+ addContextElements : function() {
+ $('div[id] > :header:first').each(function() {
+ $('\u00B6').
+ attr('href', '#' + this.id).
+ attr('title', _('Permalink to this headline')).
+ appendTo(this);
+ });
+ $('dt[id]').each(function() {
+ $('\u00B6').
+ attr('href', '#' + this.id).
+ attr('title', _('Permalink to this definition')).
+ appendTo(this);
+ });
+ },
+
+ /**
+ * workaround a firefox stupidity
+ * see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075
+ */
+ fixFirefoxAnchorBug : function() {
+ if (document.location.hash && $.browser.mozilla)
+ window.setTimeout(function() {
+ document.location.href += '';
+ }, 10);
+ },
+
+ /**
+ * highlight the search words provided in the url in the text
+ */
+ highlightSearchWords : function() {
+ var params = $.getQueryParameters();
+ var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : [];
+ if (terms.length) {
+ var body = $('div.body');
+ if (!body.length) {
+ body = $('body');
+ }
+ window.setTimeout(function() {
+ $.each(terms, function() {
+ body.highlightText(this.toLowerCase(), 'highlighted');
+ });
+ }, 10);
+ $('