From 77905c596b0ff2b13f03aa862f7f93f6de68ae1b Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Tue, 30 Jun 2015 09:21:13 +0200 Subject: [PATCH 01/10] Moved default SQLite database to DATA_DIR --- src/pretix/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pretix/settings.py b/src/pretix/settings.py index d36b429e2b..83cb8c701f 100644 --- a/src/pretix/settings.py +++ b/src/pretix/settings.py @@ -38,7 +38,7 @@ DEBUG = TEMPLATE_DEBUG = config.getboolean('django', 'debug', fallback=False) DATABASES = { 'default': { 'ENGINE': 'django.db.backends.' + config.get('database', 'backend', fallback='sqlite3'), - 'NAME': config.get('database', 'name', fallback=os.path.join(BASE_DIR, 'db.sqlite3')), + 'NAME': config.get('database', 'name', fallback=os.path.join(DATA_DIR, 'db.sqlite3')), 'USER': config.get('database', 'user', fallback=''), 'PASSWORD': config.get('database', 'password', fallback=''), 'HOST': config.get('database', 'host', fallback=''), From 235769ce4fefb6f5dd04d3299fd1e4fe718610dc Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Tue, 30 Jun 2015 16:48:00 +0200 Subject: [PATCH 02/10] Added the possibility to use Redis for quota locks --- deployment/docker/standalone/Dockerfile | 1 + doc/admin/config.rst | 21 +++++ src/pretix/base/models.py | 31 +------ src/pretix/base/services/locking.py | 111 ++++++++++++++++++++++++ src/pretix/settings.py | 31 +++++-- src/requirements/redis.txt | 2 + 6 files changed, 165 insertions(+), 32 deletions(-) create mode 100644 src/pretix/base/services/locking.py create mode 100644 src/requirements/redis.txt diff --git a/deployment/docker/standalone/Dockerfile b/deployment/docker/standalone/Dockerfile index 3915fce4fe..1969e1ea0d 100644 --- a/deployment/docker/standalone/Dockerfile +++ b/deployment/docker/standalone/Dockerfile @@ -27,6 +27,7 @@ RUN pip3 install -r requirements.txt RUN pip3 install -r requirements/mysql.txt RUN pip3 install -r requirements/postgres.txt RUN pip3 install -r requirements/memcached.txt +RUN pip3 install -r requirements/redis.txt RUN pip3 install gunicorn RUN make diff --git a/doc/admin/config.rst b/doc/admin/config.rst index 04b562d523..ce723e8f1c 100644 --- a/doc/admin/config.rst +++ b/doc/admin/config.rst @@ -177,4 +177,25 @@ You can use an existing memcached server as pretix's caching backend:: If no memcached is configures, pretix will use Django's built-in local-memory caching method. + +Redis +----- + +If a redis server is configured, pretix can use it for locking, caching and session storage +to speed up various operations:: + + [redis] + location=redis://127.0.0.1:6379/1 + sessions=false + +``location`` + The location of memcached, as an URL of the form ``redis://[:password]@localhost:6379/0`` + or ``unix://[:password]@/path/to/socket.sock?db=0`` + +``session`` + When this is set to true, redis will be used as the session storage. + +If no redis is configured, pretix will store sessions and locks in the database. If memcached +is configured, memcached will be used for caching instead of redis. + .. _Python documentation: https://docs.python.org/3/library/configparser.html?highlight=configparser#supported-ini-file-structure \ No newline at end of file diff --git a/src/pretix/base/models.py b/src/pretix/base/models.py index 4bda0b2e03..96a06a164f 100644 --- a/src/pretix/base/models.py +++ b/src/pretix/base/models.py @@ -1408,22 +1408,8 @@ class Quota(Versionable): :raises Quota.LockTimeoutException: if the quota is locked every time we try to obtain the lock """ - retries = 5 - for i in range(retries): - dt = now() - updated = Quota.objects.current.filter( - Q(identity=self.identity) - & Q(Q(locked__lt=dt - timedelta(seconds=120)) | Q(locked__isnull=True)) - & Q(version_end_date__isnull=True) - ).update( - locked=dt - ) - if updated: - self.locked_here = dt - self.locked = dt - return True - time.sleep(2 ** i / 100) - raise Quota.LockTimeoutException() + from .services import locking + return locking.lock_quota(self) def release(self, force=False): """ @@ -1431,17 +1417,8 @@ class Quota(Versionable): the lock will only be released if it was issued in _this_ python representation of the database object. """ - if not self.locked_here and not force: - return False - updated = Quota.objects.current.filter( - identity=self.identity, - version_end_date__isnull=True - ).update( - locked=None - ) - self.locked_here = None - self.locked = None - return updated + from .services import locking + return locking.release_quota(self, force) class Order(Versionable): diff --git a/src/pretix/base/services/locking.py b/src/pretix/base/services/locking.py new file mode 100644 index 0000000000..1cc2b42c6e --- /dev/null +++ b/src/pretix/base/services/locking.py @@ -0,0 +1,111 @@ +from datetime import timedelta +import logging +import time +from django.db.models import Q +from django.utils.timezone import now +from pretix import settings + +from pretix.base.models import Quota +from redis import RedisError + +logger = logging.getLogger('pretix.base.locking') + + +def lock_quota(quota): + """ + Issue a lock on this quota so nobody can take tickets from this quota until + you release the lock. Will retry 5 times on failure. + + :raises Quota.LockTimeoutException: if the quota is locked every time we try + to obtain the lock + """ + if settings.HAS_REDIS: + return lock_quota_redis(quota) + else: + return lock_quota_db(quota) + + +def lock_quota_db(quota): + retries = 5 + for i in range(retries): + dt = now() + + updated = Quota.objects.current.filter( + Q(identity=quota.identity) + & Q(Q(locked__lt=dt - timedelta(seconds=120)) | Q(locked__isnull=True)) + & Q(version_end_date__isnull=True) + ).update( + locked=dt + ) + if updated: + quota.locked_here = dt + quota.locked = dt + return True + time.sleep(2 ** i / 100) + raise Quota.LockTimeoutException() + + +def release_quota(quota, force=False): + """ + Release a lock placed by :py:meth:`lock()`. If the parameter force is not set to ``True``, + the lock will only be released if it was issued in _this_ python + representation of the database object. + """ + if not quota.locked_here and not force: + return False + if settings.HAS_REDIS: + return release_quota_redis(quota) + else: + return release_quota_db(quota) + + +def release_quota_db(quota): + updated = Quota.objects.current.filter( + identity=quota.identity, + version_end_date__isnull=True + ).update( + locked=None + ) + quota.locked_here = None + quota.locked = None + return updated + + +def redis_lock_from_quota(quota): + from django_redis import get_redis_connection + from redis.lock import Lock + + if not hasattr(quota, '_redis_lock'): + rc = get_redis_connection("redis") + quota._redis_lock = Lock(redis=rc, name='pretix_quota_%s' % quota.identity, timeout=120) + return quota._redis_lock + + +def lock_quota_redis(quota): + from redis.exceptions import RedisError + lock = redis_lock_from_quota(quota) + retries = 5 + for i in range(retries): + dt = now() + try: + if lock.acquire(False): + quota.locked_here = dt + quota.locked = dt + return True + except RedisError: + logger.exception('Error locking a quota') + raise Quota.LockTimeoutException() + time.sleep(2 ** i / 100) + raise Quota.LockTimeoutException() + + +def release_quota_redis(quota): + lock = redis_lock_from_quota(quota) + try: + lock.release() + except RedisError: + logger.exception('Error releasing a quota lock') + raise Quota.LockTimeoutException() + quota.locked_here = None + quota.locked = None + return True diff --git a/src/pretix/settings.py b/src/pretix/settings.py index 83cb8c701f..dd3a12bac0 100644 --- a/src/pretix/settings.py +++ b/src/pretix/settings.py @@ -76,13 +76,34 @@ SESSION_COOKIE_SECURE = SESSION_COOKIE_HTTPONLY = config.getboolean( LANGUAGE_COOKIE_DOMAIN = SESSION_COOKIE_DOMAIN = CSRF_COOKIE_DOMAIN = config.get( 'pretix', 'cookiedomain', fallback=None) -if config.has_option('memcached', 'location'): - CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache', - 'LOCATION': config.get('memcached', 'location'), +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'LOCATION': 'unique-snowflake', + } +} + +HAS_MEMCACHED = config.has_option('memcached', 'location') +if HAS_MEMCACHED: + CACHES['default'] = { + 'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache', + 'LOCATION': config.get('memcached', 'location'), + } + +HAS_REDIS = config.has_option('redis', 'location') +if HAS_REDIS: + CACHES['redis'] = { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": config.get('redis', 'location'), + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", } } + if not HAS_MEMCACHED: + CACHES['default'] = CACHES['redis'] + if config.getboolean('redis', 'sessions', fallback=False): + SESSION_ENGINE = "django.contrib.sessions.backends.cache" + SESSION_CACHE_ALIAS = "redis" # Internal settings diff --git a/src/requirements/redis.txt b/src/requirements/redis.txt new file mode 100644 index 0000000000..11ae9e7154 --- /dev/null +++ b/src/requirements/redis.txt @@ -0,0 +1,2 @@ +django-redis>=4.1,<4.2 +redis>=2.10,<2.11 From 7ca3389d08b9c6c068d54e81463ea612a3d71379 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Tue, 30 Jun 2015 18:15:55 +0200 Subject: [PATCH 03/10] Fixed an ImportError when redis-py is not installed --- src/pretix/base/services/locking.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pretix/base/services/locking.py b/src/pretix/base/services/locking.py index 1cc2b42c6e..16c9b810c1 100644 --- a/src/pretix/base/services/locking.py +++ b/src/pretix/base/services/locking.py @@ -6,7 +6,6 @@ from django.utils.timezone import now from pretix import settings from pretix.base.models import Quota -from redis import RedisError logger = logging.getLogger('pretix.base.locking') @@ -100,6 +99,7 @@ def lock_quota_redis(quota): def release_quota_redis(quota): + from redis import RedisError lock = redis_lock_from_quota(quota) try: lock.release() From f6eca700f9f1d4ea678b91cb35a71b20b10c0ce6 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Tue, 30 Jun 2015 18:29:25 +0200 Subject: [PATCH 04/10] Fixed a KeyError in the banktransfer plugin --- src/pretix/plugins/banktransfer/payment.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pretix/plugins/banktransfer/payment.py b/src/pretix/plugins/banktransfer/payment.py index 6dec274034..f13cefe35e 100644 --- a/src/pretix/plugins/banktransfer/payment.py +++ b/src/pretix/plugins/banktransfer/payment.py @@ -53,7 +53,6 @@ class BankTransfer(BasePaymentProvider): def order_control_render(self, request, order) -> str: if order.payment_info: payment_info = json.loads(order.payment_info) - payment_info['amount'] /= 100 else: payment_info = None template = get_template('pretixplugins/banktransfer/control.html') From d301bb55baaba8ad081378b641df32fcca23547e Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Tue, 30 Jun 2015 19:19:41 +0200 Subject: [PATCH 05/10] Fixed #77 -- Enabled and improved responsiveness --- .../static/pretixcontrol/less/main.less | 12 +- .../templates/pretixcontrol/auth/base.html | 1 + .../control/templates/pretixcontrol/base.html | 24 +++- .../pretixcontrol/event/permissions.html | 76 ++++++------ .../pretixcontrol/item/variations_1d.html | 40 +++--- .../pretixcontrol/item/variations_nd.html | 56 ++++----- .../pretixcontrol/items/categories.html | 44 +++---- .../templates/pretixcontrol/items/index.html | 46 +++---- .../pretixcontrol/items/properties.html | 38 +++--- .../pretixcontrol/items/questions.html | 42 ++++--- .../templates/pretixcontrol/items/quotas.html | 62 +++++----- .../templates/pretixcontrol/orders/index.html | 50 ++++---- .../pretixcontrol/orders/overview.html | 114 +++++++++--------- .../static/pretixpresale/less/event.less | 8 ++ .../static/pretixpresale/less/main.less | 2 +- .../templates/pretixpresale/event/base.html | 1 + .../templates/pretixpresale/event/order.html | 1 + .../templates/pretixpresale/event/orders.html | 59 ++++----- 18 files changed, 369 insertions(+), 307 deletions(-) diff --git a/src/pretix/control/static/pretixcontrol/less/main.less b/src/pretix/control/static/pretixcontrol/less/main.less index 4094d2cc4d..9b54c49c2b 100644 --- a/src/pretix/control/static/pretixcontrol/less/main.less +++ b/src/pretix/control/static/pretixcontrol/less/main.less @@ -18,6 +18,11 @@ nav.navbar { margin: 0; } +.navbar-header .navbar-events { + color: white; + padding-top: 6px; + padding-bottom: 6px; +} .nav-pills { margin-bottom: 10px; } @@ -66,4 +71,9 @@ nav.navbar { transform: rotateX(180deg); -ms-transform: rotateX(180deg); /* IE 9 */ -webkit-transform: rotateX(180deg); /* Safari and Chrome */ -} \ No newline at end of file +} +@media (max-width: @screen-sm-max) { + .navbar-nav { + margin-left: 0; + } +} diff --git a/src/pretix/control/templates/pretixcontrol/auth/base.html b/src/pretix/control/templates/pretixcontrol/auth/base.html index 970b14a708..b47e10833d 100644 --- a/src/pretix/control/templates/pretixcontrol/auth/base.html +++ b/src/pretix/control/templates/pretixcontrol/auth/base.html @@ -8,6 +8,7 @@ {% compress css %} {% endcompress %} +
diff --git a/src/pretix/control/templates/pretixcontrol/base.html b/src/pretix/control/templates/pretixcontrol/base.html index bf1e1656fc..5f46d54864 100644 --- a/src/pretix/control/templates/pretixcontrol/base.html +++ b/src/pretix/control/templates/pretixcontrol/base.html @@ -18,20 +18,26 @@ {% endcompress %} {{ html_head|safe }} +