+ {% block title %}{% endblock %} +
++
diff --git a/.github/contributing.md b/.github/contributing.md
index c3081aa0b..fb3bc3c0e 100644
--- a/.github/contributing.md
+++ b/.github/contributing.md
@@ -15,7 +15,8 @@ in a pull request.
## Questions
Are you just using the software and have a question or improvement?
-* Ask it on the [gitter channel](https://gitter.im/wger-project/wger)
+* Ask it on the [gitter channel](https://gitter.im/wger-project/wger),
+* the [discord server](https://discord.gg/rPWFv6W)
* or just [open an issue](https://github.com/wger-project/wger/issues)
## Issues
diff --git a/.github/linters/.eslintrc.yml b/.github/linters/.eslintrc.yml
index 3af1b093c..72d6b56f5 100644
--- a/.github/linters/.eslintrc.yml
+++ b/.github/linters/.eslintrc.yml
@@ -1,7 +1,8 @@
{
"env": {
"browser": true,
- "jquery": true
+ "jquery": true,
+ "es6": true
},
"globals": {
"d3": true,
diff --git a/.github/linters/.python-lint b/.github/linters/.python-lint
index b6327a3a0..5149f8f31 100644
--- a/.github/linters/.python-lint
+++ b/.github/linters/.python-lint
@@ -55,7 +55,7 @@ confidence=
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once). You can also use "--disable=all" to
-# disable everything first and then reenable specific checks. For example, if
+# disable everything first and then re-enable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use "--disable=all --enable=classes
@@ -143,8 +143,8 @@ disable=import-error,
comprehension-escape
# Enable the message, report, category or checker with the given id(s). You can
-# either give multiple identifier separated by comma (,) or put this option
-# multiple time (only on the command line, not in the configuration file where
+# either give multiple identifiers separated by comma (,) or put this option
+# multiple times (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
enable=c-extension-no-member
@@ -180,7 +180,7 @@ score=yes
max-nested-blocks=5
# Complete name of functions that never returns. When checking for
-# inconsistent-return-statements if a never returning function is called then
+# inconsistent-return-statements, if a never returning function is called then
# it will be considered as an explicit return statement and no message will be
# printed.
never-returning-functions=sys.exit
@@ -504,7 +504,7 @@ max-attributes=7
# Maximum number of boolean expressions in an if statement (see R0916).
max-bool-expr=5
-# Maximum number of branch for function / method body.
+# Maximum number of branches for function / method body.
max-branches=12
# Maximum number of locals for function / method body.
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 53e966220..3e8f5143a 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -6,38 +6,44 @@ on:
- master
jobs:
- deploy:
+ path-context:
runs-on: ubuntu-latest
steps:
- - name: Checkout code
- uses: actions/checkout@v2
+ - name: Checkout
+ uses: actions/checkout@v2
- - name: Build base image
- uses: docker/build-push-action@v1
- with:
- username: ${{ secrets.DOCKERHUB_USERNAME }}
- password: ${{ secrets.DOCKERHUB_TOKEN }}
- repository: wger/base
- dockerfile: extras/docker/base/Dockerfile
- tags: latest,2.0-dev
- tag_with_ref: true
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v1
- - name: Build dev image
- uses: docker/build-push-action@v1
- with:
- username: ${{ secrets.DOCKERHUB_USERNAME }}
- password: ${{ secrets.DOCKERHUB_TOKEN }}
- repository: wger/devel
- dockerfile: extras/docker/development/Dockerfile
- tags: latest,2.0-dev
- tag_with_ref: true
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v1
- - name: Build apache image
- uses: docker/build-push-action@v1
- with:
- username: ${{ secrets.DOCKERHUB_USERNAME }}
- password: ${{ secrets.DOCKERHUB_TOKEN }}
- repository: wger/apache
- dockerfile: extras/docker/apache/Dockerfile
- tags: latest,2.0-dev
- tag_with_ref: true
+ - name: Login to DockerHub
+ uses: docker/login-action@v1
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - name: Build base image
+ uses: docker/build-push-action@v2
+ with:
+ context: .
+ file: extras/docker/base/Dockerfile
+ push: true
+ tags: wger/base:latest,wger/base:2.0-dev
+
+ - name: Build apache image
+ uses: docker/build-push-action@v2
+ with:
+ context: .
+ file: extras/docker/apache/Dockerfile
+ push: true
+ tags: wger/apache:latest,wger/apache:2.0-dev
+
+ - name: Build dev image
+ uses: docker/build-push-action@v2
+ with:
+ context: .
+ file: extras/docker/development/Dockerfile
+ push: true
+ tags: wger/devel:latest,wger/devel:2.0-dev
diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml
index 37d7e405d..c1fa515ac 100644
--- a/.github/workflows/linter.yml
+++ b/.github/workflows/linter.yml
@@ -46,8 +46,9 @@ jobs:
# Run Linter against code base #
################################
- name: Lint Code Base
- uses: docker://github/super-linter:v3.5.1
+ uses: docker://github/super-linter:v3.9.3
env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VALIDATE_PYTHON_FLAKE8: true
VALIDATE_MD: true
VALIDATE_JAVASCRIPT_ES: true
diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml
index d021fc2ff..3860538c8 100644
--- a/.github/workflows/pypi-publish.yml
+++ b/.github/workflows/pypi-publish.yml
@@ -1,4 +1,4 @@
-# This workflows will upload a Python Package using Twine when a release is created
+# This workflow will upload a Python Package using Twine when a release is created
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
name: Upload Package to PyPI
diff --git a/.travis.yml b/.travis.yml
index 243260206..e0b8d4e67 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -27,7 +27,7 @@ env:
# Install the application
install:
# Install requirements
- - pip install -r requirements_devel.txt
+ - pip install -r requirements_dev.txt
- python setup.py develop
- cd wger
- if [[ "$DB" = "postgresql" ]]; then pip install psycopg2; fi
diff --git a/AUTHORS.rst b/AUTHORS.rst
index b9a3585af..1146adc9e 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -25,6 +25,11 @@ Developers
* Malcolm Jones: https://github.com/DevloperMal
* Boniface Mwenda: https://github.com/andela-bmwenda
* Scott Peshak: https://github.com/speshak
+* Musanje Louis Michael: https://github.com/louiCoder
+* Kevin Antonio Rateni Iatauro: https://github.com/WalkingPizza
+* Sven - https://github.com/Svn-Sp
+* Christopher OConnell - https://github.com/oconnelc
+* Biplov - https://github.com/beingbiplov
Translators
-----------
diff --git a/MANIFEST.in b/MANIFEST.in
index 69dc7c1fa..b29980052 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -4,7 +4,7 @@ include AUTHORS.txt
include AGPL.txt
include CC-BY-SA.txt
include requirements.txt
-include requirements_devel.txt
+include requirements_dev.txt
# Application folder as well as extras
recursive-include extras *.*
diff --git a/README.md b/README.md
new file mode 100644
index 000000000..2695440bf
--- /dev/null
+++ b/README.md
@@ -0,0 +1,146 @@
+ {% trans "Something happened that caused an error." %}
+
+# wger
+
+wger (ˈvɛɡɐ) Workout Manager is a free, open source web application that help
+you manage your personal workouts, weight and diet plans and can also be used
+as a simple gym management utility. It offers a REST API as well, for easy
+integration with other projects and tools.
+
+For a live system, refer to the project's site:
- – {{ current_workout.creation_date }} -
+
+ – {{ current_workout.creation_date }} +
{% endif %}
+ {% trans "Protein" %}: {{entry.get_nutritional_values.protein|floatformat:"1"}}
+ {% trans "Carbohydrates" %}: {{entry.get_nutritional_values.carbohydrates|floatformat:"1"}} ({{entry.get_nutritional_values.carbohydrates_sugar|floatformat:"1"}})
+ {% trans "Fat" %}: {{entry.get_nutritional_values.fat|floatformat:"1"}} ({{entry.get_nutritional_values.fat|floatformat:"1"}})
+ {% if entry.get_nutritional_values.fibres %}
+ {% trans "Fibres" %}: {{entry.get_nutritional_values.fibres|floatformat:"1"}}
+ {% endif %}
+ {% if entry.get_nutritional_values.sodium %}
+ {% trans "Sodium" %}: {{entry.get_nutritional_values.sodium|floatformat:"1"}}
+ {% endif %}
+
{% trans "Add custom diary entry" %}
+{% endif %} {% endblock %} diff --git a/wger/nutrition/tests/test_ingredient.py b/wger/nutrition/tests/test_ingredient.py index fee2142ce..03a8f541a 100644 --- a/wger/nutrition/tests/test_ingredient.py +++ b/wger/nutrition/tests/test_ingredient.py @@ -236,14 +236,14 @@ class IngredientValuesTestCase(WgerTestCase): self.assertEqual(response.status_code, 200) result = json.loads(response.content.decode('utf8')) self.assertEqual(len(result), 8) - self.assertEqual(result, {u'sodium': u'0.01', - u'energy': u'1.76', - u'fat': u'0.08', - u'carbohydrates_sugar': u'0.00', - u'fat_saturated': u'0.03', - u'fibres': u'0.00', - u'protein': u'0.26', - u'carbohydrates': u'0.00'}) + self.assertEqual(result, {'sodium': '0.01', + 'energy': '1.76', + 'fat': '0.08', + 'carbohydrates_sugar': '0.00', + 'fat_saturated': '0.03', + 'fibres': '0.00', + 'protein': '0.26', + 'carbohydrates': '0.00'}) # Get the nutritional values in 1 unit of product response = self.client.get(reverse('api-ingredient-get-values', kwargs={'pk': 1}), @@ -254,14 +254,14 @@ class IngredientValuesTestCase(WgerTestCase): self.assertEqual(response.status_code, 200) result = json.loads(response.content.decode('utf8')) self.assertEqual(len(result), 8) - self.assertEqual(result, {u'sodium': u'0.61', - u'energy': u'196.24', - u'fat': u'9.13', - u'carbohydrates_sugar': u'0.00', - u'fat_saturated': u'3.62', - u'fibres': u'0.00', - u'protein': u'28.58', - u'carbohydrates': u'0.14'}) + self.assertEqual(result, {'sodium': '0.61', + 'energy': '196.24', + 'fat': '9.13', + 'carbohydrates_sugar': '0.00', + 'fat_saturated': '3.62', + 'fibres': '0.00', + 'protein': '28.58', + 'carbohydrates': '0.14'}) def test_calculate_value_anonymous(self): """ diff --git a/wger/nutrition/tests/test_nutrition_diary.py b/wger/nutrition/tests/test_nutrition_diary.py index f1403d904..2b29d7a2c 100644 --- a/wger/nutrition/tests/test_nutrition_diary.py +++ b/wger/nutrition/tests/test_nutrition_diary.py @@ -181,6 +181,45 @@ class NutritionDiaryTestCase(WgerTestCase): self.assertEqual(response.status_code, 403) self.assertEqual(LogItem.objects.filter(plan=plan).count(), 0) + def test_log_plan(self): + """ + Tests that logging a plan creates a log entry for all meals within the plan + """ + plan = NutritionPlan.objects.get(pk=1) + LogItem.objects.all().delete() + self.assertFalse(LogItem.objects.filter(plan=plan)) + self.user_login('test') + response = self.client.get( + reverse('nutrition:log:log_plan', kwargs={"plan_pk": 1})) + self.assertEqual(response.status_code, 302) + self.assertEqual(LogItem.objects.filter(plan=plan).count(), 3) + + def test_log_plan_logged_out(self): + """ + Tests that logging a plan doesn't work for a logged out user + """ + plan = NutritionPlan.objects.get(pk=1) + LogItem.objects.all().delete() + self.assertFalse(LogItem.objects.filter(plan=plan)) + response = self.client.get( + reverse('nutrition:log:log_plan', kwargs={"plan_pk": 1})) + self.assertEqual(response.status_code, 403) + self.assertEqual(LogItem.objects.filter(plan=plan).count(), 0) + + def test_log_plan_other_user(self): + """ + Tests that logging a plan doesn't work for a logged out user + """ + plan = NutritionPlan.objects.get(pk=1) + LogItem.objects.all().delete() + self.assertFalse(LogItem.objects.filter(plan=plan)) + self.user_login('admin') + response = self.client.get( + reverse('nutrition:log:log_plan', kwargs={"plan_pk": 1})) + + self.assertEqual(response.status_code, 403) + self.assertEqual(LogItem.objects.filter(plan=plan).count(), 0) + class AddMealItemUnitTestCase(WgerAddTestCase): """ diff --git a/wger/nutrition/tests/test_nutritional_cache.py b/wger/nutrition/tests/test_nutritional_cache.py new file mode 100644 index 000000000..cbd8ad9e7 --- /dev/null +++ b/wger/nutrition/tests/test_nutritional_cache.py @@ -0,0 +1,94 @@ +# Django +from django.contrib.auth.models import User +from django.core.cache import cache + +# wger +from wger.core.models import Language +from wger.core.tests.base_testcase import WgerTestCase +from wger.nutrition.models import ( + Meal, + MealItem, + NutritionPlan +) +from wger.utils.cache import cache_mapper + + +class NutritionaCacheTestCase(WgerTestCase): + + def create_nutrition_plan(self): + ''' + Create a nutrition plan and set dummy attributes that are required + Create meal and set dummy attributes that are required + Create meal item and set dummy attributes that are required + ''' + nutrition_plan = NutritionPlan() + nutrition_plan.user = User.objects.create_user(username='example_user_1') + nutrition_plan.language = Language.objects.get(short_name="en") + nutrition_plan.save() + meal = Meal() + meal.plan = nutrition_plan + meal.order = 1 + meal.save() + meal_item = MealItem() + meal_item.id = 1 + meal_item.meal = meal + meal_item.amount = 1 + meal_item.ingredient_id = 1 + meal_item.order = 1 + test_objects = [nutrition_plan, meal, meal_item] + return test_objects + + def test_cache_setting(self): + ''' + Test that a cache is set once the nutritional instance is created + ''' + nutrition_object = self.create_nutrition_plan()[0] + self.assertFalse(cache.get(cache_mapper.get_nutrition_cache_by_key(nutrition_object))) + nutrition_object.get_nutritional_values() + self.assertTrue(cache.get(cache_mapper.get_nutrition_cache_by_key(nutrition_object))) + + def test_nutrition_save_and_delete(self): + ''' + Test that cache is deleted when a nutrition is created or deleted. + ''' + nutrition_object = self.create_nutrition_plan()[0] + nutrition_object.get_nutritional_values() + self.assertTrue(cache.get(cache_mapper.get_nutrition_cache_by_key(nutrition_object))) + nutrition_object.save() + self.assertFalse(cache.get(cache_mapper.get_nutrition_cache_by_key(nutrition_object))) + nutrition_object.get_nutritional_values() + self.assertTrue(cache.get(cache_mapper.get_nutrition_cache_by_key(nutrition_object))) + nutrition_object.delete() + self.assertFalse(cache.get(cache_mapper.get_nutrition_cache_by_key(nutrition_object))) + + def test_meal_save_delete(self): + ''' + Test that the cache is deleted once a meal undergoes a save or delete operation + ''' + test_object_list = self.create_nutrition_plan() + nutritional_object = test_object_list[0] + meal = test_object_list[1] + nutritional_object.get_nutritional_values() + self.assertTrue(cache.get(cache_mapper.get_nutrition_cache_by_key(nutritional_object))) + meal.save() + self.assertFalse(cache.get(cache_mapper.get_nutrition_cache_by_key(nutritional_object.pk))) + nutritional_object.get_nutritional_values() + self.assertTrue(cache.get(cache_mapper.get_nutrition_cache_by_key(nutritional_object))) + meal.delete() + self.assertFalse(cache.get(cache_mapper.get_nutrition_cache_by_key(nutritional_object))) + + def test_meal_item_save_delete(self): + ''' + Test that the cache is deleted once a meal undergoes a save or delete operation + ''' + test_object_list = self.create_nutrition_plan() + nutritional_object = test_object_list[0] + meal_item = test_object_list[2] + nutritional_object.get_nutritional_values() + self.assertTrue(cache.get(cache_mapper.get_nutrition_cache_by_key(nutritional_object))) + meal_item.save() + self.assertFalse(cache.get(cache_mapper.get_nutrition_cache_by_key(nutritional_object.pk))) + nutritional_object.get_nutritional_values() + self.assertTrue(cache.get(cache_mapper.get_nutrition_cache_by_key(nutritional_object))) + meal_item.delete() + self.assertFalse(cache.get(cache_mapper.get_nutrition_cache_by_key(meal_item))) diff --git a/wger/nutrition/tests/test_pdf.py b/wger/nutrition/tests/test_pdf.py index aca200638..26098a6f5 100644 --- a/wger/nutrition/tests/test_pdf.py +++ b/wger/nutrition/tests/test_pdf.py @@ -47,8 +47,8 @@ class NutritionalPlanPdfExportTestCase(WgerTestCase): 'attachment; filename=nutritional-plan.pdf') # Approximate size - self.assertGreater(int(response['Content-Length']), 29000) - self.assertLess(int(response['Content-Length']), 34000) + self.assertGreater(int(response['Content-Length']), 38000) + self.assertLess(int(response['Content-Length']), 42000) def export_pdf(self, fail=False): """ @@ -68,8 +68,8 @@ class NutritionalPlanPdfExportTestCase(WgerTestCase): 'attachment; filename=nutritional-plan.pdf') # Approximate size - self.assertGreater(int(response['Content-Length']), 29000) - self.assertLess(int(response['Content-Length']), 34000) + self.assertGreater(int(response['Content-Length']), 38000) + self.assertLess(int(response['Content-Length']), 42000) # Create an empty plan user = User.objects.get(pk=2) @@ -90,8 +90,8 @@ class NutritionalPlanPdfExportTestCase(WgerTestCase): 'attachment; filename=nutritional-plan.pdf') # Approximate size - self.assertGreater(int(response['Content-Length']), 29000) - self.assertLess(int(response['Content-Length']), 33420) + self.assertGreater(int(response['Content-Length']), 38000) + self.assertLess(int(response['Content-Length']), 42000) def test_export_pdf_anonymous(self): """ diff --git a/wger/nutrition/urls.py b/wger/nutrition/urls.py index aafac4454..f198722c2 100644 --- a/wger/nutrition/urls.py +++ b/wger/nutrition/urls.py @@ -200,6 +200,9 @@ patterns_diary = [ url(r'^log-meal/(?PYou can also generate a token via the login endpoint. Send a
+username and password and you will get the user's token or a new one will be
+generated. At the moment it is not possible to register via the API.
You should always use HTTPS if possible when communicating with the server.
+
- When accessing exercises, consider that by default all exercises are
- returned, including those submitted by users but not yed approved. You will
- very probably want to add a &status=2 to your URL to only get the
- ones already added to the database.
-
Also note that, at the moment, to actually retrieve all the details for an exercise
you will need to fire up different queries for the images, comments, etc.
diff --git a/wger/tasks.py b/wger/tasks.py
index f6a1dea31..bc73c2a20 100644
--- a/wger/tasks.py
+++ b/wger/tasks.py
@@ -15,14 +15,10 @@
# You should have received a copy of the GNU Affero General Public License
# Standard Library
-import ctypes
import logging
import os
-import socket
+import pathlib
import sys
-import threading
-import time
-import webbrowser
# Django
import django
@@ -41,8 +37,7 @@ logger = logging.getLogger(__name__)
@task(help={'address': 'Address to bind to. Default: localhost',
'port': 'Port to use. Default: 8000',
- 'browser': 'Whether to open the application in a browser window. Default: false',
- 'settings-path': 'Path to settings file (absolute path recommended). Leave empty for '
+ 'settings-path': 'Path to settings file (absolute path). Leave empty for '
'default',
'extra-args': 'Additional arguments to pass to the builtin server. Pass as string: '
'"--arg1 --arg2=value". Default: none'})
@@ -51,8 +46,6 @@ def start(context, address='localhost', port=8000, browser=False, settings_path=
"""
Start the application using django's built in webserver
"""
- if browser:
- start_browser("http://{0}:{1}".format(address, port))
# Find the path to the settings and setup the django environment
setup_django_environment(settings_path)
@@ -65,37 +58,22 @@ def start(context, address='localhost', port=8000, browser=False, settings_path=
execute_from_command_line(argv)
-@task(help={'settings-path': 'Path to settings file (absolute path recommended). Leave empty for '
+@task(help={'settings-path': 'Path to settings file (absolute path). Leave empty for '
'default',
- 'database-path': 'Path to sqlite database (absolute path recommended). Leave empty '
- 'for default',
- 'address': 'Address to use. Default: localhost',
- 'port': 'Port to use. Default: 8000',
- 'browser': 'Whether to open the application in a browser window. Default: false',
- 'start-server': 'Whether to start the development server. Default: true'})
+ 'database-path': 'Path to sqlite database (absolute path). Leave empty '
+ 'for default'})
def bootstrap(context,
settings_path=None,
- database_path=None,
- address='localhost',
- port=8000,
- browser=False,
- start_server=True):
+ database_path=None):
"""
Performs all steps necessary to bootstrap the application
"""
- # Find url to wger
- address, port = detect_listen_opts(address, port)
- if port == 80:
- url = "http://{0}".format(address)
- else:
- url = "http://{0}:{1}".format(address, port)
-
# Create settings if necessary
if settings_path is None:
- settings_path = get_user_config_path('wger', 'settings.py')
+ settings_path = get_path('settings.py')
if not os.path.exists(settings_path):
- create_settings(context, settings_path=settings_path, database_path=database_path, url=url)
+ create_settings(context, settings_path=settings_path, database_path=database_path)
# Find the path to the settings and setup the django environment
setup_django_environment(settings_path)
@@ -109,41 +87,38 @@ def bootstrap(context,
# Download JS and CSS libraries
context.run("yarn install")
- context.run("sass core/static/scss/main.scss:core/static/yarn/bootstrap-compiled.css")
-
- # Start the webserver
- if start_server:
- print('*** Bootstraping complete, starting application')
- start(context, address=address, port=port, browser=browser, settings_path=settings_path)
+ context.run("yarn build:css:sass")
-@task(help={'settings-path': 'Path to settings file (absolute path recommended). Leave empty for '
+@task(help={'settings-path': 'Path to settings file (absolute path). Leave empty for '
'default',
- 'database-path': 'Path to sqlite database (absolute path recommended). Leave empty '
+ 'database-path': 'Path to sqlite database (absolute path). Leave empty '
'for default',
'database-type': 'Database type to use. Supported: sqlite3, postgresql. Default: '
'sqlite3',
- 'key-length': 'Lenght of the generated secret key. Default: 50'})
-def create_settings(context, settings_path=None, database_path=None, url=None,
- database_type='sqlite3', key_length=50):
+ 'key-length': 'Length of the generated secret key. Default: 50'})
+def create_settings(context,
+ settings_path=None,
+ database_path=None,
+ database_type='sqlite3',
+ key_length=50):
"""
Creates a local settings file
"""
if settings_path is None:
- settings_path = get_user_config_path('wger', 'settings.py')
+ settings_path = get_path('settings.py')
settings_module = os.path.dirname(settings_path)
print("*** Creating settings file at {0}".format(settings_module))
if database_path is None:
- database_path = get_user_data_path('wger', 'database.sqlite')
- dbpath_value = repr(database_path)
+ database_path = get_path('database.sqlite').as_posix()
+ dbpath_value = database_path
- media_folder_path = repr(get_user_data_path('wger', 'media'))
+ media_folder_path = get_path('media').as_posix()
# Use localhost with default django port if no URL given
- if url is None:
- url = 'http://localhost:8000'
+ url = 'http://localhost:8000'
# Fill in the config file template
settings_template = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'settings.tpl')
@@ -153,7 +128,7 @@ def create_settings(context, settings_path=None, database_path=None, url=None,
# The environment variable is set by travis during testing
if database_type == 'postgresql':
dbengine = 'postgresql_psycopg2'
- dbname = "'test_wger'"
+ dbname = 'test_wger'
dbuser = 'postgres'
dbpassword = ''
dbhost = '127.0.0.1'
@@ -191,7 +166,7 @@ def create_settings(context, settings_path=None, database_path=None, url=None,
settings_file.write(settings_content)
-@task(help={'settings-path': 'Path to settings file (absolute path recommended). Leave empty for '
+@task(help={'settings-path': 'Path to settings file (absolute path). Leave empty for '
'default'})
def create_or_reset_admin(context, settings_path=None):
"""
@@ -217,7 +192,7 @@ def create_or_reset_admin(context, settings_path=None):
call_command("loaddata", path + "users.json")
-@task(help={'settings-path': 'Path to settings file (absolute path recommended). Leave empty for '
+@task(help={'settings-path': 'Path to settings file (absolute path). Leave empty for '
'default'})
def migrate_db(context, settings_path=None):
"""
@@ -230,7 +205,7 @@ def migrate_db(context, settings_path=None):
call_command("migrate")
-@task(help={'settings-path': 'Path to settings file (absolute path recommended). Leave empty for '
+@task(help={'settings-path': 'Path to settings file (absolute path). Leave empty for '
'default'})
def load_fixtures(context, settings_path=None):
"""
@@ -290,9 +265,9 @@ def config_location(context):
Returns the default location for the settings file and the data folder
"""
print('Default locations:')
- print('* settings: {0}'.format(get_user_config_path('wger', 'settings.py')))
- print('* media folder: {0}'.format(get_user_data_path('wger', 'media')))
- print('* database path: {0}'.format(get_user_data_path('wger', 'database.sqlite')))
+ print('* settings: {0}'.format(get_path('settings.py')))
+ print('* media folder: {0}'.format(get_path('media')))
+ print('* database path: {0}'.format(get_path('database.sqlite')))
#
@@ -304,67 +279,15 @@ def config_location(context):
# packaged has a different sys path than the local one)
#
+def get_path(file="settings.py") -> pathlib.Path:
+ """
+ Return the path of the given file relatively to the wger source folder
-def get_user_data_path(*args):
- if sys.platform == "win32":
- return win32_get_app_data_path(*args)
-
- data_home = os.environ.get(
- 'XDG_DATA_HOME', os.path.join(
- os.path.expanduser('~'), '.local', 'share'))
-
- return os.path.join(data_home, *args)
-
-
-def get_user_config_path(*args):
- if sys.platform == "win32":
- return win32_get_app_data_path(*args)
-
- config_home = os.environ.get(
- 'XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config'))
-
- return os.path.join(config_home, *args)
-
-
-def win32_get_app_data_path(*args):
- shell32 = ctypes.WinDLL("shell32.dll")
- SHGetFolderPath = shell32.SHGetFolderPathW
- SHGetFolderPath.argtypes = (
- ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, ctypes.c_uint32,
- ctypes.c_wchar_p)
- SHGetFolderPath.restype = ctypes.c_uint32
-
- CSIDL_LOCAL_APPDATA = 0x001c
- MAX_PATH = 260
-
- buf = ctypes.create_unicode_buffer(MAX_PATH)
- res = SHGetFolderPath(0, CSIDL_LOCAL_APPDATA, 0, 0, buf)
- if res != 0:
- raise Exception("Could not deterime APPDATA path")
-
- return os.path.join(buf.value, *args)
-
-
-def detect_listen_opts(address, port):
- if address is None:
- try:
- address = socket.gethostbyname(socket.gethostname())
- except socket.error:
- address = "127.0.0.1"
-
- if port is None:
- # test if we can use port 80
- s = socket.socket()
- port = 80
- try:
- s.bind((address, port))
- s.listen(-1)
- except socket.error:
- port = 8000
- finally:
- s.close()
-
- return address, port
+ Note: one parent is the step from e.g. some-checkout/wger/settings.py
+ to some-checkout/wger, the second one to get to the source folder
+ itself.
+ """
+ return (pathlib.Path(__file__).parent.parent / file).resolve()
def setup_django_environment(settings_path):
@@ -374,7 +297,7 @@ def setup_django_environment(settings_path):
# Use default settings if the user didn't specify something else
if settings_path is None:
- settings_path = get_user_config_path('wger', 'settings.py')
+ settings_path = get_path('settings.py').as_posix()
print('*** No settings given, using {0}'.format(settings_path))
# Find out file path and fine name of settings and setup django
@@ -411,17 +334,3 @@ def database_exists():
sys.exit(0)
else:
return True
-
-
-def start_browser(url):
- """
- Start the web browser with the given URL
- """
- browser = webbrowser.get()
-
- def function():
- time.sleep(1)
- browser.open(url)
-
- thread = threading.Thread(target=function)
- thread.start()
diff --git a/wger/urls.py b/wger/urls.py
index 9ab55d4c7..0f8276180 100644
--- a/wger/urls.py
+++ b/wger/urls.py
@@ -140,6 +140,10 @@ urlpatterns += [
nutrition_api_views.search,
name='ingredient-search'),
url(r'^api/v2/', include(router.urls)),
+
+ # The api user login
+ url(r'^api/v2/login/$', core_api_views.UserAPILoginView.as_view({
+ 'post': 'post'}), name='api_user'),
]
#
diff --git a/wger/utils/api_token.py b/wger/utils/api_token.py
new file mode 100644
index 000000000..721994f7b
--- /dev/null
+++ b/wger/utils/api_token.py
@@ -0,0 +1,44 @@
+# This file is part of wger Workout Manager.
+#
+# wger Workout Manager is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# wger Workout Manager is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+
+# Standard Library
+import logging
+
+# Third Party
+from rest_framework.authtoken.models import Token
+
+
+logger = logging.getLogger(__name__)
+
+
+def create_token(user, force_new=False):
+ """
+ Creates a new token for a user or returns the existing one.
+
+ :param user: User object
+ :param force_new: forces creating a new token
+ """
+ token = False
+
+ try:
+ token = Token.objects.get(user=user)
+ except Token.DoesNotExist:
+ force_new = True
+
+ if force_new:
+ if token:
+ token.delete()
+ token = Token.objects.create(user=user)
+
+ return token
diff --git a/wger/utils/cache.py b/wger/utils/cache.py
index adafa77f1..700fd7398 100644
--- a/wger/utils/cache.py
+++ b/wger/utils/cache.py
@@ -58,6 +58,7 @@ class CacheKeyMapper(object):
INGREDIENT_CACHE_KEY = 'ingredient-{0}'
WORKOUT_CANONICAL_REPRESENTATION = 'workout-canonical-representation-{0}'
WORKOUT_LOG_LIST = 'workout-log-hash-{0}'
+ NUTRITION_CACHE_KEY = 'nutrition-cache-log-{0}'
def get_pk(self, param):
"""
@@ -100,5 +101,11 @@ class CacheKeyMapper(object):
"""
return self.WORKOUT_LOG_LIST.format(hash_value)
+ def get_nutrition_cache_by_key(self, params):
+ """
+ get nutritional info values canonical representation using primary key.
+ """
+ return self.NUTRITION_CACHE_KEY.format(self.get_pk(params))
+
cache_mapper = CacheKeyMapper()
diff --git a/wger/utils/context_processor.py b/wger/utils/context_processor.py
index aac9aee86..68ac83da6 100644
--- a/wger/utils/context_processor.py
+++ b/wger/utils/context_processor.py
@@ -36,7 +36,7 @@ def processor(request):
groups = Membership.objects.filter(user=user) if user.is_authenticated else []
for lang in settings.LANGUAGES:
- i18n_path[lang[0]] = u'/{0}{1}'.format(lang[0], full_path[3:])
+ i18n_path[lang[0]] = '/{0}{1}'.format(lang[0], full_path[3:])
context = {
# Application version
diff --git a/wger/utils/helpers.py b/wger/utils/helpers.py
index e7c0e36e1..627f35aa6 100644
--- a/wger/utils/helpers.py
+++ b/wger/utils/helpers.py
@@ -230,7 +230,7 @@ def smart_capitalize(input):
"""
out = []
for word in input.split(' '):
- if len(word) > 2 and word[0] != u'ß':
+ if len(word) > 2 and word[0] != 'ß':
out.append(word[:1].upper() + word[1:])
else:
out.append(word)
diff --git a/wger/utils/pdf.py b/wger/utils/pdf.py
index 79a450234..f0f5388b6 100644
--- a/wger/utils/pdf.py
+++ b/wger/utils/pdf.py
@@ -22,13 +22,19 @@ from django.core.exceptions import ObjectDoesNotExist
from django.utils import translation
# Third Party
+from reportlab.lib import colors
+from reportlab.lib.colors import HexColor
from reportlab.lib.styles import (
ParagraphStyle,
StyleSheet1
)
+from reportlab.lib.units import cm
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
-from reportlab.platypus import Paragraph
+from reportlab.platypus import (
+ Image,
+ Paragraph
+)
# wger
from wger import get_version
@@ -93,18 +99,29 @@ def render_footer(url, date=None):
"""
if not date:
date = datetime.date.today().strftime("%d.%m.%Y")
- p = Paragraph("""