diff --git a/.gitea/workflows/deploy_stage.yml b/.gitea/workflows/deploy_stage.yml new file mode 100644 index 0000000..566acaa --- /dev/null +++ b/.gitea/workflows/deploy_stage.yml @@ -0,0 +1,23 @@ +name: Deploy into stage environment +on: + push: + branches: + - stage + workflow_dispatch: + +env: + DEPLOY_DIR: "/var/www/touren.alpenverein-karlsruhe.de/wsgi/django-dav-events.stage" + +jobs: + deploy-on-stage: + name: Deploy into stage environment + runs-on: [django-dav-events, kitty] + steps: + - name: "Pull code" + run: git -C "$DEPLOY_DIR/src/django-dav-events" pull + - name: "Migrate database" + run: $DEPLOY_DIR/python/bin/python $DEPLOY_DIR/django/manage.py migrate --noinput + - name: "Collect static files" + run: $DEPLOY_DIR/python/bin/python $DEPLOY_DIR/django/manage.py collectstatic --noinput + - name: "Touch wsgi file" + run: touch $DEPLOY_DIR/django/main/wsgi.py diff --git a/.gitea/workflows/nightly_test.yml b/.gitea/workflows/nightly_test.yml new file mode 100644 index 0000000..25a90b9 --- /dev/null +++ b/.gitea/workflows/nightly_test.yml @@ -0,0 +1,15 @@ +name: Run tests every night at 05:05 +on: + schedule: + - cron: "05 05 * * *" + +jobs: + run-tests: + name: Execute tox to run the test suite + runs-on: [django-dav-events] + steps: + - name: "Checkout the repository" + run: git clone "${{ gitea.server_url }}/${{ gitea.repository }}" ./repository + - name: "Run test via tox" + working-directory: ./repository + run: tox diff --git a/.gitea/workflows/test_on_push.yml b/.gitea/workflows/test_on_push.yml new file mode 100644 index 0000000..38d3ada --- /dev/null +++ b/.gitea/workflows/test_on_push.yml @@ -0,0 +1,13 @@ +name: Run tests +on: [push] + +jobs: + run-tests: + name: Execute tox to run the test suite + runs-on: [django-dav-events] + steps: + - name: "Checkout the repository" + run: git clone "${{ gitea.server_url }}/${{ gitea.repository }}" ./repository + - name: "Run tests via tox" + working-directory: ./repository + run: tox diff --git a/INSTALL.rst b/INSTALL.rst index f20a374..bb3207e 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -1,19 +1,27 @@ REQUIREMENTS ============ -- Python 3 -- Python package virtualenv (in most cases this is distributed or installed together with python) -- Django (will be installed automatically) -- Several additional django related python packages (will be installed automatically) +- Python >= 3.12 +- Django and some other python packages, that will be installed throughout + the installation process For production use you surly want a real web server that supports WSGI -(e.g. Apache httpd with mod_wsgi) and a real Database like PostgreSQL. +(e.g. Apache httpd with mod_wsgi) and a real Database like PostgreSQL (psycopg2). QUICK INSTALLATION FOR THE IMPATIENT ==================================== -- python setup.py mkpyenv +- python -m venv ./etc/python - source env/python/bin/activate -- python setup.py quickdev +- python -m pip install -r requirements.txt +- python -m pip install -e . +- django-dav-events-admin setup ./env/django +- python ./env/django/manage.py enable_module dav_auth +- python ./env/django/manage.py enable_module dav_events +- python ./env/django/manage.py enable_module dav_registration +- python ./env/django/manage.py enable_module dav_event_office +- python ./env/django/manage.py makemigrations +- python ./env/django/manage.py migrate +- python ./env/django/manage.py createsuperuser INSTALLATION @@ -24,14 +32,9 @@ INSTALLATION It is strongly recommended to create a separated python environment for this django project. But it is not exactly necessary. -The creation of a separated python environment is very easy with the -virtualenv tool (a python package). - -If you decide to not use virtualenv, proceed with step 2. - - Create the python environment in a directory called ./env/python: - ``virtualenv --prompt="(dav)" ./env/python`` + ``python -m venv --prompt="(dav)" ./env/python`` - If you use a posix compatible shell (like bash, the linux default shell), you have to activate the environment for the current shell session @@ -50,16 +53,29 @@ If you have left the session or deactivated the environment and want to reactivate the environment (e.g. to execute a python command) use the previous ``source ...`` command. -2. Install files ----------------- +2. Install requirements +----------------------- + +- ``python -m pip install -r requirements.txt`` + +3. Install project code in development mode +------------------------------------------- + +- ``python -m pip install -e .`` + +4. Setup django project root +---------------------------- + +To run a django app, you need a django project root directory, with some +static and variable files in it. +In the last step a tool was installed, that can be used to create such +a project directory with all the neccessary subdirectories and files. +Our example will create the django project in ./etc/django and we will +call this directory *project root* for now on. -- ``python setup.py develop`` - ``django-dav-events-admin setup ./env/django`` -The django project directory ('./env/django' within the previous example) -will be called *project root* for now on. - -3. Enable modules +5. Enable modules ----------------- Our web application consist of several modules, that care about single aspects of the whole picture. @@ -71,6 +87,8 @@ and run - ``python manage.py enable_module dav_auth`` - ``python manage.py enable_module dav_events`` +- ``python manage.py enable_module dav_registration`` +- ``python manage.py enable_module dav_event_office`` 4. Create the database schema / Populate the database ----------------------------------------------------- @@ -94,3 +112,12 @@ While you still are in the *project root* directory, run Now you should be able to connect to the test server via http://localhost:8000 + +7. Configure production web server +---------------------------------- +For production use you do not want to run the test server, +but have a real web server like apache or nginx running the +django app via the WSGI interface. +The entry point for your WSGI server is the file +``main/wsgi.py`` within the *project root* directory. + diff --git a/MANIFEST.in b/MANIFEST.in index fa47c47..07a2b08 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,3 @@ -# common dist files -include README.rst INSTALL.rst -include setup.py requirements.txt # dav_base recursive-include dav_base/console_scripts/django_project_config *.py recursive-include dav_base/static * diff --git a/Makefile b/Makefile deleted file mode 100644 index d2b8c8b..0000000 --- a/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -PYTHON := python - -DEPLOY_DIR ?= /var/www/wsgi/django-dav-events - -.PHONY: default help test deploy - -default: help - -help: - @echo "The make stuff is used by our CI/CD buildbot." - -test: - tox - -deploy: - git -C "$(DEPLOY_DIR)/src/django-dav-events" pull - "$(DEPLOY_DIR)/python/bin/python" "$(DEPLOY_DIR)/django/manage.py" migrate --noinput - "$(DEPLOY_DIR)/python/bin/python" "$(DEPLOY_DIR)/django/manage.py" collectstatic --noinput - touch "$(DEPLOY_DIR)/django/main/wsgi.py" - diff --git a/README.rst b/README.rst index b1d97bc..12cb170 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,8 @@ ABOUT ===== -This is the DAV Events django project. +django-dav-events is a django based web app to drive the +"Touren- & Kurseportal" of +"Sektion Karlsruhe im Deutschen Alpenverein (DAV) e.V." REQUIREMENTS diff --git a/dav_auth/tests/generic.py b/dav_auth/tests/generic.py index ace764a..eede63a 100644 --- a/dav_auth/tests/generic.py +++ b/dav_auth/tests/generic.py @@ -9,7 +9,7 @@ class SeleniumAuthMixin: username_field = self.wait_on_presence(driver, (By.ID, 'id_username')) username_field.clear() username_field.send_keys(username) - password_field = driver.find_element_by_id('id_password') + password_field = driver.find_element(By.ID, 'id_password') password_field.clear() password_field.send_keys(password) password_field.send_keys(Keys.RETURN) diff --git a/dav_auth/tests/test_screenshots.py b/dav_auth/tests/test_screenshots.py index 168476a..9d30265 100644 --- a/dav_auth/tests/test_screenshots.py +++ b/dav_auth/tests/test_screenshots.py @@ -31,25 +31,25 @@ class TestCase(ScreenshotTestCase): # Go to login page via login button on root page -> save plain login form c = self.selenium c.get(self.complete_url('/')) - link = c.find_element_by_css_selector('#login-widget a') + link = c.find_element(By.CSS_SELECTOR, '#login-widget a') link.click() self.wait_on_presence(c, (By.ID, 'id_username')) self.save_screenshot('empty_login_form', sequence=sequence_name) # Fill in a password -> Save dots in password field - username_field = c.find_element_by_id('id_username') + username_field = c.find_element(By.ID, 'id_username') username_field.clear() username_field.send_keys('username') - password_field = c.find_element_by_id('id_password') + password_field = c.find_element(By.ID, 'id_password') password_field.clear() password_field.send_keys(self.test_password) self.save_screenshot('filled_login_form', sequence=sequence_name) # Wrong username -> save error message - username_field = c.find_element_by_id('id_username') + username_field = c.find_element(By.ID, 'id_username') username_field.clear() username_field.send_keys(self.test_username[::-1]) - password_field = c.find_element_by_id('id_password') + password_field = c.find_element(By.ID, 'id_password') password_field.clear() password_field.send_keys(self.test_password) password_field.send_keys(Keys.RETURN) @@ -58,10 +58,10 @@ class TestCase(ScreenshotTestCase): alert_button.click() # Wrong password -> save error message - username_field = c.find_element_by_id('id_username') + username_field = c.find_element(By.ID, 'id_username') username_field.clear() username_field.send_keys(self.test_username) - password_field = c.find_element_by_id('id_password') + password_field = c.find_element(By.ID, 'id_password') password_field.clear() password_field.send_keys(self.test_password[::-1]) password_field.send_keys(Keys.RETURN) @@ -72,10 +72,10 @@ class TestCase(ScreenshotTestCase): # Login of inactive user -> save error message self.user.is_active = False self.user.save() - username_field = c.find_element_by_id('id_username') + username_field = c.find_element(By.ID, 'id_username') username_field.clear() username_field.send_keys(self.test_username) - password_field = c.find_element_by_id('id_password') + password_field = c.find_element(By.ID, 'id_password') password_field.clear() password_field.send_keys(self.test_password) password_field.send_keys(Keys.RETURN) @@ -87,10 +87,10 @@ class TestCase(ScreenshotTestCase): self.user.save() # Login -> save success message - username_field = c.find_element_by_id('id_username') + username_field = c.find_element(By.ID, 'id_username') username_field.clear() username_field.send_keys(self.test_username) - password_field = c.find_element_by_id('id_password') + password_field = c.find_element(By.ID, 'id_password') password_field.clear() password_field.send_keys(self.test_password) password_field.send_keys(Keys.RETURN) @@ -104,18 +104,18 @@ class TestCase(ScreenshotTestCase): self.save_screenshot('user_menu', sequence=sequence_name) # Click on 'set password' -> save set password page - user_menu = c.find_element_by_css_selector('#login-widget ul') - link = user_menu.find_element_by_partial_link_text(ugettext('Passwort ändern')) + user_menu = c.find_element(By.CSS_SELECTOR, '#login-widget ul') + link = user_menu.find_element(By.PARTIAL_LINK_TEXT, ugettext('Passwort ändern')) link.click() password_field = self.wait_on_presence(c, (By.ID, 'id_new_password')) self.save_screenshot('empty_set_password_form', sequence=sequence_name) # Fill in a password -> Save dots in password field - send_mail_field = c.find_element_by_id('id_send_password_mail') + send_mail_field = c.find_element(By.ID, 'id_send_password_mail') send_mail_field.click() password_field.clear() password_field.send_keys(self.test_password) - password2_field = c.find_element_by_id('id_new_password_repeat') + password2_field = c.find_element(By.ID, 'id_new_password_repeat') password2_field.clear() password2_field.send_keys(self.test_password) self.save_screenshot('filled_set_password_form', sequence=sequence_name) @@ -131,10 +131,10 @@ class TestCase(ScreenshotTestCase): # New passwords too common and too short -> save error message password = 'abcdef' - password_field = c.find_element_by_id('id_new_password') + password_field = c.find_element(By.ID, 'id_new_password') password_field.clear() password_field.send_keys(password) - password2_field = c.find_element_by_id('id_new_password_repeat') + password2_field = c.find_element(By.ID, 'id_new_password_repeat') password2_field.clear() password2_field.send_keys(password) password2_field.send_keys(Keys.RETURN) @@ -143,10 +143,10 @@ class TestCase(ScreenshotTestCase): # New passwords entirely_numeric -> save error message password = '9126735804' - password_field = c.find_element_by_id('id_new_password') + password_field = c.find_element(By.ID, 'id_new_password') password_field.clear() password_field.send_keys(password) - password2_field = c.find_element_by_id('id_new_password_repeat') + password2_field = c.find_element(By.ID, 'id_new_password_repeat') password2_field.clear() password2_field.send_keys(password) password2_field.send_keys(Keys.RETURN) @@ -155,10 +155,10 @@ class TestCase(ScreenshotTestCase): # New passwords too similar -> save error message password = self.test_username - password_field = c.find_element_by_id('id_new_password') + password_field = c.find_element(By.ID, 'id_new_password') password_field.clear() password_field.send_keys(password) - password2_field = c.find_element_by_id('id_new_password_repeat') + password2_field = c.find_element(By.ID, 'id_new_password_repeat') password2_field.clear() password2_field.send_keys(password) password2_field.send_keys(Keys.RETURN) @@ -167,10 +167,10 @@ class TestCase(ScreenshotTestCase): # Change password -> save success message password = self.test_password[::-1] - password_field = c.find_element_by_id('id_new_password') + password_field = c.find_element(By.ID, 'id_new_password') password_field.clear() password_field.send_keys(password) - password2_field = c.find_element_by_id('id_new_password_repeat') + password2_field = c.find_element(By.ID, 'id_new_password_repeat') password2_field.clear() password2_field.send_keys(password) password2_field.send_keys(Keys.RETURN) @@ -179,7 +179,7 @@ class TestCase(ScreenshotTestCase): # Get password recreate page -> since we are logged in, it should # redirect to set password page again -> save - html = c.find_element_by_tag_name('html') + html = c.find_element(By.TAG_NAME, 'html') c.get(self.complete_url(reverse('dav_auth:recreate_password'))) self.wait_until_stale(c, html) self.wait_on_presence(c, (By.ID, 'id_new_password')) @@ -188,19 +188,19 @@ class TestCase(ScreenshotTestCase): # Click on 'logout' -> save page dropdown_button = self.wait_on_presence(c, (By.ID, 'user_dropdown_button')) dropdown_button.click() - user_menu = c.find_element_by_css_selector('#login-widget ul') - link = user_menu.find_element_by_partial_link_text(ugettext('Logout')) + user_menu = c.find_element(By.CSS_SELECTOR, '#login-widget ul') + link = user_menu.find_element(By.PARTIAL_LINK_TEXT, ugettext('Logout')) link.click() self.wait_until_stale(c, user_menu) self.save_screenshot('logout_succeed', sequence=sequence_name) # Click on 'login' to access password recreate link - link = c.find_element_by_css_selector('#login-widget a') + link = c.find_element(By.CSS_SELECTOR, '#login-widget a') link.click() self.wait_on_presence(c, (By.ID, 'id_username')) # Locate password recreate link, click it -> save password recreate form - link = c.find_element_by_partial_link_text(ugettext('Passwort vergessen')) + link = c.find_element(By.PARTIAL_LINK_TEXT, ugettext('Passwort vergessen')) link.click() username_field = self.wait_on_presence(c, (By.ID, 'id_username')) self.save_screenshot('empty_recreate_password_form', sequence=sequence_name) @@ -212,7 +212,7 @@ class TestCase(ScreenshotTestCase): self.save_screenshot('recreate_password_invalid_user', sequence=sequence_name) # Locate password recreate link, click it - link = c.find_element_by_partial_link_text(ugettext('Passwort vergessen')) + link = c.find_element(By.PARTIAL_LINK_TEXT, ugettext('Passwort vergessen')) link.click() username_field = self.wait_on_presence(c, (By.ID, 'id_username')) diff --git a/dav_auth/tests/test_templates.py b/dav_auth/tests/test_templates.py index 211d7c0..b33ca12 100644 --- a/dav_auth/tests/test_templates.py +++ b/dav_auth/tests/test_templates.py @@ -40,7 +40,7 @@ class TestCase(SeleniumAuthMixin, SeleniumTestCase): c = self.selenium c.get(self.complete_url('/')) try: - c.find_element_by_css_selector('#login-widget a') + c.find_element(By.CSS_SELECTOR, '#login-widget a') except NoSuchElementException as e: # pragma: no cover self.fail(str(e)) @@ -50,9 +50,9 @@ class TestCase(SeleniumAuthMixin, SeleniumTestCase): location = reverse('dav_auth:login') c.get(self.complete_url(location)) - field = c.find_element_by_id('id_username') + field = c.find_element(By.ID, 'id_username') self.assertEqual(field.get_attribute('required'), 'true') - field = c.find_element_by_id('id_password') + field = c.find_element(By.ID, 'id_password') self.assertEqual(field.get_attribute('required'), 'true') def test_required_fields_in_set_password_form(self): @@ -61,7 +61,7 @@ class TestCase(SeleniumAuthMixin, SeleniumTestCase): c.get(self.complete_url(reverse('dav_auth:set_password'))) field = self.wait_on_presence(c, (By.ID, 'id_new_password')) self.assertEqual(field.get_attribute('required'), 'true') - field = c.find_element_by_id('id_new_password_repeat') + field = c.find_element(By.ID, 'id_new_password_repeat') self.assertEqual(field.get_attribute('required'), 'true') - field = c.find_element_by_id('id_send_password_mail') + field = c.find_element(By.ID, 'id_send_password_mail') self.assertEqual(field.get_attribute('required'), None) diff --git a/dav_base/tests/utils.py b/dav_base/tests/utils.py index 92bf16e..0355f82 100644 --- a/dav_base/tests/utils.py +++ b/dav_base/tests/utils.py @@ -4,10 +4,7 @@ import os from tempfile import mkdtemp as _mkdtemp -def mkdtemp(prefix): - dirname = os.path.dirname - pkg_base_dir = dirname(dirname(dirname(__file__))) - tmp_dir = os.path.join(pkg_base_dir, 'tmp') - if not os.path.exists(tmp_dir): - os.makedirs(tmp_dir) - return _mkdtemp(prefix=prefix, dir=tmp_dir) +def mkdtemp(prefix, base_dir): + if not os.path.exists(base_dir): + os.makedirs(base_dir) + return _mkdtemp(prefix=prefix, dir=base_dir) diff --git a/dav_events/choices.py b/dav_events/choices.py index 6388955..32b23de 100644 --- a/dav_events/choices.py +++ b/dav_events/choices.py @@ -31,6 +31,10 @@ class ChoiceSet(object): else: return False + @property + def codes(self): + return self._codes + def get_label(self, code): return self._labels[code] diff --git a/dav_events/django_project_config/settings-dav_events.py b/dav_events/django_project_config/settings-dav_events.py index c0df80f..fc8ca9c 100644 --- a/dav_events/django_project_config/settings-dav_events.py +++ b/dav_events/django_project_config/settings-dav_events.py @@ -133,125 +133,149 @@ FORM_INITIALS = { # EventCreateForm and sub classes # FORMS_DEVELOPMENT_INIT = False MATRIX_CONFIG = { - '0': {'description': _(u'Keiner / direkte Abrechnung (Tageswanderung)'), - 'trainer_fee': 0, - 'trainer_day_fee': 0, - 'participant_fee': 0, - 'participant_day_fee': 0, - 'pre_meeting_fee': 0, - 'pubtrans_bonus': 0, - 'min_participants': 0, - 'max_participants': 0, + '0': {'description': _(u'Keiner / direkte Abrechnung (Tageswanderung)'), + 'orga_compensation': 0, + 'pubtrans_compensation': 0, + 'trainer_compensation': 0, + 'trainer_daily_compensation': 0, + 'pre_meeting_compensation': 0, + 'participant_fee': 0, + 'participant_daily_fee': 0, + 'pubtrans_bonus': 0, + 'min_participants': 0, + 'max_participants': 0, }, - 'A': {'description': _(u'A (Mehrtageswanderung Mittelgebirge)'), - 'trainer_fee': 40, - 'trainer_day_fee': 50, - 'participant_fee': 10, - 'participant_day_fee': 10, - 'pre_meeting_fee': 20, - 'pubtrans_bonus': 20, - 'min_participants': 5, - 'max_participants': 8, + 'A': {'description': _(u'A (Mehrtageswanderung Mittelgebirge)'), + 'orga_compensation': 30, + 'pubtrans_compensation': 30, + 'trainer_compensation': 38, + 'trainer_daily_compensation': 48, + 'pre_meeting_compensation': 20, + 'participant_fee': 11, + 'participant_daily_fee': 11, + 'pubtrans_bonus': 20, + 'min_participants': 5, + 'max_participants': 8, }, - 'B': {'description': _(u'B (Alpine Mehrtageswanderung)'), - 'trainer_fee': 50, - 'trainer_day_fee': 75, - 'participant_fee': 10, - 'participant_day_fee': 20, - 'pre_meeting_fee': 20, - 'pubtrans_bonus': 30, - 'min_participants': 3, - 'max_participants': 6, + 'B': {'description': _(u'B (Alpine Mehrtageswanderung)'), + 'orga_compensation': 30, + 'pubtrans_compensation': 50, + 'trainer_compensation': 48, + 'trainer_daily_compensation': 73, + 'pre_meeting_compensation': 20, + 'participant_fee': 11, + 'participant_daily_fee': 21, + 'pubtrans_bonus': 30, + 'min_participants': 3, + 'max_participants': 6, }, - 'C': {'description': _(u'C (Tour/Kurs ohne Übernachtung)'), - 'trainer_fee': 30, - 'trainer_day_fee': 60, - 'participant_fee': 10, - 'participant_day_fee': 30, - 'pre_meeting_fee': 20, - 'pubtrans_bonus': 0, - 'min_participants': 3, - 'max_participants': 5, + 'C': {'description': _(u'C (Tour/Kurs ohne Übernachtung)'), + 'orga_compensation': 30, + 'pubtrans_compensation': 30, + 'trainer_compensation': 28, + 'trainer_daily_compensation': 57, + 'pre_meeting_compensation': 20, + 'participant_fee': 11, + 'participant_daily_fee': 32, + 'pubtrans_bonus': 20, + 'min_participants': 3, + 'max_participants': 5, }, - 'D': {'description': _(u'D (Tour/Kurs Mittelgebirge)'), - 'trainer_fee': 50, - 'trainer_day_fee': 75, - 'participant_fee': 20, - 'participant_day_fee': 25, - 'pre_meeting_fee': 20, - 'pubtrans_bonus': 30, - 'min_participants': 3, - 'max_participants': 5, + 'D': {'description': _(u'D (Tour/Kurs Mittelgebirge)'), + 'orga_compensation': 30, + 'pubtrans_compensation': 30, + 'trainer_compensation': 48, + 'trainer_daily_compensation': 73, + 'pre_meeting_compensation': 20, + 'participant_fee': 21, + 'participant_daily_fee': 26, + 'pubtrans_bonus': 20, + 'min_participants': 3, + 'max_participants': 5, }, - 'E': {'description': _(u'E (Alpine Klettertour DE/AT)'), - 'trainer_fee': 80, - 'trainer_day_fee': 75, - 'participant_fee': 40, - 'participant_day_fee': 40, - 'pre_meeting_fee': 20, - 'pubtrans_bonus': 30, - 'min_participants': 2, - 'max_participants': 3, + 'E': {'description': _(u'E (Alpine Klettertour DE/AT)'), + 'orga_compensation': 40, + 'pubtrans_compensation': 50, + 'trainer_compensation': 76, + 'trainer_daily_compensation': 73, + 'pre_meeting_compensation': 20, + 'participant_fee': 42, + 'participant_daily_fee': 42, + 'pubtrans_bonus': 30, + 'min_participants': 2, + 'max_participants': 3, }, - 'F': {'description': _(u'F (Alpine Klettertour CH/FR/IT/..)'), - 'trainer_fee': 80, - 'trainer_day_fee': 85, - 'participant_fee': 40, - 'participant_day_fee': 45, - 'pre_meeting_fee': 20, - 'pubtrans_bonus': 30, - 'min_participants': 2, - 'max_participants': 3, + 'F': {'description': _(u'F (Alpine Klettertour CH/FR/IT/..)'), + 'orga_compensation': 40, + 'pubtrans_compensation': 50, + 'trainer_compensation': 76, + 'trainer_daily_compensation': 82, + 'pre_meeting_compensation': 20, + 'participant_fee': 42, + 'participant_daily_fee': 47, + 'pubtrans_bonus': 30, + 'min_participants': 2, + 'max_participants': 3, }, - 'G': {'description': _(u'G (Alpiner Kurs DE/AT)'), - 'trainer_fee': 100, - 'trainer_day_fee': 75, - 'participant_fee': 35, - 'participant_day_fee': 30, - 'pre_meeting_fee': 20, - 'pubtrans_bonus': 30, - 'min_participants': 3, - 'max_participants': 4, + 'G': {'description': _(u'G (Alpiner Kurs DE/AT)'), + 'orga_compensation': 50, + 'pubtrans_compensation': 50, + 'trainer_compensation': 96, + 'trainer_daily_compensation': 73, + 'pre_meeting_compensation': 20, + 'participant_fee': 37, + 'participant_daily_fee': 32, + 'pubtrans_bonus': 30, + 'min_participants': 3, + 'max_participants': 4, }, - 'H': {'description': _(u'H (Alpiner Kurs CH/FR/IT/..)'), - 'trainer_fee': 100, - 'trainer_day_fee': 85, - 'participant_fee': 35, - 'participant_day_fee': 30, - 'pre_meeting_fee': 20, - 'pubtrans_bonus': 30, - 'min_participants': 3, - 'max_participants': 4, + 'H': {'description': _(u'H (Alpiner Kurs CH/FR/IT/..)'), + 'orga_compensation': 50, + 'pubtrans_compensation': 50, + 'trainer_compensation': 96, + 'trainer_daily_compensation': 82, + 'pre_meeting_compensation': 20, + 'participant_fee': 37, + 'participant_daily_fee': 32, + 'pubtrans_bonus': 30, + 'min_participants': 3, + 'max_participants': 4, }, - 'I': {'description': _(u'I (Alpine MTB/Ski-Tour DE/AT)'), - 'trainer_fee': 80, - 'trainer_day_fee': 75, - 'participant_fee': 25, - 'participant_day_fee': 25, - 'pre_meeting_fee': 20, - 'pubtrans_bonus': 30, - 'min_participants': 3, - 'max_participants': 6, + 'I': {'description': _(u'I (Alpine MTB/Ski-Tour DE/AT)'), + 'orga_compensation': 40, + 'pubtrans_compensation': 50, + 'trainer_compensation': 76, + 'trainer_daily_compensation': 73, + 'pre_meeting_compensation': 20, + 'participant_fee': 26, + 'participant_daily_fee': 26, + 'pubtrans_bonus': 30, + 'min_participants': 3, + 'max_participants': 6, }, - 'J': {'description': _(u'J (Alpine MTB/Ski-Tour CH/FR/IT/..)'), - 'trainer_fee': 80, - 'trainer_day_fee': 85, - 'participant_fee': 25, - 'participant_day_fee': 25, - 'pre_meeting_fee': 20, - 'pubtrans_bonus': 30, - 'min_participants': 3, - 'max_participants': 6, + 'J': {'description': _(u'J (Alpine MTB/Ski-Tour CH/FR/IT/..)'), + 'orga_compensation': 40, + 'pubtrans_compensation': 50, + 'trainer_compensation': 76, + 'trainer_daily_compensation': 82, + 'pre_meeting_compensation': 20, + 'participant_fee': 26, + 'participant_daily_fee': 26, + 'pubtrans_bonus': 30, + 'min_participants': 3, + 'max_participants': 6, }, - 'K': {'description': _(u'K (Ski-Tour/-Kurs mit Liftbenutzung)'), - 'trainer_fee': 80, - 'trainer_day_fee': 130, - 'participant_fee': 40, - 'participant_day_fee': 40, - 'pre_meeting_fee': 20, - 'pubtrans_bonus': 30, - 'min_participants': 3, - 'max_participants': 4, + 'K': {'description': _(u'K (Ski-Tour/-Kurs mit Liftbenutzung)'), + 'orga_compensation': 40, + 'pubtrans_compensation': 50, + 'trainer_compensation': 76, + 'trainer_daily_compensation': 124, + 'pre_meeting_compensation': 20, + 'participant_fee': 42, + 'participant_daily_fee': 42, + 'pubtrans_bonus': 30, + 'min_participants': 3, + 'max_participants': 4, }, } diff --git a/dav_events/forms/events.py b/dav_events/forms/events.py index ff3d2c7..487fb5c 100644 --- a/dav_events/forms/events.py +++ b/dav_events/forms/events.py @@ -773,26 +773,35 @@ class ChargesForm(EventCreateForm): _form_title = _(u'Kosten') _next_form_name = 'DescriptionForm' + pubtrans_planned = forms.BooleanField(disabled=True, required=False, + label=_(u'An-/Abreise mit Bahn/Bus geplant'), + ) charge_key = forms.CharField(disabled=True, label=_(u'Kostenschlüssel'), ) - trainer_fee = forms.FloatField(disabled=True, - label=_(u'Pauschale Trainer*in'), - ) - trainer_day_fee = forms.FloatField(disabled=True, - label=_(u'Tagespauschale Trainer*in'), - ) + orga_compensation = forms.FloatField(disabled=True, + label=_(u'Aufwand für Tourenleitung'), + ) + pubtrans_compensation = forms.FloatField(disabled=True, + label=_(u'Aufwand für Organisation Bahn/Bus'), + ) + trainer_compensation = forms.FloatField(disabled=True, + label=_(u'Aufwand für Trainer*in'), + ) + trainer_daily_compensation = forms.FloatField(disabled=True, + label=_(u'Täglicher Aufwand für Trainer*in'), + ) + pre_meeting_compensation = forms.FloatField(disabled=True, + label=_(u'Aufwand pro Vortreffen'), + ) participant_fee = forms.FloatField(disabled=True, - label=_(u'Pauschale Teilnehmer*in'), - ) - participant_day_fee = forms.FloatField(disabled=True, - label=_(u'Tagespauschale Teilnehmer*in'), - ) - pre_meeting_fee = forms.FloatField(disabled=True, - label=_(u'Pauschale pro Vortreffen'), + label=_(u'Beitrag für Teilnehmer*in'), ) + participant_daily_fee = forms.FloatField(disabled=True, + label=_(u'Täglicher Beitrag für Teilnehmer*in'), + ) pubtrans_bonus = forms.FloatField(disabled=True, - label=_(u'Bonus bei Benutzung öffentlicher Verkehrsmittel'), + label=_(u'Rückzahlung bei Benutzung Bahn/Bus'), ) trainer1_reward = forms.FloatField(disabled=True, label=_(u'Aufwandsentschädigung Tourenleiter*in'), @@ -832,6 +841,10 @@ class ChargesForm(EventCreateForm): additional_costs_text = u'' if transport != 'coach': additional_costs_text += ugettext(u'Fahrtkosten') + if transport == 'public': + pubtrans_planned = True + else: + pubtrans_planned = False if last_day: timedelta = last_day - first_day @@ -850,57 +863,83 @@ class ChargesForm(EventCreateForm): else: n_pre_meetings = 0 - trainer_reward = ( - matrix_config['trainer_fee'] - + ndays * matrix_config['trainer_day_fee'] - + n_pre_meetings * matrix_config['pre_meeting_fee'] + trainer1_reward = ( + matrix_config['orga_compensation'] + + matrix_config['trainer_compensation'] + + ndays * matrix_config['trainer_daily_compensation'] + + n_pre_meetings * matrix_config['pre_meeting_compensation'] + ) + trainer23_reward = ( + matrix_config['trainer_compensation'] + + ndays * matrix_config['trainer_daily_compensation'] + + n_pre_meetings * matrix_config['pre_meeting_compensation'] ) charge = ( matrix_config['participant_fee'] - + ndays * matrix_config['participant_day_fee'] + + ndays * matrix_config['participant_daily_fee'] ) - if arrival_previous_day: - trainer_reward += matrix_config['trainer_day_fee'] / 2.0 - charge += matrix_config['participant_day_fee'] / 2.0 + if pubtrans_planned: + trainer1_reward += matrix_config['pubtrans_compensation'] + if arrival_previous_day: + trainer1_reward += matrix_config['trainer_daily_compensation'] / 2.0 + trainer23_reward += matrix_config['trainer_daily_compensation'] / 2.0 + charge += matrix_config['participant_daily_fee'] / 2.0 + + self.fields['pubtrans_planned'].initial = pubtrans_planned self.fields['charge_key'].initial = matrix_config['description'] or matrix_key - self.fields['trainer_fee'].initial = matrix_config['trainer_fee'] - self.fields['trainer_day_fee'].initial = matrix_config['trainer_day_fee'] + self.fields['orga_compensation'].initial = matrix_config['orga_compensation'] + self.fields['pubtrans_compensation'].initial = matrix_config['pubtrans_compensation'] + self.fields['trainer_compensation'].initial = matrix_config['trainer_compensation'] + self.fields['trainer_daily_compensation'].initial = matrix_config['trainer_daily_compensation'] + self.fields['pre_meeting_compensation'].initial = matrix_config['pre_meeting_compensation'] self.fields['participant_fee'].initial = matrix_config['participant_fee'] - self.fields['participant_day_fee'].initial = matrix_config['participant_day_fee'] - self.fields['pre_meeting_fee'].initial = matrix_config['pre_meeting_fee'] + self.fields['participant_daily_fee'].initial = matrix_config['participant_daily_fee'] self.fields['pubtrans_bonus'].initial = matrix_config['pubtrans_bonus'] self.fields['charge'].initial = charge - self.fields['trainer1_reward'].initial = trainer_reward - self.fields['trainer23_reward'].initial = trainer_reward * 0.95 + self.fields['trainer1_reward'].initial = trainer1_reward + self.fields['trainer23_reward'].initial = trainer23_reward self.fields['pubtrans_bonus'].widget.attrs['title'] = ugettext(u'Der Bonus wird nachträglich' u' auf Meldung der Tourenleitung' u' verrechnet und ist noch nicht' u' in den hier dargestellten Zahlen enthalten.') - self.fields['charge'].widget.attrs['title'] = (u'%d € Pauschale \n' - u'+ %d Tage * %d € Tagespauschale \n' - u'+ %d halben Anreisetag * %d € Tagespauschale / 2' + self.fields['charge'].widget.attrs['title'] = (u'%d € Beitrag \n' + u'+ %d Tage * %d € täglicher Beitrag \n' + u'+ %d halben Anreisetag * %d € halbtäglicher Beitrag' % ( matrix_config['participant_fee'], - ndays, matrix_config['participant_day_fee'], - int(arrival_previous_day), matrix_config['participant_day_fee'], + ndays, matrix_config['participant_daily_fee'], + int(arrival_previous_day), matrix_config['participant_daily_fee']/2, ) ) - self.fields['trainer1_reward'].widget.attrs['title'] = (u'%d € Pauschale \n' - u'+ %d Tage * %d € Tagespauschale \n' - u'+ %d halben Anreisetag * %d € Tagespauschale / 2 \n' - u'+ %d Vortreffen * %d € Vortreffenpauschale' + self.fields['trainer1_reward'].widget.attrs['title'] = (u'%d € Aufwand für Tourenleitung\n' + u'+ %d € Aufwand für Organisation Bahn/Bus\n' + u'+ %d € Aufwand für Trainer*in\n' + u'+ %d Tage * %d € täglicher Aufwand \n' + u'+ %d halben Anreisetag * %d € halbtäglicher Aufwand\n' + u'+ %d Vortreffen * %d € Aufwand pro Vortreffen' % ( - matrix_config['trainer_fee'], - ndays, matrix_config['trainer_day_fee'], - int(arrival_previous_day), matrix_config['trainer_day_fee'], - n_pre_meetings, matrix_config['pre_meeting_fee'] + matrix_config['orga_compensation'], + int(pubtrans_planned)*matrix_config['pubtrans_compensation'], + matrix_config['trainer_compensation'], + ndays, matrix_config['trainer_daily_compensation'], + int(arrival_previous_day), matrix_config['trainer_daily_compensation']/2, + n_pre_meetings, matrix_config['pre_meeting_compensation'] + ) + ) + self.fields['trainer23_reward'].widget.attrs['title'] = (u'%d € Aufwand für Trainer*in\n' + u'+ %d Tage * %d € täglicher Aufwand \n' + u'+ %d halben Anreisetag * %d € halbtäglicher Aufwand\n' + u'+ %d Vortreffen * %d € Aufwand pro Vortreffen' + % ( + matrix_config['trainer_compensation'], + ndays, matrix_config['trainer_daily_compensation'], + int(arrival_previous_day), matrix_config['trainer_daily_compensation']/2, + n_pre_meetings, matrix_config['pre_meeting_compensation'] ) ) - self.fields['trainer23_reward'].widget.attrs['title'] = ugettext(u'95% der Aufwandsentschädigung' - u' Tourenleiter*in') self.fields['additional_costs'].widget.attrs['placeholder'] = ugettext(u'Kann freigelassen werden') self.fields['additional_costs'].initial = additional_costs_text diff --git a/dav_events/management/__init__.py b/dav_events/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dav_events/management/commands/__init__.py b/dav_events/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dav_events/management/commands/create_test_data.py b/dav_events/management/commands/create_test_data.py new file mode 100644 index 0000000..eac774d --- /dev/null +++ b/dav_events/management/commands/create_test_data.py @@ -0,0 +1,196 @@ +import datetime +import json +from django.apps import apps +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group +from django.core.management.base import BaseCommand, CommandError +from django.utils.termcolors import colorize + +from dav_events.models import Event, EventStatus, OneClickAction + + +class Command(BaseCommand): + help = 'Populate database with test data.' + + def _substitute_data_vars(self, data_dict, vars_dict): + for k in data_dict: + try: + data_dict[k] = data_dict[k].format(**vars_dict) + except AttributeError: + pass + return data_dict + + def _get_groups_for_role(self, role_name): + settings = self._app_settings + var_name = 'groups_{}'.format(role_name) + group_names = getattr(settings, var_name) + return group_names + + def _get_group_for_role(self, role_name): + group_names = self._get_groups_for_role(role_name) + group_name = group_names[0] + return group_name + + def _grant_role_to_user(self, user, role_name): + group_name = self._get_group_for_role(role_name) + group = Group.objects.get(name=group_name) + user.groups.add(group) + self.stdout.write(self.style.SUCCESS( + 'Added user \'{}\' to group \'{}\''.format(user, group) + )) + + def _create_group(self, group_name): + group, created = Group.objects.get_or_create(name=group_name) + if created: + self.stdout.write(self.style.SUCCESS('Created group \'{}\''.format(group))) + else: + self.stdout.write(self.style.WARNING('Recycled group \'{}\''.format(group))) + return group + + def _create_groups(self): + roles = ( + 'manager_super', + 'manager_w', + 'manager_s', + 'manager_m', + 'manager_k', + 'manager_b', + 'publisher_print', + 'publisher_web', + 'office', + ) + for role_name in roles: + group_names = self._get_groups_for_role(role_name) + for group_name in group_names: + self._create_group(group_name) + + def _create_user(self, username, email, first_name, last_name, password=None, is_superuser=False): + if password is None: + password = email + + user_model = get_user_model() + try: + user = user_model.objects.get(username=username) + self.stdout.write(self.style.WARNING('Recycled user \'{}\''.format(user))) + user.groups.clear() + except user_model.DoesNotExist: + user = user_model.objects.create_user(username=username, + email=email, + first_name=first_name, + last_name=last_name, + password=password) + self.stdout.write(self.style.SUCCESS('Created user \'{}\''.format(user))) + + if is_superuser: + user.is_staff = True + user.is_superuser = True + user.save() + self.stdout.write(self.style.SUCCESS('Made user \'{}\' a super user'.format(user))) + + return user + + def _create_users(self, user_list, data_vars=None): + if data_vars is None: + data_vars = {} + + for data_set in user_list: + if data_vars: + data_set = self._substitute_data_vars(data_set, data_vars) + + if 'username' not in data_set: + data_set['username'] = data_set['email'] + if 'roles' not in data_set: + data_set['roles'] = () + if 'is_superuser' not in data_set: + data_set['is_superuser'] = False + if 'password' not in data_set: + data_set['password'] = None + + user = self._create_user(username=data_set['username'], + email=data_set['email'], + first_name=data_set['first_name'], + last_name=data_set['last_name'], + password=data_set['password'], + is_superuser=data_set['is_superuser']) + + for role_name in data_set['roles']: + self._grant_role_to_user(user, role_name) + + def _create_event(self, event_data): + event = Event(**event_data) + event.save() + self.stdout.write(self.style.SUCCESS('Created event \'{}\''.format(event))) + return event + + def _create_events(self, event_list, data_vars=None): + if data_vars is None: + data_vars = {} + + for data_set in event_list: + if data_vars: + data_set = self._substitute_data_vars(data_set, data_vars) + + for date_key in ('first_day', 'last_day', 'alt_first_day', 'alt_last_day', 'deadline'): + k = '{}_from_today'.format(date_key) + if k in data_set: + d = datetime.date.today() + datetime.timedelta(data_set[k]) + data_set[date_key] = d + del data_set[k] + if 'pre_meeting_1_from_today' in data_set: + day = datetime.date.today() + datetime.timedelta(data_set['pre_meeting_1_from_today']) + data_set['pre_meeting_1'] = datetime.datetime.combine(day, datetime.time(19, 30)) + del data_set['pre_meeting_1_from_today'] + if 'pre_meeting_2_from_today' in data_set: + day = datetime.date.today() + datetime.timedelta(data_set['pre_meeting_2_from_today']) + data_set['pre_meeting_2'] = datetime.datetime.combine(day, datetime.time(19, 30)) + del data_set['pre_meeting_2_from_today'] + + status_updates = [] + if 'status_updates' in data_set: + status_updates = data_set['status_updates'] + del data_set['status_updates'] + + event = self._create_event(data_set) + + for status in status_updates: + event.workflow.update_status(status, event.owner) + + def add_arguments(self, parser): + parser.add_argument('--purge', action='store_true', help='remove all data from the database beforehand') + parser.add_argument('--var', '-e', action='append', dest='data_vars', metavar='KEY=VALUE', + help='Give a key=value pair for variable substitution in the test data') + parser.add_argument('data_file', metavar='FILE.json', help='File containing the test data as JSON') + + def handle(self, *args, **options): + app_config = apps.get_containing_app_config(__package__) + app_settings = app_config.settings + self._app_settings = app_settings + + with open(options['data_file'], 'r') as f: + test_data = json.load(f) + + data_vars = test_data['vars'] + if options['data_vars'] is not None: + for pair in options['data_vars']: + key = pair.split('=', 1)[0] + value = pair.split('=', 1)[1] + data_vars[key] = value + + if options['purge']: + self.stdout.write(self.style.NOTICE('Purge database...')) + OneClickAction.objects.all().delete() + Event.objects.all().delete() + EventStatus.objects.all().delete() + user_model = get_user_model() + user_model.objects.all().delete() + Group.objects.all().delete() + + self.stdout.write(colorize('Creating groups...', fg='cyan', opts=('bold',))) + self._create_groups() + self.stdout.write(colorize('Creating users...', fg='cyan', opts=('bold',))) + self._create_users(test_data['users'], data_vars) + self.stdout.write(colorize('Creating events...', fg='cyan', opts=('bold',))) + self._create_events(test_data['events'], data_vars) + + # raise CommandError('This is an error') + diff --git a/dav_events/models/event.py b/dav_events/models/event.py index 97fa65e..eb4b550 100644 --- a/dav_events/models/event.py +++ b/dav_events/models/event.py @@ -527,3 +527,98 @@ class Event(models.Model): template_name = os.path.join('dav_events', 'event', 'default.html') template = get_template(template_name) return template.render(self.get_template_context()) + + def as_dict(self, json=True, add_registration_url=False): + d = { + 'id': self.id, + 'number': self.get_number(), + 'title': self.title, + 'description': self.description, + + 'mode': self.mode, + 'mode_display': self.get_mode_display(), + 'sport': self.sport, + 'sport_display': self.get_sport_display(), + 'level': self.level, + 'level_display': self.get_level_display(), + + 'first_day': self.first_day, + 'last_day': self.last_day, + 'alt_first_day': self.alt_first_day, + 'alt_last_day': self.alt_last_day, + + 'location': self.location, + 'basecamp': self.basecamp, + + 'meeting_time': self.meeting_time, + 'departure_time': self.departure_time, + 'departure_ride': self.departure_ride, + 'return_departure_time': self.return_departure_time, + 'return_arrival_time': self.return_arrival_time, + + 'requirements': self.requirements, + 'equipment': self.equipment, + + 'pre_meeting_1': self.pre_meeting_1, + 'pre_meeting_2': self.pre_meeting_2, + + 'trainer_fullname': self.get_trainer_full_name(), + 'trainer_firstname': self.trainer_firstname, + 'trainer_familyname': self.trainer_familyname, + 'trainer_email': self.trainer_email, + 'trainer_phone': self.trainer_phone, + 'trainer_2_fullname': self.trainer_2_fullname, + 'trainer_2_email': self.trainer_2_email, + 'trainer_2_phone': self.trainer_2_phone, + 'trainer_3_fullname': self.trainer_3_fullname, + 'trainer_3_email': self.trainer_3_email, + 'trainer_3_phone': self.trainer_3_phone, + + 'min_participants': self.min_participants, + 'max_participants': self.max_participants, + 'registration_required': self.registration_required, + 'registration_howto': self.registration_howto, + 'deadline': self.deadline, + 'registration_closed': self.registration_closed, + + 'charge': self.charge, + 'additional_costs': self.additional_costs, + + 'course_topic_1': self.course_topic_1, + 'course_topic_2': self.course_topic_2, + 'course_topic_3': self.course_topic_3, + 'course_topic_4': self.course_topic_4, + 'course_topic_5': self.course_topic_5, + 'course_topic_6': self.course_topic_6, + 'course_goal_1': self.course_goal_1, + 'course_goal_2': self.course_goal_2, + 'course_goal_3': self.course_goal_3, + 'course_goal_4': self.course_goal_4, + 'course_goal_5': self.course_goal_5, + 'course_goal_6': self.course_goal_6, + } + if json: + d['country'] = str(self.country) + d['country_display'] = self.get_country_display() + for field in ('transport', 'meeting_point', 'accommodation', 'meals'): + value = getattr(self, field) + d[field] = value + display_field_name = '%s_display' % field + if value == 'NONE': + d[display_field_name] = None + elif value == 'OTHER': + other = getattr(self, '%s_other' % field) + d[display_field_name] = other + else: + func = getattr(self, 'get_%s_display' % field) + d[display_field_name] = func() + else: + d['country'] = self.country + for field in ('transport', 'meeting_point', 'accommodation', 'meals'): + value = getattr(self, field) + d[field] = value + + if add_registration_url: + d['registration_url'] = reverse('dav_registration:event', kwargs={'pk': self.pk}) + + return d \ No newline at end of file diff --git a/dav_events/templates/dav_events/event_create/ChargesForm.html b/dav_events/templates/dav_events/event_create/ChargesForm.html index 1616fa2..00be9e2 100644 --- a/dav_events/templates/dav_events/event_create/ChargesForm.html +++ b/dav_events/templates/dav_events/event_create/ChargesForm.html @@ -11,21 +11,29 @@ {% bootstrap_field form.charge_key %}
| Sport | -Level | + | Attributes |
|---|---|---|---|
| {{ event.sport }} | -{{ event.level }} | + | {{ event.sport }},{{ event.level }}, |