├── .flake8 ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml ├── pull_request_template.md └── workflows │ ├── django.yml │ └── lint-and-format.yml ├── .gitignore ├── 2f870ea104fe8e3fe891bf40e9ecf6a5.jpg ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── analytics ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ └── __init__.py ├── models.py ├── tests.py ├── urls.py └── views.py ├── api ├── __init__.py ├── routes │ └── __init__.py ├── serializers │ └── __init__.py └── views │ └── __init__.py ├── community ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_discussion_description.py │ └── __init__.py ├── models.py ├── tests.py ├── urls.py └── views.py ├── datasets ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_initial.py │ ├── 0003_tag_remove_dataset_file_remove_dataset_format_and_more.py │ ├── 0004_alter_datasetversion_description_and_more.py │ ├── 0005_alter_rating_rating.py │ └── __init__.py ├── models.py ├── tests │ ├── __init__.py │ └── test_view.py ├── urls.py └── views.py ├── f02481bd7faa4ab33c4d08e7880b2b58(1).jpg ├── flake8-config ├── home ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ └── __init__.py ├── models.py ├── tests.py ├── urls.py └── views.py ├── manage.py ├── moderation ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ └── __init__.py ├── models.py ├── tests.py ├── urls.py └── views.py ├── package.json ├── postcss.config.js ├── pyproject.toml ├── pytest.ini ├── registration ├── __init__.py ├── admin.py ├── apps.py ├── auth_urls.py ├── backends │ ├── __init__.py │ └── default │ │ ├── __init__.py │ │ ├── urls.py │ │ └── views.py ├── forms.py ├── locale │ ├── ar │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── bg │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── ca │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── cs │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── da │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── de │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── el │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── en │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── es │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── es_AR │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── fa │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── fr │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── he │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── hr │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── hu │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── id │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── is │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── it │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── ja │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── ko │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── nb │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── nl │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── pl │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── pt │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── pt_BR │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── ru │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── sl │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── sr │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── sv │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── th │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── tr_TR │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── zh_CN │ │ └── LC_MESSAGES │ │ │ └── django.po │ └── zh_TW │ │ └── LC_MESSAGES │ │ └── django.po ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── cleanupregistration.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── signals.py ├── test_app │ ├── __init__.py │ ├── models.py │ ├── settings.py │ ├── settings_test.py │ ├── templates │ │ ├── base.html │ │ ├── index.html │ │ └── profile.html │ └── urls_default.py ├── tests │ ├── __init__.py │ ├── test_forms.py │ ├── test_forms_custom_user.py │ ├── test_models.py │ └── urls.py ├── urls.py ├── users.py ├── utils.py └── views.py ├── requirements.txt ├── search ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ └── __init__.py ├── models.py ├── tests.py ├── urls.py └── views.py ├── static ├── css │ ├── tailwind.css │ └── tailwind.output.css ├── images │ ├── contributor1.png │ ├── create-account-office-dark.jpeg │ ├── create-account-office.jpeg │ ├── dashboard.png │ ├── dog-image.png │ ├── forgot-password-office-dark.jpeg │ ├── forgot-password-office.jpeg │ ├── github.svg │ ├── login-office-dark.jpeg │ ├── login-office.jpeg │ ├── logo.png │ ├── person-with-dog.png │ └── twitter.svg └── js │ ├── focus-trap.js │ └── init-alpine.js ├── tailwind.config.js ├── templates ├── 404.html ├── base.html ├── community │ ├── create_discussion.html │ ├── discussion_detail.html │ ├── discussion_list.html │ └── edit_comment.html ├── dashboard.html ├── dashboard_navbar.html ├── dashboard_sidebar.html ├── datasets │ ├── add_version.html │ ├── dataset_detail.html │ ├── dataset_list.html │ └── upload_dataset.html ├── footer.html ├── home │ └── home.html ├── navbar.html ├── profiles │ ├── create_profile.html │ ├── home.html │ └── update.html ├── registration │ ├── activate.html │ ├── activation_complete.html │ ├── activation_email.html │ ├── activation_email.txt │ ├── activation_email_subject.txt │ ├── login.html │ ├── logout.html │ ├── password_change_done.html │ ├── password_change_form.html │ ├── password_reset_complete.html │ ├── password_reset_confirm.html │ ├── password_reset_done.html │ ├── password_reset_email.html │ ├── password_reset_form.html │ ├── registration_base.html │ ├── registration_closed.html │ ├── registration_complete.html │ ├── registration_form.html │ ├── resend_activation_complete.html │ └── resend_activation_form.html └── search │ └── search_results.html └── vetdatahub ├── __init__.py ├── asgi.py ├── settings.py ├── settings_dev.py ├── settings_staging.py ├── urls.py └── wsgi.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | ignore = E203, W503, F403, F401 ,E501, E402,E266, E303, E405, F405 4 | exclude = 5 | .git, 6 | venv, 7 | __pycache__, 8 | migrations, 9 | static, 10 | media, 11 | manage.py 12 | 13 | max-complexity = 10 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug 2 | description: Report an issue to help improve the project. 3 | title: '[BUG] ' 4 | labels: ['bug', 'status: awaiting triage'] 5 | body: 6 | - type: checkboxes 7 | id: duplicates 8 | attributes: 9 | label: Has this bug been raised before? 10 | description: Increase the chances of your issue being accepted by making sure it has not been raised before. 11 | options: 12 | - label: I have checked "open" AND "closed" issues and this is not a duplicate 13 | required: true 14 | - type: textarea 15 | id: description 16 | attributes: 17 | label: Description 18 | description: A clear description of the bug you have found. Please include relevant information and resources (for example the steps to reproduce the bug) 19 | validations: 20 | required: true 21 | - type: textarea 22 | id: steps 23 | attributes: 24 | label: Steps to Reproduce 25 | description: To help us recreate the bug, provide a numbered list of the exact steps taken to trigger the buggy behavior. 26 | value: | 27 | Include any relevant details like: 28 | 29 | - What page you were on... 30 | - What you were trying to do... 31 | - What went wrong... 32 | validations: 33 | required: true 34 | - type: textarea 35 | id: screenshots 36 | attributes: 37 | label: Screenshots 38 | description: Please add screenshots if applicable 39 | validations: 40 | required: false 41 | - type: dropdown 42 | id: assignee 43 | attributes: 44 | label: Do you want to work on this issue? 45 | multiple: false 46 | options: 47 | - 'No' 48 | - 'Yes' 49 | default: 0 50 | validations: 51 | required: false 52 | - type: textarea 53 | id: extrainfo 54 | attributes: 55 | label: If "yes" to above, please explain how you would technically implement this 56 | description: For example reference any existing code 57 | validations: 58 | required: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 💡 General Feature Request 2 | description: Have a new idea/feature? Let us know... 3 | title: '[FEATURE] ' 4 | labels: ['enhancement', 'feature', 'status: awaiting triage'] 5 | body: 6 | - type: checkboxes 7 | id: duplicates 8 | attributes: 9 | label: Is this a unique feature? 10 | description: Increase the chances of your issue being accepted by making sure it has not been raised before. 11 | options: 12 | - label: I have checked "open" AND "closed" issues and this is not a duplicate 13 | required: true 14 | - type: textarea 15 | attributes: 16 | label: Is your feature request related to a problem/unavailable functionality? Please describe. 17 | description: A clear and concise description of what the problem is (for example "I'm always frustrated when [...]"). 18 | validations: 19 | required: true 20 | - type: textarea 21 | id: description 22 | attributes: 23 | label: Proposed Solution 24 | description: A clear description of the enhancement you propose. Please include relevant information and resources (for example another project's implementation of this feature). 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: screenshots 29 | attributes: 30 | label: Screenshots 31 | description: Please add screenshots of the before and/or after the proposed changes. 32 | validations: 33 | required: false 34 | - type: dropdown 35 | id: assignee 36 | attributes: 37 | label: Do you want to work on this issue? 38 | multiple: false 39 | options: 40 | - 'No' 41 | - 'Yes' 42 | default: 0 43 | validations: 44 | required: false 45 | - type: textarea 46 | id: extrainfo 47 | attributes: 48 | label: If "yes" to above, please explain how you would technically implement this (issue will not be assigned if this is skipped) 49 | description: For example reference any existing code or library 50 | validations: 51 | required: false -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | #### THIS IS A PULL REQUEST TEMPLATE! 2 | 3 | ⚠️ This helps you complete a pull request. Please follow the instructions below. ⚠️ 4 | 5 | ### Submission Checklist 6 | 7 | --- 8 | 9 | Please complete the below checklist. Failure to follow these instructions may result in your pull request being closed without review. 10 | 11 | ### What issue does this PR closes? 12 | 13 | 14 | ### Description of file changes 15 | 16 | 17 | ### Is this PR still in progress or completed? 18 | - [ ] This PR is still a work in progress (WIP) 19 | - [ ] This PR is completed and ready for review 20 | 21 | ### Screenshot if applicable 22 | 23 | 24 | ### Additional Information 25 | -------------------------------------------------------------------------------- /.github/workflows/django.yml: -------------------------------------------------------------------------------- 1 | name: Django CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | max-parallel: 4 15 | matrix: 16 | python-version: ["3.10"] 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v3 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install Dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install -r requirements.txt 28 | - name: Run Tests 29 | run: | 30 | pytest 31 | 32 | -------------------------------------------------------------------------------- /.github/workflows/lint-and-format.yml: -------------------------------------------------------------------------------- 1 | name: Lint and Format 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: '3.8' 23 | 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install flake8 black 28 | 29 | - name: Run Flake8 30 | run: flake8 . 31 | 32 | - name: Run Black 33 | run: black --check . 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | upload_file.csv 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | .idea/ 163 | node_modules 164 | media 165 | -------------------------------------------------------------------------------- /2f870ea104fe8e3fe891bf40e9ecf6a5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/2f870ea104fe8e3fe891bf40e9ecf6a5.jpg -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # **VetDataHub Code of Conduct** 2 | 3 | ## **Introduction** 4 | At VetDataHub, we are committed to creating a safe, inclusive, and respectful environment for all contributors, users, and members of our community. This Code of Conduct outlines our expectations for behavior and provides guidance for reporting unacceptable behavior. 5 | 6 | ## **Scope** 7 | This Code of Conduct applies to all community spaces, including the VetDataHub platform, forums, GitHub repositories, Discord server, social media, and any in-person or virtual events organized by VetDataHub. 8 | 9 | --- 10 | 11 | ## **Our Standards** 12 | 13 | ### **Expected Behavior** 14 | - Be respectful and considerate in speech and actions. 15 | - Actively promote inclusivity and diversity. 16 | - Provide constructive feedback and engage in healthy discussions. 17 | - Respect differing opinions, viewpoints, and experiences. 18 | - Comply with all applicable laws and regulations. 19 | 20 | ### **Unacceptable Behavior** 21 | - Harassment, bullying, or discrimination of any kind, including on the basis of gender, sexual orientation, disability, ethnicity, religion, or age. 22 | - The use of inappropriate, abusive, or threatening language or imagery. 23 | - Posting or sharing illegal or inappropriate content, including data that violates privacy or ethical guidelines. 24 | - Disruptive behavior, such as trolling or spamming. 25 | - Unauthorized access to or misuse of the VetDataHub platform or resources. 26 | 27 | --- 28 | 29 | ## **Reporting Issues** 30 | If you witness or experience any unacceptable behavior, please report it to the VetDataHub team by emailing **[vetdatahub@gmail.com]**. All reports will be treated with confidentiality and addressed promptly. 31 | 32 | --- 33 | 34 | ## **Enforcement** 35 | VetDataHub moderators and administrators are responsible for enforcing this Code of Conduct. They have the authority to: 36 | - Remove comments, data, or other contributions that violate the Code of Conduct. 37 | - Restrict access to or remove individuals from community spaces for severe or repeated violations. 38 | - Take additional actions as necessary to maintain a safe and respectful environment. 39 | 40 | --- 41 | 42 | ## **Acknowledgment** 43 | This Code of Conduct is inspired by the **Contributor Covenant** and adapted to fit the needs of the VetDataHub community. 44 | 45 | --- 46 | 47 | ## **Contact Us** 48 | If you have any questions or concerns about this Code of Conduct, please reach out to the VetDataHub team at **[vetdatahub@gmail.com]**. 49 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to VetDataHub 2 | 3 | Thank you for your interest in contributing to **VetDataHub**! We appreciate your help in making our project better. To maintain a smooth collaboration process, please follow the guidelines outlined below. 4 | 5 | ## How to Contribute 6 | 7 | ### 1. Fork the Repository 8 | 9 | Start by forking the repository to your own GitHub account. This allows you to make changes freely without affecting the original project. 10 | 11 | ### 2. Clone Your Fork 12 | 13 | Clone your forked repository to your local machine: 14 | 15 | ```bash 16 | git clone https://github.com/yourusername/vetdatahub.git 17 | cd vetdatahub 18 | ``` 19 | 20 | ### 3. Create a New Branch 21 | 22 | Create a new branch for your feature or bug fix. It's essential to keep your changes organized and separate from the main codebase. 23 | 24 | ```bash 25 | git checkout -b your-feature-branch 26 | ``` 27 | 28 | ### 4. Set Up the Development Environment 29 | 30 | To maintain code quality, we use Black for code formatting and Flake8 for linting. You will need to set them up before contributing. 31 | Install Black and Flake8 32 | 33 | First, make sure you have the required dependencies: 34 | 35 | ```bash 36 | 37 | pip install -r requirements.txt 38 | ``` 39 | 40 | ### 5. Make Your Changes 41 | 42 | Implement your changes and make sure to follow the project's coding style and standards. 43 | 44 | - Ensure your code is clean, well-documented, and follows Python's PEP 8 style guide. 45 | - Use Black to format your code: 46 | 47 | ```bash 48 | 49 | black . 50 | ``` 51 | 52 | - Use Flake8 to lint your code and catch any style violations: 53 | 54 | ```bash 55 | 56 | flake8 . 57 | ``` 58 | 59 | - Write clear, concise commit messages. 60 | 61 | - Ensure your code is well-documented. 62 | 63 | - If applicable, add tests for your changes to ensure they work as intended. Make sure all tests pass before submitting your contribution. 64 | 65 | ### 6. Commit Your Changes 66 | 67 | When you're satisfied with your changes, commit them with a clear and descriptive commit message. 68 | 69 | ```bash 70 | git add . 71 | git commit -m "Add a brief description of your changes" 72 | ``` 73 | 74 | ### 7. Push to Your Branch 75 | 76 | Push your changes to your forked repository: 77 | 78 | ```bash 79 | git push origin your-feature-branch 80 | ``` 81 | 82 | ### 6. Open a Pull Request 83 | 84 | Go to the original **VetDataHub** repository and open a pull request (PR). Include a detailed description of your changes and reference any related issues. 85 | 86 | ### 7. Participate in the Review Process 87 | 88 | Once your PR is submitted, the maintainers will review your changes. Be open to feedback and make necessary adjustments as requested. 89 | 90 | ## Reporting Issues 91 | 92 | If you encounter a bug or have a feature request, please open an issue in the repository. Include the following information: 93 | 94 | - A clear and descriptive title. 95 | - A detailed description of the issue or feature. 96 | - Steps to reproduce the issue (if applicable). 97 | - Any relevant screenshots or code snippets. 98 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Johanan Oppong Amoateng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | django = "==5.1.1" 8 | black = "*" 9 | flake8 = "*" 10 | 11 | [dev-packages] 12 | 13 | [requires] 14 | python_version = "3.12" 15 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "3a3d6607976b6ff2ea64422abc0483df81aa287e60ad0e53bf363da69eaa36ec" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.12" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "asgiref": { 20 | "hashes": [ 21 | "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", 22 | "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590" 23 | ], 24 | "markers": "python_version >= '3.8'", 25 | "version": "==3.8.1" 26 | }, 27 | "black": { 28 | "hashes": [ 29 | "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f", 30 | "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd", 31 | "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea", 32 | "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981", 33 | "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b", 34 | "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7", 35 | "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8", 36 | "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175", 37 | "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d", 38 | "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392", 39 | "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad", 40 | "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f", 41 | "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f", 42 | "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b", 43 | "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875", 44 | "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3", 45 | "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800", 46 | "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65", 47 | "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2", 48 | "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812", 49 | "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50", 50 | "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e" 51 | ], 52 | "index": "pypi", 53 | "markers": "python_version >= '3.9'", 54 | "version": "==24.10.0" 55 | }, 56 | "click": { 57 | "hashes": [ 58 | "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", 59 | "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" 60 | ], 61 | "markers": "python_version >= '3.7'", 62 | "version": "==8.1.7" 63 | }, 64 | "django": { 65 | "hashes": [ 66 | "sha256:021ffb7fdab3d2d388bc8c7c2434eb9c1f6f4d09e6119010bbb1694dda286bc2", 67 | "sha256:71603f27dac22a6533fb38d83072eea9ddb4017fead6f67f2562a40402d61c3f" 68 | ], 69 | "index": "pypi", 70 | "markers": "python_version >= '3.10'", 71 | "version": "==5.1.1" 72 | }, 73 | "flake8": { 74 | "hashes": [ 75 | "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38", 76 | "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213" 77 | ], 78 | "index": "pypi", 79 | "markers": "python_full_version >= '3.8.1'", 80 | "version": "==7.1.1" 81 | }, 82 | "mccabe": { 83 | "hashes": [ 84 | "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", 85 | "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" 86 | ], 87 | "markers": "python_version >= '3.6'", 88 | "version": "==0.7.0" 89 | }, 90 | "mypy-extensions": { 91 | "hashes": [ 92 | "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", 93 | "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" 94 | ], 95 | "markers": "python_version >= '3.5'", 96 | "version": "==1.0.0" 97 | }, 98 | "packaging": { 99 | "hashes": [ 100 | "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", 101 | "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" 102 | ], 103 | "markers": "python_version >= '3.8'", 104 | "version": "==24.1" 105 | }, 106 | "pathspec": { 107 | "hashes": [ 108 | "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", 109 | "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" 110 | ], 111 | "markers": "python_version >= '3.8'", 112 | "version": "==0.12.1" 113 | }, 114 | "platformdirs": { 115 | "hashes": [ 116 | "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", 117 | "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb" 118 | ], 119 | "markers": "python_version >= '3.8'", 120 | "version": "==4.3.6" 121 | }, 122 | "pycodestyle": { 123 | "hashes": [ 124 | "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3", 125 | "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521" 126 | ], 127 | "markers": "python_version >= '3.8'", 128 | "version": "==2.12.1" 129 | }, 130 | "pyflakes": { 131 | "hashes": [ 132 | "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", 133 | "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a" 134 | ], 135 | "markers": "python_version >= '3.8'", 136 | "version": "==3.2.0" 137 | }, 138 | "sqlparse": { 139 | "hashes": [ 140 | "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4", 141 | "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e" 142 | ], 143 | "markers": "python_version >= '3.8'", 144 | "version": "==0.5.1" 145 | } 146 | }, 147 | "develop": {} 148 | } 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # VetDataHub_Website 3 | 4 | ![VetDataHub Logo](./2f870ea104fe8e3fe891bf40e9ecf6a5.jpg) 5 | 6 | **VetDataHub** is an open-source veterinary dataset repository aimed at facilitating the sharing and exchange of diverse datasets in the veterinary field. The platform is designed to handle large datasets efficiently, support collaboration among researchers and practitioners, and provide tools for exploring and contributing to datasets. 7 | 8 | ## Features 9 | 10 | - **Dataset Hosting:** Easily upload and host your veterinary datasets. 11 | - **Metadata Management:** Manage metadata for each dataset, including descriptions, keywords, and more. 12 | - **Search Functionality:** Quickly search for datasets based on keywords and filters. 13 | - **API Access:** Access datasets programmatically through a well-documented API. 14 | - **Community Discussions:** Engage with other users in community discussion forums. 15 | - **Version Control:** Keep track of different versions of datasets and revert if necessary. 16 | - **Rating and Reviews:** Rate datasets and leave reviews to help others in the community. 17 | - **User Profiles:** Create profiles showcasing your contributions and activity on the platform. 18 | - **Responsive Design:** Access the platform on any device with a mobile-friendly design. 19 | 20 | ## Installation 21 | 22 | To set up the VetDataHub locally, follow these steps: 23 | 24 | 1. **Clone the repository:** 25 | ```bash 26 | git clone https://github.com/yourusername/VetDataHub_Website.git 27 | cd vetdatahub 28 | ``` 29 | 30 | 2. **Install dependencies:** 31 | Make sure you have Python and pip installed. Then run: 32 | ```bash 33 | pip install -r requirements.txt 34 | ``` 35 | 36 | 3. **Set up the database:** 37 | Configure your database settings in the `settings.py` file, which you can find in the ``vetdatahub``, folder and run: 38 | ```bash 39 | python manage.py migrate 40 | ``` 41 | 42 | 4. **Run the development server:** 43 | ```bash 44 | python manage.py runserver 45 | ``` 46 | 47 | 5. **Access the application:** 48 | Open your web browser and go to `http://127.0.0.1:8000`. 49 | 50 | ## Usage 51 | 52 | Once the application is running, you can: 53 | 54 | - **Create an account** to upload datasets and engage with the community. 55 | - **Browse datasets** by categories or use the search functionality. 56 | - **Participate in discussions** and provide feedback on datasets. 57 | - **Access the API** for programmatic access to datasets. 58 | 59 | ## Commmunity 60 | You can join the VetDataHub community on discord [here](https://discord.gg/XEKB767wgT) 61 | 62 | ## Contributing 63 | 64 | We welcome contributions to improve VetDataHub! To contribute: 65 | [Read](./CONTRIBUTING.md) 66 | 67 | ## 🙇‍♂️ Acknowledgments 68 | 69 |

This project is proudly supported by:

70 |

71 | 72 | 73 | 74 |

75 | ## License 76 | 77 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 78 | 79 | ## Contact 80 | 81 | For questions or feedback, please reach out: 82 | 83 | - **Project Lead:** [Johanan Oppong Amoateng](mailto:johananoppongamoateng2001@gmail.com) 84 | - **GitHub:** [JohananOppongAmoateng](https://github.com/JohananOppongAmoateng) 85 | -------------------------------------------------------------------------------- /analytics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/analytics/__init__.py -------------------------------------------------------------------------------- /analytics/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /analytics/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AnalyticsConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "analytics" 7 | -------------------------------------------------------------------------------- /analytics/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/analytics/migrations/__init__.py -------------------------------------------------------------------------------- /analytics/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /analytics/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /analytics/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | urlpatterns = [] 4 | -------------------------------------------------------------------------------- /analytics/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/api/__init__.py -------------------------------------------------------------------------------- /api/routes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/api/routes/__init__.py -------------------------------------------------------------------------------- /api/serializers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/api/serializers/__init__.py -------------------------------------------------------------------------------- /api/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/api/views/__init__.py -------------------------------------------------------------------------------- /community/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/community/__init__.py -------------------------------------------------------------------------------- /community/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | from community.models import Discussion, Comment 5 | 6 | admin.site.register(Discussion) 7 | admin.site.register(Comment) 8 | -------------------------------------------------------------------------------- /community/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CommunityConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "community" 7 | -------------------------------------------------------------------------------- /community/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from community.models import Discussion, Comment 3 | 4 | 5 | class DiscussionForm(forms.ModelForm): 6 | """ 7 | Form to create or update a discussion. 8 | """ 9 | 10 | class Meta: 11 | model = Discussion 12 | fields = ["title", "description"] 13 | widgets = { 14 | "title": forms.TextInput( 15 | attrs={ 16 | "class": "form-control", 17 | "placeholder": "Enter discussion title", 18 | } 19 | ), 20 | } 21 | 22 | 23 | class CommentForm(forms.ModelForm): 24 | """ 25 | Form to create a comment on a discussion. 26 | """ 27 | 28 | class Meta: 29 | model = Comment 30 | fields = ["content"] 31 | widgets = { 32 | "content": forms.Textarea( 33 | attrs={ 34 | "class": "form-control", 35 | "placeholder": "Write your comment here", 36 | } 37 | ), 38 | } 39 | -------------------------------------------------------------------------------- /community/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-11-16 20:50 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ( 14 | "datasets", 15 | "0003_tag_remove_dataset_file_remove_dataset_format_and_more", 16 | ), 17 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 18 | ] 19 | 20 | operations = [ 21 | migrations.CreateModel( 22 | name="Discussion", 23 | fields=[ 24 | ( 25 | "id", 26 | models.BigAutoField( 27 | auto_created=True, 28 | primary_key=True, 29 | serialize=False, 30 | verbose_name="ID", 31 | ), 32 | ), 33 | ( 34 | "title", 35 | models.CharField( 36 | help_text="Title of the discussion.", max_length=255 37 | ), 38 | ), 39 | ( 40 | "created_at", 41 | models.DateTimeField( 42 | auto_now_add=True, 43 | help_text="Timestamp when the discussion was created.", 44 | ), 45 | ), 46 | ( 47 | "updated_at", 48 | models.DateTimeField( 49 | auto_now=True, 50 | help_text="Timestamp when the discussion was last updated.", 51 | ), 52 | ), 53 | ( 54 | "slug", 55 | models.SlugField( 56 | blank=True, 57 | help_text="Slug for the discussion.", 58 | unique=True, 59 | ), 60 | ), 61 | ( 62 | "created_by", 63 | models.ForeignKey( 64 | help_text="The user who started the discussion.", 65 | on_delete=django.db.models.deletion.CASCADE, 66 | related_name="created_discussions", 67 | to=settings.AUTH_USER_MODEL, 68 | ), 69 | ), 70 | ( 71 | "dataset", 72 | models.ForeignKey( 73 | help_text="The dataset this discussion is related to.", 74 | on_delete=django.db.models.deletion.CASCADE, 75 | related_name="discussions", 76 | to="datasets.dataset", 77 | ), 78 | ), 79 | ], 80 | ), 81 | migrations.CreateModel( 82 | name="Comment", 83 | fields=[ 84 | ( 85 | "id", 86 | models.BigAutoField( 87 | auto_created=True, 88 | primary_key=True, 89 | serialize=False, 90 | verbose_name="ID", 91 | ), 92 | ), 93 | ( 94 | "content", 95 | models.TextField(help_text="Content of the comment."), 96 | ), 97 | ( 98 | "created_at", 99 | models.DateTimeField( 100 | auto_now_add=True, 101 | help_text="Timestamp when the comment was created.", 102 | ), 103 | ), 104 | ( 105 | "updated_at", 106 | models.DateTimeField( 107 | auto_now=True, 108 | help_text="Timestamp when the comment was last updated.", 109 | ), 110 | ), 111 | ( 112 | "created_by", 113 | models.ForeignKey( 114 | help_text="The user who created the comment.", 115 | on_delete=django.db.models.deletion.CASCADE, 116 | related_name="comments", 117 | to=settings.AUTH_USER_MODEL, 118 | ), 119 | ), 120 | ( 121 | "discussion", 122 | models.ForeignKey( 123 | help_text="The discussion this comment belongs to.", 124 | on_delete=django.db.models.deletion.CASCADE, 125 | related_name="comments", 126 | to="community.discussion", 127 | ), 128 | ), 129 | ], 130 | ), 131 | ] 132 | -------------------------------------------------------------------------------- /community/migrations/0002_discussion_description.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-12-07 20:59 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("community", "0001_initial"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="discussion", 15 | name="description", 16 | field=models.TextField(default="dsadsasdsadasd"), 17 | preserve_default=False, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /community/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/community/migrations/__init__.py -------------------------------------------------------------------------------- /community/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth import get_user_model 3 | from django.utils.text import slugify 4 | from datetime import timedelta 5 | from django.utils.timezone import now 6 | from datasets.models import Dataset 7 | 8 | 9 | User = get_user_model() 10 | 11 | 12 | class Discussion(models.Model): 13 | """ 14 | Represents a discussion thread associated with a specific dataset. 15 | """ 16 | 17 | title = models.CharField( 18 | max_length=255, help_text="Title of the discussion." 19 | ) 20 | description = models.TextField() 21 | dataset = models.ForeignKey( 22 | Dataset, 23 | on_delete=models.CASCADE, 24 | related_name="discussions", 25 | help_text="The dataset this discussion is related to.", 26 | ) 27 | created_by = models.ForeignKey( 28 | User, 29 | on_delete=models.CASCADE, 30 | related_name="created_discussions", 31 | help_text="The user who started the discussion.", 32 | ) 33 | created_at = models.DateTimeField( 34 | auto_now_add=True, 35 | help_text="Timestamp when the discussion was created.", 36 | ) 37 | updated_at = models.DateTimeField( 38 | auto_now=True, 39 | help_text="Timestamp when the discussion was last updated.", 40 | ) 41 | slug = models.SlugField( 42 | unique=True, blank=True, help_text="Slug for the discussion." 43 | ) 44 | 45 | def save(self, *args, **kwargs): 46 | if not self.slug: 47 | self.slug = slugify(self.title) 48 | super().save(*args, **kwargs) 49 | 50 | def __str__(self): 51 | return self.title 52 | 53 | @property 54 | def get_comments(self): 55 | return self.comments.all() 56 | 57 | 58 | class Comment(models.Model): 59 | """ 60 | Represents a comment on a discussion thread. 61 | """ 62 | 63 | discussion = models.ForeignKey( 64 | Discussion, 65 | on_delete=models.CASCADE, 66 | related_name="comments", 67 | help_text="The discussion this comment belongs to.", 68 | ) 69 | created_by = models.ForeignKey( 70 | User, 71 | on_delete=models.CASCADE, 72 | related_name="comments", 73 | help_text="The user who created the comment.", 74 | ) 75 | content = models.TextField(help_text="Content of the comment.") 76 | created_at = models.DateTimeField( 77 | auto_now_add=True, help_text="Timestamp when the comment was created." 78 | ) 79 | updated_at = models.DateTimeField( 80 | auto_now=True, help_text="Timestamp when the comment was last updated." 81 | ) 82 | 83 | def __str__(self): 84 | return f"Comment by {self.created_by} on {self.discussion}" 85 | 86 | def is_editable(self): 87 | """ 88 | Determines if the comment can still be edited (10-minute window). 89 | """ 90 | return now() <= self.created_at + timedelta(minutes=10) 91 | -------------------------------------------------------------------------------- /community/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /community/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from community import views 3 | 4 | urlpatterns = [ 5 | path( 6 | "discussions/", 7 | views.discussion_list, 8 | name="discussion_list", 9 | ), 10 | path( 11 | "/discussions/", 12 | views.discussion_list_by_dataset, 13 | name="discussion_list_by_dataset", 14 | ), 15 | path( 16 | "discussion//", 17 | views.discussion_detail, 18 | name="discussion_detail", 19 | ), 20 | path( 21 | "discussion//create-discussion/", 22 | views.create_discussion, 23 | name="create_discussion", 24 | ), 25 | path( 26 | "discussion//delete/", 27 | views.delete_discussion, 28 | name="delete_discussion", 29 | ), 30 | path( 31 | "comment//edit/", 32 | views.edit_comment, 33 | name="edit_comment", 34 | ), 35 | path( 36 | "comment//delete/", 37 | views.delete_comment, 38 | name="delete_comment", 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /community/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, get_object_or_404, redirect 2 | from django.contrib.auth.decorators import login_required 3 | from django.urls import reverse 4 | from django.core.paginator import Paginator 5 | from django.http import HttpResponseForbidden 6 | from .models import Discussion, Comment 7 | from .forms import DiscussionForm, CommentForm 8 | 9 | 10 | def discussion_list_by_dataset(request, dataset_id): 11 | """ 12 | Displays a paginated list of discussions for a specific dataset. 13 | """ 14 | discussions = Discussion.objects.filter(dataset_id=dataset_id).order_by( 15 | "-created_at" 16 | ) 17 | paginator = Paginator(discussions, 10) # Show 10 discussions per page 18 | page_number = request.GET.get("page") 19 | page_obj = paginator.get_page(page_number) 20 | 21 | return render( 22 | request, "community/discussion_list.html", {"page_obj": page_obj} 23 | ) 24 | 25 | 26 | def discussion_list(request): 27 | """ 28 | Displays a paginated list of discussions. 29 | """ 30 | discussions = Discussion.objects.all().order_by("-created_at") 31 | paginator = Paginator(discussions, 10) # Show 10 discussions per page 32 | page_number = request.GET.get("page") 33 | page_obj = paginator.get_page(page_number) 34 | 35 | return render( 36 | request, "community/discussion_list.html", {"page_obj": page_obj} 37 | ) 38 | 39 | 40 | @login_required 41 | def discussion_detail(request, slug): 42 | """ 43 | Displays details of a discussion, including comments and allows you to edit comment. 44 | """ 45 | discussion = get_object_or_404(Discussion, slug=slug) 46 | comments = discussion.comments.order_by("-created_at") 47 | comment_to_edit = None 48 | 49 | if request.method == "POST": 50 | comment_id = request.POST.get("edit") 51 | if comment_id: 52 | comment_to_edit = get_object_or_404( 53 | Comment, 54 | id=comment_id, 55 | created_by=request.user, 56 | discussion=discussion, 57 | ) 58 | comment_form = CommentForm(request.POST, instance=comment_to_edit) 59 | else: 60 | comment_form = CommentForm(request.POST) 61 | if comment_form.is_valid(): 62 | comment = comment_form.save(commit=False) 63 | comment.created_by = request.user 64 | comment.discussion = discussion 65 | comment.save() 66 | return redirect( 67 | reverse("discussion_detail", args=[discussion.slug]) 68 | ) 69 | else: 70 | if "edit" in request.GET: 71 | comment_id = request.GET.get("edit") 72 | comment_to_edit = get_object_or_404( 73 | Comment, 74 | id=comment_id, 75 | created_by=request.user, 76 | discussion=discussion, 77 | ) 78 | comment_form = ( 79 | CommentForm(instance=comment_to_edit) 80 | if comment_to_edit 81 | else CommentForm() 82 | ) 83 | 84 | return render( 85 | request, 86 | "community/discussion_detail.html", 87 | { 88 | "discussion": discussion, 89 | "comments": comments, 90 | "comment_form": comment_form, 91 | "comment_to_edit": comment_to_edit, 92 | }, 93 | ) 94 | 95 | 96 | @login_required 97 | def create_discussion(request, dataset_id): 98 | """ 99 | Allows a user to create a new discussion for a specific dataset. 100 | """ 101 | if request.method == "POST": 102 | form = DiscussionForm(request.POST) 103 | if form.is_valid(): 104 | discussion = form.save(commit=False) 105 | discussion.created_by = request.user 106 | discussion.dataset_id = dataset_id 107 | discussion.save() 108 | return redirect(reverse("discussion_list", args=[dataset_id])) 109 | else: 110 | form = DiscussionForm() 111 | 112 | return render( 113 | request, 114 | "community/create_discussion.html", 115 | {"form": form, "dataset_id": dataset_id}, 116 | ) 117 | 118 | 119 | @login_required 120 | def delete_discussion(request, slug): 121 | """ 122 | Allows the author of a discussion to delete it. 123 | """ 124 | discussion = get_object_or_404(Discussion, slug=slug) 125 | if request.user != discussion.created_by: 126 | return HttpResponseForbidden( 127 | "You are not allowed to delete this discussion." 128 | ) 129 | dataset_id = discussion.dataset_id 130 | discussion.delete() 131 | return redirect(reverse("discussion_list", args=[dataset_id])) 132 | 133 | 134 | @login_required 135 | def edit_comment(request, comment_id): 136 | """ 137 | Allows a user to edit their comment if within the editable time limit. 138 | """ 139 | comment = get_object_or_404(Comment, id=comment_id) 140 | 141 | if request.user != comment.created_by: 142 | return HttpResponseForbidden( 143 | "You are not allowed to edit this comment." 144 | ) 145 | 146 | if not comment.is_editable(): 147 | return HttpResponseForbidden( 148 | "The edit window for this comment has expired." 149 | ) 150 | 151 | if request.method == "POST": 152 | form = CommentForm(request.POST, instance=comment) 153 | if form.is_valid(): 154 | form.save() 155 | return redirect( 156 | reverse("discussion_detail", args=[comment.discussion.slug]) 157 | ) 158 | else: 159 | form = CommentForm(instance=comment) 160 | 161 | return render( 162 | request, 163 | "discussions/edit_comment.html", 164 | {"form": form, "comment": comment}, 165 | ) 166 | 167 | 168 | @login_required 169 | def delete_comment(request, comment_id): 170 | """ 171 | Allows the author of a comment to delete it. 172 | """ 173 | comment = get_object_or_404(Comment, id=comment_id) 174 | if request.user != comment.created_by: 175 | return HttpResponseForbidden( 176 | "You are not allowed to delete this comment." 177 | ) 178 | discussion_slug = comment.discussion.slug 179 | comment.delete() 180 | return redirect(reverse("discussion_detail", args=[discussion_slug])) 181 | -------------------------------------------------------------------------------- /datasets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/datasets/__init__.py -------------------------------------------------------------------------------- /datasets/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | from .models import Dataset, Rating, DatasetVersion, Tag 5 | 6 | admin.site.register(Dataset) 7 | admin.site.register(Rating) 8 | admin.site.register(DatasetVersion) 9 | admin.site.register(Tag) 10 | -------------------------------------------------------------------------------- /datasets/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DatasetsConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "datasets" 7 | -------------------------------------------------------------------------------- /datasets/forms.py: -------------------------------------------------------------------------------- 1 | from django.forms import ModelForm 2 | from datasets.models import Dataset, DatasetVersion, Tag, Rating 3 | from django import forms 4 | 5 | 6 | class DatasetUploadForm(forms.ModelForm): 7 | # Include DatasetVersion fields 8 | file = forms.FileField(label="Dataset File", required=True) 9 | version_description = forms.CharField( 10 | label="Version Description", 11 | required=False, 12 | ) 13 | 14 | class Meta: 15 | model = Dataset 16 | fields = [ 17 | "name", 18 | "description", 19 | "tags", 20 | "dataset_type", 21 | ] 22 | widgets = { 23 | "tags": forms.CheckboxSelectMultiple(), 24 | "description": forms.Textarea(attrs={"rows": 5}), 25 | } 26 | 27 | def save(self, commit=True, user=None): 28 | # Save the Dataset instance 29 | dataset = super().save(commit=commit) 30 | 31 | # Save the associated DatasetVersion instance 32 | if commit: 33 | DatasetVersion.objects.create( 34 | dataset=dataset, 35 | version_number=1, # First version for a new dataset 36 | description=self.cleaned_data.get("version_description"), 37 | file=self.cleaned_data["file"], 38 | created_by=user, 39 | is_latest=True, 40 | ) 41 | 42 | return dataset 43 | 44 | 45 | class DatasetVersionForm(forms.ModelForm): 46 | class Meta: 47 | model = DatasetVersion 48 | fields = ["file", "description"] 49 | 50 | def __init__(self, *args, **kwargs): 51 | self.user = kwargs.pop("user") 52 | self.dataset = kwargs.pop("dataset") 53 | super().__init__(*args, **kwargs) 54 | 55 | def save(self, commit=True): 56 | version = super().save(commit=False) 57 | version.created_by = self.user 58 | version.dataset = self.dataset 59 | version.save() 60 | return version 61 | 62 | 63 | class RatingForm(forms.ModelForm): 64 | class Meta: 65 | model = Rating 66 | fields = ["rating", "review"] 67 | widgets = { 68 | "rating": forms.RadioSelect( 69 | choices=[(i, str(i)) for i in range(1, 11)] 70 | ), 71 | "review": forms.Textarea(attrs={"rows": 3}), 72 | } 73 | 74 | def __init__(self, *args, **kwargs): 75 | self.user = kwargs.pop("user") 76 | self.dataset = kwargs.pop("dataset") 77 | super().__init__(*args, **kwargs) 78 | 79 | def save(self, commit=True): 80 | # Associate rating with the current user and dataset 81 | rating = super().save(commit=False) 82 | rating.user = self.user 83 | rating.dataset = self.dataset 84 | rating.save() 85 | return rating 86 | 87 | 88 | class TagForm(forms.ModelForm): 89 | class Meta: 90 | model = Tag 91 | fields = ["name"] 92 | widgets = { 93 | "name": forms.TextInput(attrs={"placeholder": "Enter tag name"}), 94 | } 95 | -------------------------------------------------------------------------------- /datasets/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-11-05 04:52 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="Dataset", 15 | fields=[ 16 | ( 17 | "id", 18 | models.BigAutoField( 19 | auto_created=True, 20 | primary_key=True, 21 | serialize=False, 22 | verbose_name="ID", 23 | ), 24 | ), 25 | ("name", models.CharField(max_length=255)), 26 | ("description", models.TextField()), 27 | ("tags", models.CharField(max_length=255)), 28 | ("created_at", models.DateTimeField(auto_now_add=True)), 29 | ("updated_at", models.DateTimeField(auto_now=True)), 30 | ("is_public", models.BooleanField(default=True)), 31 | ( 32 | "dataset_type", 33 | models.CharField( 34 | choices=[ 35 | ("image", "Image"), 36 | ("clinical", "Clinical Record"), 37 | ("text", "Text Data"), 38 | ("audio", "Audio Data"), 39 | ("video", "Video Data"), 40 | ("other", "Other Data"), 41 | ], 42 | max_length=50, 43 | ), 44 | ), 45 | ( 46 | "license", 47 | models.CharField(choices=[("mit", "MIT")], max_length=50), 48 | ), 49 | ("format", models.CharField(max_length=255)), 50 | ("file", models.FileField(upload_to="datasets/")), 51 | ("version", models.IntegerField(default=1)), 52 | ], 53 | ), 54 | migrations.CreateModel( 55 | name="Rating", 56 | fields=[ 57 | ( 58 | "id", 59 | models.BigAutoField( 60 | auto_created=True, 61 | primary_key=True, 62 | serialize=False, 63 | verbose_name="ID", 64 | ), 65 | ), 66 | ( 67 | "rating", 68 | models.IntegerField( 69 | choices=[ 70 | (1, 1), 71 | (2, 2), 72 | (3, 3), 73 | (4, 4), 74 | (5, 5), 75 | ] 76 | ), 77 | ), 78 | ("review", models.TextField(blank=True, null=True)), 79 | ("created_at", models.DateTimeField(auto_now_add=True)), 80 | ], 81 | ), 82 | ] 83 | -------------------------------------------------------------------------------- /datasets/migrations/0002_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-11-05 04:52 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ("datasets", "0001_initial"), 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.AddField( 19 | model_name="dataset", 20 | name="uploader", 21 | field=models.ForeignKey( 22 | on_delete=django.db.models.deletion.CASCADE, 23 | to=settings.AUTH_USER_MODEL, 24 | ), 25 | ), 26 | migrations.AddField( 27 | model_name="rating", 28 | name="dataset", 29 | field=models.ForeignKey( 30 | on_delete=django.db.models.deletion.CASCADE, 31 | related_name="ratings", 32 | to="datasets.dataset", 33 | ), 34 | ), 35 | migrations.AddField( 36 | model_name="rating", 37 | name="user", 38 | field=models.ForeignKey( 39 | on_delete=django.db.models.deletion.CASCADE, 40 | to=settings.AUTH_USER_MODEL, 41 | ), 42 | ), 43 | migrations.AlterUniqueTogether( 44 | name="rating", 45 | unique_together={("user", "dataset")}, 46 | ), 47 | ] 48 | -------------------------------------------------------------------------------- /datasets/migrations/0003_tag_remove_dataset_file_remove_dataset_format_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-11-16 20:50 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ("datasets", "0002_initial"), 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name="Tag", 18 | fields=[ 19 | ( 20 | "id", 21 | models.BigAutoField( 22 | auto_created=True, 23 | primary_key=True, 24 | serialize=False, 25 | verbose_name="ID", 26 | ), 27 | ), 28 | ("name", models.CharField(max_length=120, unique=True)), 29 | ], 30 | ), 31 | migrations.RemoveField( 32 | model_name="dataset", 33 | name="file", 34 | ), 35 | migrations.RemoveField( 36 | model_name="dataset", 37 | name="format", 38 | ), 39 | migrations.RemoveField( 40 | model_name="dataset", 41 | name="is_public", 42 | ), 43 | migrations.RemoveField( 44 | model_name="dataset", 45 | name="license", 46 | ), 47 | migrations.RemoveField( 48 | model_name="dataset", 49 | name="uploader", 50 | ), 51 | migrations.RemoveField( 52 | model_name="dataset", 53 | name="version", 54 | ), 55 | migrations.RemoveField( 56 | model_name="dataset", 57 | name="tags", 58 | ), 59 | migrations.AddField( 60 | model_name="dataset", 61 | name="tags", 62 | field=models.ManyToManyField( 63 | related_name="datasets", to="datasets.tag" 64 | ), 65 | ), 66 | migrations.CreateModel( 67 | name="DatasetVersion", 68 | fields=[ 69 | ( 70 | "id", 71 | models.BigAutoField( 72 | auto_created=True, 73 | primary_key=True, 74 | serialize=False, 75 | verbose_name="ID", 76 | ), 77 | ), 78 | ("version_number", models.PositiveIntegerField()), 79 | ( 80 | "description", 81 | models.TextField( 82 | blank=True, help_text="Changes in this version" 83 | ), 84 | ), 85 | ("created_at", models.DateTimeField(auto_now_add=True)), 86 | ("file", models.FileField(upload_to="datasets/%Y/%m/%d/")), 87 | ("is_latest", models.BooleanField(default=False)), 88 | ( 89 | "created_by", 90 | models.ForeignKey( 91 | blank=True, 92 | null=True, 93 | on_delete=django.db.models.deletion.SET_NULL, 94 | to=settings.AUTH_USER_MODEL, 95 | ), 96 | ), 97 | ( 98 | "dataset", 99 | models.ForeignKey( 100 | on_delete=django.db.models.deletion.CASCADE, 101 | related_name="versions", 102 | to="datasets.dataset", 103 | ), 104 | ), 105 | ], 106 | options={ 107 | "ordering": ["-created_at"], 108 | "unique_together": {("dataset", "version_number")}, 109 | }, 110 | ), 111 | ] 112 | -------------------------------------------------------------------------------- /datasets/migrations/0004_alter_datasetversion_description_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-12-07 20:59 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ( 10 | "datasets", 11 | "0003_tag_remove_dataset_file_remove_dataset_format_and_more", 12 | ), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name="datasetversion", 18 | name="description", 19 | field=models.TextField(help_text="Changes in this version"), 20 | ), 21 | migrations.AlterField( 22 | model_name="datasetversion", 23 | name="file", 24 | field=models.FileField(upload_to="datasets"), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /datasets/migrations/0005_alter_rating_rating.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-12-09 17:03 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("datasets", "0004_alter_datasetversion_description_and_more"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="rating", 15 | name="rating", 16 | field=models.IntegerField(choices=[(1, 1), (2, 2), (3, 3), (4, 4)]), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /datasets/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/datasets/migrations/__init__.py -------------------------------------------------------------------------------- /datasets/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth import get_user_model 3 | from django.urls import reverse 4 | from django.db.models import Avg 5 | 6 | 7 | class Tag(models.Model): 8 | name = models.CharField(max_length=120, unique=True) 9 | 10 | def __str__(self): 11 | return self.name 12 | 13 | 14 | class Dataset(models.Model): 15 | """Dataset Model""" 16 | 17 | DATASET_TYPES = [ 18 | ("image", "Image"), 19 | ("clinical", "Clinical Record"), 20 | ("text", "Text Data"), 21 | ("audio", "Audio Data"), 22 | ("video", "Video Data"), 23 | ("other", "Other Data"), 24 | ] 25 | 26 | name = models.CharField(max_length=255) 27 | description = models.TextField() 28 | tags = models.ManyToManyField(Tag, related_name="datasets") 29 | created_at = models.DateTimeField(auto_now_add=True) 30 | updated_at = models.DateTimeField(auto_now=True) 31 | dataset_type = models.CharField(max_length=50, choices=DATASET_TYPES) 32 | 33 | def __str__(self): 34 | return self.name 35 | 36 | def get_absolute_url(self): 37 | return reverse("dataset_detail", kwargs={"pk": self.pk}) 38 | 39 | @property 40 | def get_all_versions(self): 41 | return self.versions.all() 42 | 43 | @property 44 | def average_rating(self): 45 | return self.ratings.aggregate(average=Avg("rating"))["average"] or 0 46 | 47 | 48 | class DatasetVersion(models.Model): 49 | dataset = models.ForeignKey( 50 | Dataset, related_name="versions", on_delete=models.CASCADE 51 | ) 52 | version_number = models.PositiveIntegerField() 53 | description = models.TextField( 54 | blank=False, help_text="Changes in this version" 55 | ) 56 | created_by = models.ForeignKey( 57 | get_user_model(), on_delete=models.SET_NULL, null=True, blank=True 58 | ) 59 | created_at = models.DateTimeField(auto_now_add=True) 60 | file = models.FileField(upload_to="datasets") 61 | is_latest = models.BooleanField( 62 | default=False 63 | ) # Indicates if this is the latest version 64 | 65 | class Meta: 66 | unique_together = ("dataset", "version_number") 67 | ordering = ["-created_at"] # Latest versions first 68 | 69 | def __str__(self): 70 | return f"{self.dataset.name} (v{self.version_number})" 71 | 72 | def save(self, *args, **kwargs): 73 | # Automatically set the version number 74 | if not self.pk: # When creating a new version 75 | latest_version = ( 76 | DatasetVersion.objects.filter(dataset=self.dataset) 77 | .order_by("-version_number") 78 | .first() 79 | ) 80 | if latest_version: 81 | self.version_number = latest_version.version_number + 1 82 | latest_version.is_latest = False 83 | latest_version.save() 84 | else: 85 | self.version_number = 1 86 | 87 | # Mark this version as the latest 88 | self.is_latest = True 89 | super().save(*args, **kwargs) 90 | 91 | 92 | class Rating(models.Model): 93 | user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) 94 | dataset = models.ForeignKey( 95 | Dataset, on_delete=models.CASCADE, related_name="ratings" 96 | ) 97 | rating = models.IntegerField( 98 | choices=[(i, i) for i in range(1, 5)] 99 | ) # 1 to 10 rating 100 | review = models.TextField(blank=True, null=True) # Optional review 101 | created_at = models.DateTimeField(auto_now_add=True) 102 | 103 | class Meta: 104 | unique_together = ( 105 | "user", 106 | "dataset", 107 | ) # Ensure one rating per user per dataset 108 | 109 | def __str__(self): 110 | return f"{self.user} rated {self.dataset} - {self.rating} stars" 111 | 112 | def get_absolute_url(self): 113 | return reverse("rating_detail", kwargs={"pk": self.pk}) 114 | -------------------------------------------------------------------------------- /datasets/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/datasets/tests/__init__.py -------------------------------------------------------------------------------- /datasets/tests/test_view.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase, Client 2 | from django.urls import reverse 3 | from django.contrib.auth import get_user_model 4 | from datasets.models import Dataset, DatasetVersion, Rating, Tag 5 | from datasets.forms import ( 6 | DatasetUploadForm, 7 | DatasetVersionForm, 8 | RatingForm, 9 | ) 10 | from django.core.files.uploadedfile import SimpleUploadedFile 11 | 12 | 13 | class DatasetTestCase(TestCase): 14 | @classmethod 15 | def setUpTestData(cls): 16 | # Create user 17 | cls.user = get_user_model().objects.create_user( 18 | username="testuser", password="testpassword" 19 | ) 20 | 21 | # Create tags 22 | cls.tag = Tag.objects.create(name="Science") 23 | 24 | # Create a dataset 25 | cls.dataset = Dataset.objects.create( 26 | name="Test Dataset", 27 | description="Test description", 28 | dataset_type="image", 29 | ) 30 | cls.dataset.tags.add(cls.tag) 31 | 32 | # Create a dataset version 33 | cls.version_file = SimpleUploadedFile( 34 | "test_version.csv", b"file_content" 35 | ) 36 | cls.version = DatasetVersion.objects.create( 37 | dataset=cls.dataset, 38 | version_number=1, 39 | description="Initial version", 40 | file=cls.version_file, 41 | created_by=cls.user, 42 | is_latest=True, 43 | ) 44 | 45 | # Create a rating 46 | cls.rating = Rating.objects.create( 47 | user=cls.user, dataset=cls.dataset, rating=4, review="Great dataset" 48 | ) 49 | 50 | # Client for authentication 51 | cls.client = Client() 52 | 53 | 54 | class ModelTests(DatasetTestCase): 55 | def test_dataset_model_str(self): 56 | self.assertEqual(str(self.dataset), "Test Dataset") 57 | 58 | def test_version_model_str(self): 59 | self.assertEqual(str(self.version), f"{self.dataset.name} (v1)") 60 | 61 | def test_average_rating(self): 62 | avg_rating = self.dataset.average_rating 63 | self.assertEqual(avg_rating, 4) 64 | 65 | def test_dataset_versions(self): 66 | self.assertEqual(len(self.dataset.get_all_versions), 1) 67 | 68 | def test_rating_model_str(self): 69 | self.assertEqual( 70 | str(self.rating), 71 | f"{self.user} rated {self.dataset} - 4 stars", 72 | ) 73 | 74 | def test_tag_model_str(self): 75 | self.assertEqual(str(self.tag), "Science") 76 | 77 | 78 | class FormTests(DatasetTestCase): 79 | def test_dataset_upload_form_valid(self): 80 | form_data = { 81 | "name": "Dataset 2", 82 | "description": "Another dataset", 83 | "tags": [self.tag.id], 84 | "dataset_type": "text", 85 | "version_description": "Initial version", 86 | } 87 | form_file = SimpleUploadedFile("file.csv", b"file_content") 88 | form = DatasetUploadForm(data=form_data, files={"file": form_file}) 89 | self.assertTrue(form.is_valid()) 90 | 91 | def test_dataset_version_form_valid(self): 92 | form_data = {"description": "New version"} 93 | form_file = SimpleUploadedFile("version2.csv", b"new_content") 94 | form = DatasetVersionForm( 95 | data=form_data, 96 | files={"file": form_file}, 97 | user=self.user, 98 | dataset=self.dataset, 99 | ) 100 | self.assertTrue(form.is_valid()) 101 | 102 | 103 | class ViewTests(DatasetTestCase): 104 | def setUp(self): 105 | self.client.login(username="testuser", password="testpassword") 106 | 107 | # Dataset Upload View 108 | def test_upload_dataset_view(self): 109 | upload_url = reverse("datasets:add_dataset") 110 | with open("upload_file.csv", "wb") as f: 111 | f.write(b"content") 112 | with open("upload_file.csv", "rb") as file: 113 | response = self.client.post( 114 | upload_url, 115 | { 116 | "name": "New Dataset", 117 | "description": "Uploaded dataset", 118 | "tags": [self.tag.id], 119 | "dataset_type": "audio", 120 | "file": file, 121 | "version_description": "Version 1", 122 | }, 123 | follow=True, 124 | ) 125 | self.assertEqual(response.status_code, 200) 126 | self.assertTrue(Dataset.objects.filter(name="New Dataset").exists()) 127 | 128 | # Dataset List View 129 | def test_dataset_list_view(self): 130 | response = self.client.get(reverse("datasets:dataset_list")) 131 | self.assertEqual(response.status_code, 200) 132 | self.assertContains(response, self.dataset.name) 133 | 134 | # Dataset Detail View 135 | def test_dataset_detail_view(self): 136 | response = self.client.get( 137 | reverse("datasets:dataset_detail", kwargs={"pk": self.dataset.pk}) 138 | ) 139 | self.assertEqual(response.status_code, 200) 140 | self.assertContains(response, "Initial version") 141 | self.assertContains(response, "4") 142 | 143 | # Add Dataset Version View 144 | def test_add_version_view(self): 145 | add_version_url = reverse( 146 | "datasets:add_dataset_version", 147 | kwargs={"dataset_id": self.dataset.pk}, 148 | ) 149 | form_file = SimpleUploadedFile("version2.csv", b"new_content") 150 | response = self.client.post( 151 | add_version_url, 152 | {"file": form_file, "description": "Version 2"}, 153 | follow=True, 154 | ) 155 | self.assertEqual(response.status_code, 200) 156 | self.assertTrue( 157 | DatasetVersion.objects.filter(version_number=2).exists() 158 | ) 159 | 160 | # Rate Dataset View 161 | def test_rate_dataset_view(self): 162 | rate_url = reverse( 163 | "datasets:rate_dataset", kwargs={"dataset_id": self.dataset.pk} 164 | ) 165 | response = self.client.post( 166 | rate_url, 167 | {"rating": 5, "review": "Excellent dataset"}, 168 | follow=True, 169 | ) 170 | self.assertEqual(response.status_code, 200) 171 | self.assertTrue( 172 | Rating.objects.filter(rating=5, review="Excellent dataset").exists() 173 | ) 174 | -------------------------------------------------------------------------------- /datasets/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | app_name = "datasets" 5 | 6 | urlpatterns = [ 7 | # Dataset URLs 8 | path("", views.dataset_list, name="dataset_list"), 9 | path("/", views.dataset_detail, name="dataset_detail"), 10 | path("upload/", views.upload_dataset, name="add_dataset"), 11 | # DatasetVersion URLs 12 | path( 13 | "/add-version/", 14 | views.add_dataset_version, 15 | name="add_dataset_version", 16 | ), 17 | # Rating URLs 18 | path( 19 | "/rate/", 20 | views.rate_dataset, 21 | name="rate_dataset", 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /datasets/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect, get_object_or_404 2 | from django.contrib.auth.decorators import login_required 3 | from datasets.forms import ( 4 | DatasetUploadForm, 5 | DatasetVersionForm, 6 | RatingForm, 7 | TagForm, 8 | ) 9 | from datasets.models import Dataset, DatasetVersion, Rating 10 | 11 | 12 | @login_required() 13 | def upload_dataset(request): 14 | if request.method == "POST": 15 | form = DatasetUploadForm(request.POST, request.FILES) 16 | if form.is_valid(): 17 | form.save(user=request.user) # Pass the logged-in user 18 | return redirect("datasets:dataset_list") 19 | else: 20 | form = DatasetUploadForm() 21 | return render(request, "datasets/upload_dataset.html", {"form": form}) 22 | 23 | 24 | def dataset_list(request): 25 | datasets = Dataset.objects.all() 26 | return render(request, "datasets/dataset_list.html", {"datasets": datasets}) 27 | 28 | 29 | @login_required() 30 | def dataset_detail(request, pk): 31 | dataset = get_object_or_404(Dataset, pk=pk) 32 | average_rating = dataset.average_rating 33 | versions = dataset.get_all_versions 34 | print(versions) 35 | 36 | return render( 37 | request, 38 | "datasets/dataset_detail.html", 39 | { 40 | "dataset": dataset, 41 | "average_rating": average_rating, 42 | "versions": versions, 43 | }, 44 | ) 45 | 46 | 47 | @login_required() 48 | def add_dataset_version(request, dataset_id): 49 | dataset = get_object_or_404(Dataset, pk=dataset_id) 50 | if request.method == "POST": 51 | print(request.FILES) 52 | form = DatasetVersionForm( 53 | request.POST, request.FILES, user=request.user, dataset=dataset 54 | ) 55 | if form.is_valid(): 56 | form.save() 57 | return redirect("datasets:dataset_detail", pk=dataset_id) 58 | print(form.errors) 59 | else: 60 | form = DatasetVersionForm(user=request.user, dataset=dataset) 61 | return render( 62 | request, "datasets/add_version.html", {"form": form, "dataset": dataset} 63 | ) 64 | 65 | 66 | @login_required 67 | def rate_dataset(request, dataset_id): 68 | user = request.user 69 | dataset = get_object_or_404(Dataset, pk=dataset_id) 70 | 71 | if request.method == "POST": 72 | # Get the rating and optional review from POST data 73 | new_rating = request.POST.get("rating", 1) 74 | review = request.POST.get( 75 | "review", "" 76 | ) # Default to empty string if no review 77 | rating = Rating.objects.filter(dataset=dataset, user=user).first() 78 | 79 | if rating: 80 | rating.rating = new_rating 81 | rating.review = review 82 | rating.save() 83 | else: 84 | # If no rating exists, create a new one 85 | Rating.objects.create( 86 | dataset=dataset, user=user, rating=new_rating, review=review 87 | ) 88 | 89 | return redirect("datasets:dataset_detail", pk=dataset.pk) 90 | 91 | return redirect("datasets:dataset_detail", pk=dataset.pk) 92 | -------------------------------------------------------------------------------- /f02481bd7faa4ab33c4d08e7880b2b58(1).jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/f02481bd7faa4ab33c4d08e7880b2b58(1).jpg -------------------------------------------------------------------------------- /flake8-config: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 117 3 | builtins = 4 | raw_input, 5 | unicode, 6 | exclude = .git,__pycache__,frontend,node_modules,migrations/*,*/migrations/* 7 | ignore = 8 | # too many blank lines 9 | E303, 10 | # too many #'s for comments 11 | E266, 12 | # line break before binary operator 13 | W503, 14 | # module level import not at top of file 15 | E402, 16 | # whitespace before ':' 17 | E203, 18 | # Black will do this: when here, it complains about docstring because it sees all the different lines as one 19 | E501, 20 | 21 | F405 -------------------------------------------------------------------------------- /home/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/home/__init__.py -------------------------------------------------------------------------------- /home/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /home/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class HomeConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "home" 7 | -------------------------------------------------------------------------------- /home/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | 4 | class ContactForm(forms.Form): 5 | name = forms.CharField( 6 | label="Name", 7 | required=True, 8 | widget=forms.TextInput( 9 | attrs={ 10 | "class": "w-full px-4 py-2 border rounded-lg", 11 | "placeholder": "display name", 12 | } 13 | ), 14 | ) 15 | email = forms.EmailField( 16 | label="Email", 17 | required=True, 18 | widget=forms.TextInput( 19 | attrs={ 20 | "class": "w-full px-4 py-2 border rounded-lg", 21 | "placeholder": "email", 22 | } 23 | ), 24 | ) 25 | subject = forms.CharField( 26 | label="Subject", 27 | required=True, 28 | widget=forms.TextInput( 29 | attrs={ 30 | "class": "w-full px-4 py-2 border rounded-lg", 31 | } 32 | ), 33 | ) 34 | message = forms.CharField( 35 | label="Message", 36 | required=True, 37 | widget=forms.Textarea( 38 | attrs={ 39 | "class": "w-full px-4 py-2 border rounded-lg", 40 | } 41 | ), 42 | ) 43 | -------------------------------------------------------------------------------- /home/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/home/migrations/__init__.py -------------------------------------------------------------------------------- /home/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /home/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase, Client 2 | from django.urls import reverse 3 | from django.core import mail 4 | from django.core.mail import BadHeaderError 5 | from unittest.mock import patch 6 | from .forms import ContactForm 7 | 8 | 9 | class HomeViewTests(TestCase): 10 | def setUp(self): 11 | self.client = Client() 12 | self.url = reverse("home:home") 13 | 14 | def test_home_view_get_request(self): 15 | """Test if the form is rendered correctly for a GET request.""" 16 | response = self.client.get(self.url) 17 | self.assertEqual(response.status_code, 200) 18 | self.assertTemplateUsed(response, "home/home.html") 19 | self.assertIn("form", response.context) 20 | 21 | @patch("home.views.send_mail", side_effect=BadHeaderError) 22 | def test_home_view_bad_header(self, mock_send_mail): 23 | """Test if BadHeaderError is handled correctly.""" 24 | data = { 25 | "name": "John Doe", 26 | "email": "johndoe@example.com", 27 | "subject": "Test Subject", 28 | "message": "Test message content", 29 | } 30 | 31 | response = self.client.post(self.url, data=data) 32 | self.assertContains(response, "Invalid header found", status_code=200) 33 | 34 | def test_home_view_post_invalid_form(self): 35 | """Test if an invalid POST request does not send an email and re-renders form.""" 36 | invalid_data = { # Missing required fields 37 | "name": "", 38 | "email": "not-an-email", 39 | "subject": "", 40 | "message": "", 41 | } 42 | 43 | response = self.client.post(self.url, data=invalid_data) 44 | self.assertEqual(response.status_code, 200) 45 | self.assertTemplateUsed(response, "home/home.html") 46 | self.assertIn("form", response.context) 47 | 48 | # Check that no email was sent 49 | self.assertEqual(len(mail.outbox), 0) 50 | 51 | 52 | class ContactFormTests(TestCase): 53 | def test_valid_form(self): 54 | """Test form with valid data.""" 55 | form_data = { 56 | "name": "John Doe", 57 | "email": "john@example.com", 58 | "subject": "Test Subject", 59 | "message": "This is a test message.", 60 | } 61 | form = ContactForm(data=form_data) 62 | self.assertTrue(form.is_valid()) 63 | 64 | def test_invalid_form_missing_fields(self): 65 | """Test form with missing required fields.""" 66 | form_data = { 67 | "name": "John Doe", 68 | "email": "", 69 | "subject": "", 70 | "message": "", 71 | } 72 | form = ContactForm(data=form_data) 73 | self.assertFalse(form.is_valid()) 74 | self.assertIn("email", form.errors) 75 | self.assertIn("subject", form.errors) 76 | self.assertIn("message", form.errors) 77 | 78 | def test_invalid_email_format(self): 79 | """Test form with invalid email format.""" 80 | form_data = { 81 | "name": "John Doe", 82 | "email": "invalid-email", 83 | "subject": "Test Subject", 84 | "message": "This is a test message.", 85 | } 86 | form = ContactForm(data=form_data) 87 | self.assertFalse(form.is_valid()) 88 | self.assertIn("email", form.errors) 89 | -------------------------------------------------------------------------------- /home/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from home.views import home 3 | 4 | app_name = "home" 5 | urlpatterns = [ 6 | path("", home, name="home"), 7 | ] 8 | -------------------------------------------------------------------------------- /home/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect 2 | from django.core.mail import send_mail, BadHeaderError 3 | from django.http import HttpResponse 4 | from home.forms import ContactForm 5 | 6 | 7 | # Create your views here. 8 | def home(request): 9 | if request.method == "GET": 10 | form = ContactForm() 11 | else: 12 | form = ContactForm(request.POST) 13 | if form.is_valid(): 14 | name = form.cleaned_data["name"] 15 | email = form.cleaned_data["email"] 16 | subject = form.cleaned_data["subject"] 17 | message = form.cleaned_data["message"] 18 | try: 19 | send_mail( 20 | subject, message, email, ["vetdatahub@gmail.com"], name 21 | ) 22 | except BadHeaderError: 23 | return HttpResponse("Invalid header found") 24 | return redirect("home") 25 | 26 | return render(request, "home/home.html", context={"form": form}) 27 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "vetdatahub.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /moderation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/moderation/__init__.py -------------------------------------------------------------------------------- /moderation/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /moderation/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ModerationConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "moderation" 7 | -------------------------------------------------------------------------------- /moderation/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/moderation/migrations/__init__.py -------------------------------------------------------------------------------- /moderation/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /moderation/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /moderation/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | urlpatterns = [] 4 | -------------------------------------------------------------------------------- /moderation/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "windmill-dashboard", 3 | "version": "1.0.2", 4 | "description": "A multi theme, completely accessible, with components and pages examples, ready for production dashboard.", 5 | "scripts": { 6 | "tailwind": "tailwindcss build public/assets/css/tailwind.css -o public/assets/css/tailwind.output.css", 7 | "build": "postcss static/css/tailwind.css -o static/css/tailwind.output.css" 8 | }, 9 | "author": "Estevan Maito ", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "autoprefixer": "^10.4.20", 13 | "color": "3.1.2", 14 | "commitizen": "4.1.2", 15 | "cssnano": "4.1.10", 16 | "cz-conventional-changelog": "3.2.0", 17 | "postcss": "^8.4.49", 18 | "postcss-cli": "7.1.1", 19 | "release-it": "13.6.4", 20 | "tailwindcss": "^3.4.16" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('tailwindcss'), 4 | require('autoprefixer'), 5 | require('cssnano')({ 6 | preset: 'default', 7 | }), 8 | ], 9 | } 10 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 80 3 | skip-string-normalization = false 4 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE = vetdatahub.settings_dev 3 | python_files = tests.py test_*.py *_tests.py 4 | env = 5 | DJANGO_ALLOW_ASYNC_UNSAFE = true 6 | -------------------------------------------------------------------------------- /registration/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = (2, 13, 0, "final", 0) 2 | 3 | # for Django-3.1 and prior 4 | default_app_config = "registration.apps.RegistrationConfig" 5 | 6 | 7 | def get_version(): 8 | "Returns a PEP 386-compliant version number from VERSION." 9 | assert len(VERSION) == 5 10 | assert VERSION[3] in ("alpha", "beta", "rc", "final") 11 | 12 | # Now build the two parts of the version number: 13 | # main = X.Y[.Z] 14 | # sub = .devN - for pre-alpha releases 15 | # | {a|b|c}N - for alpha, beta and rc releases 16 | 17 | parts = 2 if VERSION[2] == 0 else 3 18 | main = ".".join(str(x) for x in VERSION[:parts]) 19 | 20 | sub = "" 21 | if VERSION[3] != "final": 22 | mapping = {"alpha": "a", "beta": "b", "rc": "c"} 23 | sub = mapping[VERSION[3]] + str(VERSION[4]) 24 | 25 | return str(main + sub) 26 | -------------------------------------------------------------------------------- /registration/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.sites.shortcuts import get_current_site 3 | from django.contrib.auth import get_user_model 4 | from .models import RegistrationProfile, Profile 5 | from .users import UsernameField 6 | from .utils import _ 7 | 8 | 9 | class RegistrationAdmin(admin.ModelAdmin): 10 | actions = ["activate_users", "resend_activation_email"] 11 | list_display = ("user", "activation_key_expired") 12 | raw_id_fields = ["user"] 13 | search_fields = ( 14 | "user__{0}".format(UsernameField()), 15 | "user__first_name", 16 | "user__last_name", 17 | ) 18 | 19 | def activate_users(self, request, queryset): 20 | """ 21 | Activates the selected users, if they are not already 22 | activated. 23 | 24 | """ 25 | 26 | site = get_current_site(request) 27 | for profile in queryset: 28 | RegistrationProfile.objects.activate_user( 29 | profile.activation_key, site 30 | ) 31 | 32 | activate_users.short_description = _("Activate users") 33 | 34 | def resend_activation_email(self, request, queryset): 35 | """ 36 | Re-sends activation emails for the selected users. 37 | 38 | Note that this will *only* send activation emails for users 39 | who are eligible to activate; emails will not be sent to users 40 | whose activation keys have expired or who have already 41 | activated. 42 | 43 | """ 44 | 45 | site = get_current_site(request) 46 | for profile in queryset: 47 | user = profile.user 48 | RegistrationProfile.objects.resend_activation_mail( 49 | user.email, site, request 50 | ) 51 | 52 | resend_activation_email.short_description = _("Re-send activation emails") 53 | 54 | 55 | User = get_user_model() 56 | admin.site.register(RegistrationProfile, RegistrationAdmin) 57 | admin.site.register(User) 58 | admin.site.register(Profile) 59 | -------------------------------------------------------------------------------- /registration/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class RegistrationConfig(AppConfig): 5 | name = "registration" 6 | verbose_name = "Registration" 7 | default_auto_field = "django.db.models.AutoField" 8 | -------------------------------------------------------------------------------- /registration/auth_urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URL patterns for the views included in ``django.contrib.auth``. 3 | 4 | Including these URLs (via the ``include()`` directive) will set up the 5 | following patterns based at whatever URL prefix they are included 6 | under: 7 | 8 | * User login at ``login/``. 9 | 10 | * User logout at ``logout/``. 11 | 12 | * The two-step password change at ``password/change/`` and 13 | ``password/change/done/``. 14 | 15 | * The four-step password reset at ``password/reset/``, 16 | ``password/reset/confirm/``, ``password/reset/complete/`` and 17 | ``password/reset/done/``. 18 | 19 | The default registration backend already has an ``include()`` for 20 | these URLs, so under the default setup it is not necessary to manually 21 | include these views. Other backends may or may not include them; 22 | consult a specific backend's documentation for details. 23 | 24 | """ 25 | 26 | from django.contrib.auth import views as auth_views 27 | from django.urls import path 28 | from django.urls import reverse_lazy 29 | 30 | urlpatterns = [ 31 | path( 32 | "login/", 33 | auth_views.LoginView.as_view(template_name="registration/login.html"), 34 | name="auth_login", 35 | ), 36 | path( 37 | "logout/", 38 | auth_views.LogoutView.as_view(template_name="registration/logout.html"), 39 | name="auth_logout", 40 | ), 41 | path( 42 | "password/change/", 43 | auth_views.PasswordChangeView.as_view( 44 | success_url=reverse_lazy("auth_password_change_done") 45 | ), 46 | name="auth_password_change", 47 | ), 48 | path( 49 | "password/change/done/", 50 | auth_views.PasswordChangeDoneView.as_view(), 51 | name="auth_password_change_done", 52 | ), 53 | path( 54 | "password/reset/", 55 | auth_views.PasswordResetView.as_view( 56 | success_url=reverse_lazy("auth_password_reset_done") 57 | ), 58 | name="auth_password_reset", 59 | ), 60 | path( 61 | "password/reset/complete/", 62 | auth_views.PasswordResetCompleteView.as_view(), 63 | name="auth_password_reset_complete", 64 | ), 65 | path( 66 | "password/reset/done/", 67 | auth_views.PasswordResetDoneView.as_view(), 68 | name="auth_password_reset_done", 69 | ), 70 | path( 71 | "password/reset/confirm///", 72 | auth_views.PasswordResetConfirmView.as_view( 73 | success_url=reverse_lazy("auth_password_reset_complete") 74 | ), 75 | name="auth_password_reset_confirm", 76 | ), 77 | ] 78 | -------------------------------------------------------------------------------- /registration/backends/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/registration/backends/__init__.py -------------------------------------------------------------------------------- /registration/backends/default/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/registration/backends/default/__init__.py -------------------------------------------------------------------------------- /registration/backends/default/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URLconf for registration and activation, using django-registration's 3 | default backend. 4 | 5 | If the default behavior of these views is acceptable to you, simply 6 | use a line like this in your root URLconf to set up the default URLs 7 | for registration:: 8 | 9 | (r'^accounts/', include('registration.backends.default.urls')), 10 | 11 | This will also automatically set up the views in 12 | ``django.contrib.auth`` at sensible default locations. 13 | 14 | If you'd like to customize registration behavior, feel free to set up 15 | your own URL patterns for these views instead. 16 | 17 | """ 18 | 19 | from django.conf import settings 20 | from django.conf.urls import include 21 | from django.urls import path 22 | from django.views.generic.base import TemplateView 23 | 24 | from .views import ActivationView 25 | from .views import RegistrationView 26 | from .views import ResendActivationView 27 | 28 | urlpatterns = [ 29 | path( 30 | "activate/complete/", 31 | TemplateView.as_view( 32 | template_name="registration/activation_complete.html" 33 | ), 34 | name="registration_activation_complete", 35 | ), 36 | path( 37 | "activate/resend/", 38 | ResendActivationView.as_view(), 39 | name="registration_resend_activation", 40 | ), 41 | # Activation keys get matched by \w+ instead of the more specific 42 | # [a-fA-F0-9]{40} because a bad activation key should still get to the view; 43 | # that way it can return a sensible "invalid key" message instead of a 44 | # confusing 404. 45 | path( 46 | "activate//", 47 | ActivationView.as_view(), 48 | name="registration_activate", 49 | ), 50 | path( 51 | "register/complete/", 52 | TemplateView.as_view( 53 | template_name="registration/registration_complete.html" 54 | ), 55 | name="registration_complete", 56 | ), 57 | path( 58 | "register/closed/", 59 | TemplateView.as_view( 60 | template_name="registration/registration_closed.html" 61 | ), 62 | name="registration_disallowed", 63 | ), 64 | ] 65 | 66 | if getattr(settings, "INCLUDE_REGISTER_URL", True): 67 | urlpatterns += [ 68 | path( 69 | "register/", 70 | RegistrationView.as_view(), 71 | name="registration_register", 72 | ), 73 | ] 74 | 75 | if getattr(settings, "INCLUDE_AUTH_URLS", True): 76 | urlpatterns += [ 77 | path("", include("registration.auth_urls")), 78 | ] 79 | -------------------------------------------------------------------------------- /registration/backends/default/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.sites.shortcuts import get_current_site 3 | from django.shortcuts import render 4 | 5 | from ... import signals 6 | from ...models import RegistrationProfile 7 | from ...users import UserModel 8 | from ...views import ActivationView as BaseActivationView 9 | from ...views import RegistrationView as BaseRegistrationView 10 | from ...views import ResendActivationView as BaseResendActivationView 11 | 12 | 13 | class RegistrationView(BaseRegistrationView): 14 | """ 15 | A registration backend which follows a simple workflow: 16 | 17 | 1. User signs up, inactive account is created. 18 | 19 | 2. Email is sent to user with activation link. 20 | 21 | 3. User clicks activation link, account is now active. 22 | 23 | Using this backend requires that 24 | 25 | * ``registration`` be listed in the ``INSTALLED_APPS`` setting 26 | (since this backend makes use of models defined in this 27 | application). 28 | 29 | * The setting ``ACCOUNT_ACTIVATION_DAYS`` be supplied, specifying 30 | (as an integer) the number of days from registration during 31 | which a user may activate their account (after that period 32 | expires, activation will be disallowed). 33 | 34 | * The creation of the templates 35 | ``registration/activation_email_subject.txt`` and 36 | ``registration/activation_email.txt``, which will be used for 37 | the activation email. See the notes for this backends 38 | ``register`` method for details regarding these templates. 39 | 40 | When subclassing this view, you can set the ``SEND_ACTIVATION_EMAIL`` 41 | class variable to False to skip sending the new user a confirmation 42 | email or set ``SEND_ACTIVATION_EMAIL`` to ``False``. Doing so implies 43 | that you will have to activate the user manually from the admin site or 44 | send an activation by some other method. For example, by listening for 45 | the ``user_registered`` signal. 46 | 47 | Additionally, registration can be temporarily closed by adding the 48 | setting ``REGISTRATION_OPEN`` and setting it to 49 | ``False``. Omitting this setting, or setting it to ``True``, will 50 | be interpreted as meaning that registration is currently open and 51 | permitted. 52 | 53 | Internally, this is accomplished via storing an activation key in 54 | an instance of ``registration.models.RegistrationProfile``. See 55 | that model and its custom manager for full documentation of its 56 | fields and supported operations. 57 | 58 | """ 59 | 60 | SEND_ACTIVATION_EMAIL = getattr(settings, "SEND_ACTIVATION_EMAIL", True) 61 | success_url = "registration_complete" 62 | 63 | registration_profile = RegistrationProfile 64 | 65 | def register(self, form): 66 | """ 67 | Given a username, email address and password, register a new 68 | user account, which will initially be inactive. 69 | 70 | Along with the new ``User`` object, a new 71 | ``registration.models.RegistrationProfile`` will be created, 72 | tied to that ``User``, containing the activation key which 73 | will be used for this account. 74 | 75 | An email will be sent to the supplied email address; this 76 | email should contain an activation link. The email will be 77 | rendered using two templates. See the documentation for 78 | ``RegistrationProfile.send_activation_email()`` for 79 | information about these templates and the contexts provided to 80 | them. 81 | 82 | After the ``User`` and ``RegistrationProfile`` are created and 83 | the activation email is sent, the signal 84 | ``registration.signals.user_registered`` will be sent, with 85 | the new ``User`` as the keyword argument ``user`` and the 86 | class of this backend as the sender. 87 | 88 | """ 89 | site = get_current_site(self.request) 90 | 91 | if hasattr(form, "save"): 92 | new_user_instance = form.save(commit=False) 93 | else: 94 | new_user_instance = UserModel().objects.create_user( 95 | **form.cleaned_data 96 | ) 97 | 98 | new_user = self.registration_profile.objects.create_inactive_user( 99 | new_user=new_user_instance, 100 | site=site, 101 | send_email=self.SEND_ACTIVATION_EMAIL, 102 | request=self.request, 103 | ) 104 | signals.user_registered.send( 105 | sender=self.__class__, user=new_user, request=self.request 106 | ) 107 | return new_user 108 | 109 | def registration_allowed(self): 110 | """ 111 | Indicate whether account registration is currently permitted, 112 | based on the value of the setting ``REGISTRATION_OPEN``. This 113 | is determined as follows: 114 | 115 | * If ``REGISTRATION_OPEN`` is not specified in settings, or is 116 | set to ``True``, registration is permitted. 117 | 118 | * If ``REGISTRATION_OPEN`` is both specified and set to 119 | ``False``, registration is not permitted. 120 | 121 | """ 122 | return getattr(settings, "REGISTRATION_OPEN", True) 123 | 124 | 125 | class ActivationView(BaseActivationView): 126 | 127 | registration_profile = RegistrationProfile 128 | 129 | def activate(self, *args, **kwargs): 130 | """ 131 | Given an an activation key, look up and activate the user 132 | account corresponding to that key (if possible). 133 | 134 | After successful activation, the signal 135 | ``registration.signals.user_activated`` will be sent, with the 136 | newly activated ``User`` as the keyword argument ``user`` and 137 | the class of this backend as the sender. 138 | 139 | """ 140 | activation_key = kwargs.get("activation_key", "") 141 | site = get_current_site(self.request) 142 | user, activated = self.registration_profile.objects.activate_user( 143 | activation_key, site 144 | ) 145 | if activated: 146 | signals.user_activated.send( 147 | sender=self.__class__, user=user, request=self.request 148 | ) 149 | return user 150 | 151 | def get_success_url(self, user): 152 | return ("registration_activation_complete", (), {}) 153 | 154 | 155 | class ResendActivationView(BaseResendActivationView): 156 | 157 | registration_profile = RegistrationProfile 158 | 159 | def resend_activation(self, form): 160 | """ 161 | Given an email, look up user by email and resend activation key 162 | if user is not already activated or previous activation key has 163 | not expired. Note that if multiple users exist with the given 164 | email, no emails will be sent. 165 | 166 | Returns True if activation key was successfully sent, False otherwise. 167 | 168 | """ 169 | site = get_current_site(self.request) 170 | email = form.cleaned_data["email"] 171 | return self.registration_profile.objects.resend_activation_mail( 172 | email, site, self.request 173 | ) 174 | 175 | def render_form_submitted_template(self, form): 176 | """ 177 | Renders resend activation complete template with the submitted email. 178 | 179 | """ 180 | email = form.cleaned_data["email"] 181 | context = {"email": email} 182 | return render( 183 | self.request, 184 | "registration/resend_activation_complete.html", 185 | context, 186 | ) 187 | -------------------------------------------------------------------------------- /registration/forms.py: -------------------------------------------------------------------------------- 1 | """ 2 | Forms and validation code for user registration. 3 | 4 | Note that all of these forms assume Django's bundle default ``User`` 5 | model; since it's not possible for a form to anticipate in advance the 6 | needs of custom user models, you will need to write your own forms if 7 | you're using a custom model. 8 | 9 | """ 10 | 11 | from django import forms 12 | from django.contrib.auth.forms import BaseUserCreationForm 13 | 14 | from .users import UserModel 15 | from .users import UsernameField 16 | from .utils import _ 17 | from .models import Profile 18 | from crispy_forms.helper import FormHelper 19 | from crispy_forms.layout import Layout, Submit 20 | 21 | 22 | User = UserModel() 23 | 24 | 25 | class RegistrationForm(BaseUserCreationForm): 26 | """ 27 | Form for registering a new user account. 28 | 29 | Validates that the requested username is not already in use, and 30 | requires the password to be entered twice to catch typos. 31 | 32 | Subclasses should feel free to add any additional validation they 33 | need, but should avoid defining a ``save()`` method -- the actual 34 | saving of collected user data is delegated to the active 35 | registration backend. 36 | 37 | """ 38 | 39 | required_css_class = "required " 40 | email = forms.EmailField(label=_("E-mail")) 41 | 42 | class Meta: 43 | model = User 44 | fields = (UsernameField(), "email") 45 | 46 | 47 | class ResendActivationForm(forms.Form): 48 | required_css_class = "required" 49 | email = forms.EmailField(label=_("E-mail")) 50 | 51 | 52 | class UpdateForm(forms.ModelForm): 53 | 54 | class Meta: 55 | model = Profile 56 | fields = ( 57 | "name", 58 | "surname", 59 | "profile_image", 60 | "profession", 61 | "organization", 62 | "website", 63 | ) 64 | 65 | def __init__(self, *args, **kwargs): 66 | super(UpdateForm, self).__init__(*args, **kwargs) 67 | 68 | self.helper = FormHelper() 69 | self.helper.form_id = "id-Crispy_UpdateForm" 70 | self.helper.form_class = "form-horizontal" 71 | self.helper.add_input(Submit("update", "Update Profile")) 72 | -------------------------------------------------------------------------------- /registration/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/registration/management/__init__.py -------------------------------------------------------------------------------- /registration/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/registration/management/commands/__init__.py -------------------------------------------------------------------------------- /registration/management/commands/cleanupregistration.py: -------------------------------------------------------------------------------- 1 | """ 2 | A management command which deletes expired accounts (e.g., 3 | accounts which signed up but never activated) from the database. 4 | 5 | Calls ``RegistrationProfile.objects.delete_expired_users()``, which 6 | contains the actual logic for determining which accounts are deleted. 7 | 8 | """ 9 | 10 | from django.core.management.base import BaseCommand 11 | 12 | from ...models import RegistrationProfile 13 | 14 | 15 | class Command(BaseCommand): 16 | help = "Delete expired user registrations from the database" 17 | 18 | def handle(self, *args, **options): 19 | self.stdout.write("Running cleanupregistration.") 20 | deleted_count = RegistrationProfile.objects.delete_expired_users() 21 | if deleted_count == 0: 22 | self.stdout.write( 23 | "cleanupregistration completed. There is no user that has to be deleted." 24 | ) 25 | else: 26 | self.stdout.write( 27 | "cleanupregistration completed. Deleted user count=%d" 28 | % deleted_count 29 | ) 30 | -------------------------------------------------------------------------------- /registration/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/registration/migrations/__init__.py -------------------------------------------------------------------------------- /registration/signals.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.auth import get_backends 3 | from django.contrib.auth import login 4 | from django.dispatch import Signal 5 | 6 | # An admin has approved a user's account 7 | user_approved = Signal() 8 | 9 | # A new user has registered. 10 | user_registered = Signal() 11 | 12 | # A user has activated his or her account. 13 | user_activated = Signal() 14 | 15 | 16 | def login_user(sender, user, request, **kwargs): 17 | """Automatically authenticate the user when activated""" 18 | backend = get_backends()[0] # Hack to bypass `authenticate()`. 19 | user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__) 20 | login(request, user) 21 | request.session["REGISTRATION_AUTO_LOGIN"] = True 22 | request.session.modified = True 23 | 24 | 25 | if getattr(settings, "REGISTRATION_AUTO_LOGIN", False): 26 | user_activated.connect(login_user) 27 | -------------------------------------------------------------------------------- /registration/test_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/registration/test_app/__init__.py -------------------------------------------------------------------------------- /registration/test_app/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractBaseUser 2 | from django.contrib.auth.models import BaseUserManager 3 | from django.db import models 4 | 5 | 6 | class CustomUser(AbstractBaseUser): 7 | new_field = models.CharField(max_length=25) 8 | objects = BaseUserManager() 9 | 10 | USERNAME_FIELD = "new_field" 11 | -------------------------------------------------------------------------------- /registration/test_app/settings.py: -------------------------------------------------------------------------------- 1 | DATABASES = { 2 | "default": { 3 | "ENGINE": "django.db.backends.sqlite3", 4 | "NAME": "dr.sqlite3", 5 | }, 6 | } 7 | 8 | INSTALLED_APPS = ( 9 | "django.contrib.auth", 10 | "django.contrib.sites", 11 | "django.contrib.sessions", 12 | "django.contrib.contenttypes", 13 | "registration", 14 | "django.contrib.admin", 15 | "test_app", 16 | ) 17 | 18 | DEBUG = True 19 | 20 | ALLOWED_HOSTS = ["*"] 21 | 22 | SECRET_KEY = "_" 23 | 24 | SITE_ID = 1 25 | 26 | ROOT_URLCONF = "test_app.urls_default" 27 | 28 | TEMPLATES = [ 29 | { 30 | "BACKEND": "django.template.backends.django.DjangoTemplates", 31 | "OPTIONS": { 32 | "context_processors": [ 33 | "django.contrib.auth.context_processors.auth", 34 | "django.template.context_processors.debug", 35 | "django.template.context_processors.i18n", 36 | "django.template.context_processors.media", 37 | "django.template.context_processors.static", 38 | "django.template.context_processors.tz", 39 | "django.contrib.messages.context_processors.messages", 40 | ], 41 | "loaders": [ 42 | "django.template.loaders.app_directories.Loader", 43 | ], 44 | }, 45 | }, 46 | ] 47 | 48 | MIDDLEWARE = ( 49 | "django.middleware.common.CommonMiddleware", 50 | "django.contrib.sessions.middleware.SessionMiddleware", 51 | "django.middleware.csrf.CsrfViewMiddleware", 52 | "django.contrib.auth.middleware.AuthenticationMiddleware", 53 | "django.contrib.messages.middleware.MessageMiddleware", 54 | ) 55 | 56 | ACCOUNT_ACTIVATION_DAYS = 7 57 | REGISTRATION_EMAIL_SUBJECT_PREFIX = "[Django Registration Test App]" 58 | SEND_ACTIVATION_EMAIL = True 59 | REGISTRATION_AUTO_LOGIN = False 60 | 61 | EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" 62 | -------------------------------------------------------------------------------- /registration/test_app/settings_test.py: -------------------------------------------------------------------------------- 1 | DATABASES = { 2 | "default": { 3 | "ENGINE": "django.db.backends.sqlite3", 4 | "NAME": ":memory:", 5 | }, 6 | } 7 | 8 | INSTALLED_APPS = ( 9 | "django.contrib.auth", 10 | "django.contrib.admin", 11 | "django.contrib.sites", 12 | "django.contrib.sessions", 13 | "django.contrib.contenttypes", 14 | "registration", 15 | "test_app", 16 | ) 17 | 18 | DEBUG = True 19 | ALLOWED_HOSTS = ["*"] 20 | SECRET_KEY = "_" 21 | SITE_ID = 1 22 | ROOT_URLCONF = "test_app.urls_admin_approval" 23 | 24 | TEMPLATES = [ 25 | { 26 | "BACKEND": "django.template.backends.django.DjangoTemplates", 27 | "OPTIONS": { 28 | "context_processors": [ 29 | "django.contrib.auth.context_processors.auth", 30 | "django.template.context_processors.debug", 31 | "django.template.context_processors.i18n", 32 | "django.template.context_processors.media", 33 | "django.template.context_processors.static", 34 | "django.template.context_processors.tz", 35 | "django.contrib.messages.context_processors.messages", 36 | ], 37 | "loaders": [ 38 | "django.template.loaders.app_directories.Loader", 39 | ], 40 | }, 41 | }, 42 | ] 43 | 44 | MIDDLEWARE = ( 45 | "django.middleware.common.CommonMiddleware", 46 | "django.contrib.sessions.middleware.SessionMiddleware", 47 | "django.middleware.csrf.CsrfViewMiddleware", 48 | "django.contrib.auth.middleware.AuthenticationMiddleware", 49 | "django.contrib.messages.middleware.MessageMiddleware", 50 | ) 51 | 52 | ADMINS = ( 53 | ("admin1", "admin1@mail.server.com"), 54 | ("admin2", "admin2@mail.server.com"), 55 | ) 56 | 57 | 58 | REGISTRATION_ADMINS = ( 59 | ("admin1", "registration_admin1@mail.server.com"), 60 | ("admin2", "registration_admin2@mail.server.com"), 61 | ) 62 | 63 | ACCOUNT_ACTIVATION_DAYS = 7 64 | -------------------------------------------------------------------------------- /registration/test_app/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 4 | 5 | 6 | Django Registration Test App 7 | 8 | 9 | 10 | 23 | 24 |
25 |

{% block title %}{% endblock %}

26 | {% block content %} 27 | 28 | {% endblock content %} 29 |
30 | 31 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /registration/test_app/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}Django Registration Test App{% endblock %} 5 | 6 | {% block content %} 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /registration/test_app/templates/profile.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}Account Profile{% endblock %} 5 | 6 | {% block content %} 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /registration/test_app/urls_default.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include 2 | from django.contrib import admin 3 | from django.contrib.auth import views as auth_views 4 | from django.urls import path 5 | from django.views.generic import TemplateView 6 | 7 | urlpatterns = [ 8 | path("", TemplateView.as_view(template_name="index.html"), name="index"), 9 | path("accounts/", include("registration.backends.default.urls")), 10 | path( 11 | "accounts/profile/", 12 | TemplateView.as_view(template_name="profile.html"), 13 | name="profile", 14 | ), 15 | path( 16 | "login/", 17 | auth_views.LoginView.as_view(template_name="registration/login.html"), 18 | name="login", 19 | ), 20 | path("admin/", admin.site.urls, name="admin"), 21 | ] 22 | -------------------------------------------------------------------------------- /registration/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from registration import admin 2 | from registration.backends.default import urls 3 | 4 | 5 | def test(): 6 | assert admin 7 | assert urls 8 | -------------------------------------------------------------------------------- /registration/tests/test_forms.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django.test import TestCase 3 | 4 | from registration import forms 5 | from registration.users import UserModel 6 | 7 | 8 | class RegistrationFormTests(TestCase): 9 | """ 10 | Test the default registration forms. 11 | 12 | """ 13 | 14 | def test_registration_form(self): 15 | """ 16 | Test that ``RegistrationForm`` enforces username constraints 17 | and matching passwords. 18 | 19 | """ 20 | # Create a user so we can verify that duplicate usernames aren't 21 | # permitted. 22 | UserModel().objects.create_user("alice", "alice@example.com", "secret") 23 | bad_username_error = ( 24 | "Enter a valid username. This value may contain only letters, " 25 | "numbers, and @/./+/-/_ characters." 26 | ) 27 | password_didnt_match_error = "The two password fields didn't match." 28 | if django.VERSION >= (3, 0): 29 | password_didnt_match_error = "The two password fields didn’t match." 30 | 31 | invalid_data_dicts = [ 32 | # Non-alphanumeric username. 33 | { 34 | "data": { 35 | "username": "foo/bar", 36 | "email": "foo@example.com", 37 | "password1": "foo", 38 | "password2": "foo", 39 | }, 40 | "error": ("username", [bad_username_error]), 41 | }, 42 | # Already-existing username. 43 | { 44 | "data": { 45 | "username": "alice", 46 | "email": "alice@example.com", 47 | "password1": "secret", 48 | "password2": "secret", 49 | }, 50 | "error": ( 51 | "username", 52 | ["A user with that username already exists."], 53 | ), 54 | }, 55 | # Mismatched passwords. 56 | { 57 | "data": { 58 | "username": "foo", 59 | "email": "foo@example.com", 60 | "password1": "foo", 61 | "password2": "bar", 62 | }, 63 | "error": ("password2", [password_didnt_match_error]), 64 | }, 65 | ] 66 | 67 | for invalid_dict in invalid_data_dicts: 68 | form = forms.RegistrationForm(data=invalid_dict["data"]) 69 | self.assertFalse(form.is_valid()) 70 | self.assertEqual( 71 | form.errors[invalid_dict["error"][0]], invalid_dict["error"][1] 72 | ) 73 | 74 | form = forms.RegistrationForm( 75 | data={ 76 | "username": "foo", 77 | "email": "foo@example.com", 78 | "password1": "o09o@exa.c9om3", 79 | "password2": "o09o@exa.c9om3", 80 | } 81 | ) 82 | self.assertTrue(form.is_valid()) 83 | -------------------------------------------------------------------------------- /registration/tests/test_forms_custom_user.py: -------------------------------------------------------------------------------- 1 | from importlib import reload 2 | 3 | from django.test import TestCase 4 | from django.test.utils import override_settings 5 | 6 | from registration import forms 7 | from registration.users import UsernameField 8 | 9 | 10 | class RegistrationFormTests(TestCase): 11 | """ 12 | Test the default registration forms. 13 | 14 | """ 15 | 16 | def setUp(self): 17 | # The form's Meta class is created on import. We have to reload() 18 | # to apply the new AUTH_USER_MODEL to the Meta class. 19 | reload(forms) 20 | 21 | def test_registration_form_adds_custom_user_name_field(self): 22 | """ 23 | Test that ``RegistrationForm`` adds custom username 24 | field and does not raise errors 25 | 26 | """ 27 | 28 | form = forms.RegistrationForm() 29 | 30 | self.assertTrue(UsernameField() in form.fields) 31 | 32 | def test_registration_form_subclass_is_valid(self): 33 | """ 34 | Test that ``RegistrationForm`` subclasses can save 35 | 36 | """ 37 | data = { 38 | "username": "foo", 39 | "email": "foo@example.com", 40 | "password1": "o09o@exa.c9om3", 41 | "password2": "o09o@exa.c9om3", 42 | } 43 | 44 | form = forms.RegistrationForm(data=data) 45 | print(form.errors) 46 | 47 | self.assertTrue(form.is_valid()) 48 | -------------------------------------------------------------------------------- /registration/tests/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URLs used in the unit tests for django-registration. 3 | 4 | You should not attempt to use these URLs in any sort of real or 5 | development environment; instead, use 6 | ``registration/backends/default/urls.py``. This URLconf includes those 7 | URLs, and also adds several additional URLs which serve no purpose 8 | other than to test that optional keyword arguments are properly 9 | handled. 10 | 11 | """ 12 | 13 | from django.conf.urls import include 14 | from django.urls import path 15 | from django.views.generic import TemplateView 16 | 17 | from registration.views import ActivationView 18 | from registration.views import RegistrationView 19 | 20 | urlpatterns = [ 21 | # Test the 'activate' view with custom template 22 | # name. 23 | path( 24 | "activate-with-template-name//", 25 | ActivationView.as_view(), 26 | { 27 | "template_name": "registration/test_template_name.html", 28 | "backend": "registration.backends.default.DefaultBackend", 29 | }, 30 | name="registration_test_activate_template_name", 31 | ), 32 | # Test the 'activate' view with 33 | # extra_context_argument. 34 | path( 35 | "activate-extra-context//", 36 | ActivationView.as_view(), 37 | { 38 | "extra_context": {"foo": "bar", "callable": lambda: "called"}, 39 | "backend": "registration.backends.default.DefaultBackend", 40 | }, 41 | name="registration_test_activate_extra_context", 42 | ), 43 | # Test the 'activate' view with success_url argument. 44 | path( 45 | "activate-with-success-url//", 46 | ActivationView.as_view(), 47 | { 48 | "success_url": "registration_test_custom_success_url", 49 | "backend": "registration.backends.default.DefaultBackend", 50 | }, 51 | name="registration_test_activate_success_url", 52 | ), 53 | # Test the 'register' view with custom template 54 | # name. 55 | path( 56 | "register-with-template-name/", 57 | RegistrationView.as_view(), 58 | { 59 | "template_name": "registration/test_template_name.html", 60 | "backend": "registration.backends.default.DefaultBackend", 61 | }, 62 | name="registration_test_register_template_name", 63 | ), 64 | # Test the'register' view with extra_context 65 | # argument. 66 | path( 67 | "register-extra-context/", 68 | RegistrationView.as_view(), 69 | { 70 | "extra_context": {"foo": "bar", "callable": lambda: "called"}, 71 | "backend": "registration.backends.default.DefaultBackend", 72 | }, 73 | name="registration_test_register_extra_context", 74 | ), 75 | # Test the 'register' view with custom URL for 76 | # closed registration. 77 | path( 78 | "register-with-disallowed-url/", 79 | RegistrationView.as_view(), 80 | { 81 | "disallowed_url": "registration_test_custom_disallowed", 82 | "backend": "registration.backends.default.DefaultBackend", 83 | }, 84 | name="registration_test_register_disallowed_url", 85 | ), 86 | # Set up a pattern which will correspond to the 87 | # custom 'disallowed_url' above. 88 | path( 89 | "custom-disallowed/", 90 | TemplateView.as_view( 91 | template_name="registration/registration_closed.html" 92 | ), 93 | name="registration_test_custom_disallowed", 94 | ), 95 | # Test the 'register' view with custom redirect 96 | # on successful registration. 97 | path( 98 | "register-with-success_url/", 99 | RegistrationView.as_view(), 100 | { 101 | "success_url": "registration_test_custom_success_url", 102 | "backend": "registration.backends.default.DefaultBackend", 103 | }, 104 | name="registration_test_register_success_url", 105 | ), 106 | # Pattern for custom redirect set above. 107 | path( 108 | "custom-success/", 109 | TemplateView.as_view( 110 | template_name="registration/test_template_name.html" 111 | ), 112 | name="registration_test_custom_success_url", 113 | ), 114 | path("", include("registration.backends.default.urls")), 115 | ] 116 | -------------------------------------------------------------------------------- /registration/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, re_path 2 | from django.contrib.auth.decorators import login_required 3 | from .views import CreateProfileView, UpdateProfileView, ProfileView 4 | 5 | app_name = "registration" 6 | 7 | urlpatterns = [ 8 | path( 9 | "create_profile/", 10 | login_required(CreateProfileView.as_view()), 11 | name="create_profile", 12 | ), 13 | path( 14 | "update//", UpdateProfileView.as_view(), name="profile_update" 15 | ), 16 | path("", login_required(ProfileView.as_view()), name="profile_home"), 17 | ] 18 | -------------------------------------------------------------------------------- /registration/users.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.auth import get_user_model 3 | 4 | UserModel = get_user_model 5 | 6 | 7 | def UserModelString(): 8 | try: 9 | return settings.AUTH_USER_MODEL 10 | except AttributeError: 11 | return "auth.User" 12 | 13 | 14 | def UsernameField(): 15 | return getattr(UserModel(), "USERNAME_FIELD", "username") 16 | -------------------------------------------------------------------------------- /registration/utils.py: -------------------------------------------------------------------------------- 1 | from django import VERSION as DJANGO_VERSION 2 | 3 | if DJANGO_VERSION[0] < 3: 4 | from django.utils.translation import ugettext_lazy as _ # noqa 5 | else: 6 | from django.utils.translation import gettext_lazy as _ # noqa 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/requirements.txt -------------------------------------------------------------------------------- /search/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/search/__init__.py -------------------------------------------------------------------------------- /search/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /search/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SearchConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "search" 7 | -------------------------------------------------------------------------------- /search/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/search/migrations/__init__.py -------------------------------------------------------------------------------- /search/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /search/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /search/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import search 3 | 4 | urlpatterns = [ 5 | path("", search, name="search"), 6 | ] 7 | -------------------------------------------------------------------------------- /search/views.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Q 2 | from django.shortcuts import render 3 | from datasets.models import Dataset 4 | from community.models import Discussion, Comment 5 | 6 | 7 | def search(request): 8 | query = request.GET.get("q", "").strip() 9 | dataset_results, discussion_results, comment_results = [], [], [] 10 | if query: 11 | # Separate queries for each model 12 | dataset_results = Dataset.objects.filter( 13 | Q(name__icontains=query) | Q(description__icontains=query) 14 | ) 15 | discussion_results = Discussion.objects.filter( 16 | Q(title__icontains=query) | Q(description__icontains=query) 17 | ) 18 | comment_results = Comment.objects.filter(Q(content__icontains=query)) 19 | 20 | return render( 21 | request, 22 | "search/search_results.html", 23 | { 24 | "query": query, 25 | "dataset_results": dataset_results, 26 | "discussion_results": discussion_results, 27 | "comment_results": comment_results, 28 | }, 29 | ) 30 | -------------------------------------------------------------------------------- /static/css/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /static/images/contributor1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/static/images/contributor1.png -------------------------------------------------------------------------------- /static/images/create-account-office-dark.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/static/images/create-account-office-dark.jpeg -------------------------------------------------------------------------------- /static/images/create-account-office.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/static/images/create-account-office.jpeg -------------------------------------------------------------------------------- /static/images/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/static/images/dashboard.png -------------------------------------------------------------------------------- /static/images/dog-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/static/images/dog-image.png -------------------------------------------------------------------------------- /static/images/forgot-password-office-dark.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/static/images/forgot-password-office-dark.jpeg -------------------------------------------------------------------------------- /static/images/forgot-password-office.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/static/images/forgot-password-office.jpeg -------------------------------------------------------------------------------- /static/images/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/images/login-office-dark.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/static/images/login-office-dark.jpeg -------------------------------------------------------------------------------- /static/images/login-office.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/static/images/login-office.jpeg -------------------------------------------------------------------------------- /static/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/static/images/logo.png -------------------------------------------------------------------------------- /static/images/person-with-dog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/static/images/person-with-dog.png -------------------------------------------------------------------------------- /static/images/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/js/focus-trap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Limit focus to focusable elements inside `element` 3 | * @param {HTMLElement} element - DOM element to focus trap inside 4 | * @return {Function} cleanup function 5 | */ 6 | function focusTrap(element) { 7 | const focusableElements = getFocusableElements(element) 8 | const firstFocusableEl = focusableElements[0] 9 | const lastFocusableEl = focusableElements[focusableElements.length - 1] 10 | 11 | // Wait for the case the element was not yet rendered 12 | setTimeout(() => firstFocusableEl.focus(), 50) 13 | 14 | /** 15 | * Get all focusable elements inside `element` 16 | * @param {HTMLElement} element - DOM element to focus trap inside 17 | * @return {HTMLElement[]} List of focusable elements 18 | */ 19 | function getFocusableElements(element = document) { 20 | return [ 21 | ...element.querySelectorAll( 22 | 'a, button, details, input, select, textarea, [tabindex]:not([tabindex="-1"])' 23 | ), 24 | ].filter((e) => !e.hasAttribute('disabled')) 25 | } 26 | 27 | function handleKeyDown(e) { 28 | const TAB = 9 29 | const isTab = e.key.toLowerCase() === 'tab' || e.keyCode === TAB 30 | 31 | if (!isTab) return 32 | 33 | if (e.shiftKey) { 34 | if (document.activeElement === firstFocusableEl) { 35 | lastFocusableEl.focus() 36 | e.preventDefault() 37 | } 38 | } else { 39 | if (document.activeElement === lastFocusableEl) { 40 | firstFocusableEl.focus() 41 | e.preventDefault() 42 | } 43 | } 44 | } 45 | 46 | element.addEventListener('keydown', handleKeyDown) 47 | 48 | return function cleanup() { 49 | element.removeEventListener('keydown', handleKeyDown) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /static/js/init-alpine.js: -------------------------------------------------------------------------------- 1 | function data() { 2 | function getThemeFromLocalStorage() { 3 | // if user already changed the theme, use it 4 | if (window.localStorage.getItem('dark')) { 5 | return JSON.parse(window.localStorage.getItem('dark')) 6 | } 7 | 8 | // else return their preferences 9 | return ( 10 | !!window.matchMedia && 11 | window.matchMedia('(prefers-color-scheme: dark)').matches 12 | ) 13 | } 14 | 15 | function setThemeToLocalStorage(value) { 16 | window.localStorage.setItem('dark', value) 17 | } 18 | 19 | return { 20 | dark: getThemeFromLocalStorage(), 21 | toggleTheme() { 22 | this.dark = !this.dark 23 | setThemeToLocalStorage(this.dark) 24 | }, 25 | isSideMenuOpen: false, 26 | toggleSideMenu() { 27 | this.isSideMenuOpen = !this.isSideMenuOpen 28 | }, 29 | closeSideMenu() { 30 | this.isSideMenuOpen = false 31 | }, 32 | isNotificationsMenuOpen: false, 33 | toggleNotificationsMenu() { 34 | this.isNotificationsMenuOpen = !this.isNotificationsMenuOpen 35 | }, 36 | closeNotificationsMenu() { 37 | this.isNotificationsMenuOpen = false 38 | }, 39 | isProfileMenuOpen: false, 40 | toggleProfileMenu() { 41 | this.isProfileMenuOpen = !this.isProfileMenuOpen 42 | }, 43 | closeProfileMenu() { 44 | this.isProfileMenuOpen = false 45 | }, 46 | isPagesMenuOpen: false, 47 | togglePagesMenu() { 48 | this.isPagesMenuOpen = !this.isPagesMenuOpen 49 | }, 50 | // Modal 51 | isModalOpen: false, 52 | trapCleanup: null, 53 | openModal() { 54 | this.isModalOpen = true 55 | this.trapCleanup = focusTrap(document.querySelector('#modal')) 56 | }, 57 | closeModal() { 58 | this.isModalOpen = false 59 | this.trapCleanup() 60 | }, 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['templates/**/*.html'], 3 | theme: { 4 | extend: {}, 5 | }, 6 | plugins: [], 7 | } 8 | -------------------------------------------------------------------------------- /templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | {% load static %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% block title %}{% endblock %} 21 | 22 | 23 | 25 | 26 | 27 | 31 | 32 | 36 | 37 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
51 | {% block content %} 52 | {% endblock %} 53 |
54 | 55 | -------------------------------------------------------------------------------- /templates/community/create_discussion.html: -------------------------------------------------------------------------------- 1 | {% extends 'dashboard.html' %} 2 | {% load tailwind_filters %} 3 | {% block title %}New Discussion{% endblock %} 4 | {% block main %} 5 |

New Discussion

6 |
7 | {% csrf_token %} 8 | {{ form|crispy }} 9 | 12 | 13 |
14 | 15 | {% endblock %} -------------------------------------------------------------------------------- /templates/community/discussion_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'dashboard.html' %} 2 | {% load tailwind_filters %} 3 | {% block title %}{% endblock %} 4 | {% block main %} 5 |
6 | 7 |
8 |

9 | {{ discussion.title }} 10 |

11 |
12 | Started by 13 | {{ discussion.created_by }} 14 | on {{ discussion.created_at }} 15 |
16 |

17 | {{ discussion.description }} 18 |

19 |
20 | 21 |
22 |

23 | {% if comment_to_edit %} 24 | Edit Comment 25 | {% else %} 26 | Add a Comment 27 | {% endif %} 28 |

29 |
30 | 31 | {% csrf_token %} 32 | 33 | {{ comment_form|crispy }} 34 | 44 |
45 |
46 | 47 | 48 |
49 |

Comments

50 | 51 | {% for comment in comments %} 52 | 53 |
54 |
55 | Comment by 56 | {{ comment.created_by }} 57 | on {{ comment.created_at }} 58 |
59 |

60 | {{ comment.content }} 61 |

62 | 63 | 64 | 68 | Edit 69 | 70 | 71 | 76 | Delete 77 | 78 | 79 |
80 | {% endfor %} 81 |
82 |
83 | 84 | {% endblock %} -------------------------------------------------------------------------------- /templates/community/discussion_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'dashboard.html' %} 2 | {% block title %}{% endblock %} 3 | {% block main %} 4 |
5 | 6 |
7 |

Discussions

8 |
9 | 10 | 11 |
12 | 13 | {% for discussion in page_obj %} 14 |
15 | 16 | 17 |

18 | 19 | {{ discussion.title }} 20 | 21 |

22 |
23 |
24 | Started by 25 | {{ discussion.created_by }} 26 | on {{ discussion.created_at }} 27 |
28 |
29 | {{ discussion.get_comments.count }} comments 30 |
31 | 32 |
33 | 34 |
35 | {% endfor %} 36 | 37 | 38 |
39 | 40 | 41 |
42 | {% if page_obj.has_previous %} 43 | « first 44 | previous 45 | {% endif %} 46 | {% if page_obj.has_next %} 47 | next 48 | last » 49 | {% endif %} 50 |
51 |
52 | 53 | {% endblock %} -------------------------------------------------------------------------------- /templates/community/edit_comment.html: -------------------------------------------------------------------------------- 1 | {% extends 'dashboard.html' %} 2 | {% block title %}{% endblock %} 3 | {% block main %} 4 | 5 | {% endblock %} -------------------------------------------------------------------------------- /templates/dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 |
8 | {% include 'dashboard_sidebar.html' %} 9 |
10 | {% include 'dashboard_navbar.html' %} 11 |
12 | 13 |
14 | {% block main %} 15 | {% endblock %} 16 |
17 |
18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /templates/datasets/add_version.html: -------------------------------------------------------------------------------- 1 | {% extends 'dashboard.html' %} 2 | {% load tailwind_filters %} 3 | {% block title %}New Dataset Version{% endblock %} 4 | {% block main %} 5 |

Add New Dataset Version

6 |
7 | {% csrf_token %} 8 | {{ form|crispy }} 9 | 12 | 13 |
14 | 15 | 25 | 26 | 27 |
28 | 83 | 84 | 85 | {% endblock %} -------------------------------------------------------------------------------- /templates/datasets/dataset_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'dashboard.html' %} 2 | {% block title %}List Datasets{% endblock %} 3 | {% block main %} 4 |
5 |

Datasets

6 |

7 | Browse our collection of datasets for your next project. 8 |

9 |
10 | 11 | 12 | {% if datasets %} 13 |
14 | {% for dataset in datasets%} 15 | 16 |
19 |
20 |

21 | {{dataset.name}} 22 |

23 |

24 | {{dataset.description}} 25 | 26 |

27 |
28 |
29 | 32 | 39 | 45 | 46 | 47 | Category 48 | {% for tag in dataset.tags.all %} 49 |

{{ tag.name }}

50 | {% endfor %} 51 |
52 | 56 | View Details 57 | 58 | 59 |
60 | 61 |
62 | 63 |
64 |

65 | {{dataset.created_at}} 66 |

67 |
68 |
69 |
70 | {% endfor %} 71 | {% else %} 72 |

73 | No Datasets currently uploaded 74 |

75 | {% endif %} 76 | {% endblock %} -------------------------------------------------------------------------------- /templates/datasets/upload_dataset.html: -------------------------------------------------------------------------------- 1 | {% extends 'dashboard.html' %} 2 | {% load tailwind_filters %} 3 | {% block title %}Upload Dataset{% endblock %} 4 | {% block main %} 5 | 6 |

Add New Dataset

7 |
8 | {% csrf_token %} 9 | {{ form|crispy }} 10 | 11 | 12 | 15 | 16 |
17 | 18 | {% endblock %} -------------------------------------------------------------------------------- /templates/footer.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/templates/footer.html -------------------------------------------------------------------------------- /templates/home/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Home{% endblock %} 4 | {% block content %} 5 | {% load static %} 6 | {% load tailwind_filters %} 7 | {% include "navbar.html" %} 8 |
9 | Dog 10 |

VetDataHub is an open-source platform for sharing veterinary datasets to advance medical research.

11 |
12 | 13 | 14 | 15 | 16 |
17 |
18 |

Explore Our Features

19 |

Key features of this project include secure user login and authentication, dataset management, and a user-friendly interface.

20 |
21 |
22 |
23 | 24 |

View Datasets

25 |
26 |
27 | 28 |

Upload Datasets

29 |
30 |
31 | 32 |

Download Datasets

33 |
34 |
35 | 36 |

Explore Datasets

37 |
38 |
39 |
40 |
41 | 42 | 43 |
44 | 45 |

Diverse Datasets

46 |

47 | Explore a wide range of veterinary datasets, including medical images, clinical records, genomic data, epidemiological data, and more. 48 |

49 |
50 | 51 | 52 |
53 | 54 |

Community Collaboration

55 |

56 | Engage with a vibrant community of contributors and users passionate about leveraging data science for animal welfare. 57 |

58 |
59 | 60 | 61 |
62 | 63 |

Open Access

64 |

65 | Access datasets freely, empowering researchers, students, and practitioners to address pressing challenges in veterinary medicine. 66 |

67 |
68 | 69 | 70 |
71 | 72 |

Ethical Standards

73 |

74 | Uphold ethical standards and data privacy principles to ensure responsible data sharing and usage. 75 |

76 |
77 | 78 | 79 |
80 | 81 |

Documentation and Support

82 |

83 | Benefit from comprehensive documentation, tutorials, and user support to facilitate dataset discovery, understanding, and utilization. 84 |

85 |
86 | 87 |
88 | 89 | 90 |
91 |

About Us

92 |
93 | 94 |
95 | Person with Dog 100 |
101 |
102 | 103 |

104 | The project aims to make open-source veterinary data available to veterinary professionals, researchers, and data scientists from around the world to contribute, discover, and utilize valuable datasets for research, diagnosis, and decision-making in animal health. 105 |

106 |
107 |
108 | 109 | 110 |
111 |
112 |
113 |

Get in touch

114 |

We'd love to hear from you. Our friendly team is always here to chat.

115 |
    116 |
  • Email: vetdatahub@gmail.com
  • 117 |
118 |
119 |
120 |

Send us a message

121 |
122 | {% csrf_token %} 123 | 124 | {{ form }} 125 | 126 |
127 |
128 |
129 |
130 | {% endblock %} 131 | {% include "footer.html" %} 132 | -------------------------------------------------------------------------------- /templates/navbar.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 28 | 29 | 40 | -------------------------------------------------------------------------------- /templates/profiles/create_profile.html: -------------------------------------------------------------------------------- 1 | {% extends "dashboard.html" %} 2 | {% load i18n tz static tailwind_filters %} 3 | {% block meta_title %}{% if page %}{{ page.meta_title }}{% else %}{% trans "Profile || VetDataHub" %}{% endif %}{% endblock %} 4 | {% block title %}Profile{% endblock %} 5 | 6 | {% block main %} 7 |
8 |
9 |
10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |

18 | Create {% if user_profile %} 19 | {{ request.user.user_profile.first_name }} {{ request.user.user_profile.other_names }} 20 | {{ request.user.user_profile.last_name }} 21 | {% else %} 22 | {% firstof user.get_short_name user.get_username|capfirst %} 23 | {% endif %}'s profile below 📝 24 |

25 |
26 |
27 | 28 |
29 | 30 |
36 | {% csrf_token %} 37 |
38 | 64 | {{ form|crispy }} 65 | {{ form.media }} 66 |
67 |
68 | 75 |
76 |
77 |
78 | 79 |
80 | 81 |
82 | 110 |
111 |
112 | 113 |
114 |
115 |
116 | 117 | {% endblock %} -------------------------------------------------------------------------------- /templates/profiles/home.html: -------------------------------------------------------------------------------- 1 | {% extends "dashboard.html" %} 2 | {% load i18n tz static %} 3 | {% block meta_title %}{% if page %}{{ page.meta_title }}{% else %}{% trans "Profile || VetDataHub" %}{% endif %}{% endblock %} 4 | {% block title %}Profile{% endblock %} 5 | 6 | {% block main %} 7 |
8 |
9 |
10 | 11 | {% if user.user_profile.profile_image %} 12 |
13 | 14 |
15 | {{ user.user_profile.get_full_name }} 20 |
21 |

22 | {{ user.user_profile.get_full_name }} 23 |

24 |

{{ user.email }}

25 |
26 |
27 | 28 | 29 |
30 |

Profile Details

31 |
    32 |
  • 33 | Profession: 34 | 35 | {% if user.user_profile.profession %} 36 | {{ user.user_profile.profession }} 37 | {% else %} 38 | Not provided 39 | {% endif %} 40 | 41 |
  • 42 |
  • 43 | Organization: 44 | 45 | {% if user.user_profile.organization %} 46 | {{ user.user_profile.organization }} 47 | {% else %} 48 | Not provided 49 | {% endif %} 50 | 51 |
  • 52 |
  • 53 | Website: 54 | 59 | {% if user.user_profile.website %} 60 | {{ user.user_profile.website }} 61 | {% else %} 62 | Not provided 63 | {% endif %} 64 | 65 |
  • 66 |
  • 67 | Date Created: 68 | 69 | {{ user.user_profile.date_created|date:"F j, Y" }} 70 | 71 |
  • 72 |
  • 73 | Last Updated: 74 | 75 | {{ user.user_profile.updated|date:"F j, Y, g:i a" }} 76 | 77 |
  • 78 |
79 |
80 | 81 | 82 | 91 |
92 | {% else %} 93 |

94 | You currently dont have a profile. Create one here 95 |

96 | {% endif%} 97 |
98 |
99 |
100 | 101 | {% endblock %} 102 | -------------------------------------------------------------------------------- /templates/profiles/update.html: -------------------------------------------------------------------------------- 1 | {% extends "dashboard.html" %} 2 | {% load i18n tailwind_filters %} 3 | {% block meta_title %}{% if page %}{{ page.meta_title }}{% else %}{% trans "Update Profile || PyCon Africa" %}{% endif %}{% endblock %} 4 | 5 | 6 | {% block main %} 7 |
8 |
9 |
10 |
11 | 12 |
13 |
14 |

15 | Update your details below 16 |

17 |
23 | {% csrf_token %} 24 | {{ form|crispy }} 25 | {{ form.media }} 26 |
27 | 35 |
36 |
37 |
38 |
39 | 40 |
41 |
42 |

43 | Note: Ensure all fields are filled out accurately 44 | to keep your profile up to date. 45 |

46 |
47 |
48 |
49 |
50 |
51 |
52 | 53 | {% endblock %} 54 | 55 | -------------------------------------------------------------------------------- /templates/registration/activate.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/registration_base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Activation Failure" %}{% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |

{% trans "Activation Failure" %}

10 |

{% trans "Account activation failed." %}

11 | 15 | {% trans "Resend Activation Email" %} 16 | 17 |
18 |
19 | {% endblock %} 20 | 21 | {% comment %} 22 | **registration/activate.html** 23 | 24 | Used if account activation fails. With the default setup, has the following context: 25 | 26 | ``activation_key`` 27 | The activation key used during the activation attempt. 28 | {% endcomment %} 29 | -------------------------------------------------------------------------------- /templates/registration/activation_complete.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/registration_base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Account Activated" %}{% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |

{% trans "Account Activated" %}

10 |

11 | {% trans "Your account is now activated." %} 12 |

13 | {% if not user.is_authenticated %} 14 |

15 | {% trans "You can log in." %} 16 |

17 | 21 | {% trans "Log In" %} 22 | 23 | {% endif %} 24 |
25 |
26 | {% endblock %} 27 | 28 | {% comment %} 29 | **registration/activation_complete.html** 30 | 31 | Used after successful account activation. This template has no context 32 | variables of its own, and should simply inform the user that their 33 | account is now active. 34 | {% endcomment %} 35 | -------------------------------------------------------------------------------- /templates/registration/activation_email.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 4 | 5 | 6 | {{ site.name }} {% trans "registration" %} 7 | 8 | 9 | 10 |

11 | {% blocktrans with site_name=site.name %} 12 | You (or someone pretending to be you) have asked to register an account at 13 | {{ site_name }}. If this wasn't you, please ignore this email 14 | and your address will be removed from our records. 15 | {% endblocktrans %} 16 |

17 |

18 | {% blocktrans %} 19 | To activate this account, please click the following link within the next 20 | {{ expiration_days }} days: 21 | {% endblocktrans %} 22 |

23 | 24 |

25 | 26 | {{site.domain}}{% url 'registration_activate' activation_key %} 27 | 28 |

29 |

30 | {% blocktrans with site_name=site.name %} 31 | Sincerely, 32 | {{ site_name }} Management 33 | {% endblocktrans %} 34 |

35 | 36 | 37 | 38 | 39 | 40 | {% comment %} 41 | **registration/activation_email.html** 42 | 43 | Used to generate the html body of the activation email. Should display a 44 | link the user can click to activate the account. This template has the 45 | following context: 46 | 47 | ``activation_key`` 48 | The activation key for the new account. 49 | 50 | ``expiration_days`` 51 | The number of days remaining during which the account may be 52 | activated. 53 | 54 | ``site`` 55 | An object representing the site on which the user registered; 56 | depending on whether ``django.contrib.sites`` is installed, this 57 | may be an instance of either ``django.contrib.sites.models.Site`` 58 | (if the sites application is installed) or 59 | ``django.contrib.sites.requests.RequestSite`` (if not). Consult `the 60 | documentation for the Django sites framework 61 | `_ for 62 | details regarding these objects' interfaces. 63 | 64 | ``user`` 65 | The new user account 66 | 67 | ``request`` 68 | ``HttpRequest`` instance for better flexibility. 69 | For example it can be used to compute absolute register URL: 70 | 71 | {{ request.scheme }}://{{ request.get_host }}{% url 'registration_activate' activation_key %} 72 | {% endcomment %} 73 | -------------------------------------------------------------------------------- /templates/registration/activation_email.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans with site_name=site.name %} 3 | You (or someone pretending to be you) have asked to register an account at 4 | {{ site_name }}. If this wasn't you, please ignore this email 5 | and your address will be removed from our records. 6 | {% endblocktrans %} 7 | {% blocktrans %} 8 | To activate this account, please click the following link within the next 9 | {{ expiration_days }} days: 10 | {% endblocktrans %} 11 | 12 | http://{{site.domain}}{% url 'registration_activate' activation_key %} 13 | 14 | {% blocktrans with site_name=site.name %} 15 | Sincerely, 16 | {{ site_name }} Management 17 | {% endblocktrans %} 18 | 19 | 20 | {% comment %} 21 | **registration/activation_email.txt** 22 | 23 | Used to generate the text body of the activation email. Should display a 24 | link the user can click to activate the account. This template has the 25 | following context: 26 | 27 | ``activation_key`` 28 | The activation key for the new account. 29 | 30 | ``expiration_days`` 31 | The number of days remaining during which the account may be 32 | activated. 33 | 34 | ``site`` 35 | An object representing the site on which the user registered; 36 | depending on whether ``django.contrib.sites`` is installed, this 37 | may be an instance of either ``django.contrib.sites.models.Site`` 38 | (if the sites application is installed) or 39 | ``django.contrib.sites.requests.RequestSite`` (if not). Consult `the 40 | documentation for the Django sites framework 41 | `_ for 42 | details regarding these objects' interfaces. 43 | 44 | ``user`` 45 | The new user account 46 | 47 | ``request`` 48 | ``HttpRequest`` instance for better flexibility. 49 | For example it can be used to compute absolute register URL: 50 | 51 | {{ request.scheme }}://{{ request.get_host }}{% url 'registration_activate' activation_key %} 52 | {% endcomment %} 53 | -------------------------------------------------------------------------------- /templates/registration/activation_email_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %}{% trans "Account activation on" %} {{ site.name }} 2 | 3 | 4 | {% comment %} 5 | **registration/activation_email_subject.txt** 6 | 7 | Used to generate the subject line of the activation email. Because the 8 | subject line of an email must be a single line of text, any output 9 | from this template will be forcibly condensed to a single line before 10 | being used. This template has the following context: 11 | 12 | ``activation_key`` 13 | The activation key for the new account. 14 | 15 | ``expiration_days`` 16 | The number of days remaining during which the account may be 17 | activated. 18 | 19 | ``site`` 20 | An object representing the site on which the user registered; 21 | depending on whether ``django.contrib.sites`` is installed, this 22 | may be an instance of either ``django.contrib.sites.models.Site`` 23 | (if the sites application is installed) or 24 | ``django.contrib.sites.requests.RequestSite`` (if not). Consult `the 25 | documentation for the Django sites framework 26 | `_ for 27 | details regarding these objects' interfaces. 28 | {% endcomment %} 29 | -------------------------------------------------------------------------------- /templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/registration_base.html" %} 2 | {% load i18n static %} 3 | {% load tailwind_filters %} 4 | {% block title %}{% trans "Log in" %}{% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |
10 | Logo 11 |
12 |

Login

13 |

"Please enter your login details below to access your account."

14 | 15 |
16 | {% csrf_token %} 17 | {{ form|crispy }} 18 | 22 | 23 | 26 | 27 |

28 | Don’t have an account? Sign up 30 |

31 |
32 |
33 |
34 | 35 | {% endblock %} 36 | 37 | {% comment %} 38 | **registration/login.html** 39 | 40 | It's your responsibility to provide the login form in a template called 41 | registration/login.html by default. This template gets passed four 42 | template context variables: 43 | 44 | ``form`` 45 | A Form object representing the login form. See the forms 46 | documentation for more on Form objects. 47 | 48 | ``next`` 49 | The URL to redirect to after successful login. This may contain a 50 | query string, too. 51 | 52 | ``site`` 53 | The current Site, according to the SITE_ID setting. If you don't 54 | have the site framework installed, this will be set to an instance 55 | of RequestSite, which derives the site name and domain from the 56 | current HttpRequest. 57 | 58 | ``site_name`` 59 | An alias for site.name. If you don't have the site framework 60 | installed, this will be set to the value of 61 | request.META['SERVER_NAME']. For more on sites, see The 62 | "sites" framework. 63 | {% endcomment %} 64 | -------------------------------------------------------------------------------- /templates/registration/logout.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/registration_base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Logged out" %}{% endblock %} 5 | 6 | {% block content %} 7 |

{% trans "Successfully logged out" %}.

8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /templates/registration/password_change_done.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/registration_base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Password Changed" %}{% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |

{% trans "Password Changed" %}

10 |

11 | {% trans "Your password has been successfully changed!" %} 12 |

13 | 17 | {% trans "Log In" %} 18 | 19 |
20 |
21 | {% endblock %} 22 | 23 | {# This is used by django.contrib.auth #} 24 | -------------------------------------------------------------------------------- /templates/registration/password_change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/registration_base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Change Password" %}{% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |

{% trans "Change Password" %}

10 |
11 | {% csrf_token %} 12 | {{ form|crispy }} 13 |
14 | 20 |
21 |
22 |
23 |
24 | {% endblock %} 25 | 26 | {# This is used by django.contrib.auth #} 27 | -------------------------------------------------------------------------------- /templates/registration/password_reset_complete.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/registration_base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Password Reset Complete" %}{% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |

{% trans "Password Reset Complete" %}

10 |

11 | {% trans "Your password has been reset!" %} 12 |

13 |

14 | {% blocktrans %}You may now 15 | 16 | {% trans "log in" %} 17 | . 18 | {% endblocktrans %} 19 |

20 |
21 |
22 | {% endblock %} 23 | 24 | {# This is used by django.contrib.auth #} 25 | -------------------------------------------------------------------------------- /templates/registration/password_reset_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/registration_base.html" %} 2 | {% load i18n %} 3 | 4 | {% block meta %} 5 | 7 | 8 | {% endblock %} 9 | 10 | {% block title %}{% trans "Confirm Password Reset" %}{% endblock %} 11 | 12 | {% block content %} 13 |
14 |
15 | {% if validlink %} 16 |

{% trans "Confirm Password Reset" %}

17 |

18 | {% trans "Enter your new password below to reset your password:" %} 19 |

20 |
21 | {% csrf_token %} 22 |
23 | {{ form | crispy }} 24 |
25 | 28 |
29 | {% else %} 30 |

{% trans "Password Reset Unsuccessful" %}

31 |

32 | {% trans "The password reset link is invalid or has expired." %} 33 | 34 | {% trans "Try again" %} 35 | . 36 |

37 | {% endif %} 38 |
39 |
40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /templates/registration/password_reset_done.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/registration_base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Password Reset" %}{% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |

{% trans "Password Reset" %}

10 |

11 | {% blocktrans %} 12 | We have sent you an email with a link to reset your password. Please check 13 | your email and click the link to continue. 14 | {% endblocktrans %} 15 |

16 | 17 | {% trans "Return to Login" %} 18 | 19 |
20 |
21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /templates/registration/password_reset_email.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | {% blocktrans %}Greetings{% endblocktrans %} {% if user.get_full_name %}{{ user.get_full_name }}{% else %}{{ user }}{% endif %}, 4 | 5 | {% blocktrans %} 6 | You are receiving this email because you (or someone pretending to be you) 7 | requested that your password be reset on the {{ domain }} site. If you do not 8 | wish to reset your password, please ignore this message. 9 | {% endblocktrans %} 10 | 11 | {% blocktrans %} 12 | To reset your password, please click the following link, or copy and paste it 13 | into your web browser: 14 | {% endblocktrans %} 15 | 16 | 17 | {{ protocol }}://{{ domain }}{% url 'auth_password_reset_confirm' uid token %} 18 | 19 | 20 | {% blocktrans %}Your username, in case you've forgotten:{% endblocktrans %} {{ user.get_username }} 21 | 22 | 23 | {% blocktrans %}Best regards{% endblocktrans %}, 24 | {{ site_name }} {% blocktrans %}Management{% endblocktrans %} 25 | 26 | 27 | {# This is used by django.contrib.auth #} 28 | -------------------------------------------------------------------------------- /templates/registration/password_reset_form.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/registration_base.html" %} 2 | {% load i18n tailwind_filters %} 3 | 4 | {% block title %}{% trans "Reset Password" %}{% endblock %} 5 | 6 | {% block content %} 7 |
8 |

9 | {% blocktrans %} 10 | Forgot your password? Enter your email in the form below and we'll send you instructions for creating a new one. 11 | {% endblocktrans %} 12 |

13 | 14 |
15 | {% csrf_token %} 16 | {{ form|crispy }} 17 | 20 |
21 |
22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /templates/registration/registration_base.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | -------------------------------------------------------------------------------- /templates/registration/registration_closed.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/registration_base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Registration is closed" %}{% endblock %} 5 | 6 | {% block content %} 7 |

{% trans "Sorry, but registration is closed at this moment. Come back later." %}

8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /templates/registration/registration_complete.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/registration_base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Activation Email Sent" %}{% endblock %} 5 | 6 | {% block content %} 7 |
8 |

9 | {% blocktrans with email=request.session.registration_email %} 10 | Please check your email, {{ email }}, to complete the registration process. 11 | {% endblocktrans %} 12 |

13 |
14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /templates/registration/registration_form.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/registration_base.html" %} 2 | {% load i18n static %} 3 | {% load tailwind_filters %} 4 | {% block title %}{% trans "Register for an account" %}{% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |
10 | 11 | Logo 12 | 13 |

Sign up

14 |

"Please enter your sign up details below to access your 15 | account."

16 |
17 | 18 |
19 | {% csrf_token %} 20 | {{ form|crispy }} 21 | 22 | 26 | 27 | 28 |

29 | Already have an account? Log in 31 |

32 | 33 |
34 |
35 |
36 | {% endblock %} 37 | 38 | 39 | {% comment %} 40 | **registration/registration_form.html** 41 | Used to show the form users will fill out to register. By default, has 42 | the following context: 43 | 44 | ``form`` 45 | The registration form. This will be an instance of some subclass 46 | of ``django.forms.Form``; consult `Django's forms documentation 47 | `_ for 48 | information on how to display this in a template. 49 | {% endcomment %} 50 | -------------------------------------------------------------------------------- /templates/registration/resend_activation_complete.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/registration_base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Account Activation Resent" %}{% endblock %} 5 | 6 | {% block content %} 7 |
8 |

9 | {% blocktrans %} 10 | We have sent an email to {{ email }} with further instructions. 11 | {% endblocktrans %} 12 |

13 |
14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /templates/registration/resend_activation_form.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/registration_base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Resend Activation Email" %}{% endblock %} 5 | 6 | {% block content %} 7 |
8 |

9 | {% trans "Resend Activation Email" %} 10 |

11 |
12 | {% csrf_token %} 13 |
14 | {{ form | crispy }} 15 |
16 | 19 |
20 |
21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /templates/search/search_results.html: -------------------------------------------------------------------------------- 1 | {% extends 'dashboard.html' %} 2 | {% block title %} Search Results{% endblock %} 3 | {% block main %} 4 |
5 |

Search Results

6 | 7 | {% if query %} 8 |

Results for "{{ query }}":

9 | 10 |
11 | 12 | 32 | 33 | 34 |
35 | 36 |
37 |

Datasets

38 | {% if dataset_results %} 39 |
    40 | {% for dataset in dataset_results %} 41 |
  • 42 |

    43 | 44 | {{ dataset.name }} 45 | 46 |

    47 |

    {{ dataset.description|truncatewords:20 }}

    48 |
  • 49 | {% endfor %} 50 |
51 | {% else %} 52 |

No datasets found.

53 | {% endif %} 54 |
55 | 56 | 57 |
58 |

Discussions

59 | {% if discussion_results %} 60 |
    61 | {% for discussion in discussion_results %} 62 |
  • 63 |

    64 | 65 | {{ discussion.title }} 66 | 67 |

    68 |

    {{ discussion.description|truncatewords:20 }}

    69 |
  • 70 | {% endfor %} 71 |
72 | {% else %} 73 |

No discussions found.

74 | {% endif %} 75 |
76 | 77 | 78 |
79 |

Comments

80 | {% if comment_results %} 81 | 93 | {% else %} 94 |

No comments found.

95 | {% endif %} 96 |
97 |
98 |
99 | {% else %} 100 |

Enter a query to search.

101 | {% endif %} 102 |
103 | {% endblock %} 104 | -------------------------------------------------------------------------------- /vetdatahub/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vetdatahub/VetDataHub_Website/b8730067b78374109d3f8cc40cc73609ae5f3621/vetdatahub/__init__.py -------------------------------------------------------------------------------- /vetdatahub/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for vetdatahub project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "vetdatahub.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /vetdatahub/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for vetdatahub project. 3 | 4 | Generated by 'django-admin startproject' using Django 5.1.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/5.1/ref/settings/ 11 | """ 12 | 13 | import os 14 | from pathlib import Path 15 | import sentry_sdk 16 | 17 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 18 | BASE_DIR = Path(__file__).resolve().parent.parent 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = os.environ.get("SECRET_KEY") 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = False 28 | 29 | ALLOWED_HOSTS = ["vetdatagub.pythonanywhere.com"] 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | "django.contrib.admin", 35 | "django.contrib.auth", 36 | "django.contrib.contenttypes", 37 | "django.contrib.sessions", 38 | "django.contrib.messages", 39 | "django.contrib.staticfiles", 40 | "django.contrib.sites", 41 | "analytics", 42 | "community", 43 | "datasets", 44 | "moderation", 45 | "home", 46 | "search", 47 | "registration", 48 | "crispy_forms", 49 | "crispy_tailwind", 50 | "storages", 51 | ] 52 | 53 | MIDDLEWARE = [ 54 | "django.middleware.security.SecurityMiddleware", 55 | "django.contrib.sessions.middleware.SessionMiddleware", 56 | "django.middleware.common.CommonMiddleware", 57 | "django.middleware.csrf.CsrfViewMiddleware", 58 | "django.contrib.auth.middleware.AuthenticationMiddleware", 59 | "django.contrib.messages.middleware.MessageMiddleware", 60 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 61 | ] 62 | 63 | ROOT_URLCONF = "vetdatahub.urls" 64 | 65 | TEMPLATES = [ 66 | { 67 | "BACKEND": "django.template.backends.django.DjangoTemplates", 68 | "DIRS": [os.path.join(BASE_DIR, "templates")], 69 | "APP_DIRS": True, 70 | "OPTIONS": { 71 | "context_processors": [ 72 | "django.template.context_processors.debug", 73 | "django.template.context_processors.request", 74 | "django.contrib.auth.context_processors.auth", 75 | "django.contrib.messages.context_processors.messages", 76 | ], 77 | }, 78 | }, 79 | ] 80 | 81 | WSGI_APPLICATION = "vetdatahub.wsgi.application" 82 | 83 | # Database 84 | # https://docs.djangoproject.com/en/5.1/ref/settings/#databases 85 | 86 | DATABASES = { 87 | "default": { 88 | "ENGINE": "django.db.backends.postgresql", 89 | "NAME": os.environ.get("DB_NAME"), 90 | "HOST": os.environ.get("DB_HOST"), 91 | "PASSWORD": os.environ.get("DB_PASSWORD"), 92 | "USER": os.environ.get("DB_USERNAME"), 93 | } 94 | } 95 | 96 | # Password validation 97 | # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators 98 | 99 | AUTH_PASSWORD_VALIDATORS = [ 100 | { 101 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 102 | }, 103 | { 104 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 105 | }, 106 | { 107 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 108 | }, 109 | { 110 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 111 | }, 112 | ] 113 | 114 | # Internationalization 115 | # https://docs.djangoproject.com/en/5.1/topics/i18n/ 116 | 117 | LANGUAGE_CODE = "en-us" 118 | 119 | TIME_ZONE = "UTC" 120 | 121 | USE_I18N = True 122 | 123 | USE_TZ = True 124 | 125 | # Default primary key field type 126 | # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field 127 | 128 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 129 | 130 | # Custom User Model 131 | # https://docs.djangoproject.com/en/5.1/topics/auth/customizing/#using-a-custom-user-model-when-starting-a-project 132 | 133 | AUTH_USER_MODEL = "registration.User" 134 | 135 | SITE_ID = 1 136 | 137 | CRISPY_ALLOWED_TEMPLATE_PACKS = "tailwind" 138 | 139 | CRISPY_TEMPLATE_PACK = "tailwind" 140 | 141 | LOGIN_REDIRECT_URL = "/datasets/" 142 | SIGNUP_REDIRECT_URL = "/datasets/" 143 | LOGOUT_REDIRECT_URL = "/" 144 | 145 | ACCOUNT_ACTIVATION_DAYS = 7 146 | 147 | STORAGES = { 148 | "default": { 149 | "BACKEND": "django.core.files.storage.FileSystemStorage", 150 | }, 151 | "staticfiles": { 152 | "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", 153 | }, 154 | } 155 | 156 | 157 | sentry_sdk.init( 158 | dsn="https://9bb1da4c2e46d64f90eaf84bcd6eaf50@o4508472325505024.ingest.us.sentry.io/4508472341626880", 159 | # Set traces_sample_rate to 1.0 to capture 100% 160 | # of transactions for tracing. 161 | traces_sample_rate=1.0, 162 | _experiments={ 163 | # Set continuous_profiling_auto_start to True 164 | # to automatically start the profiler on when 165 | # possible. 166 | "continuous_profiling_auto_start": True, 167 | }, 168 | ) 169 | -------------------------------------------------------------------------------- /vetdatahub/settings_dev.py: -------------------------------------------------------------------------------- 1 | from .settings import * 2 | 3 | # SECURITY WARNING: keep the secret key used in production secret! 4 | SECRET_KEY = "jnodq38ru2rf3ufif3dp occ9ucn8m238nucc921c-_u%!6^k%&5*o5peu+27%w3fiamx_&%gd1l7!c!q0i((jzhh9)q" 5 | 6 | # SECURITY WARNING: don't run with debug turned on in production! 7 | DEBUG = True 8 | 9 | ALLOWED_HOSTS = ["*"] 10 | 11 | 12 | # Database 13 | # https://docs.djangoproject.com/en/5.1/ref/settings/#databases 14 | 15 | DATABASES = { 16 | "default": { 17 | "ENGINE": "django.db.backends.sqlite3", 18 | "NAME": BASE_DIR / "db.sqlite3", 19 | } 20 | } 21 | 22 | STATIC_URL = "static/" 23 | STATICFILES_DIRS = [BASE_DIR / "static"] 24 | MEDIA_URL = "media/" 25 | MEDIA_ROOT = os.path.join(BASE_DIR, "media") 26 | -------------------------------------------------------------------------------- /vetdatahub/settings_staging.py: -------------------------------------------------------------------------------- 1 | import os 2 | import dj_database_url 3 | from .settings import * 4 | from dotenv import load_dotenv 5 | 6 | 7 | load_dotenv() 8 | # Static files (CSS, JavaScript, Images) 9 | # https://docs.djangoproject.com/en/5.1/howto/static-files/ 10 | 11 | DATABASES["default"] = dj_database_url.config( 12 | default=os.environ.get("DEFAULT_DB_URL"), 13 | conn_max_age=600, 14 | conn_health_checks=True, 15 | ) 16 | ALLOWED_HOSTS = ["vetdatahub.tech", "www.vetdatahub.tech"] 17 | 18 | STATIC_URL = "/static/" 19 | 20 | STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")] 21 | 22 | 23 | STATIC_ROOT = "/var/www/sites/production/static" 24 | 25 | AWS_ACCESS_KEY_ID = os.environ.get("DO_SPACES_ACCESS_KEY_ID") 26 | AWS_SECRET_ACCESS_KEY = os.environ.get("DO_SPACES_SECRET_ACCESS_KEY") 27 | AWS_STORAGE_BUCKET_NAME = os.environ.get("DO_SPACES_BUCKET_NAME") 28 | AWS_S3_ENDPOINT_URL = os.environ.get("DO_SPACES_ENDPOINT_URL") # Endpoint URL 29 | AWS_S3_OBJECT_PARAMETERS = { 30 | "CacheControl": "max-age=86400", 31 | } 32 | AWS_LOCATION = os.environ.get("DO_SPACES_LOCATION") 33 | DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" 34 | 35 | 36 | MEDIA_URL = "/media/" 37 | STORAGES["default"] = {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"} 38 | -------------------------------------------------------------------------------- /vetdatahub/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URL configuration for vetdatahub project. 3 | 4 | The `urlpatterns` list routes URLs to views. For more information please see: 5 | https://docs.djangoproject.com/en/5.1/topics/http/urls/ 6 | Examples: 7 | Function views 8 | 1. Add an import: from my_app import views 9 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 10 | Class-based views 11 | 1. Add an import: from other_app.views import Home 12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 13 | Including another URLconf 14 | 1. Import the include() function: from django.urls import include, path 15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 16 | """ 17 | 18 | from django.contrib import admin 19 | from django.urls import path, include 20 | from django.conf import settings 21 | from django.conf.urls.static import static 22 | 23 | 24 | urlpatterns = [ 25 | path("admin/", admin.site.urls), 26 | path("", include("home.urls")), 27 | path("accounts/", include("registration.backends.default.urls")), 28 | path( 29 | "accounts/profile/", include("registration.urls", namespace="profiles") 30 | ), 31 | path("community/", include("community.urls")), 32 | path("datasets/", include("datasets.urls")), 33 | path("analytics/", include("analytics.urls")), 34 | path("search/", include("search.urls")), 35 | ] 36 | 37 | if settings.DEBUG: 38 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 39 | urlpatterns += static( 40 | settings.STATIC_URL, document_root=settings.STATIC_ROOT 41 | ) 42 | -------------------------------------------------------------------------------- /vetdatahub/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for vetdatahub project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | from dotenv import load_dotenv 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | load_dotenv() 15 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "vetdatahub.settings") 16 | 17 | application = get_wsgi_application() 18 | --------------------------------------------------------------------------------