Compare commits

...

No commits in common. "gh-pages" and "main" have entirely different histories.

202 changed files with 9636 additions and 8615 deletions

15
.github/workflows/main.yml vendored Normal file
View file

@ -0,0 +1,15 @@
name: ci
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: 3.x
- run: pip install mkdocs-material
- run: mkdocs gh-deploy --force

506
.gitignore vendored Normal file
View file

@ -0,0 +1,506 @@
.env
**/data.json
rc_hip/static
rc_hip/static/**
rc_hip/data
rc_hip/data/**
repapp/static
repapp/static/**
repapp/data
repapp/data/**
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# Created by https://www.toptal.com/developers/gitignore/api/django,linux,windows,macos,python,visualstudiocode
# Edit at https://www.toptal.com/developers/gitignore?templates=django,linux,windows,macos,python,visualstudiocode
### Django ###
*.log
*.pot
*.pyc
__pycache__/
local_settings.py
db.sqlite3
db.sqlite3-journal
media
# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
# in your Git repository. Update and uncomment the following line accordingly.
# <django-project-name>/staticfiles/
### Django.Python Stack ###
# Byte-compiled / optimized / DLL files
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
# Django stuff:
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### Python ###
# Byte-compiled / optimized / DLL files
# C extensions
# Distribution / packaging
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
# Installer logs
# Unit test / coverage reports
# Translations
# Django stuff:
# Flask stuff:
# Scrapy stuff:
# Sphinx documentation
# PyBuilder
# Jupyter Notebook
# IPython
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
# Celery stuff
# SageMath parsed files
# Environments
# Spyder project settings
# Rope project settings
# mkdocs documentation
# mypy
# Pyre type checker
# pytype static type analyzer
# Cython debug symbols
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
### Python Patch ###
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
poetry.toml
# ruff
.ruff_cache/
# LSP config files
pyrightconfig.json
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/django,linux,windows,macos,python,visualstudiocode

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "bootstrap"]
path = bootstrap
url = https://github.com/twbs/bootstrap.git

5
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,5 @@
{
"cSpell.words": [
"Reparateur"
]
}

136
404.html
View file

@ -1,136 +0,0 @@
<!DOCTYPE html>
<html class="writer-html5" lang="en" >
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="shortcut icon" href="/RepApp/img/favicon.ico" />
<title>RepApp - Makespace Repair-Café Hilpoltstein</title>
<link rel="stylesheet" href="/RepApp/css/theme.css" />
<link rel="stylesheet" href="/RepApp/css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github.min.css" />
<!--[if lt IE 9]>
<script src="/RepApp/js/html5shiv.min.js"></script>
<![endif]-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href="/RepApp/." class="icon icon-home"> RepApp - Makespace Repair-Café Hilpoltstein
</a><div role="search">
<form id ="rtd-search-form" class="wy-form" action="/RepApp//search.html" method="get">
<input type="text" name="q" placeholder="Search docs" aria-label="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="Navigation menu">
<p class="caption"><span class="caption-text">RepApp</span></p>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/RepApp/.">Beschreibung</a>
</li>
</ul>
<p class="caption"><span class="caption-text">Developer documentation</span></p>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/RepApp/dev/">Overview</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="/RepApp/dev/users/">Users</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="/RepApp/dev/stories/">Stories</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="/RepApp/dev/use_cases/">Use cases</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="/RepApp/dev/apps/">Apps</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="/RepApp/dev/objects/">Objects</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="/RepApp/dev/configuration/">Repapp configuration</a>
</li>
</ul>
<p class="caption"><span class="caption-text">Repair-Café & Makespace</span></p>
<ul>
<li class="toctree-l1"><a class="" href="https://www.repaircafe-hilpoltstein.de">Repair-Café</a>
</li>
<li class="toctree-l1"><a class="" href="https://makes-hacks-hip.de">Makespace</a>
</li>
<li class="toctree-l1"><a class="" href="https://makes-hacks-hip.github.io">Makerspace@Github</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="Mobile navigation menu">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="/RepApp/.">RepApp - Makespace Repair-Café Hilpoltstein</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content"><div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="/RepApp/." class="icon icon-home" aria-label="Docs"></a> &raquo;</li>
<li class="wy-breadcrumbs-aside">
</li>
</ul>
<hr/>
</div>
<div role="main" class="document" itemscope="itemscope" itemtype="http://schema.org/Article">
<div class="section" itemprop="articleBody">
<h1 id="404-page-not-found">404</h1>
<p><strong>Page not found</strong></p>
</div>
</div><footer>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/readthedocs/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="Versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/makes-hacks-hip/RepApp/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
</span>
</div>
<script src="/RepApp/js/jquery-3.6.0.min.js"></script>
<script>var base_url = "/RepApp/";</script>
<script src="/RepApp/js/theme_extra.js"></script>
<script src="/RepApp/js/theme.js"></script>
<script src="/RepApp/search/main.js"></script>
<script>
jQuery(function () {
SphinxRtdTheme.Navigation.enable(true);
});
</script>
</body>
</html>

33
Dockerfile Normal file
View file

@ -0,0 +1,33 @@
FROM python:3-bullseye
# install nginx
RUN apt-get update && apt-get install nginx vim -y --no-install-recommends
COPY nginx.default /etc/nginx/sites-available/default
RUN ln -sf /dev/stdout /var/log/nginx/access.log \
&& ln -sf /dev/stderr /var/log/nginx/error.log
# copy source and install dependencies
RUN mkdir -p /opt/app
RUN mkdir -p /opt/app/pip_cache
RUN mkdir -p /opt/app/rc_hip
RUN mkdir -p /opt/app/rc_hip/data
RUN mkdir -p /opt/app/rc_hip/media
RUN mkdir -p /opt/app/rc_hip/static
COPY rc_hip /opt/app/rc_hip
RUN rm -f /opt/app/rc_hip/data/db.sqlite3
WORKDIR /opt/app
COPY requirements_prod.txt /opt/app/
RUN pip install -r requirements_prod.txt --cache-dir /opt/app/pip_cache
COPY start_server.sh /opt/app/
RUN chmod +x /opt/app/start_server.sh
RUN chown -R www-data:www-data /opt/app
# start server
EXPOSE 8020
STOPSIGNAL SIGTERM
CMD ["/opt/app/start_server.sh"]

201
LICENSE Normal file
View file

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

47
README.md Normal file
View file

@ -0,0 +1,47 @@
# RepApp
Web App um Reparaturtermine zu vereinbaren.
For documentation see https://makes-hacks-hip.github.io/RepApp/.
## Setup for development
- Clone repo
- `python -m venv venv`
- `source venv/bin/activate`
- `cd rc_hip`
- `python manage.py makemigrations`
- `python manage.py migrate`
- `python manage.py createsuperuser`
- `python manage.py runserver`
## Build Docker image
- Build the image: `docker build -t repapp .`
- Run the image:
```bash
docker run --rm -d \
-p 127.0.0.1:8020:8020 \
--name repapp \
--env DJANGO_SUPERUSER_USERNAME=suba \
--env DJANGO_SUPERUSER_PASSWORD=ThePassword \
--env DJANGO_SUPERUSER_EMAIL=noreply@example.com \
repapp`
```
- Open `http://127.0.0.1:8020/` in your browser
## Run tests
### Unit tests
Run `python manage.py test`
### LiveServer Tests
Run `python manage.py test repapp.live_tests.WorkflowTests`
### Coverage
- `coverage run --source='.' manage.py test`
- `coverage report` or `coverage html`

8
RepApp.code-workspace Normal file
View file

@ -0,0 +1,8 @@
{
"folders": [
{
"path": "."
}
],
"settings": {}
}

1
bootstrap Submodule

@ -0,0 +1 @@
Subproject commit 803617e9c595adebf8a43a560fcfe8d439424f94

30
cspell.json Normal file
View file

@ -0,0 +1,30 @@
{
"version": "0.2",
"ignorePaths": [],
"dictionaryDefinitions": [],
"dictionaries": [],
"words": [
"Ammeldelink",
"anmeldung",
"hilpoltstein",
"Keycloak",
"KeycloakOIDCAB",
"oidc",
"onetimelogin",
"orga",
"organisator",
"organisators",
"repa",
"repaircafe",
"repapp",
"reparateur",
"Reparateur",
"Reparateure",
"reparateurs",
"Reparateurs",
"tabindex",
"johnpassword"
],
"ignoreWords": [],
"import": []
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View file

@ -1,191 +0,0 @@
/*
* Wrap inline code samples otherwise they shoot of the side and
* can't be read at all.
*
* https://github.com/mkdocs/mkdocs/issues/313
* https://github.com/mkdocs/mkdocs/issues/233
* https://github.com/mkdocs/mkdocs/issues/834
*/
.rst-content code {
white-space: pre-wrap;
word-wrap: break-word;
padding: 2px 5px;
}
/**
* Make code blocks display as blocks and give them the appropriate
* font size and padding.
*
* https://github.com/mkdocs/mkdocs/issues/855
* https://github.com/mkdocs/mkdocs/issues/834
* https://github.com/mkdocs/mkdocs/issues/233
*/
.rst-content pre code {
white-space: pre;
word-wrap: normal;
display: block;
padding: 12px;
font-size: 12px;
}
/**
* Fix code colors
*
* https://github.com/mkdocs/mkdocs/issues/2027
*/
.rst-content code {
color: #E74C3C;
}
.rst-content pre code {
color: #000;
background: #f8f8f8;
}
/*
* Fix link colors when the link text is inline code.
*
* https://github.com/mkdocs/mkdocs/issues/718
*/
a code {
color: #2980B9;
}
a:hover code {
color: #3091d1;
}
a:visited code {
color: #9B59B6;
}
/*
* The CSS classes from highlight.js seem to clash with the
* ReadTheDocs theme causing some code to be incorrectly made
* bold and italic.
*
* https://github.com/mkdocs/mkdocs/issues/411
*/
pre .cs, pre .c {
font-weight: inherit;
font-style: inherit;
}
/*
* Fix some issues with the theme and non-highlighted code
* samples. Without and highlighting styles attached the
* formatting is broken.
*
* https://github.com/mkdocs/mkdocs/issues/319
*/
.rst-content .no-highlight {
display: block;
padding: 0.5em;
color: #333;
}
/*
* Additions specific to the search functionality provided by MkDocs
*/
.search-results {
margin-top: 23px;
}
.search-results article {
border-top: 1px solid #E1E4E5;
padding-top: 24px;
}
.search-results article:first-child {
border-top: none;
}
form .search-query {
width: 100%;
border-radius: 50px;
padding: 6px 12px; /* csslint allow: box-model */
border-color: #D1D4D5;
}
/*
* Improve inline code blocks within admonitions.
*
* https://github.com/mkdocs/mkdocs/issues/656
*/
.rst-content .admonition code {
color: #404040;
border: 1px solid #c7c9cb;
border: 1px solid rgba(0, 0, 0, 0.2);
background: #f8fbfd;
background: rgba(255, 255, 255, 0.7);
}
/*
* Account for wide tables which go off the side.
* Override borders to avoid weirdness on narrow tables.
*
* https://github.com/mkdocs/mkdocs/issues/834
* https://github.com/mkdocs/mkdocs/pull/1034
*/
.rst-content .section .docutils {
width: 100%;
overflow: auto;
display: block;
border: none;
}
td, th {
border: 1px solid #e1e4e5 !important; /* csslint allow: important */
border-collapse: collapse;
}
/*
* Without the following amendments, the navigation in the theme will be
* slightly cut off. This is due to the fact that the .wy-nav-side has a
* padding-bottom of 2em, which must not necessarily align with the font-size of
* 90 % on the .rst-current-version container, combined with the padding of 12px
* above and below. These amendments fix this in two steps: First, make sure the
* .rst-current-version container has a fixed height of 40px, achieved using
* line-height, and then applying a padding-bottom of 40px to this container. In
* a second step, the items within that container are re-aligned using flexbox.
*
* https://github.com/mkdocs/mkdocs/issues/2012
*/
.wy-nav-side {
padding-bottom: 40px;
}
/*
* The second step of above amendment: Here we make sure the items are aligned
* correctly within the .rst-current-version container. Using flexbox, we
* achieve it in such a way that it will look like the following:
*
* [No repo_name]
* Next >> // On the first page
* << Previous Next >> // On all subsequent pages
*
* [With repo_name]
* <repo_name> Next >> // On the first page
* <repo_name> << Previous Next >> // On all subsequent pages
*
* https://github.com/mkdocs/mkdocs/issues/2012
*/
.rst-versions .rst-current-version {
padding: 0 12px;
display: flex;
font-size: initial;
justify-content: space-between;
align-items: center;
line-height: 40px;
}
/*
* Please note that this amendment also involves removing certain inline-styles
* from the file ./mkdocs/themes/readthedocs/versions.html.
*
* https://github.com/mkdocs/mkdocs/issues/2012
*/
.rst-current-version span {
flex: 1;
text-align: center;
}

View file

@ -1,219 +0,0 @@
<!DOCTYPE html>
<html class="writer-html5" lang="en" >
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><link rel="canonical" href="https://makes-hacks-hip.github.io/RepApp/dev/apps/" />
<link rel="shortcut icon" href="../../img/favicon.ico" />
<title>Apps - RepApp - Makespace Repair-Café Hilpoltstein</title>
<link rel="stylesheet" href="../../css/theme.css" />
<link rel="stylesheet" href="../../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "Apps";
var mkdocs_page_input_path = "dev/apps.md";
var mkdocs_page_url = "/RepApp/dev/apps/";
</script>
<!--[if lt IE 9]>
<script src="../../js/html5shiv.min.js"></script>
<![endif]-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href="../.." class="icon icon-home"> RepApp - Makespace Repair-Café Hilpoltstein
</a><div role="search">
<form id ="rtd-search-form" class="wy-form" action="../../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" aria-label="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="Navigation menu">
<p class="caption"><span class="caption-text">RepApp</span></p>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../..">Beschreibung</a>
</li>
</ul>
<p class="caption"><span class="caption-text">Developer documentation</span></p>
<ul class="current">
<li class="toctree-l1"><a class="reference internal" href="../">Overview</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="../users/">Users</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="../stories/">Stories</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="../use_cases/">Use cases</a>
</li>
<li class="toctree-l1 current"><a class="reference internal current" href="./">Apps</a>
<ul class="current">
<li class="toctree-l2"><a class="reference internal" href="#one-time-login">One time login</a>
<ul>
<li class="toctree-l3"><a class="reference internal" href="#configuration">Configuration</a>
</li>
</ul>
</li>
<li class="toctree-l2"><a class="reference internal" href="#email-interface">Email interface</a>
<ul>
<li class="toctree-l3"><a class="reference internal" href="#configuration_1">Configuration</a>
</li>
</ul>
</li>
</ul>
</li>
<li class="toctree-l1"><a class="reference internal" href="../objects/">Objects</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="../configuration/">Repapp configuration</a>
</li>
</ul>
<p class="caption"><span class="caption-text">Repair-Café & Makespace</span></p>
<ul>
<li class="toctree-l1"><a class="" href="https://www.repaircafe-hilpoltstein.de">Repair-Café</a>
</li>
<li class="toctree-l1"><a class="" href="https://makes-hacks-hip.de">Makespace</a>
</li>
<li class="toctree-l1"><a class="" href="https://makes-hacks-hip.github.io">Makerspace@Github</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="Mobile navigation menu">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="../..">RepApp - Makespace Repair-Café Hilpoltstein</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content"><div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="../.." class="icon icon-home" aria-label="Docs"></a> &raquo;</li>
<li>Developer documentation &raquo;</li>
<li class="breadcrumb-item active">Apps</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/makes-hacks-hip/RepApp/edit/main/docs/dev/apps.md" class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main" class="document" itemscope="itemscope" itemtype="http://schema.org/Article">
<div class="section" itemprop="articleBody">
<h1 id="apps">Apps</h1>
<p>The repapp is split into different modules, realized as Django apps, to improve the maintainability.</p>
<h2 id="one-time-login">One time login</h2>
<p>The one time login module provides a login for guests using a one time secret as part of an URL.</p>
<ul>
<li>The templates use the <code>settings.ORGANIZATION</code> variable to fill the organization name.</li>
<li>The Django mail module is used to send emails. See https://docs.djangoproject.com/en/4.2/topics/email/ for details how to configure the mail host.</li>
</ul>
<h3 id="configuration">Configuration</h3>
<p>To enable the one time login links, an additional authentication backend must get added:</p>
<pre><code class="language-Python"># Repapp specific authentication backends
AUTHENTICATION_BACKENDS = [
# Backend for one time logins
&quot;one_time_login.authentication_backends.OneTimeLoginBackend&quot;,
]
</code></pre>
<p>The one time login urls must get added using namespace <code>one_time_login</code>:</p>
<pre><code class="language-Python">urlpatterns = i18n_patterns(
...
path(_('one_time_login/'),
include('one_time_login.urls', namespace='one_time_login')),
...
)
</code></pre>
<h2 id="email-interface">Email interface</h2>
<p>The email interface modules supports handling of mails. It support sending of mails, including HTML formatting and attachments, and it supports receiving of plain text and HTML formatted mails, including attachments.</p>
<ul>
<li>The Django mail module is used to send emails. See https://docs.djangoproject.com/en/4.2/topics/email/ for details how to configure the mail host.</li>
<li>CKEditor is used for email content editing. See https://django-ckeditor.readthedocs.io/en/latest/ for details.</li>
<li>Crispy forms is used for form rendering. See https://django-crispy-forms.readthedocs.io/en/latest/ for details.</li>
<li>Bootstrap 5 theme for crispy forms is used. See https://github.com/django-crispy-forms/crispy-bootstrap5 for details.</li>
</ul>
<h3 id="configuration_1">Configuration</h3>
<p>The email interface urls must get added using namespace <code>email_interface</code>:</p>
<pre><code class="language-Python">urlpatterns = i18n_patterns(
...
path(_('emails/'),
include('email_interface.urls', namespace='email_interface')),
...
)
</code></pre>
<p>To use CKEditor as authenticated user, the following additional urls must get added:</p>
<pre><code class="language-Python">urlpatterns = i18n_patterns(
...
# CKEditor upload views
path('upload/', login_required(ckeditor_views.upload), name=&quot;ckeditor_upload&quot;),
path('browse/', never_cache(login_required(ckeditor_views.browse)),
name=&quot;ckeditor_browse&quot;),
...
)
</code></pre>
<p>To be abel to use CKEditor also the static an media content must get served. For development this is done with:</p>
<pre><code class="language-Python">if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)
</code></pre>
</div>
</div><footer>
<div class="rst-footer-buttons" role="navigation" aria-label="Footer Navigation">
<a href="../use_cases/" class="btn btn-neutral float-left" title="Use cases"><span class="icon icon-circle-arrow-left"></span> Previous</a>
<a href="../objects/" class="btn btn-neutral float-right" title="Objects">Next <span class="icon icon-circle-arrow-right"></span></a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/readthedocs/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="Versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/makes-hacks-hip/RepApp/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../use_cases/" style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="../objects/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script src="../../js/jquery-3.6.0.min.js"></script>
<script>var base_url = "../..";</script>
<script src="../../js/theme_extra.js"></script>
<script src="../../js/theme.js"></script>
<script src="../../search/main.js"></script>
<script>
jQuery(function () {
SphinxRtdTheme.Navigation.enable(true);
});
</script>
</body>
</html>

View file

@ -1,284 +0,0 @@
<!DOCTYPE html>
<html class="writer-html5" lang="en" >
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><link rel="canonical" href="https://makes-hacks-hip.github.io/RepApp/dev/configuration/" />
<link rel="shortcut icon" href="../../img/favicon.ico" />
<title>Repapp configuration - RepApp - Makespace Repair-Café Hilpoltstein</title>
<link rel="stylesheet" href="../../css/theme.css" />
<link rel="stylesheet" href="../../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "Repapp configuration";
var mkdocs_page_input_path = "dev/configuration.md";
var mkdocs_page_url = "/RepApp/dev/configuration/";
</script>
<!--[if lt IE 9]>
<script src="../../js/html5shiv.min.js"></script>
<![endif]-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href="../.." class="icon icon-home"> RepApp - Makespace Repair-Café Hilpoltstein
</a><div role="search">
<form id ="rtd-search-form" class="wy-form" action="../../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" aria-label="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="Navigation menu">
<p class="caption"><span class="caption-text">RepApp</span></p>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../..">Beschreibung</a>
</li>
</ul>
<p class="caption"><span class="caption-text">Developer documentation</span></p>
<ul class="current">
<li class="toctree-l1"><a class="reference internal" href="../">Overview</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="../users/">Users</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="../stories/">Stories</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="../use_cases/">Use cases</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="../apps/">Apps</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="../objects/">Objects</a>
</li>
<li class="toctree-l1 current"><a class="reference internal current" href="./">Repapp configuration</a>
<ul class="current">
<li class="toctree-l2"><a class="reference internal" href="#debug-mode">Debug mode</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#organization-name">Organization name</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#email-settings">Email settings</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#translations">Translations</a>
<ul>
<li class="toctree-l3"><a class="reference internal" href="#rosetta">Rosetta</a>
</li>
</ul>
</li>
<li class="toctree-l2"><a class="reference internal" href="#one-time-login">One time login</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#email-interface">Email interface</a>
</li>
</ul>
</li>
</ul>
<p class="caption"><span class="caption-text">Repair-Café & Makespace</span></p>
<ul>
<li class="toctree-l1"><a class="" href="https://www.repaircafe-hilpoltstein.de">Repair-Café</a>
</li>
<li class="toctree-l1"><a class="" href="https://makes-hacks-hip.de">Makespace</a>
</li>
<li class="toctree-l1"><a class="" href="https://makes-hacks-hip.github.io">Makerspace@Github</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="Mobile navigation menu">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="../..">RepApp - Makespace Repair-Café Hilpoltstein</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content"><div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="../.." class="icon icon-home" aria-label="Docs"></a> &raquo;</li>
<li>Developer documentation &raquo;</li>
<li class="breadcrumb-item active">Repapp configuration</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/makes-hacks-hip/RepApp/edit/main/docs/dev/configuration.md" class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main" class="document" itemscope="itemscope" itemtype="http://schema.org/Article">
<div class="section" itemprop="articleBody">
<h1 id="repapp-configuration">Repapp configuration</h1>
<h2 id="debug-mode">Debug mode</h2>
<p>The Django debug mode is activated if there is a environment variable <code>DJANGO_DEBUG</code> with the value "true", "1" or "t". By default, the debug mode is disabled. </p>
<h2 id="organization-name">Organization name</h2>
<p>Some templates use and organization name. This value is taken from <code>settings.ORGANIZATION</code> and defaults to "Repair-Café Hilpoltstein".</p>
<h2 id="email-settings">Email settings</h2>
<p>The following environment variables are used to configure the Django email feature:</p>
<pre><code class="language-Python">EMAIL_HOST = os.getenv(&quot;DJANGO_EMAIL_HOST&quot;, &quot;&quot;)
EMAIL_PORT = (int)(os.getenv(&quot;DJANGO_EMAIL_PORT&quot;, &quot;25&quot;))
EMAIL_HOST_USER = os.getenv(&quot;DJANGO_EMAIL_HOST_USER&quot;, None)
EMAIL_HOST_PASSWORD = os.getenv(&quot;DJANGO_EMAIL_HOST_PASSWORD&quot;, None)
EMAIL_USE_TLS = os.getenv(&quot;DJANGO_EMAIL_USE_TLS&quot;, &quot;true&quot;) in (&quot;true&quot;, &quot;1&quot;, &quot;t&quot;)
DEFAULT_FROM_EMAIL = os.getenv(&quot;DJANGO_SENDER_ADDRESS&quot;, &quot;&quot;)
</code></pre>
<p>See https://docs.djangoproject.com/en/4.2/topics/email/ for more details.</p>
<h2 id="translations">Translations</h2>
<p>Repapp is using localization, and comes with german and english translations.</p>
<p>Enable the <code>LocaleMiddleware</code>:</p>
<pre><code class="language-Python">MIDDLEWARE = [
...
'django.contrib.sessions.middleware.SessionMiddleware',
# enable translation
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
...
]
</code></pre>
<p>We use the following localization settings:</p>
<pre><code class="language-Python">LANGUAGE_CODE = 'de'
TIME_ZONE = 'Europe/Berlin'
USE_I18N = True
USE_L10N = True
USE_TZ = True
LANGUAGES = [
('de', _('German')),
('en', _('English')),
]
LOCALE_PATHS = [
BASE_DIR / 'locale',
]
</code></pre>
<p>Also the URLs are localized, and the language is choosen using i18n URL patterns:</p>
<pre><code class="language-Python">...
from django.conf.urls.i18n import i18n_patterns
from django.utils.translation import gettext_lazy as _
urlpatterns = i18n_patterns(
path('admin/', admin.site.urls),
path(_('one_time_login/'),
include('one_time_login.urls', namespace='one_time_login')),
# URLs for rosetta translation interface
path('rosetta/', include('rosetta.urls')),
)
</code></pre>
<h3 id="rosetta">Rosetta</h3>
<p>For translating the interface, we use rosetta. For more details see https://pypi.org/project/django-rosetta/.</p>
<p>You need to install rosetta (<code>pip install django-rosetta</code>) and enable it in the apps:</p>
<pre><code class="language-Python">INSTALLED_APPS = [
...
# enable rosetta online translation interface
'rosetta',
...
]
</code></pre>
<p>Rosatta is made availabel at <code>rosetta/</code>.</p>
<h2 id="one-time-login">One time login</h2>
<p>To enable the one time login links, an additional authentication backend must get added:</p>
<pre><code class="language-Python"># Repapp specific authentication backends
AUTHENTICATION_BACKENDS = [
# Backend for one time logins
&quot;one_time_login.authentication_backends.OneTimeLoginBackend&quot;,
]
</code></pre>
<h2 id="email-interface">Email interface</h2>
<p>The email interface uses CKEditor, crispy forms and easy thumbnails. The apps are enabled with:</p>
<pre><code class="language-Python">INSTALLED_APPS = [
...
# CKEditor for mail content editing
&quot;ckeditor&quot;,
&quot;ckeditor_uploader&quot;,
# Crispy forms for bootstrap 5 form design
&quot;crispy_forms&quot;,
&quot;crispy_bootstrap5&quot;,
# for creating thumbnails, used by email_interface demo views
'easy_thumbnails',
...
]
</code></pre>
<p>For CKEditor, we use the following settings:</p>
<pre><code class="language-Python"># editor upload path, used e.g. for send mail demo
CKEDITOR_UPLOAD_PATH = &quot;editor_uploads/&quot;
CKEDITOR_IMAGE_BACKEND = 'pillow'
# CKEditor default config
CKEDITOR_CONFIGS = {
'default': {
'toolbar': 'full',
'height': 300,
'width': '100%',
},
}
</code></pre>
<p>To enable the image upload of CKEditor, also the Django media paths and urls must get configured:</p>
<pre><code class="language-Python"># Django media and static file config, required for CKEditor
STATIC_URL = &quot;static/&quot;
MEDIA_URL = &quot;media/&quot;
# Use subfolders of project folder
STATIC_ROOT = os.path.join(BASE_DIR, &quot;static&quot;)
MEDIA_ROOT = os.path.join(BASE_DIR, &quot;media&quot;)
</code></pre>
<p>For crispy forms, we use the bootstrap 5 theme:</p>
<pre><code class="language-Python"># Crispy forms settings
CRISPY_ALLOWED_TEMPLATE_PACKS = &quot;bootstrap5&quot;
CRISPY_TEMPLATE_PACK = &quot;bootstrap5&quot;
</code></pre>
<p>The static files for bootstrap must also be made available.</p>
</div>
</div><footer>
<div class="rst-footer-buttons" role="navigation" aria-label="Footer Navigation">
<a href="../objects/" class="btn btn-neutral float-left" title="Objects"><span class="icon icon-circle-arrow-left"></span> Previous</a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/readthedocs/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="Versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/makes-hacks-hip/RepApp/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../objects/" style="color: #fcfcfc">&laquo; Previous</a></span>
</span>
</div>
<script src="../../js/jquery-3.6.0.min.js"></script>
<script>var base_url = "../..";</script>
<script src="../../js/theme_extra.js"></script>
<script src="../../js/theme.js"></script>
<script src="../../search/main.js"></script>
<script>
jQuery(function () {
SphinxRtdTheme.Navigation.enable(true);
});
</script>
</body>
</html>

View file

@ -1,205 +0,0 @@
<!DOCTYPE html>
<html class="writer-html5" lang="en" >
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><link rel="canonical" href="https://makes-hacks-hip.github.io/RepApp/dev/" />
<link rel="shortcut icon" href="../img/favicon.ico" />
<title>Overview - RepApp - Makespace Repair-Café Hilpoltstein</title>
<link rel="stylesheet" href="../css/theme.css" />
<link rel="stylesheet" href="../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "Overview";
var mkdocs_page_input_path = "dev/index.md";
var mkdocs_page_url = "/RepApp/dev/";
</script>
<!--[if lt IE 9]>
<script src="../js/html5shiv.min.js"></script>
<![endif]-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href=".." class="icon icon-home"> RepApp - Makespace Repair-Café Hilpoltstein
</a><div role="search">
<form id ="rtd-search-form" class="wy-form" action="../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" aria-label="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="Navigation menu">
<p class="caption"><span class="caption-text">RepApp</span></p>
<ul>
<li class="toctree-l1"><a class="reference internal" href="..">Beschreibung</a>
</li>
</ul>
<p class="caption"><span class="caption-text">Developer documentation</span></p>
<ul class="current">
<li class="toctree-l1 current"><a class="reference internal current" href="./">Overview</a>
<ul class="current">
<li class="toctree-l2"><a class="reference internal" href="#local-development-setup">Local development setup</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#docker-image">Docker image</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#tests">Tests</a>
<ul>
<li class="toctree-l3"><a class="reference internal" href="#coverage">Coverage</a>
</li>
</ul>
</li>
</ul>
</li>
<li class="toctree-l1"><a class="reference internal" href="users/">Users</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="stories/">Stories</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="use_cases/">Use cases</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="apps/">Apps</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="objects/">Objects</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="configuration/">Repapp configuration</a>
</li>
</ul>
<p class="caption"><span class="caption-text">Repair-Café & Makespace</span></p>
<ul>
<li class="toctree-l1"><a class="" href="https://www.repaircafe-hilpoltstein.de">Repair-Café</a>
</li>
<li class="toctree-l1"><a class="" href="https://makes-hacks-hip.de">Makespace</a>
</li>
<li class="toctree-l1"><a class="" href="https://makes-hacks-hip.github.io">Makerspace@Github</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="Mobile navigation menu">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="..">RepApp - Makespace Repair-Café Hilpoltstein</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content"><div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href=".." class="icon icon-home" aria-label="Docs"></a> &raquo;</li>
<li>Developer documentation &raquo;</li>
<li class="breadcrumb-item active">Overview</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/makes-hacks-hip/RepApp/edit/main/docs/dev/index.md" class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main" class="document" itemscope="itemscope" itemtype="http://schema.org/Article">
<div class="section" itemprop="articleBody">
<h1 id="repapp-developer-documentation">Repapp - developer documentation</h1>
<p>The Repapp is a repair assignment booking and management tool.
It tries to support all common workflows happening while organizing a repair café event.
It's build using the Django web framework.</p>
<p>The code of the Repapp is hosted on Github: https://github.com/makes-hacks-hip/RepApp</p>
<h2 id="local-development-setup">Local development setup</h2>
<ul>
<li>Clone the Github repository</li>
<li>Create a Python environment: <code>python -m venv venv</code></li>
<li>Activate the Python environment: <code>source venv/bin/activate</code></li>
<li>Install the dependencies: <code>pip install -r requirements_prod.txt</code> and <code>pip install -r requirements_dev.txt</code></li>
<li>Change to the Django project folder: <code>cd repapp</code></li>
<li>Setup the database:</li>
<li><code>python manage.py makemigrations</code></li>
<li><code>python manage.py migrate</code></li>
<li>Setup an admin user: <code>python manage.py createsuperuser</code></li>
<li>Run the local test server: <code>python manage.py runserver</code></li>
</ul>
<h2 id="docker-image">Docker image</h2>
<ul>
<li>Build the image: <code>docker build -t repapp .</code></li>
<li>Run the image:</li>
</ul>
<pre><code class="language-bash">docker run --rm -d \
-p 127.0.0.1:8020:8020 \
--name repapp \
--env DJANGO_SUPERUSER_USERNAME=suba \
--env DJANGO_SUPERUSER_PASSWORD=ThePassword \
--env DJANGO_SUPERUSER_EMAIL=noreply@example.com \
repapp`
</code></pre>
<ul>
<li>Open http://127.0.0.1:8020/ in your browser</li>
</ul>
<h2 id="tests">Tests</h2>
<ul>
<li>Unit tests: <code>python manage.py test</code></li>
<li>LiveServer tests: <code>python manage.py test repapp.live_tests</code></li>
</ul>
<h3 id="coverage">Coverage</h3>
<ul>
<li><code>coverage run --source='.' manage.py test</code></li>
<li><code>coverage report</code> or <code>coverage html</code></li>
</ul>
</div>
</div><footer>
<div class="rst-footer-buttons" role="navigation" aria-label="Footer Navigation">
<a href=".." class="btn btn-neutral float-left" title="Beschreibung"><span class="icon icon-circle-arrow-left"></span> Previous</a>
<a href="users/" class="btn btn-neutral float-right" title="Users">Next <span class="icon icon-circle-arrow-right"></span></a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/readthedocs/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="Versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/makes-hacks-hip/RepApp/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href=".." style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="users/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script src="../js/jquery-3.6.0.min.js"></script>
<script>var base_url = "..";</script>
<script src="../js/theme_extra.js"></script>
<script src="../js/theme.js"></script>
<script src="../search/main.js"></script>
<script>
jQuery(function () {
SphinxRtdTheme.Navigation.enable(true);
});
</script>
</body>
</html>

View file

@ -1,196 +0,0 @@
<!DOCTYPE html>
<html class="writer-html5" lang="en" >
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><link rel="canonical" href="https://makes-hacks-hip.github.io/RepApp/dev/objects/" />
<link rel="shortcut icon" href="../../img/favicon.ico" />
<title>Objects - RepApp - Makespace Repair-Café Hilpoltstein</title>
<link rel="stylesheet" href="../../css/theme.css" />
<link rel="stylesheet" href="../../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "Objects";
var mkdocs_page_input_path = "dev/objects.md";
var mkdocs_page_url = "/RepApp/dev/objects/";
</script>
<!--[if lt IE 9]>
<script src="../../js/html5shiv.min.js"></script>
<![endif]-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href="../.." class="icon icon-home"> RepApp - Makespace Repair-Café Hilpoltstein
</a><div role="search">
<form id ="rtd-search-form" class="wy-form" action="../../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" aria-label="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="Navigation menu">
<p class="caption"><span class="caption-text">RepApp</span></p>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../..">Beschreibung</a>
</li>
</ul>
<p class="caption"><span class="caption-text">Developer documentation</span></p>
<ul class="current">
<li class="toctree-l1"><a class="reference internal" href="../">Overview</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="../users/">Users</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="../stories/">Stories</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="../use_cases/">Use cases</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="../apps/">Apps</a>
</li>
<li class="toctree-l1 current"><a class="reference internal current" href="./">Objects</a>
<ul class="current">
<li class="toctree-l2"><a class="reference internal" href="#repair_cafe">Repair_Cafe</a>
<ul>
<li class="toctree-l3"><a class="reference internal" href="#fields">Fields</a>
</li>
</ul>
</li>
<li class="toctree-l2"><a class="reference internal" href="#device">Device</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#appointment">Appointment</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#guest">Guest</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#repairer">Repairer</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#organizer">Organizer</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#consultation">Consultation</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#comment">Comment</a>
</li>
</ul>
</li>
<li class="toctree-l1"><a class="reference internal" href="../configuration/">Repapp configuration</a>
</li>
</ul>
<p class="caption"><span class="caption-text">Repair-Café & Makespace</span></p>
<ul>
<li class="toctree-l1"><a class="" href="https://www.repaircafe-hilpoltstein.de">Repair-Café</a>
</li>
<li class="toctree-l1"><a class="" href="https://makes-hacks-hip.de">Makespace</a>
</li>
<li class="toctree-l1"><a class="" href="https://makes-hacks-hip.github.io">Makerspace@Github</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="Mobile navigation menu">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="../..">RepApp - Makespace Repair-Café Hilpoltstein</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content"><div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="../.." class="icon icon-home" aria-label="Docs"></a> &raquo;</li>
<li>Developer documentation &raquo;</li>
<li class="breadcrumb-item active">Objects</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/makes-hacks-hip/RepApp/edit/main/docs/dev/objects.md" class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main" class="document" itemscope="itemscope" itemtype="http://schema.org/Article">
<div class="section" itemprop="articleBody">
<h1 id="objects">Objects</h1>
<p>A <strong>device</strong> is an object owned by a <strong>guest</strong> which has some issue. To get support for fixing the issue, a <strong>guest</strong> provides the information about the <strong>device</strong> and requests a repair <strong>appointment</strong> during a <strong>repair_cafe</strong>.</p>
<p><img alt="Objects" src="../uml/objects_other.png" /></p>
<p>A <strong>repair_cafe</strong> happens at a specific date and location. It's an event which consists of <strong>appointments</strong> of <strong>repairers</strong> with <strong>guests</strong> to fix a <strong>device</strong>.</p>
<p>To organize a <strong>repair_cafe</strong>, a <strong>organizer</strong> needs to know which <strong>repairer</strong> will attend a <strong>repair_cafe</strong>, to schedule repair <strong>appointments</strong>.</p>
<p><img alt="Appointment" src="../uml/objects_appointment.png" /></p>
<p>In case of ambiguities, a <strong>organizer</strong> or a <strong>repairer</strong> can raise a <strong>consultation</strong> request. This <strong>consultation</strong> request is sent to the <strong>guest</strong> to get the needed information. In addition, a <strong>organizer</strong> or a <strong>repairer</strong> can add an <strong>comment</strong> to a <strong>device</strong>.</p>
<p><img alt="Consultation" src="../uml/objects_consultation.png" /></p>
<p><img alt="Comment" src="../uml/objects_comment.png" /></p>
<h2 id="repair_cafe">Repair_Cafe</h2>
<p>A <strong>repair_cafe</strong> is a event happening at a specific date at a specific location. During a <strong>repair_cafe</strong> <strong>appointments</strong> of <strong>guests</strong> with <strong>repairers</strong> happens to fix broken <em>devices</em>.</p>
<h3 id="fields">Fields</h3>
<ul>
<li><em>location</em>: Char (200) -- location of the event</li>
<li><em>address</em>: Char (200) -- address where the event happens</li>
<li><em>event_date</em>: Date -- date when the event happens</li>
<li><em>repairer</em>: many to many -- will attend the event</li>
</ul>
<h2 id="device">Device</h2>
<h2 id="appointment">Appointment</h2>
<h2 id="guest">Guest</h2>
<h2 id="repairer">Repairer</h2>
<h2 id="organizer">Organizer</h2>
<h2 id="consultation">Consultation</h2>
<h2 id="comment">Comment</h2>
</div>
</div><footer>
<div class="rst-footer-buttons" role="navigation" aria-label="Footer Navigation">
<a href="../apps/" class="btn btn-neutral float-left" title="Apps"><span class="icon icon-circle-arrow-left"></span> Previous</a>
<a href="../configuration/" class="btn btn-neutral float-right" title="Repapp configuration">Next <span class="icon icon-circle-arrow-right"></span></a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/readthedocs/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="Versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/makes-hacks-hip/RepApp/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../apps/" style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="../configuration/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script src="../../js/jquery-3.6.0.min.js"></script>
<script>var base_url = "../..";</script>
<script src="../../js/theme_extra.js"></script>
<script src="../../js/theme.js"></script>
<script src="../../search/main.js"></script>
<script>
jQuery(function () {
SphinxRtdTheme.Navigation.enable(true);
});
</script>
</body>
</html>

View file

@ -1,230 +0,0 @@
<!DOCTYPE html>
<html class="writer-html5" lang="en" >
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><link rel="canonical" href="https://makes-hacks-hip.github.io/RepApp/dev/stories/" />
<link rel="shortcut icon" href="../../img/favicon.ico" />
<title>Stories - RepApp - Makespace Repair-Café Hilpoltstein</title>
<link rel="stylesheet" href="../../css/theme.css" />
<link rel="stylesheet" href="../../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "Stories";
var mkdocs_page_input_path = "dev/stories.md";
var mkdocs_page_url = "/RepApp/dev/stories/";
</script>
<!--[if lt IE 9]>
<script src="../../js/html5shiv.min.js"></script>
<![endif]-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href="../.." class="icon icon-home"> RepApp - Makespace Repair-Café Hilpoltstein
</a><div role="search">
<form id ="rtd-search-form" class="wy-form" action="../../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" aria-label="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="Navigation menu">
<p class="caption"><span class="caption-text">RepApp</span></p>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../..">Beschreibung</a>
</li>
</ul>
<p class="caption"><span class="caption-text">Developer documentation</span></p>
<ul class="current">
<li class="toctree-l1"><a class="reference internal" href="../">Overview</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="../users/">Users</a>
</li>
<li class="toctree-l1 current"><a class="reference internal current" href="./">Stories</a>
<ul class="current">
<li class="toctree-l2"><a class="reference internal" href="#repairer-cancels-on-short-notice">Repairer cancels on short notice</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#guest-cancels-a-repair-appointment-on-short-notice">Guest cancels a repair appointment on short notice</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#guest-misses-a-repair-appointment-or-causes-trouble">Guest misses a repair appointment or causes trouble</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#the-last-repair-appointment-of-a-repair-cafe-must-get-canceled">The last repair appointment of a repair-cafe must get canceled</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#all-slots-for-a-repair-cafe-are-very-early-assigned">All slots for a repair-cafe are very early assigned</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#all-slots-for-some-device-types-are-already-very-relay-assigned">All slots for some device types are already very relay assigned</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#creation-and-handling-of-waiting-lists">Creation and handling of waiting lists</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#the-guest-doesnt-confirm-the-appointment-and-doesnt-reply-to-mails">The guest doesn't confirm the appointment and doesn't reply to mails</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#some-device-types-are-not-repaired-during-a-repair-cafe">Some device types are not repaired during a repair-cafe</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#strategic-overbooking">strategic overbooking</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#communication-with-guests">Communication with guests</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#where-is-the-device">Where is the device?</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#criteria-for-repair-assignments">Criteria for repair-assignments</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#paperwork-during-the-event">Paperwork during the event</a>
</li>
</ul>
</li>
<li class="toctree-l1"><a class="reference internal" href="../use_cases/">Use cases</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="../apps/">Apps</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="../objects/">Objects</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="../configuration/">Repapp configuration</a>
</li>
</ul>
<p class="caption"><span class="caption-text">Repair-Café & Makespace</span></p>
<ul>
<li class="toctree-l1"><a class="" href="https://www.repaircafe-hilpoltstein.de">Repair-Café</a>
</li>
<li class="toctree-l1"><a class="" href="https://makes-hacks-hip.de">Makespace</a>
</li>
<li class="toctree-l1"><a class="" href="https://makes-hacks-hip.github.io">Makerspace@Github</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="Mobile navigation menu">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="../..">RepApp - Makespace Repair-Café Hilpoltstein</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content"><div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="../.." class="icon icon-home" aria-label="Docs"></a> &raquo;</li>
<li>Developer documentation &raquo;</li>
<li class="breadcrumb-item active">Stories</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/makes-hacks-hip/RepApp/edit/main/docs/dev/stories.md" class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main" class="document" itemscope="itemscope" itemtype="http://schema.org/Article">
<div class="section" itemprop="articleBody">
<h1 id="stories">Stories</h1>
<p>User stories describing real world repair-cafe situations and workflows.</p>
<h2 id="repairer-cancels-on-short-notice">Repairer cancels on short notice</h2>
<p>One of the repairers cancels his attendance to a repair-cafe on short notice.</p>
<p>If possible, assign existing repair appointments to other repairers, and align new time schedule with the guests using phone calls. If not possible, cancel repair assignments using phone calls.</p>
<h2 id="guest-cancels-a-repair-appointment-on-short-notice">Guest cancels a repair appointment on short notice</h2>
<p>A guest cancels a repair appointment on short notice. This frees a timeslot for an additional repair appointment.</p>
<p>If there are fitting devices on the waiting list, the owner is called and a repair appointment is offered. If no fitting devices are available, the last appointment of the day is re-scheduled to the free slot, in alignment with the guest.</p>
<h2 id="guest-misses-a-repair-appointment-or-causes-trouble">Guest misses a repair appointment or causes trouble</h2>
<p>The guest is added to a blacklist.</p>
<h2 id="the-last-repair-appointment-of-a-repair-cafe-must-get-canceled">The last repair appointment of a repair-cafe must get canceled</h2>
<p>Because of delays or other issues, the last repair appointment of a repair-cafe event cannot happen anymore.</p>
<p>If possible, the last repair appointment is cancelled using a phone call with the guest.</p>
<h2 id="all-slots-for-a-repair-cafe-are-very-early-assigned">All slots for a repair-cafe are very early assigned</h2>
<p>Deactivate registration form and give a hint on the homepage that the event is already fully booked.</p>
<h2 id="all-slots-for-some-device-types-are-already-very-relay-assigned">All slots for some device types are already very relay assigned</h2>
<p>Further registration is still possible, but a hint is added that repair assignments for devices types XYZ are not possible anymore.</p>
<h2 id="creation-and-handling-of-waiting-lists">Creation and handling of waiting lists</h2>
<p>If a repair appointment is not possible for a device, because of high demand for this device type of cancellation of a repairer, the guest is contacted and asked to register the device again for a later repair-cafe, or if the device shall be registered for a later repair-cafe.</p>
<p>Guest registers again: It's possible that again no slots are free, and the repair appointment is canceled again. The information that the guest had the device already registered for a repair-cafe is lost.</p>
<p>Organizer registers device: Increased organization effort and there is a high probability that the guest doesn't keep the repair appointment.</p>
<p>All canceled devices are moved to the "canceled devices" section of the forum manually. These devices are considered during the planning of the next repair-cafe, and the forum entry is moved to the section of the next event.</p>
<h2 id="the-guest-doesnt-confirm-the-appointment-and-doesnt-reply-to-mails">The guest doesn't confirm the appointment and doesn't reply to mails</h2>
<p>Try to call guest by phone.</p>
<p>If the organizers cannot reach the guest, the repair-appointment is canceled by mail. If possible, another guest form the waiting list is contacted.</p>
<h2 id="some-device-types-are-not-repaired-during-a-repair-cafe">Some device types are not repaired during a repair-cafe</h2>
<p>Some device types are not repaired during a repair-cafe. These devices are assigned to repairers for "offline" repair.</p>
<p>Align with repairer: If the repairer accepts the device, the contact data of repairer are given to the guest.</p>
<p>For TV: Type plate and issue description is given to a repairer, and options are evaluated. If a repair is possible, the guest shall bring the TV to the Haus Einstein. When the repair is done, the guest get's an update by mail.</p>
<h2 id="strategic-overbooking">strategic overbooking</h2>
<p>If all slots are booked, further devices are accepted on best effort. These devices are brought by the guests at the beginning of a repair-cafe and get collected at the end of a repair-cafe. When a repairer has some time between two repair-assignments, he can pick such a device.Having such devices is wanted by the repairers.</p>
<p>This is only used for guests living very close to the event, and if the repair effort should be very limited.</p>
<h2 id="communication-with-guests">Communication with guests</h2>
<p>Communication shall primary happen by e-mail. If further information is needed for a device, a mail is written to the guest. This communication often involves embedded images and attachments.</p>
<p>A FAQ on the website of the repair-cafe, which can get referenced, may help for this communication.</p>
<h2 id="where-is-the-device">Where is the device?</h2>
<p>It can happen that it's not clear where a device is located, for example if a device stays with the repair-cafe team for a follow up repair, or if a device is accepted outside of an repair-cafe event.</p>
<p>Current workflow:</p>
<p>For devices which get brought by the owner and collected later: If there was no time to repair the device, or if spare parts were missing, we may offer the guest to keep the device till the next repair-cafe event happens. Such devices are kept in Haus Einstein.</p>
<p>Another such case may be that a guest brings a device a few days before the repair-cafe event, because of personal availability. Such devices are also stored in Haus Einstein.</p>
<p>It may also happen that a repairer agrees with a guest to keep the device and continue with the repair activity outside of an repair-cafe event. Especially for such cases, the current location of a device may be not defined and a documentation is missing.</p>
<h2 id="criteria-for-repair-assignments">Criteria for repair-assignments</h2>
<ul>
<li>Will a skilled repairer be available?</li>
<li>Point of time of registration (waiting lists and registration date)</li>
<li>Is the device and failure a candidate for an overbooking device?</li>
<li>Is there a feedback of a repairer that he wants to take a look at the device?</li>
<li>Is all needed information available?</li>
</ul>
<h2 id="paperwork-during-the-event">Paperwork during the event</h2>
<p>Based on an Excel sheet, repair slips (guest data and signature) and routing slips (device data and fields for notes) are created. Both slips have the same date and id, so it's possible to match them if needed. This is required in case of issues or complaints of the guest.</p>
</div>
</div><footer>
<div class="rst-footer-buttons" role="navigation" aria-label="Footer Navigation">
<a href="../users/" class="btn btn-neutral float-left" title="Users"><span class="icon icon-circle-arrow-left"></span> Previous</a>
<a href="../use_cases/" class="btn btn-neutral float-right" title="Use cases">Next <span class="icon icon-circle-arrow-right"></span></a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/readthedocs/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="Versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/makes-hacks-hip/RepApp/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../users/" style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="../use_cases/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script src="../../js/jquery-3.6.0.min.js"></script>
<script>var base_url = "../..";</script>
<script src="../../js/theme_extra.js"></script>
<script src="../../js/theme.js"></script>
<script src="../../search/main.js"></script>
<script>
jQuery(function () {
SphinxRtdTheme.Navigation.enable(true);
});
</script>
</body>
</html>

View file

@ -1,210 +0,0 @@
<!DOCTYPE html>
<html class="writer-html5" lang="en" >
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><link rel="canonical" href="https://makes-hacks-hip.github.io/RepApp/dev/use_cases/" />
<link rel="shortcut icon" href="../../img/favicon.ico" />
<title>Use cases - RepApp - Makespace Repair-Café Hilpoltstein</title>
<link rel="stylesheet" href="../../css/theme.css" />
<link rel="stylesheet" href="../../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "Use cases";
var mkdocs_page_input_path = "dev/use_cases.md";
var mkdocs_page_url = "/RepApp/dev/use_cases/";
</script>
<!--[if lt IE 9]>
<script src="../../js/html5shiv.min.js"></script>
<![endif]-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href="../.." class="icon icon-home"> RepApp - Makespace Repair-Café Hilpoltstein
</a><div role="search">
<form id ="rtd-search-form" class="wy-form" action="../../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" aria-label="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="Navigation menu">
<p class="caption"><span class="caption-text">RepApp</span></p>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../..">Beschreibung</a>
</li>
</ul>
<p class="caption"><span class="caption-text">Developer documentation</span></p>
<ul class="current">
<li class="toctree-l1"><a class="reference internal" href="../">Overview</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="../users/">Users</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="../stories/">Stories</a>
</li>
<li class="toctree-l1 current"><a class="reference internal current" href="./">Use cases</a>
<ul class="current">
<li class="toctree-l2"><a class="reference internal" href="#guest">Guest</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#repairer">Repairer</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#organizer">Organizer</a>
</li>
</ul>
</li>
<li class="toctree-l1"><a class="reference internal" href="../apps/">Apps</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="../objects/">Objects</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="../configuration/">Repapp configuration</a>
</li>
</ul>
<p class="caption"><span class="caption-text">Repair-Café & Makespace</span></p>
<ul>
<li class="toctree-l1"><a class="" href="https://www.repaircafe-hilpoltstein.de">Repair-Café</a>
</li>
<li class="toctree-l1"><a class="" href="https://makes-hacks-hip.de">Makespace</a>
</li>
<li class="toctree-l1"><a class="" href="https://makes-hacks-hip.github.io">Makerspace@Github</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="Mobile navigation menu">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="../..">RepApp - Makespace Repair-Café Hilpoltstein</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content"><div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="../.." class="icon icon-home" aria-label="Docs"></a> &raquo;</li>
<li>Developer documentation &raquo;</li>
<li class="breadcrumb-item active">Use cases</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/makes-hacks-hip/RepApp/edit/main/docs/dev/use_cases.md" class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main" class="document" itemscope="itemscope" itemtype="http://schema.org/Article">
<div class="section" itemprop="articleBody">
<h1 id="use-cases">Use cases</h1>
<p>The repapp shall support the following use cases:</p>
<h2 id="guest">Guest</h2>
<ul>
<li>[ ] As a guest, I want to register a device for a repair-cafe event.</li>
<li>[ ] As a guest, I want to get a confirmation for my device registration.</li>
<li>[ ] As a guest, I want to get notified about a repair-assignment or a rejection of my device.</li>
<li>[ ] As a guest, I want to get notified about information request for my device.</li>
<li>[ ] As a guest, I want to be able to answer information requests for my device.</li>
<li>[ ] As a guest, I want to be able to contact the repair-cafe organizers.</li>
<li>[ ] As a guest, I want to be able to do all interaction by mail only (except the device registration).</li>
<li>[ ] As a guest, I want to confirm a repair-assignment by mail.</li>
<li>[ ] As a guest, I want to be able to see my device data and the status of my device registration.</li>
<li>[ ] As a guest, I want to be able to register my device for a follow up repair.</li>
<li>[ ] As a guest, I want to be able to view my personal data.</li>
<li>[ ] As a guest, I want to be able to view my device data.</li>
<li>[ ] As a guest, I want to be able to edit my personal data.</li>
<li>[ ] As a guest, I want to be able to edit my device data.</li>
<li>[ ] As a guest, I want to be able to delete my personal data.</li>
<li>[ ] As a guest, I want to be able to delete my device data.</li>
<li>[ ] As a guest, I want to be able to cancel my device registration.</li>
</ul>
<h2 id="repairer">Repairer</h2>
<ul>
<li>[ ] As a repairer, I want to be able to view the registered devices.</li>
<li>[ ] As a repairer, I want to show interest for a device.</li>
<li>[ ] As a repairer, I want to request additional information for a device.</li>
<li>[ ] As a repairer, I want to enable or disable notifications.</li>
<li>[ ] As a repairer, I want to get notified when an information request gets answered.</li>
<li>[ ] As a repairer, I want to get notified when a device gets assigned to me.</li>
<li>[ ] As a repairer, I want to confirm the participation at a repair-cafe.</li>
<li>[ ] As a repairer, I want to decline the participation at a repair-cafe.</li>
<li>[ ] As a repairer, I want to reject a device.</li>
<li>[ ] As a repairer, I want to get notified if a device assigned to me gets canceled.</li>
</ul>
<h2 id="organizer">Organizer</h2>
<ul>
<li>[ ] As a organizer, I want to get notified when a new device is registered.</li>
<li>[ ] As a organizer, I want to schedule new repair-cafe events.</li>
<li>[ ] As a organizer, I want to view device details. </li>
<li>[ ] As a organizer, I want to request further information for a device.</li>
<li>[ ] As a organizer, I want to reject a device.</li>
<li>[ ] As a organizer, I want to accept a device.</li>
<li>[ ] As a organizer, I want to create a repair-assignment.</li>
<li>[ ] As a organizer, I want to confirm the participation of a repairer for a repair-cafe event.</li>
<li>[ ] As a organizer, I want to decline the participation of a repairer for a repair-cafe event.</li>
<li>[ ] As a organizer, I want to edit the device data.</li>
<li>[ ] As a organizer, I want to edit the guest data.</li>
<li>[ ] As a organizer, I want to unregister a device.</li>
<li>[ ] As a organizer, I want to get notified if a guest deletes a device registration.</li>
<li>[ ] As a organizer, I want to get export all data for a repair-cafe.</li>
<li>[ ] As a organizer, I want to assign devices to repair-cafes.</li>
</ul>
</div>
</div><footer>
<div class="rst-footer-buttons" role="navigation" aria-label="Footer Navigation">
<a href="../stories/" class="btn btn-neutral float-left" title="Stories"><span class="icon icon-circle-arrow-left"></span> Previous</a>
<a href="../apps/" class="btn btn-neutral float-right" title="Apps">Next <span class="icon icon-circle-arrow-right"></span></a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/readthedocs/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="Versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/makes-hacks-hip/RepApp/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../stories/" style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="../apps/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script src="../../js/jquery-3.6.0.min.js"></script>
<script>var base_url = "../..";</script>
<script src="../../js/theme_extra.js"></script>
<script src="../../js/theme.js"></script>
<script src="../../search/main.js"></script>
<script>
jQuery(function () {
SphinxRtdTheme.Navigation.enable(true);
});
</script>
</body>
</html>

View file

@ -1,156 +0,0 @@
<!DOCTYPE html>
<html class="writer-html5" lang="en" >
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><link rel="canonical" href="https://makes-hacks-hip.github.io/RepApp/dev/users/" />
<link rel="shortcut icon" href="../../img/favicon.ico" />
<title>Users - RepApp - Makespace Repair-Café Hilpoltstein</title>
<link rel="stylesheet" href="../../css/theme.css" />
<link rel="stylesheet" href="../../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "Users";
var mkdocs_page_input_path = "dev/users.md";
var mkdocs_page_url = "/RepApp/dev/users/";
</script>
<!--[if lt IE 9]>
<script src="../../js/html5shiv.min.js"></script>
<![endif]-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href="../.." class="icon icon-home"> RepApp - Makespace Repair-Café Hilpoltstein
</a><div role="search">
<form id ="rtd-search-form" class="wy-form" action="../../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" aria-label="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="Navigation menu">
<p class="caption"><span class="caption-text">RepApp</span></p>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../..">Beschreibung</a>
</li>
</ul>
<p class="caption"><span class="caption-text">Developer documentation</span></p>
<ul class="current">
<li class="toctree-l1"><a class="reference internal" href="../">Overview</a>
</li>
<li class="toctree-l1 current"><a class="reference internal current" href="./">Users</a>
<ul class="current">
</ul>
</li>
<li class="toctree-l1"><a class="reference internal" href="../stories/">Stories</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="../use_cases/">Use cases</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="../apps/">Apps</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="../objects/">Objects</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="../configuration/">Repapp configuration</a>
</li>
</ul>
<p class="caption"><span class="caption-text">Repair-Café & Makespace</span></p>
<ul>
<li class="toctree-l1"><a class="" href="https://www.repaircafe-hilpoltstein.de">Repair-Café</a>
</li>
<li class="toctree-l1"><a class="" href="https://makes-hacks-hip.de">Makespace</a>
</li>
<li class="toctree-l1"><a class="" href="https://makes-hacks-hip.github.io">Makerspace@Github</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="Mobile navigation menu">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="../..">RepApp - Makespace Repair-Café Hilpoltstein</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content"><div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="../.." class="icon icon-home" aria-label="Docs"></a> &raquo;</li>
<li>Developer documentation &raquo;</li>
<li class="breadcrumb-item active">Users</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/makes-hacks-hip/RepApp/edit/main/docs/dev/users.md" class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main" class="document" itemscope="itemscope" itemtype="http://schema.org/Article">
<div class="section" itemprop="articleBody">
<h1 id="users">Users</h1>
<p>Repapp distinguishes two user categories.</p>
<p>A <em>guest</em> is a owner of an device. The <em>guest</em> is booking a repair appointment to get support from a repair café member to fix his device.</p>
<p>A <em>member</em> can be an <em>organizer</em> or a <em>repairer</em>. A <em>organizer</em> is taking care of arranging the repair assignments, and needs to access the private contact data of a guest to do this job. A <em>repairer</em> is supporting the <em>guest</em> in fixing his device. The <em>repairer</em> only needs the details of the device in advance, but not the private contact data of the guest.</p>
<p><img alt="Users" src="../uml/users.png" /></p>
</div>
</div><footer>
<div class="rst-footer-buttons" role="navigation" aria-label="Footer Navigation">
<a href="../" class="btn btn-neutral float-left" title="Overview"><span class="icon icon-circle-arrow-left"></span> Previous</a>
<a href="../stories/" class="btn btn-neutral float-right" title="Stories">Next <span class="icon icon-circle-arrow-right"></span></a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/readthedocs/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="Versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/makes-hacks-hip/RepApp/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../" style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="../stories/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script src="../../js/jquery-3.6.0.min.js"></script>
<script>var base_url = "../..";</script>
<script src="../../js/theme_extra.js"></script>
<script src="../../js/theme.js"></script>
<script src="../../search/main.js"></script>
<script>
jQuery(function () {
SphinxRtdTheme.Navigation.enable(true);
});
</script>
</body>
</html>

76
docs/dev/apps.md Normal file
View file

@ -0,0 +1,76 @@
# Apps
The repapp is split into different modules, realized as Django apps, to improve the maintainability.
## One time login
The one time login module provides a login for guests using a one time secret as part of an URL.
- The templates use the `settings.ORGANIZATION` variable to fill the organization name.
- The Django mail module is used to send emails. See https://docs.djangoproject.com/en/4.2/topics/email/ for details how to configure the mail host.
### Configuration
To enable the one time login links, an additional authentication backend must get added:
```Python
# Repapp specific authentication backends
AUTHENTICATION_BACKENDS = [
# Backend for one time logins
"one_time_login.authentication_backends.OneTimeLoginBackend",
]
```
The one time login urls must get added using namespace `one_time_login`:
```Python
urlpatterns = i18n_patterns(
...
path(_('one_time_login/'),
include('one_time_login.urls', namespace='one_time_login')),
...
)
```
## Email interface
The email interface modules supports handling of mails. It support sending of mails, including HTML formatting and attachments, and it supports receiving of plain text and HTML formatted mails, including attachments.
- The Django mail module is used to send emails. See https://docs.djangoproject.com/en/4.2/topics/email/ for details how to configure the mail host.
- CKEditor is used for email content editing. See https://django-ckeditor.readthedocs.io/en/latest/ for details.
- Crispy forms is used for form rendering. See https://django-crispy-forms.readthedocs.io/en/latest/ for details.
- Bootstrap 5 theme for crispy forms is used. See https://github.com/django-crispy-forms/crispy-bootstrap5 for details.
### Configuration
The email interface urls must get added using namespace `email_interface`:
```Python
urlpatterns = i18n_patterns(
...
path(_('emails/'),
include('email_interface.urls', namespace='email_interface')),
...
)
```
To use CKEditor as authenticated user, the following additional urls must get added:
```Python
urlpatterns = i18n_patterns(
...
# CKEditor upload views
path('upload/', login_required(ckeditor_views.upload), name="ckeditor_upload"),
path('browse/', never_cache(login_required(ckeditor_views.browse)),
name="ckeditor_browse"),
...
)
```
To be abel to use CKEditor also the static an media content must get served. For development this is done with:
```Python
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)
```

163
docs/dev/configuration.md Normal file
View file

@ -0,0 +1,163 @@
# Repapp configuration
## Debug mode
The Django debug mode is activated if there is a environment variable `DJANGO_DEBUG` with the value "true", "1" or "t". By default, the debug mode is disabled.
## Organization name
Some templates use and organization name. This value is taken from `settings.ORGANIZATION` and defaults to "Repair-Café Hilpoltstein".
## Email settings
The following environment variables are used to configure the Django email feature:
``` Python
EMAIL_HOST = os.getenv("DJANGO_EMAIL_HOST", "")
EMAIL_PORT = (int)(os.getenv("DJANGO_EMAIL_PORT", "25"))
EMAIL_HOST_USER = os.getenv("DJANGO_EMAIL_HOST_USER", None)
EMAIL_HOST_PASSWORD = os.getenv("DJANGO_EMAIL_HOST_PASSWORD", None)
EMAIL_USE_TLS = os.getenv("DJANGO_EMAIL_USE_TLS", "true") in ("true", "1", "t")
DEFAULT_FROM_EMAIL = os.getenv("DJANGO_SENDER_ADDRESS", "")
```
See https://docs.djangoproject.com/en/4.2/topics/email/ for more details.
## Translations
Repapp is using localization, and comes with german and english translations.
Enable the `LocaleMiddleware`:
```Python
MIDDLEWARE = [
...
'django.contrib.sessions.middleware.SessionMiddleware',
# enable translation
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
...
]
```
We use the following localization settings:
```Python
LANGUAGE_CODE = 'de'
TIME_ZONE = 'Europe/Berlin'
USE_I18N = True
USE_L10N = True
USE_TZ = True
LANGUAGES = [
('de', _('German')),
('en', _('English')),
]
LOCALE_PATHS = [
BASE_DIR / 'locale',
]
```
Also the URLs are localized, and the language is choosen using i18n URL patterns:
```Python
...
from django.conf.urls.i18n import i18n_patterns
from django.utils.translation import gettext_lazy as _
urlpatterns = i18n_patterns(
path('admin/', admin.site.urls),
path(_('one_time_login/'),
include('one_time_login.urls', namespace='one_time_login')),
# URLs for rosetta translation interface
path('rosetta/', include('rosetta.urls')),
)
```
### Rosetta
For translating the interface, we use rosetta. For more details see https://pypi.org/project/django-rosetta/.
You need to install rosetta (`pip install django-rosetta`) and enable it in the apps:
```Python
INSTALLED_APPS = [
...
# enable rosetta online translation interface
'rosetta',
...
]
```
Rosatta is made availabel at `rosetta/`.
## One time login
To enable the one time login links, an additional authentication backend must get added:
```Python
# Repapp specific authentication backends
AUTHENTICATION_BACKENDS = [
# Backend for one time logins
"one_time_login.authentication_backends.OneTimeLoginBackend",
]
```
## Email interface
The email interface uses CKEditor, crispy forms and easy thumbnails. The apps are enabled with:
```Python
INSTALLED_APPS = [
...
# CKEditor for mail content editing
"ckeditor",
"ckeditor_uploader",
# Crispy forms for bootstrap 5 form design
"crispy_forms",
"crispy_bootstrap5",
# for creating thumbnails, used by email_interface demo views
'easy_thumbnails',
...
]
```
For CKEditor, we use the following settings:
```Python
# editor upload path, used e.g. for send mail demo
CKEDITOR_UPLOAD_PATH = "editor_uploads/"
CKEDITOR_IMAGE_BACKEND = 'pillow'
# CKEditor default config
CKEDITOR_CONFIGS = {
'default': {
'toolbar': 'full',
'height': 300,
'width': '100%',
},
}
```
To enable the image upload of CKEditor, also the Django media paths and urls must get configured:
```Python
# Django media and static file config, required for CKEditor
STATIC_URL = "static/"
MEDIA_URL = "media/"
# Use subfolders of project folder
STATIC_ROOT = os.path.join(BASE_DIR, "static")
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
```
For crispy forms, we use the bootstrap 5 theme:
```Python
# Crispy forms settings
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
CRISPY_TEMPLATE_PACK = "bootstrap5"
```
The static files for bootstrap must also be made available.

47
docs/dev/index.md Normal file
View file

@ -0,0 +1,47 @@
# Repapp - developer documentation
The Repapp is a repair assignment booking and management tool.
It tries to support all common workflows happening while organizing a repair café event.
It's build using the Django web framework.
The code of the Repapp is hosted on Github: https://github.com/makes-hacks-hip/RepApp
## Local development setup
- Clone the Github repository
- Create a Python environment: `python -m venv venv`
- Activate the Python environment: `source venv/bin/activate`
- Install the dependencies: `pip install -r requirements_prod.txt` and `pip install -r requirements_dev.txt`
- Change to the Django project folder: `cd repapp`
- Setup the database:
- `python manage.py makemigrations`
- `python manage.py migrate`
- Setup an admin user: `python manage.py createsuperuser`
- Run the local test server: `python manage.py runserver`
## Docker image
- Build the image: `docker build -t repapp .`
- Run the image:
```bash
docker run --rm -d \
-p 127.0.0.1:8020:8020 \
--name repapp \
--env DJANGO_SUPERUSER_USERNAME=suba \
--env DJANGO_SUPERUSER_PASSWORD=ThePassword \
--env DJANGO_SUPERUSER_EMAIL=noreply@example.com \
repapp`
```
- Open http://127.0.0.1:8020/ in your browser
## Tests
- Unit tests: `python manage.py test`
- LiveServer tests: `python manage.py test repapp.live_tests`
### Coverage
- `coverage run --source='.' manage.py test`
- `coverage report` or `coverage html`

42
docs/dev/objects.md Normal file
View file

@ -0,0 +1,42 @@
# Objects
A **device** is an object owned by a **guest** which has some issue. To get support for fixing the issue, a **guest** provides the information about the **device** and requests a repair **appointment** during a **repair_cafe**.
![Objects](uml/objects_other.png)
A **repair_cafe** happens at a specific date and location. It's an event which consists of **appointments** of **repairers** with **guests** to fix a **device**.
To organize a **repair_cafe**, a **organizer** needs to know which **repairer** will attend a **repair_cafe**, to schedule repair **appointments**.
![Appointment](uml/objects_appointment.png)
In case of ambiguities, a **organizer** or a **repairer** can raise a **consultation** request. This **consultation** request is sent to the **guest** to get the needed information. In addition, a **organizer** or a **repairer** can add an **comment** to a **device**.
![Consultation](uml/objects_consultation.png)
![Comment](uml/objects_comment.png)
## Repair_Cafe
A **repair_cafe** is a event happening at a specific date at a specific location. During a **repair_cafe** **appointments** of **guests** with **repairers** happens to fix broken *devices*.
### Fields
- *location*: Char (200) -- location of the event
- *address*: Char (200) -- address where the event happens
- *event_date*: Date -- date when the event happens
- *repairer*: many to many -- will attend the event
## Device
## Appointment
## Guest
## Repairer
## Organizer
## Consultation
## Comment

93
docs/dev/stories.md Normal file
View file

@ -0,0 +1,93 @@
# Stories
User stories describing real world repair-cafe situations and workflows.
## Repairer cancels on short notice
One of the repairers cancels his attendance to a repair-cafe on short notice.
If possible, assign existing repair appointments to other repairers, and align new time schedule with the guests using phone calls. If not possible, cancel repair assignments using phone calls.
## Guest cancels a repair appointment on short notice
A guest cancels a repair appointment on short notice. This frees a timeslot for an additional repair appointment.
If there are fitting devices on the waiting list, the owner is called and a repair appointment is offered. If no fitting devices are available, the last appointment of the day is re-scheduled to the free slot, in alignment with the guest.
## Guest misses a repair appointment or causes trouble
The guest is added to a blacklist.
## The last repair appointment of a repair-cafe must get canceled
Because of delays or other issues, the last repair appointment of a repair-cafe event cannot happen anymore.
If possible, the last repair appointment is cancelled using a phone call with the guest.
## All slots for a repair-cafe are very early assigned
Deactivate registration form and give a hint on the homepage that the event is already fully booked.
## All slots for some device types are already very relay assigned
Further registration is still possible, but a hint is added that repair assignments for devices types XYZ are not possible anymore.
## Creation and handling of waiting lists
If a repair appointment is not possible for a device, because of high demand for this device type of cancellation of a repairer, the guest is contacted and asked to register the device again for a later repair-cafe, or if the device shall be registered for a later repair-cafe.
Guest registers again: It's possible that again no slots are free, and the repair appointment is canceled again. The information that the guest had the device already registered for a repair-cafe is lost.
Organizer registers device: Increased organization effort and there is a high probability that the guest doesn't keep the repair appointment.
All canceled devices are moved to the "canceled devices" section of the forum manually. These devices are considered during the planning of the next repair-cafe, and the forum entry is moved to the section of the next event.
## The guest doesn't confirm the appointment and doesn't reply to mails
Try to call guest by phone.
If the organizers cannot reach the guest, the repair-appointment is canceled by mail. If possible, another guest form the waiting list is contacted.
## Some device types are not repaired during a repair-cafe
Some device types are not repaired during a repair-cafe. These devices are assigned to repairers for "offline" repair.
Align with repairer: If the repairer accepts the device, the contact data of repairer are given to the guest.
For TV: Type plate and issue description is given to a repairer, and options are evaluated. If a repair is possible, the guest shall bring the TV to the Haus Einstein. When the repair is done, the guest get's an update by mail.
## strategic overbooking
If all slots are booked, further devices are accepted on best effort. These devices are brought by the guests at the beginning of a repair-cafe and get collected at the end of a repair-cafe. When a repairer has some time between two repair-assignments, he can pick such a device.Having such devices is wanted by the repairers.
This is only used for guests living very close to the event, and if the repair effort should be very limited.
## Communication with guests
Communication shall primary happen by e-mail. If further information is needed for a device, a mail is written to the guest. This communication often involves embedded images and attachments.
A FAQ on the website of the repair-cafe, which can get referenced, may help for this communication.
## Where is the device?
It can happen that it's not clear where a device is located, for example if a device stays with the repair-cafe team for a follow up repair, or if a device is accepted outside of an repair-cafe event.
Current workflow:
For devices which get brought by the owner and collected later: If there was no time to repair the device, or if spare parts were missing, we may offer the guest to keep the device till the next repair-cafe event happens. Such devices are kept in Haus Einstein.
Another such case may be that a guest brings a device a few days before the repair-cafe event, because of personal availability. Such devices are also stored in Haus Einstein.
It may also happen that a repairer agrees with a guest to keep the device and continue with the repair activity outside of an repair-cafe event. Especially for such cases, the current location of a device may be not defined and a documentation is missing.
## Criteria for repair-assignments
- Will a skilled repairer be available?
- Point of time of registration (waiting lists and registration date)
- Is the device and failure a candidate for an overbooking device?
- Is there a feedback of a repairer that he wants to take a look at the device?
- Is all needed information available?
## Paperwork during the event
Based on an Excel sheet, repair slips (guest data and signature) and routing slips (device data and fields for notes) are created. Both slips have the same date and id, so it's possible to match them if needed. This is required in case of issues or complaints of the guest.

View file

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

View file

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View file

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

54
docs/dev/use_cases.md Normal file
View file

@ -0,0 +1,54 @@
# Use cases
The repapp shall support the following use cases:
## Guest
- [ ] As a guest, I want to register a device for a repair-cafe event.
- [ ] As a guest, I want to get a confirmation for my device registration.
- [ ] As a guest, I want to get notified about a repair-assignment or a rejection of my device.
- [ ] As a guest, I want to get notified about information request for my device.
- [ ] As a guest, I want to be able to answer information requests for my device.
- [ ] As a guest, I want to be able to contact the repair-cafe organizers.
- [ ] As a guest, I want to be able to do all interaction by mail only (except the device registration).
- [ ] As a guest, I want to confirm a repair-assignment by mail.
- [ ] As a guest, I want to be able to see my device data and the status of my device registration.
- [ ] As a guest, I want to be able to register my device for a follow up repair.
- [ ] As a guest, I want to be able to view my personal data.
- [ ] As a guest, I want to be able to view my device data.
- [ ] As a guest, I want to be able to edit my personal data.
- [ ] As a guest, I want to be able to edit my device data.
- [ ] As a guest, I want to be able to delete my personal data.
- [ ] As a guest, I want to be able to delete my device data.
- [ ] As a guest, I want to be able to cancel my device registration.
## Repairer
- [ ] As a repairer, I want to be able to view the registered devices.
- [ ] As a repairer, I want to show interest for a device.
- [ ] As a repairer, I want to request additional information for a device.
- [ ] As a repairer, I want to enable or disable notifications.
- [ ] As a repairer, I want to get notified when an information request gets answered.
- [ ] As a repairer, I want to get notified when a device gets assigned to me.
- [ ] As a repairer, I want to confirm the participation at a repair-cafe.
- [ ] As a repairer, I want to decline the participation at a repair-cafe.
- [ ] As a repairer, I want to reject a device.
- [ ] As a repairer, I want to get notified if a device assigned to me gets canceled.
## Organizer
- [ ] As a organizer, I want to get notified when a new device is registered.
- [ ] As a organizer, I want to schedule new repair-cafe events.
- [ ] As a organizer, I want to view device details.
- [ ] As a organizer, I want to request further information for a device.
- [ ] As a organizer, I want to reject a device.
- [ ] As a organizer, I want to accept a device.
- [ ] As a organizer, I want to create a repair-assignment.
- [ ] As a organizer, I want to confirm the participation of a repairer for a repair-cafe event.
- [ ] As a organizer, I want to decline the participation of a repairer for a repair-cafe event.
- [ ] As a organizer, I want to edit the device data.
- [ ] As a organizer, I want to edit the guest data.
- [ ] As a organizer, I want to unregister a device.
- [ ] As a organizer, I want to get notified if a guest deletes a device registration.
- [ ] As a organizer, I want to get export all data for a repair-cafe.
- [ ] As a organizer, I want to assign devices to repair-cafes.

9
docs/dev/users.md Normal file
View file

@ -0,0 +1,9 @@
# Users
Repapp distinguishes two user categories.
A *guest* is a owner of an device. The *guest* is booking a repair appointment to get support from a repair café member to fix his device.
A *member* can be an *organizer* or a *repairer*. A *organizer* is taking care of arranging the repair assignments, and needs to access the private contact data of a guest to do this job. A *repairer* is supporting the *guest* in fixing his device. The *repairer* only needs the details of the device in advance, but not the private contact data of the guest.
![Users](uml/users.png)

35
docs/index.md Normal file
View file

@ -0,0 +1,35 @@
# Projekt RepApp
RepApp ist eine Web App um Reparaturen im Rahmen eines Repair-Cafés zu organisieren.
## Benutzer
RepApp unterscheidet zwischen den folgenden Benutzer-Typen:
### Gast
Ein Gast ist ein Gerätebesitzer der ein oder mehrere Geräte zur Reparatur anmelden möchte.
### Reparateur
Ein Reparateur ist ein Mitglied des Repair-Cafés das die Gäste bei der Reparatur ihrer Geräte unterstützt.
### Organisator
Ein Organisator ist Mitglied des Repair-Cafés das die Termine mit den Gästen organisiert und die Reparateure zu den Geräten zuordnet.
## Anmelde Konzept
RepApp unterstützt pro Benutzer-Gruppe verschiedenen Anmelde-Konzepte.
### Gäste
Für Gäste gibt es Einmal-Logins. Ein Einmal-Login ist ein Geheimnis und eine Ziel-URL, das dem Gast in From eines Anmelde-Links in einer eMail mitgeteilt wird. Mit diesem Link kann der Gast sich einmalig anmelden, da das Geheimnis danach potentiell Dritten bekannt ist, z.B. über die Browser History.
Nach dem erfolgreichen Login wird der Gast automatisch zu der hinterlegten URL weitergeleitet. Dieser Mechanismus erlaubt es einem Gast "per Klick" Zugriff auf geschützte Daten zu geben.
Wenn ein Gast versucht den Einmal-Link nochmals zu verwenden, schlägt dies Fehl, und dem Gast wird automatisch ein neuer Einmal-Link per eMail mitgeteilt. Dies wird dem Gast auch über entsprechende Nachrichten mitgeteilt.
### Mitarbeiter
Der Login für Mitarbeiter ist nur über das Single-Sign-On der Repair-Cafés erlaubt (Keycloak), das mittels OIDC angebunden ist. Alle Mitarbeiter im Repair-Café haben dort bereits einen Benutzer, was zum einen sicherstellt dass es sich um einen Mitarbeiter handelt, und zum anderen auch die Veraltung der Benutzer in RepApp vereinfacht.

7
env.example Normal file
View file

@ -0,0 +1,7 @@
export DJANGO_EMAIL_HOST=""
export DJANGO_EMAIL_PORT="25"
export DJANGO_EMAIL_HOST_USER=""
export DJANGO_EMAIL_HOST_PASSWORD=""
export DJANGO_EMAIL_USE_TLS="true"
export DJANGO_SENDER_ADDRESS=""

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1,184 +0,0 @@
<!DOCTYPE html>
<html class="writer-html5" lang="en" >
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><link rel="canonical" href="https://makes-hacks-hip.github.io/RepApp/" />
<link rel="shortcut icon" href="img/favicon.ico" />
<title>Beschreibung - RepApp - Makespace Repair-Café Hilpoltstein</title>
<link rel="stylesheet" href="css/theme.css" />
<link rel="stylesheet" href="css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "Beschreibung";
var mkdocs_page_input_path = "index.md";
var mkdocs_page_url = "/RepApp/";
</script>
<!--[if lt IE 9]>
<script src="js/html5shiv.min.js"></script>
<![endif]-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href="." class="icon icon-home"> RepApp - Makespace Repair-Café Hilpoltstein
</a><div role="search">
<form id ="rtd-search-form" class="wy-form" action="./search.html" method="get">
<input type="text" name="q" placeholder="Search docs" aria-label="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="Navigation menu">
<p class="caption"><span class="caption-text">RepApp</span></p>
<ul class="current">
<li class="toctree-l1 current"><a class="reference internal current" href=".">Beschreibung</a>
<ul class="current">
<li class="toctree-l2"><a class="reference internal" href="#benutzer">Benutzer</a>
<ul>
<li class="toctree-l3"><a class="reference internal" href="#gast">Gast</a>
</li>
<li class="toctree-l3"><a class="reference internal" href="#reparateur">Reparateur</a>
</li>
<li class="toctree-l3"><a class="reference internal" href="#organisator">Organisator</a>
</li>
</ul>
</li>
<li class="toctree-l2"><a class="reference internal" href="#anmelde-konzept">Anmelde Konzept</a>
<ul>
<li class="toctree-l3"><a class="reference internal" href="#gaste">Gäste</a>
</li>
<li class="toctree-l3"><a class="reference internal" href="#mitarbeiter">Mitarbeiter</a>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p class="caption"><span class="caption-text">Developer documentation</span></p>
<ul>
<li class="toctree-l1"><a class="reference internal" href="dev/">Overview</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="dev/users/">Users</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="dev/stories/">Stories</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="dev/use_cases/">Use cases</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="dev/apps/">Apps</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="dev/objects/">Objects</a>
</li>
<li class="toctree-l1"><a class="reference internal" href="dev/configuration/">Repapp configuration</a>
</li>
</ul>
<p class="caption"><span class="caption-text">Repair-Café & Makespace</span></p>
<ul>
<li class="toctree-l1"><a class="" href="https://www.repaircafe-hilpoltstein.de">Repair-Café</a>
</li>
<li class="toctree-l1"><a class="" href="https://makes-hacks-hip.de">Makespace</a>
</li>
<li class="toctree-l1"><a class="" href="https://makes-hacks-hip.github.io">Makerspace@Github</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="Mobile navigation menu">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href=".">RepApp - Makespace Repair-Café Hilpoltstein</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content"><div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="." class="icon icon-home" aria-label="Docs"></a> &raquo;</li>
<li>RepApp &raquo;</li>
<li class="breadcrumb-item active">Beschreibung</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/makes-hacks-hip/RepApp/edit/main/docs/index.md" class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main" class="document" itemscope="itemscope" itemtype="http://schema.org/Article">
<div class="section" itemprop="articleBody">
<h1 id="projekt-repapp">Projekt RepApp</h1>
<p>RepApp ist eine Web App um Reparaturen im Rahmen eines Repair-Cafés zu organisieren.</p>
<h2 id="benutzer">Benutzer</h2>
<p>RepApp unterscheidet zwischen den folgenden Benutzer-Typen:</p>
<h3 id="gast">Gast</h3>
<p>Ein Gast ist ein Gerätebesitzer der ein oder mehrere Geräte zur Reparatur anmelden möchte.</p>
<h3 id="reparateur">Reparateur</h3>
<p>Ein Reparateur ist ein Mitglied des Repair-Cafés das die Gäste bei der Reparatur ihrer Geräte unterstützt.</p>
<h3 id="organisator">Organisator</h3>
<p>Ein Organisator ist Mitglied des Repair-Cafés das die Termine mit den Gästen organisiert und die Reparateure zu den Geräten zuordnet.</p>
<h2 id="anmelde-konzept">Anmelde Konzept</h2>
<p>RepApp unterstützt pro Benutzer-Gruppe verschiedenen Anmelde-Konzepte.</p>
<h3 id="gaste">Gäste</h3>
<p>Für Gäste gibt es Einmal-Logins. Ein Einmal-Login ist ein Geheimnis und eine Ziel-URL, das dem Gast in From eines Anmelde-Links in einer eMail mitgeteilt wird. Mit diesem Link kann der Gast sich einmalig anmelden, da das Geheimnis danach potentiell Dritten bekannt ist, z.B. über die Browser History.</p>
<p>Nach dem erfolgreichen Login wird der Gast automatisch zu der hinterlegten URL weitergeleitet. Dieser Mechanismus erlaubt es einem Gast "per Klick" Zugriff auf geschützte Daten zu geben.</p>
<p>Wenn ein Gast versucht den Einmal-Link nochmals zu verwenden, schlägt dies Fehl, und dem Gast wird automatisch ein neuer Einmal-Link per eMail mitgeteilt. Dies wird dem Gast auch über entsprechende Nachrichten mitgeteilt.</p>
<h3 id="mitarbeiter">Mitarbeiter</h3>
<p>Der Login für Mitarbeiter ist nur über das Single-Sign-On der Repair-Cafés erlaubt (Keycloak), das mittels OIDC angebunden ist. Alle Mitarbeiter im Repair-Café haben dort bereits einen Benutzer, was zum einen sicherstellt dass es sich um einen Mitarbeiter handelt, und zum anderen auch die Veraltung der Benutzer in RepApp vereinfacht.</p>
</div>
</div><footer>
<div class="rst-footer-buttons" role="navigation" aria-label="Footer Navigation">
<a href="dev/" class="btn btn-neutral float-right" title="Overview">Next <span class="icon icon-circle-arrow-right"></span></a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/readthedocs/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="Versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/makes-hacks-hip/RepApp/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="dev/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script src="js/jquery-3.6.0.min.js"></script>
<script>var base_url = ".";</script>
<script src="js/theme_extra.js"></script>
<script src="js/theme.js"></script>
<script src="search/main.js"></script>
<script>
jQuery(function () {
SphinxRtdTheme.Navigation.enable(true);
});
</script>
</body>
</html>

4
js/html5shiv.min.js vendored
View file

@ -1,4 +0,0 @@
/**
* @preserve HTML5 Shiv 3.7.3 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed
*/
!function(a,b){function c(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x<style>"+b+"</style>",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=t.elements;return"string"==typeof a?a.split(" "):a}function e(a,b){var c=t.elements;"string"!=typeof c&&(c=c.join(" ")),"string"!=typeof a&&(a=a.join(" ")),t.elements=c+" "+a,j(b)}function f(a){var b=s[a[q]];return b||(b={},r++,a[q]=r,s[r]=b),b}function g(a,c,d){if(c||(c=b),l)return c.createElement(a);d||(d=f(c));var e;return e=d.cache[a]?d.cache[a].cloneNode():p.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),!e.canHaveChildren||o.test(a)||e.tagUrn?e:d.frag.appendChild(e)}function h(a,c){if(a||(a=b),l)return a.createDocumentFragment();c=c||f(a);for(var e=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)e.createElement(h[g]);return e}function i(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return t.shivMethods?g(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/[\w\-:]+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(t,b.frag)}function j(a){a||(a=b);var d=f(a);return!t.shivCSS||k||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),l||i(a,d),a}var k,l,m="3.7.3",n=a.html5||{},o=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,p=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,q="_html5shiv",r=0,s={};!function(){try{var a=b.createElement("a");a.innerHTML="<xyz></xyz>",k="hidden"in a,l=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){k=!0,l=!0}}();var t={elements:n.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:n.shivCSS!==!1,supportsUnknownElements:l,shivMethods:n.shivMethods!==!1,type:"default",shivDocument:j,createElement:g,createDocumentFragment:h,addElements:e};a.html5=t,j(b),"object"==typeof module&&module.exports&&(module.exports=t)}("undefined"!=typeof window?window:this,document);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,8 +0,0 @@
/*
* Assign 'docutils' class to tables so styling and
* JavaScript behavior is applied.
*
* https://github.com/mkdocs/mkdocs/issues/2028
*/
$('div.rst-content table').addClass('docutils');

20
mkdocs.yml Normal file
View file

@ -0,0 +1,20 @@
site_name: RepApp - Makespace Repair-Café Hilpoltstein
site_url: https://makes-hacks-hip.github.io/RepApp/
nav:
- RepApp:
- Beschreibung: 'index.md'
- 'Developer documentation':
- Overview: 'dev/index.md'
- Users: 'dev/users.md'
- Stories: 'dev/stories.md'
- 'Use cases': 'dev/use_cases.md'
- Apps: 'dev/apps.md'
- Objects: 'dev/objects.md'
- 'Repapp configuration': 'dev/configuration.md'
- 'Repair-Café & Makespace':
- Repair-Café: https://www.repaircafe-hilpoltstein.de
- Makespace: https://makes-hacks-hip.de
- Makerspace@Github: https://makes-hacks-hip.github.io
theme: readthedocs
repo_url: https://github.com/makes-hacks-hip/RepApp/
edit_uri: edit/main/docs/

18
nginx.default Normal file
View file

@ -0,0 +1,18 @@
server {
listen 8020;
server_name example.org;
client_max_body_size 20M;
location / {
proxy_pass http://127.0.0.1:8010;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /static {
root /opt/app/rc_hip;
}
location /media {
root /opt/app/rc_hip;
}
}

22
rc_hip/manage.py Executable file
View file

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'rc_hip.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

View file

16
rc_hip/rc_hip/asgi.py Normal file
View file

@ -0,0 +1,16 @@
"""
ASGI config for rc_hip project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'rc_hip.settings')
application = get_asgi_application()

208
rc_hip/rc_hip/settings.py Normal file
View file

@ -0,0 +1,208 @@
"""
Django settings for rc_hip project.
Generated by "django-admin startproject" using Django 4.2.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
import mimetypes
import os
from pathlib import Path
mimetypes.add_type("text/css", ".css", True)
# Build paths inside the project like this: BASE_DIR / "subdir".
BASE_DIR = Path(__file__).resolve().parent.parent
STATIC_ROOT = os.path.join(BASE_DIR, "static")
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
# SECRET_KEY = "django-insecure-ura&_-(r(&&b$0s!a=07c@k6c!8=8m7$5(4#w8m@79-e%rn6s*"
SECRET_KEY = os.getenv(
"DJANGO_SECRET_KEY", "django-insecure-ura&_-(r(&&b$0s!a=07c@k6c!8=8m7$5(4#w8m@79-e%rn6s*")
# SECURITY WARNING: don"t run with debug turned on in production!
DEBUG = os.getenv("DJANGO_DEBUG", "true").lower() in ("true", "1", "t")
ALLOWED_HOSTS = [
"127.0.0.1",
"localhost",
"repapp.rc-hip.de",
"anmeldung.repaircafe-hilpoltstein.de",
]
# Application definition
INSTALLED_APPS = [
"repapp.apps.RepappConfig",
"django.contrib.admin",
"django.contrib.auth",
"mozilla_django_oidc",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"crispy_forms",
"crispy_bootstrap5",
"import_export",
"ckeditor",
"ckeditor_uploader",
]
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
CRISPY_TEMPLATE_PACK = "bootstrap5"
CKEDITOR_UPLOAD_PATH = "uploads/"
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "rc_hip.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "rc_hip.wsgi.application"
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "data" / "db.sqlite3",
}
}
# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = "de"
TIME_ZONE = "Europe/Berlin"
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = "static/"
MEDIA_URL = "media/"
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
CSRF_TRUSTED_ORIGINS = [
"http://127.0.0.1:8020",
"http://localhost:8020",
"http://127.0.0.1:8000",
"http://localhost:8000",
"https://repapp.rc-hip.de",
"https://anmeldung.repaircafe-hilpoltstein.de",
]
EMAIL_HOST = os.getenv("DJANGO_EMAIL_HOST", "")
EMAIL_PORT = (int)(os.getenv("DJANGO_EMAIL_PORT", "25"))
EMAIL_HOST_USER = os.getenv("DJANGO_EMAIL_HOST_USER", None)
EMAIL_HOST_PASSWORD = os.getenv("DJANGO_EMAIL_HOST_PASSWORD", None)
EMAIL_USE_TLS = os.getenv("DJANGO_EMAIL_USE_TLS", "true") in ("true", "1", "t")
AUTH_USER_MODEL = "repapp.CustomUser"
AUTHENTICATION_BACKENDS = [
"repapp.backends.EmailBackend",
"django.contrib.auth.backends.ModelBackend",
"repapp.backends.KeycloakOIDCAB",
"repapp.backends.OneTimeLoginBackend",
]
LOGIN_REDIRECT_URL = "/guest/"
OIDC_RP_CLIENT_ID = "repapp"
OIDC_RP_CLIENT_SECRET = os.getenv("DJANGO_OIDC_RP_CLIENT_SECRET", None)
OIDC_RP_SIGN_ALGO = "RS256"
OIDC_OP_JWKS_ENDPOINT = "https://sso.makes-hacks-hip.de/realms/Makes-Hacks-Hip/protocol/openid-connect/certs"
OIDC_OP_AUTHORIZATION_ENDPOINT = "https://sso.makes-hacks-hip.de/realms/Makes-Hacks-Hip/protocol/openid-connect/auth"
OIDC_OP_TOKEN_ENDPOINT = "https://sso.makes-hacks-hip.de/realms/Makes-Hacks-Hip/protocol/openid-connect/token"
OIDC_OP_USER_ENDPOINT = "https://sso.makes-hacks-hip.de/realms/Makes-Hacks-Hip/protocol/openid-connect/userinfo"
if DEBUG:
LOGIN_REDIRECT_URL = "http://127.0.0.1:8000/member"
LOGOUT_REDIRECT_URL = "http://127.0.0.1:8000/"
else:
LOGIN_REDIRECT_URL = "https://anmeldung.repaircafe-hilpoltstein.de/member"
LOGOUT_REDIRECT_URL = "https://anmeldung.repaircafe-hilpoltstein.de/"
LOG_LEVEL = "INFO"
if DEBUG:
LOG_LEVEL = "DEBUG"
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {
"class": "logging.StreamHandler",
},
},
"root": {
"handlers": ["console"],
"level": LOG_LEVEL,
},
}

30
rc_hip/rc_hip/urls.py Normal file
View file

@ -0,0 +1,30 @@
"""
URL configuration for rc_hip project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path("", include("repapp.urls")),
path('oidc/', include('mozilla_django_oidc.urls')),
# user management for guests not needed, use one-time-login only
# path('accounts/', include('django.contrib.auth.urls')),
path('ckeditor/', include('ckeditor_uploader.urls')),
path('admin/', admin.site.urls),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

16
rc_hip/rc_hip/wsgi.py Normal file
View file

@ -0,0 +1,16 @@
"""
WSGI config for rc_hip project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'rc_hip.settings')
application = get_wsgi_application()

View file

127
rc_hip/repapp/admin.py Normal file
View file

@ -0,0 +1,127 @@
"""
Admin interface configuration for RepApp.
"""
from django.contrib import admin
from import_export.admin import ImportExportModelAdmin, ExportActionMixin
from import_export import resources
from .models import (Organisator, Cafe, Question, Device, CustomUser,
Reparateur, Appointment, Guest, Candidate, OneTimeLogin, Message)
class CafeResource(resources.ModelResource):
"""
Resource wrapper for Cafe model.
"""
class Meta:
"""
Meta data for resource model.
"""
model = Cafe
@staticmethod
def get_display_name():
return "Repair-Café"
class GuestResource(resources.ModelResource):
"""
Resource wrapper for Guest model.
"""
class Meta:
"""
Meta data for resource model.
"""
model = Guest
@staticmethod
def get_display_name():
return "Gäste"
class DeviceResource(resources.ModelResource):
"""
Resource wrapper for Device model.
"""
class Meta:
"""
Meta data for resource model.
"""
model = Device
@staticmethod
def get_display_name():
return "Geräte"
class QuestionResource(resources.ModelResource):
"""
Resource wrapper for Question model.
"""
class Meta:
"""
Meta data for resource model.
"""
model = Question
@staticmethod
def get_display_name():
return "Fragen"
class AppointmentResource(resources.ModelResource):
"""
Resource wrapper for Appointment model.
"""
class Meta:
"""
Meta data for resource model.
"""
model = Appointment
@staticmethod
def get_display_name():
return "Termine"
class CandidateResource(resources.ModelResource):
"""
Resource wrapper for Candidate model.
"""
class Meta:
"""
Meta data for resource model.
"""
model = Candidate
@staticmethod
def get_display_name():
return "Kandidaten"
class ExportAdmin(ImportExportModelAdmin, ExportActionMixin):
"""
Admin model for data export, supporting buttons and action.
"""
resource_classes = [
DeviceResource,
GuestResource,
QuestionResource,
AppointmentResource,
CandidateResource,
CafeResource,
]
# Register all models of RepApp for the admin interface.
admin.site.register(Organisator)
admin.site.register(Reparateur)
admin.site.register(CustomUser)
admin.site.register(OneTimeLogin)
admin.site.register(Message)
# Make the "data" models exportable.
admin.site.register(Cafe, ExportAdmin)
admin.site.register(Question, ExportAdmin)
admin.site.register(Guest, ExportAdmin)
admin.site.register(Device, ExportAdmin)
admin.site.register(Appointment, ExportAdmin)
admin.site.register(Candidate, ExportAdmin)

12
rc_hip/repapp/apps.py Normal file
View file

@ -0,0 +1,12 @@
"""
RepApp application.
"""
from django.apps import AppConfig
class RepappConfig(AppConfig):
"""
Django config for RepApp application.
"""
default_auto_field = 'django.db.models.BigAutoField'
name = 'repapp'

120
rc_hip/repapp/backends.py Normal file
View file

@ -0,0 +1,120 @@
"""
Authentication backends for RepApp.
"""
import unicodedata
import logging
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from mozilla_django_oidc.auth import OIDCAuthenticationBackend
from .models import OneTimeLogin, Organisator, Reparateur
logger = logging.getLogger(__name__)
def generate_username(email):
"""
Generate a valid user name form the mail address.
"""
# Using Python 3 and Django 1.11+, usernames can contain alphanumeric
# (ascii and unicode), _, @, +, . and - characters. So we normalize
# it and slice at 150 characters.
return unicodedata.normalize('NFKC', email)[:150]
def create_repapp_user(user):
"""
Create the RepApp user objects for the new user.
If a user logs in using OIDC it is a member of the Repair-Café, so it can be
either a Reparateur or an Organisator. Organisators have access to the private
data form the guests and must be nominated by an admin.
"""
organisator = Organisator.objects.filter(mail=user.email).first()
if not organisator:
# no organisator, create or update reparateur
reparateur = Reparateur.objects.filter(mail=user.email).first()
if not reparateur:
# create new reparateur
reparateur = Reparateur(
name=user.username,
mail=user.email,
)
reparateur.save()
else:
# update name of existing reparateur
reparateur.name = user.username
reparateur.save()
else:
# update name of organisator
organisator.name = user.username
organisator.save()
class OneTimeLoginBackend(ModelBackend):
"""
OneTimeLoginBackend implements a login using a onetime secret.
"""
def authenticate(self, request, username=None, password=None, **kwargs):
login = OneTimeLogin.objects.filter(secret=username).first()
if login:
return login.user
else:
return None
class EmailBackend(ModelBackend):
"""
EmailBackend allows a login using email address as username.
"""
def authenticate(self, request, username=None, password=None, **kwargs):
user_model = get_user_model()
try:
user = user_model.objects.get(email=username)
except user_model.DoesNotExist:
return None
else:
if user.check_password(password):
return user
return None
class KeycloakOIDCAB(OIDCAuthenticationBackend):
"""
KeycloakOIDCAB allows a login using Open ID Connect
(with the Repair-Café Keycloak Single Sign On server)
"""
def create_user(self, claims):
user = super(KeycloakOIDCAB, self).create_user(claims)
logger.info('Create user %s' % user.email)
fallback_name = generate_username(user.email)
try:
user.username = claims.get('preferred_username', fallback_name)
user.save()
except Exception as exception:
logger.error(exception)
logger.warning(
'Update username failed! Using fallback name %s.' % fallback_name)
user.username = fallback_name
user.save()
logger.debug(f'Updated username {user.username}')
create_repapp_user(user)
return user
def update_user(self, user, claims):
logger.debug(f'Update user {user.email} ({user.username})')
try:
user.username = claims.get(
'preferred_username', generate_username(user.email))
user.save()
except Exception as exception:
logger.error(exception)
create_repapp_user(user)
return user

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 933 KiB

View file

@ -0,0 +1,58 @@
[
{
"model": "repapp.cafe",
"pk": 1,
"fields": {
"location": "Haus Einstein' (beim Gymnasium)",
"address": "Patersholzer Weg 19, Hilpoltstein",
"event_date": "2024-05-13"
}
},
{
"model": "repapp.customuser",
"pk": 3,
"fields": {
"password": "pbkdf2_sha256$600000$O70jrsL2vmSssirJPvWBMw$lbvFy/OI7mUfz26Qvh+bxnsp19v+xsETJydvsgLxE4E=",
"last_login": "2023-04-25T18:47:07.685Z",
"is_superuser": false,
"username": "tom",
"first_name": "",
"last_name": "",
"is_staff": false,
"is_active": true,
"date_joined": "2023-04-25T07:10:40.972Z",
"email": "thomas@irgang.eu",
"groups": [],
"user_permissions": []
}
},
{
"model": "repapp.guest",
"pk": 4,
"fields": {
"identifier": "272623d8b634cef82ba1c8981593cd827a373226c492b328671d31418a19bcc1",
"name": "Thomas",
"phone": "012345678",
"residence": "Allersberg",
"mail": "thomas@irgang.eu",
"user": 3
}
},
{
"model": "repapp.device",
"pk": 2,
"fields": {
"identifier": "5f3d413409ca9c90c99540fd5677cc1147156f100d7bd29d3a1c1940e5f2c6ac",
"date": "2023-04-25",
"device": "Test Gerät",
"manufacturer": "Test Hersteller",
"error": "Fehler",
"follow_up": false,
"device_picture": "",
"type_plate_picture": "",
"confirmed": true,
"guest": 4,
"cafe": 1
}
}
]

142
rc_hip/repapp/forms.py Normal file
View file

@ -0,0 +1,142 @@
"""
Django forms for RepApp.
"""
from django import forms
from django.core.exceptions import ValidationError
from ckeditor.widgets import CKEditorWidget
from .models import Question, Device
EMPTY_VALUES = (None, '')
class HoneypotWidget(forms.TextInput):
"""
This widget shows a by CSS hidden input field. If this form gets manipulated
by a bot the form is rejected.
"""
is_hidden = True
def __init__(self, *args, attrs=None, html_comment=False, **kwargs):
self.html_comment = html_comment
super(HoneypotWidget, self).__init__(attrs, *args, **kwargs)
if 'class' not in self.attrs:
self.attrs['style'] = 'display:none'
def render(self, *args, **kwargs):
value = super(HoneypotWidget, self).render(*args, **kwargs)
if self.html_comment:
value = f'<!-- {value} -->'
return value
class HoneypotField(forms.Field):
"""
This widget implements a simple honey pot field as spam protection.
"""
widget = HoneypotWidget
def clean(self, value):
if self.initial in EMPTY_VALUES and value in EMPTY_VALUES or value == self.initial:
return value
raise ValidationError('Anti-spam field changed in value.')
class RegisterDevice(forms.Form):
"""
Form for registering a device for a repair cafe.
"""
mail = forms.EmailField(
label="eMail Adresse",
help_text="Diese eMail-Adresse wird für Aktualisierungen"
" zur Anfrage verwendet.",
max_length=200)
device = forms.CharField(
label="Art des Geräts",
help_text="z.B. Küchenmaschine, Smartphone, Stehlampe, Drucker, ...",
max_length=200
)
manufacturer = forms.CharField(
label="Hersteller & Modell/Typ",
max_length=200
)
error = forms.CharField(
label="Fehlerbeschreibung",
help_text="Beschreiben Sie hier das Problem ihres Gerätes.",
widget=forms.Textarea
)
device_picture = forms.FileField(
label="Foto vom Gerät", required=False
)
type_plate_picture = forms.FileField(
label="Foto vom Typenschild", required=False
)
follow_up = forms.BooleanField(
label="Folgetermin",
help_text="Kreuzen Sie diese Kästchen an wenn sie mit diesem Gerät "
"bereits bei einem Repair-Café Termin waren.",
required=False
)
confirm_repair = forms.BooleanField(
label="Informationen zur Reparaturabwicklung",
help_text="Ich bin mit den <a href="
"\"https://www.repaircafe-hilpoltstein.de/fileadmin/repaircafe-hilpoltstein/images/"
"repaircafe-hilpoltstein.de/docs/Infos_Reparatur-Abwicklung_v2_2023-02.pdf\">"
"Allgemeinen Bedingungen zur Reparaturabwicklung</a> einverstanden.",
required=True
)
confirm_data = forms.BooleanField(
label="Datenschutz",
help_text=" Ich bin mit der Erhebung, Verarbeitung und Speicherung meiner Daten für "
"die Bearbeitung meiner Anfrage gemäß der "
"<a href=\"https://www.repaircafe-hilpoltstein.de/impressum#c1375\">"
"Datenschutzerklärung</a> einverstanden.",
required=True
)
accept_agb = HoneypotField(label="")
class RegisterGuest(forms.Form):
"""
Form for registering new guests.
"""
name = forms.CharField(
label="Name",
max_length=200
)
phone = forms.CharField(
label="Telefonnummer",
max_length=200,
help_text="Diese Telefonnummer wird für Rückfragen verwendet."
)
residence = forms.CharField(
label="Wohnort",
max_length=200
)
accept_agb = HoneypotField(label="")
class Mail(forms.Form):
"""
Form for rejecting a device.
"""
message = forms.CharField(widget=CKEditorWidget(), label="")
class UpdateDeviceForm(forms.ModelForm):
class Meta:
model = Question
fields = ['question']
question = forms.CharField(widget=CKEditorWidget(
attrs={'style': 'width:100%;'}), label="Frage")
class MemberSettings(forms.Form):
"""
Form for editing member settings.
"""
notifications = forms.BooleanField(
label="Benachrichtigungen",
help_text="Kreuzen Sie diese Kästchen an wenn sie Benachrichtigungen erhalten möchten.",
required=False
)

203
rc_hip/repapp/live_tests.py Normal file
View file

@ -0,0 +1,203 @@
import time
import os
import datetime
import random
from pathlib import Path
from hashlib import sha256
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.webdriver import WebDriver
from selenium.webdriver import ActionChains, Keys
from .models import CustomUser, OneTimeLogin
class WorkflowTests(StaticLiveServerTestCase):
"""
Tests for workflows.
"""
fixtures = ["cafe-data.json"]
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.selenium = WebDriver()
cls.selenium.implicitly_wait(50)
cls.selenium.maximize_window()
@classmethod
def tearDownClass(cls):
cls.selenium.quit()
super().tearDownClass()
def setUp(self):
super().setUp()
self.action_chains = ActionChains(self.selenium)
self.password = "ATestPassword"
user = CustomUser.objects.get(pk=3)
user.set_password(self.password)
user.save()
self.user = user
print(f'Password of user {user.email} updated to {self.password}.')
url = "/guest/profile/"
secret = sha256(
f'{user.email}{url}{datetime.datetime.now()}{random.randint(0,9999999)}'.encode(
'utf-8')
).hexdigest()
self.secret = secret
secret_hash = sha256(secret.encode('utf-8')).hexdigest()
one_time_login = OneTimeLogin(
secret=secret_hash,
user=user,
url=url,
)
one_time_login.save()
self.one_time_login = one_time_login
def test_register_device(self):
"""
Test device registration flows.
"""
# Open landing page
self.selenium.get(f"{self.live_server_url}/")
# Click on a register link
self.selenium.find_element(By.CLASS_NAME, "register_link").click()
time.sleep(2)
# Enter new device
# Enter guest mail address
mail = self.selenium.find_element(By.NAME, "mail")
mail.send_keys("guest@example.com")
# Enter device
device = self.selenium.find_element(By.NAME, "device")
device.send_keys("A Test Device")
# Enter manufacturer
manufacturer = self.selenium.find_element(By.NAME, "manufacturer")
manufacturer.send_keys("A Test Manufacturer")
# Enter a error description
error = self.selenium.find_element(By.NAME, "error")
error.send_keys("A description of the issue\nof the device.")
# Scroll to the bottom of the page
self.selenium.find_element(
By.TAG_NAME, "body").send_keys(Keys.CONTROL, Keys.END)
time.sleep(1)
# Skip device picture and device type plate
# No follow up repair
# Accept repair conditions
self.selenium.find_element(By.NAME, "confirm_repair").click()
# Accept data protection notice
self.selenium.find_element(By.NAME, "confirm_data").click()
time.sleep(1)
# Send form
self.selenium.find_element(By.CLASS_NAME, 'submit_button').click()
time.sleep(1)
# Register new guest
# Enter guest name
name = self.selenium.find_element(By.NAME, "name")
name.send_keys("A Guest")
# Enter guest phone
phone = self.selenium.find_element(By.NAME, "phone")
phone.send_keys("0911 123 456")
# Enter guest residence
residence = self.selenium.find_element(By.NAME, "residence")
residence.send_keys("Musterstadt")
time.sleep(1)
# Send form
self.selenium.find_element(By.CLASS_NAME, 'submit_button').click()
time.sleep(1)
self.assertTrue("confirm" in self.selenium.current_url)
self.assertTrue("Anmeldung erfolgreich!" in self.selenium.page_source)
# Register second device
# Open landing page again
self.selenium.get(f"{self.live_server_url}/")
# Click on a register link
self.selenium.find_element(By.CLASS_NAME, "register_link").click()
time.sleep(1)
# Enter new device
# Enter guest mail address
mail = self.selenium.find_element(By.NAME, "mail")
mail.send_keys("guest@example.com")
# Enter device
device = self.selenium.find_element(By.NAME, "device")
device.send_keys("Another Test Device")
# Enter manufacturer
manufacturer = self.selenium.find_element(By.NAME, "manufacturer")
manufacturer.send_keys("Another Test Manufacturer")
# Enter a error description
error = self.selenium.find_element(By.NAME, "error")
error.send_keys("Another issue description.")
# Scroll to the bottom of the page
self.selenium.find_element(
By.TAG_NAME, "body").send_keys(Keys.CONTROL, Keys.END)
time.sleep(1)
# Add device picture
test_script_folder = Path(__file__).resolve().parent
fixtures_folder = os.path.join(test_script_folder, "fixtures")
device_picture_file = os.path.join(fixtures_folder, "Hummingbird.jpg")
device_picture = self.selenium.find_element(By.NAME, "device_picture")
device_picture.send_keys(device_picture_file)
print(f"Using device picture {device_picture_file}")
# Add type plate picture
type_plate_picture_file = os.path.join(fixtures_folder, "Nut.jpg")
type_plate_picture = self.selenium.find_element(
By.NAME, "type_plate_picture")
type_plate_picture.send_keys(type_plate_picture_file)
print(f"Using type plate picture {type_plate_picture_file}")
# Select as follow up repair
self.selenium.find_element(By.NAME, "follow_up").click()
# Accept repair conditions
self.selenium.find_element(By.NAME, "confirm_repair").click()
# Accept data protection notice
self.selenium.find_element(By.NAME, "confirm_data").click()
time.sleep(1)
# Send form
self.selenium.find_element(By.CLASS_NAME, 'submit_button').click()
time.sleep(1)
# Guest is known, no guest registration needed.
self.assertTrue("confirm" in self.selenium.current_url)
self.assertTrue("Anmeldung erfolgreich!" in self.selenium.page_source)
def test_login_and_view_profile(self):
"""
Test login and profile page.
"""
# Open login form
self.selenium.get(f"{self.live_server_url}/accounts/login/")
# Enter login data
# Enter guest mail address
username = self.selenium.find_element(By.NAME, "username")
username.send_keys(self.user.email)
# Enter password
password = self.selenium.find_element(By.NAME, "password")
password.send_keys(self.password)
time.sleep(1)
# Submit form
self.selenium.find_element(
By.XPATH, '//button[@type="submit"]').click()
time.sleep(1)
self.selenium.get(f"{self.live_server_url}/guest/profile/")
time.sleep(1)
self.assertTrue(self.user.email in self.selenium.page_source)
self.assertTrue("/guest/profile/" in self.selenium.current_url)
time.sleep(1)
def test_one_time_login_and_view_device(self):
"""
Test one time login and device page.
"""
# Open login form
self.selenium.get(
f"{self.live_server_url}/onetimelogin/{self.secret}/")
time.sleep(1)
self.assertTrue(self.user.email in self.selenium.page_source)
self.assertTrue("Login erfolgreich" in self.selenium.page_source)
self.assertTrue("/guest/profile/" in self.selenium.current_url)
time.sleep(1)
self.selenium.get(
f"{self.live_server_url}/device/5f3d413409ca9c90c99540fd5677cc1147156f100d7bd29d3a1c1940e5f2c6ac/")
time.sleep(1)
self.assertTrue("Test Gerät" in self.selenium.page_source)
self.assertTrue("Test Hersteller" in self.selenium.page_source)
self.assertTrue("/device/" in self.selenium.current_url)
time.sleep(1)

View file

@ -0,0 +1,101 @@
import os
import re
import logging
from imap_tools import MailBox, AND, MailMessage
from django.core.mail import send_mail
from .models import Guest, Message, Question, Organisator
logger = logging.getLogger(__name__)
def send_message_notification(message: Message, guest: Guest):
"""
Send new message notification.
"""
organizers = []
for organizer in Organisator.objects.all():
organizers.append(organizer.mail)
subject = f'Neue Nachricht von Gast {guest.name} ({guest.mail})'
text = f'Hallo,\n'
f'der Gast {guest.name} ({guest.mail}) hat folgende Nachricht gesendet:\n\n'
f'{message.message}'
# TODO: HTML message
# TODO: add attachments
# TODO: add link to message
send_mail(
subject=subject,
message=text,
from_email=os.getenv("DJANGO_SENDER_ADDRESS", ""),
recipient_list=organizers,
fail_silently=True
)
def process_message(message: MailMessage, guest: Guest):
"""
Process a valid message from a guest.
"""
content = message.text
if message.html:
content = message.html
result = re.search('Q#([0-9]+):', message.subject)
if result:
question_pk = result.group(1)
question = Question.objects.filter(pk=question_pk).first()
if question:
if question.device.guest.mail == message.from_:
question.answer = content
question.answered = True
question.save()
# TODO: save attachments and add to message
logger.info(
'Valid answer for Question %s from %s received.'
% (question_pk, message.from_))
# TODO: send notifications
else:
logger.warning(
'Answer for Question %s from %s received,'
' but the question was not for this guest. Message was ignored.'
% (question_pk, message.from_))
# TODO: send reply that mail address was wrong
else:
logger.warning(
'Answer for Question %s from %s received,'
' but there is no such question. Message was ignored.'
% (question_pk, message.from_))
else:
db_message = Message(
message=content,
guest=guest,
)
db_message.save()
send_message_notification(message, guest)
def process_mails():
"""
Check for new messages and handle valid messages.
"""
host = os.getenv("DJANGO_EMAIL_HOST", "")
user = os.getenv("DJANGO_EMAIL_HOST_USER", None)
password = os.getenv("DJANGO_EMAIL_HOST_PASSWORD", None)
mailbox = MailBox(host).login(user, password)
messages = mailbox.fetch(criteria=AND(seen=False),
mark_seen=True, bulk=True)
message_count = 0
for message in messages:
message_count += 1
sender = message.from_
guest = Guest.objects.filter(mail=sender).first()
if guest:
process_message(message, guest)
else:
logger.warning(
'Mail form unknown sender %s with subject %s ignored.'
% (sender, message.subject))
return message_count

View file

@ -0,0 +1,183 @@
# Generated by Django 4.2 on 2023-04-24 15:16
from django.conf import settings
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import repapp.models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='CustomUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('email', models.EmailField(max_length=254, unique=True, verbose_name='eMail Adresse')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'Benutzer',
'verbose_name_plural': 'Benutzer',
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='Cafe',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('location', models.CharField(max_length=200, verbose_name='Ort')),
('address', models.CharField(max_length=200, verbose_name='Adresse')),
('event_date', models.DateField(verbose_name='Datum')),
],
options={
'verbose_name': 'Repair-Café',
'verbose_name_plural': 'Repair-Cafés',
},
),
migrations.CreateModel(
name='Device',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('identifier', models.CharField(max_length=200, verbose_name='ID')),
('device', models.CharField(max_length=200, verbose_name='Art des Geräts')),
('manufacturer', models.CharField(max_length=200, verbose_name='Hersteller & Modell/Typ')),
('error', models.TextField(verbose_name='Fehlerbeschreibung')),
('follow_up', models.BooleanField(verbose_name='Folgetermin')),
('device_picture', models.FileField(null=True, upload_to=repapp.models.device_directory_path, verbose_name='Foto vom Gerät')),
('type_plate_picture', models.FileField(null=True, upload_to=repapp.models.device_directory_path, verbose_name='Foto vom Typenschild')),
('confirmed', models.BooleanField(verbose_name='Bestätigung gesendet?')),
('date', models.DateField(default=django.utils.timezone.now, verbose_name='Erstellungsdatum')),
('cafe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.cafe', verbose_name='Repair-Café')),
],
options={
'verbose_name': 'Gerät',
'verbose_name_plural': 'Geräte',
},
),
migrations.CreateModel(
name='Guest',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('identifier', models.CharField(max_length=200, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='Name')),
('phone', models.CharField(max_length=200, verbose_name='Telefonnummer')),
('residence', models.CharField(max_length=200, verbose_name='Wohnort')),
('mail', models.CharField(max_length=200, verbose_name='eMail')),
],
options={
'verbose_name': 'Gast',
'verbose_name_plural': 'Gäste',
},
),
migrations.CreateModel(
name='Organisator',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='Name')),
('mail', models.CharField(max_length=200, verbose_name='eMail')),
],
options={
'verbose_name': 'Organisator',
'verbose_name_plural': 'Organisatoren',
},
),
migrations.CreateModel(
name='Reparateur',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='Name')),
('mail', models.CharField(max_length=200, verbose_name='eMail')),
],
options={
'verbose_name': 'Reparateur',
'verbose_name_plural': 'Reparateure',
},
),
migrations.CreateModel(
name='Question',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('question', models.TextField(verbose_name='Frage')),
('answer', models.TextField(verbose_name='Antwort')),
('date', models.DateField(default=django.utils.timezone.now, verbose_name='Erstellungsdatum')),
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.device', verbose_name='Gerät')),
('organisator', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.organisator', verbose_name='Organisator')),
('reparateur', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.reparateur', verbose_name='Reparateur')),
],
options={
'verbose_name': 'Frage',
'verbose_name_plural': 'Fragen',
},
),
migrations.CreateModel(
name='OneTimeLogin',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('secret', models.CharField(max_length=200, unique=True, verbose_name='secret')),
('url', models.CharField(max_length=200, verbose_name='URL')),
('created', models.DateField(default=django.utils.timezone.now, verbose_name='Erstellungsdatum')),
('login_used', models.BooleanField(default=False, verbose_name='Login benutzt?')),
('login_date', models.DateField(null=True, verbose_name='Login Datum')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Benutzer')),
],
options={
'verbose_name': 'Einmal-Login',
'verbose_name_plural': 'Einmal-Login',
},
),
migrations.AddField(
model_name='device',
name='guest',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.guest', verbose_name='Gast'),
),
migrations.CreateModel(
name='Candidate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('confirmed', models.BooleanField(verbose_name='bestätigt')),
('cafe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.cafe', verbose_name='Repair-Café')),
('device', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.device', verbose_name='Gerät')),
],
options={
'verbose_name': 'Kandidat',
'verbose_name_plural': 'Kandidaten',
},
),
migrations.CreateModel(
name='Appointment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('time', models.TimeField(verbose_name='Zeit')),
('confirmed', models.BooleanField(verbose_name='bestätigt')),
('cafe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.cafe', verbose_name='Repair-Café')),
('device', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.device', verbose_name='Gerät')),
('reparateur', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.reparateur', verbose_name='Reparateur')),
],
options={
'verbose_name': 'Termin',
'verbose_name_plural': 'Termine',
},
),
]

View file

@ -0,0 +1,29 @@
# Generated by Django 4.2 on 2023-04-24 17:25
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('repapp', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Message',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('message', models.TextField(verbose_name='Nachricht')),
('answer', models.TextField(verbose_name='Antwort')),
('date', models.DateField(default=django.utils.timezone.now, verbose_name='Erstellungsdatum')),
('guest', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.guest', verbose_name='Gast')),
],
options={
'verbose_name': 'Nachricht',
'verbose_name_plural': 'Nachrichten',
},
),
]

View file

@ -0,0 +1,20 @@
# Generated by Django 4.2 on 2023-04-25 07:05
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('repapp', '0002_message'),
]
operations = [
migrations.AddField(
model_name='guest',
name='user',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Benutzer'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 4.2 on 2023-04-25 12:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('repapp', '0003_guest_user'),
]
operations = [
migrations.AlterField(
model_name='onetimelogin',
name='secret',
field=models.CharField(max_length=200, unique=True, verbose_name='Geheimnis'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 4.2 on 2023-05-29 10:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('repapp', '0004_alter_onetimelogin_secret'),
]
operations = [
migrations.AddField(
model_name='device',
name='status',
field=models.IntegerField(default=0, verbose_name='Status'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 4.2 on 2023-05-29 12:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('repapp', '0005_device_status'),
]
operations = [
migrations.AlterField(
model_name='question',
name='answer',
field=models.TextField(null=True, verbose_name='Antwort'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 4.2 on 2023-05-29 12:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('repapp', '0006_alter_question_answer'),
]
operations = [
migrations.AddField(
model_name='question',
name='open',
field=models.BooleanField(default=True, verbose_name='offen'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 4.2 on 2023-05-29 14:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('repapp', '0007_question_open'),
]
operations = [
migrations.AddField(
model_name='question',
name='sent',
field=models.BooleanField(default=False, verbose_name='gesendet'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 4.2 on 2023-05-29 14:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('repapp', '0008_question_sent'),
]
operations = [
migrations.AddField(
model_name='question',
name='answered',
field=models.BooleanField(default=False, verbose_name='beantwortet'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 4.2 on 2023-06-05 16:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('repapp', '0009_question_answered'),
]
operations = [
migrations.AddField(
model_name='customuser',
name='notifications',
field=models.BooleanField(default=True, verbose_name='Benachrichtigungen'),
),
]

View file

@ -0,0 +1,24 @@
# Generated by Django 4.2 on 2023-06-18 07:46
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('repapp', '0010_customuser_notifications'),
]
operations = [
migrations.AddField(
model_name='reparateur',
name='cafe',
field=models.ManyToManyField(to='repapp.cafe'),
),
migrations.AlterField(
model_name='device',
name='guest',
field=models.ForeignKey(choices=[(-1, 'abgelehnt'), (0, 'neu'), (1, 'in Orga Rückfrage'), (2, 'in Orga Rückfrage beantwortet'), (3, 'in der Warteliste')], null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.guest', verbose_name='Gast'),
),
]

View file

@ -0,0 +1,22 @@
# Generated by Django 4.2 on 2023-06-18 13:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('repapp', '0011_reparateur_cafe_alter_device_guest'),
]
operations = [
migrations.RemoveField(
model_name='reparateur',
name='cafe',
),
migrations.AddField(
model_name='cafe',
name='reparateur',
field=models.ManyToManyField(to='repapp.reparateur'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 4.2 on 2023-06-18 14:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('repapp', '0012_remove_reparateur_cafe_cafe_reparateur'),
]
operations = [
migrations.AddField(
model_name='device',
name='reparateur',
field=models.ManyToManyField(to='repapp.reparateur'),
),
]

View file

@ -0,0 +1,29 @@
# Generated by Django 4.2 on 2023-06-18 15:51
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('repapp', '0013_device_reparateur'),
]
operations = [
migrations.AlterField(
model_name='cafe',
name='reparateur',
field=models.ManyToManyField(null=True, to='repapp.reparateur'),
),
migrations.AlterField(
model_name='device',
name='guest',
field=models.ForeignKey(choices=[(-1, 'abgelehnt'), (0, 'neu'), (1, 'in Orga Rückfrage'), (2, 'in Orga Rückfrage beantwortet'), (3, 'in der Warteliste'), (4, 'in Repa Rückfrage'), (5, 'in Repa Rückfrage beantwortet')], null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.guest', verbose_name='Gast'),
),
migrations.AlterField(
model_name='device',
name='reparateur',
field=models.ManyToManyField(null=True, to='repapp.reparateur'),
),
]

View file

259
rc_hip/repapp/models.py Normal file
View file

@ -0,0 +1,259 @@
"""
This module implements the models of RepApp.
"""
import django.utils.timezone
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import AbstractUser
def device_directory_path(instance, filename):
"""
device_directory_path generates a device-specific storage path for file uploads.
"""
return f'device_{instance.identifier}/{filename}'
class CustomUser(AbstractUser):
"""
Custom user object with unique email.
"""
email = models.EmailField(unique=True, verbose_name=_("eMail Adresse"))
notifications = models.BooleanField(
verbose_name=_("Benachrichtigungen"), default=True)
class Meta:
verbose_name = _('Benutzer')
verbose_name_plural = _('Benutzer')
def __str__(self):
return f'{self.email}'
class Reparateur(models.Model):
"""
A Reparateur is a member of the Repair-Café who supports guests with fixing their devices.
"""
name = models.CharField(max_length=200, verbose_name=_("Name"))
mail = models.CharField(max_length=200, verbose_name=_("eMail"))
class Meta:
verbose_name = _('Reparateur')
verbose_name_plural = _('Reparateure')
def __str__(self):
return f'Reparateur {self.name} (eMail: {self.mail})'
class Cafe(models.Model):
"""
A Cafe is a "Repair-Café event.
"""
location = models.CharField(
max_length=200, verbose_name=_("Ort"))
address = models.CharField(
max_length=200, verbose_name=_("Adresse"))
event_date = models.DateField(verbose_name=_("Datum"))
reparateur = models.ManyToManyField(Reparateur, null=True)
class Meta:
verbose_name = _('Repair-Café')
verbose_name_plural = _('Repair-Cafés')
def __str__(self):
return f'Repair-Café am {self.event_date} (Ort: {self.location})'
class Organisator(models.Model):
"""
A Organisator is a member of the Repair-Café who arranges repair appointments with guests.
"""
name = models.CharField(max_length=200, verbose_name=_("Name"))
mail = models.CharField(max_length=200, verbose_name=_("eMail"))
class Meta:
verbose_name = _('Organisator')
verbose_name_plural = _('Organisatoren')
def __str__(self):
return f'Organisator {self.name} (eMail: {self.mail})'
class Guest(models.Model):
"""
A guest is a owner of a broken device who wants support in context of a Repair-Café.
"""
identifier = models.CharField(max_length=200, verbose_name=_("ID"))
name = models.CharField(max_length=200, verbose_name=_("Name"))
phone = models.CharField(max_length=200, verbose_name=_("Telefonnummer"))
residence = models.CharField(max_length=200, verbose_name=_("Wohnort"))
mail = models.CharField(max_length=200, verbose_name=_("eMail"))
user = models.ForeignKey(
CustomUser, on_delete=models.CASCADE, null=True, verbose_name=_("Benutzer"))
class Meta:
verbose_name = _('Gast')
verbose_name_plural = _('Gäste')
def __str__(self):
return f'Gast {self.name} (eMail: {self.mail})'
class Device(models.Model):
"""
A Device is a broken device owned by a guest which shall be repaired during a Repair-Café.
"""
STATUS_REJECTED = -1
STATUS_NEW = 0
STATUS_ORGA_QUESTION = 1
STATUS_ORGA_QUESTION_ANSWERED = 2
STATUS_WAITING_LIST = 3
STATUS_REPA_QUESTION = 4
STATUS_REPA_QUESTION_ANSWERED = 5
STATUS = (
(STATUS_REJECTED, 'abgelehnt'),
(STATUS_NEW, 'neu'),
(STATUS_ORGA_QUESTION, 'in Orga Rückfrage'),
(STATUS_ORGA_QUESTION_ANSWERED, 'in Orga Rückfrage beantwortet'),
(STATUS_WAITING_LIST, 'in der Warteliste'),
(STATUS_REPA_QUESTION, 'in Repa Rückfrage'),
(STATUS_REPA_QUESTION_ANSWERED, 'in Repa Rückfrage beantwortet'),
)
identifier = models.CharField(max_length=200, verbose_name=_("ID"))
date = models.DateField(verbose_name=_(
"Erstellungsdatum"), default=django.utils.timezone.now)
device = models.CharField(max_length=200, verbose_name=_("Art des Geräts"))
manufacturer = models.CharField(
max_length=200, verbose_name=_("Hersteller & Modell/Typ"))
error = models.TextField(verbose_name=_("Fehlerbeschreibung"))
follow_up = models.BooleanField(verbose_name=_("Folgetermin"))
device_picture = models.FileField(
upload_to=device_directory_path, null=True, verbose_name=_("Foto vom Gerät"))
type_plate_picture = models.FileField(
upload_to=device_directory_path, null=True, verbose_name=_("Foto vom Typenschild"))
confirmed = models.BooleanField(verbose_name=_("Bestätigung gesendet?"))
status = models.IntegerField(verbose_name=_("Status"), default=0)
guest = models.ForeignKey(
Guest, on_delete=models.CASCADE, null=True, verbose_name=_("Gast"), choices=STATUS)
cafe = models.ForeignKey(
Cafe, on_delete=models.CASCADE, null=False, verbose_name=_("Repair-Café"))
reparateur = models.ManyToManyField(Reparateur, null=True)
class Meta:
verbose_name = _('Gerät')
verbose_name_plural = _('Geräte')
def __str__(self):
return f'Gerät {self.device} - {self.manufacturer}'
class Appointment(models.Model):
"""
A Appointment is a match of a broken Device and a Reparateur during a Repair-Café
with a defined time slot.
"""
time = models.TimeField(verbose_name=_("Zeit"))
confirmed = models.BooleanField(verbose_name=_("bestätigt"))
cafe = models.ForeignKey(
Cafe, on_delete=models.CASCADE, verbose_name=_("Repair-Café"))
reparateur = models.ForeignKey(
Reparateur, on_delete=models.CASCADE, verbose_name=_("Reparateur"))
device = models.ForeignKey(
Device, on_delete=models.CASCADE, null=True, verbose_name=_("Gerät"))
class Meta:
verbose_name = _('Termin')
verbose_name_plural = _('Termine')
def __str__(self):
return f'Termin {self.cafe.event_date} {self.time} für Gerät {self.device.device}'
class Question(models.Model):
"""
A Question is a request for information form a Organisator or a Reparateur for a Device.
"""
question = models.TextField(verbose_name=_("Frage"))
answer = models.TextField(verbose_name=_("Antwort"), null=True)
date = models.DateField(verbose_name=_(
"Erstellungsdatum"), default=django.utils.timezone.now)
organisator = models.ForeignKey(
Organisator, on_delete=models.CASCADE, null=True, verbose_name=_("Organisator"))
reparateur = models.ForeignKey(
Reparateur, on_delete=models.CASCADE, null=True, verbose_name=_("Reparateur"))
device = models.ForeignKey(
Device, on_delete=models.CASCADE, verbose_name=_("Gerät"))
open = models.BooleanField(verbose_name=_("offen"), default=True)
sent = models.BooleanField(verbose_name=_("gesendet"), default=False)
answered = models.BooleanField(
verbose_name=_("beantwortet"), default=False)
class Meta:
verbose_name = _('Frage')
verbose_name_plural = _('Fragen')
def __str__(self):
return f'Frage vom {self.date} zum Gerät {self.device.device}'
class Candidate(models.Model):
"""
A Candidate is a match of a broken Device and a Repair-Café without a fixed timeslot
or pre-assigned Reparateur.
"""
confirmed = models.BooleanField(verbose_name=_("bestätigt"))
cafe = models.ForeignKey(
Cafe, on_delete=models.CASCADE, verbose_name=_("Repair-Café"))
device = models.ForeignKey(
Device, on_delete=models.CASCADE, null=True, verbose_name=_("Gerät"))
class Meta:
verbose_name = _('Kandidat')
verbose_name_plural = _('Kandidaten')
def __str__(self):
return f'Kandidat {self.cafe.event_date} für Gerät {self.device.device}'
class OneTimeLogin(models.Model):
"""
A OneTimeLogin is a secret which can be used once to login a user.
"""
secret = models.CharField(max_length=200, verbose_name=_(
"Geheimnis"), unique=True, null=False)
url = models.CharField(max_length=200, verbose_name=_("URL"))
created = models.DateField(verbose_name=_(
"Erstellungsdatum"), default=django.utils.timezone.now)
login_used = models.BooleanField(
verbose_name=_("Login benutzt?"), default=False)
login_date = models.DateField(verbose_name=_(
"Login Datum"), null=True)
user = models.ForeignKey(
CustomUser, on_delete=models.CASCADE, null=False, verbose_name=_("Benutzer"))
class Meta:
verbose_name = _('Einmal-Login')
verbose_name_plural = _('Einmal-Login')
def __str__(self):
return f'Einmal-Login {self.user.email}'
class Message(models.Model):
"""
A Message is a request from a guest.
"""
message = models.TextField(verbose_name=_("Nachricht"))
answer = models.TextField(verbose_name=_("Antwort"))
date = models.DateField(verbose_name=_(
"Erstellungsdatum"), default=django.utils.timezone.now)
guest = models.ForeignKey(
Guest, on_delete=models.CASCADE, null=True, verbose_name=_("Gast"))
class Meta:
verbose_name = _('Nachricht')
verbose_name_plural = _('Nachrichten')
def __str__(self):
return f'Nachricht vom {self.date} von {self.guest.mail}'

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,3 @@
.django-ckeditor-widget {
display: block !important
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Some files were not shown because too many files have changed in this diff Show more