mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-06-07 09:24:20 -04:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a3fe375a83 | |||
| 777b06bbeb | |||
| e89c4a3c85 | |||
| 7a02d25d0b | |||
| c7a673bbbf |
@@ -1,5 +1,18 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [8.1.3](https://github.com/mediacms-io/mediacms/compare/v8.1.2...v8.1.3) (2026-05-19)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* django connection settings ([#1529](https://github.com/mediacms-io/mediacms/issues/1529)) ([e89c4a3](https://github.com/mediacms-io/mediacms/commit/e89c4a3c8523574b5852a434ed67e281b6290584))
|
||||||
|
* prestart.sh loaddata re-runs on every container restart ([#1502](https://github.com/mediacms-io/mediacms/issues/1502)) ([777b06b](https://github.com/mediacms-io/mediacms/commit/777b06bbebf141e5b1cb27e17533fe65d57eb6cd))
|
||||||
|
|
||||||
|
## [8.1.2](https://github.com/mediacms-io/mediacms/compare/v8.1.1...v8.1.2) (2026-05-18)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* remove redundant check ([#1528](https://github.com/mediacms-io/mediacms/issues/1528)) ([c7a673b](https://github.com/mediacms-io/mediacms/commit/c7a673bbbf46efc37621dc4a5109a85fc10e1317))
|
||||||
|
|
||||||
## [8.1.1](https://github.com/mediacms-io/mediacms/compare/v8.1.0...v8.1.1) (2026-05-18)
|
## [8.1.1](https://github.com/mediacms-io/mediacms/compare/v8.1.0...v8.1.1) (2026-05-18)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ from __future__ import absolute_import
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from celery import Celery
|
from celery import Celery
|
||||||
|
from celery.signals import worker_process_init
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.db import connections
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cms.settings")
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cms.settings")
|
||||||
app = Celery("cms")
|
app = Celery("cms")
|
||||||
@@ -20,3 +22,13 @@ app.conf.task_always_eager = settings.CELERY_TASK_ALWAYS_EAGER
|
|||||||
|
|
||||||
|
|
||||||
app.conf.worker_prefetch_multiplier = 1
|
app.conf.worker_prefetch_multiplier = 1
|
||||||
|
|
||||||
|
|
||||||
|
@worker_process_init.connect
|
||||||
|
def close_db_pool_on_fork(**_):
|
||||||
|
# psycopg3's ConnectionPool is not fork-safe: children inherit dead sockets
|
||||||
|
# from the parent's pool and block on getconn() until PoolTimeout. Dispose
|
||||||
|
# the inherited pool here so each prefork child opens its own on first use.
|
||||||
|
# NB: plain conn.close() would only putconn() back into the broken pool.
|
||||||
|
for conn in connections.all():
|
||||||
|
conn.close_pool()
|
||||||
|
|||||||
+19
-1
@@ -407,7 +407,25 @@ LOGGING = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
DATABASES = {"default": {"ENGINE": "django.db.backends.postgresql", "NAME": "mediacms", "HOST": "127.0.0.1", "PORT": "5432", "USER": "mediacms", "PASSWORD": "mediacms", "OPTIONS": {'pool': True}}}
|
DATABASES = {
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.postgresql",
|
||||||
|
"NAME": "mediacms",
|
||||||
|
"HOST": "127.0.0.1",
|
||||||
|
"PORT": "5432",
|
||||||
|
"USER": "mediacms",
|
||||||
|
"PASSWORD": "mediacms",
|
||||||
|
"OPTIONS": {
|
||||||
|
"pool": {
|
||||||
|
"min_size": 2,
|
||||||
|
"max_size": 8,
|
||||||
|
"timeout": 10,
|
||||||
|
"max_lifetime": 30 * 60,
|
||||||
|
"max_idle": 10 * 60,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
REDIS_LOCATION = "redis://127.0.0.1:6379/1"
|
REDIS_LOCATION = "redis://127.0.0.1:6379/1"
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
VERSION = "8.1.1"
|
VERSION = "8.1.2"
|
||||||
|
|||||||
@@ -12,7 +12,15 @@ DATABASES = {
|
|||||||
"PORT": os.getenv('POSTGRES_PORT', '5432'),
|
"PORT": os.getenv('POSTGRES_PORT', '5432'),
|
||||||
"USER": os.getenv('POSTGRES_USER', 'mediacms'),
|
"USER": os.getenv('POSTGRES_USER', 'mediacms'),
|
||||||
"PASSWORD": os.getenv('POSTGRES_PASSWORD', 'mediacms'),
|
"PASSWORD": os.getenv('POSTGRES_PASSWORD', 'mediacms'),
|
||||||
"OPTIONS": {'pool': True},
|
"OPTIONS": {
|
||||||
|
"pool": {
|
||||||
|
"min_size": 2,
|
||||||
|
"max_size": 8,
|
||||||
|
"timeout": 10,
|
||||||
|
"max_lifetime": 30 * 60,
|
||||||
|
"max_idle": 10 * 60,
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ ADMIN_PASSWORD=${ADMIN_PASSWORD:-$RANDOM_ADMIN_PASS}
|
|||||||
if [ X"$ENABLE_MIGRATIONS" = X"yes" ]; then
|
if [ X"$ENABLE_MIGRATIONS" = X"yes" ]; then
|
||||||
echo "Running migrations service"
|
echo "Running migrations service"
|
||||||
python manage.py migrate
|
python manage.py migrate
|
||||||
EXISTING_INSTALLATION=`echo "from users.models import User; print(User.objects.exists())" |python manage.py shell`
|
EXISTING_INSTALLATION=`echo "from users.models import User; print(User.objects.exists())" |python manage.py shell 2>/dev/null | tail -1`
|
||||||
if [ "$EXISTING_INSTALLATION" = "True" ]; then
|
if [ "$EXISTING_INSTALLATION" = "True" ]; then
|
||||||
echo "Loaddata has already run"
|
echo "Loaddata has already run"
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from django.conf import settings
|
|||||||
from django.contrib.postgres.indexes import GinIndex
|
from django.contrib.postgres.indexes import GinIndex
|
||||||
from django.contrib.postgres.search import SearchVectorField
|
from django.contrib.postgres.search import SearchVectorField
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
from django.db import models
|
from django.db import models, transaction
|
||||||
from django.db.models import Func, Value
|
from django.db.models import Func, Value
|
||||||
from django.db.models.signals import m2m_changed, post_delete, post_save, pre_delete
|
from django.db.models.signals import m2m_changed, post_delete, post_save, pre_delete
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
@@ -536,7 +536,9 @@ class Media(models.Model):
|
|||||||
|
|
||||||
from .. import tasks
|
from .. import tasks
|
||||||
|
|
||||||
tasks.produce_sprite_from_video.delay(self.friendly_token)
|
# Defer until the surrounding transaction commits so the worker can
|
||||||
|
# actually find the Media row. Runs immediately if not in a tx.
|
||||||
|
transaction.on_commit(lambda token=self.friendly_token: tasks.produce_sprite_from_video.delay(token))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def encode(self, profiles=[], force=True, chunkize=True):
|
def encode(self, profiles=[], force=True, chunkize=True):
|
||||||
|
|||||||
@@ -660,7 +660,9 @@ class MediaBulkUserActions(APIView):
|
|||||||
|
|
||||||
# Prioritize category_uids
|
# Prioritize category_uids
|
||||||
if category_uids:
|
if category_uids:
|
||||||
categories = Category.objects.filter(uid__in=category_uids)
|
requested = Category.objects.filter(uid__in=category_uids)
|
||||||
|
allowed_ids = [cat.id for cat in requested if not cat.is_rbac_category or request.user.has_contributor_access_to_category(cat)]
|
||||||
|
categories = Category.objects.filter(id__in=allowed_ids)
|
||||||
elif lti_context_id:
|
elif lti_context_id:
|
||||||
# Filter categories by lti_context_id and ensure they ARE RBAC categories
|
# Filter categories by lti_context_id and ensure they ARE RBAC categories
|
||||||
potential_categories = Category.objects.filter(lti_context_id=lti_context_id, is_rbac_category=True)
|
potential_categories = Category.objects.filter(lti_context_id=lti_context_id, is_rbac_category=True)
|
||||||
@@ -691,9 +693,11 @@ class MediaBulkUserActions(APIView):
|
|||||||
if not category_uids:
|
if not category_uids:
|
||||||
return Response({"detail": "category_uids is required for remove_from_category action"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"detail": "category_uids is required for remove_from_category action"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
categories = Category.objects.filter(uid__in=category_uids)
|
requested = Category.objects.filter(uid__in=category_uids)
|
||||||
|
allowed_ids = [cat.id for cat in requested if not cat.is_rbac_category or request.user.has_contributor_access_to_category(cat)]
|
||||||
|
categories = Category.objects.filter(id__in=allowed_ids)
|
||||||
if not categories:
|
if not categories:
|
||||||
return Response({"detail": "No matching categories found"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"detail": "No matching categories found or access denied"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
removed_count = 0
|
removed_count = 0
|
||||||
for category in categories:
|
for category in categories:
|
||||||
|
|||||||
@@ -1,16 +1,7 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
from .keys import ensure_keys_exist
|
|
||||||
|
|
||||||
|
|
||||||
class LtiConfig(AppConfig):
|
class LtiConfig(AppConfig):
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
name = 'lti'
|
name = 'lti'
|
||||||
verbose_name = 'LTI 1.3 Integration'
|
verbose_name = 'LTI 1.3 Integration'
|
||||||
|
|
||||||
def ready(self):
|
|
||||||
"""Initialize LTI app - ensure keys exist"""
|
|
||||||
try:
|
|
||||||
ensure_keys_exist()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|||||||
@@ -21,10 +21,3 @@ def get_jwks():
|
|||||||
"""
|
"""
|
||||||
public_key = load_public_key()
|
public_key = load_public_key()
|
||||||
return {'keys': [public_key]}
|
return {'keys': [public_key]}
|
||||||
|
|
||||||
|
|
||||||
def ensure_keys_exist():
|
|
||||||
"""Ensure key pair exists in database, generate if not"""
|
|
||||||
from .models import LTIToolKeys
|
|
||||||
|
|
||||||
LTIToolKeys.get_or_create_keys()
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mediacms",
|
"name": "mediacms",
|
||||||
"version": "8.1.1",
|
"version": "8.1.3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@semantic-release/changelog": "^6.0.3",
|
"@semantic-release/changelog": "^6.0.3",
|
||||||
"@semantic-release/git": "^10.0.1",
|
"@semantic-release/git": "^10.0.1",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import base64
|
import base64
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
|
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
|
||||||
from allauth.socialaccount.models import SocialApp
|
from allauth.socialaccount.models import SocialApp
|
||||||
@@ -22,7 +23,10 @@ class SAMLAccountAdapter(DefaultSocialAccountAdapter):
|
|||||||
|
|
||||||
def populate_user(self, request, sociallogin, data):
|
def populate_user(self, request, sociallogin, data):
|
||||||
user = sociallogin.user
|
user = sociallogin.user
|
||||||
user.username = sociallogin.account.uid
|
raw_uid = sociallogin.account.uid or ""
|
||||||
|
# Match the user URL pattern in users/urls.py: only [\w@._-] is reverse-able.
|
||||||
|
sanitized = re.sub(r"[^\w.@-]", "_", raw_uid, flags=re.ASCII)
|
||||||
|
user.username = sanitized[:150] if sanitized else raw_uid
|
||||||
for item in ["name", "first_name", "last_name"]:
|
for item in ["name", "first_name", "last_name"]:
|
||||||
if data.get(item):
|
if data.get(item):
|
||||||
setattr(user, item, data[item])
|
setattr(user, item, data[item])
|
||||||
|
|||||||
+2
-2
@@ -7,8 +7,8 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
@deconstructible
|
@deconstructible
|
||||||
class ASCIIUsernameValidator(validators.RegexValidator):
|
class ASCIIUsernameValidator(validators.RegexValidator):
|
||||||
regex = r"^[\w.@]+$"
|
regex = r"^[\w.@-]+$"
|
||||||
message = _("Enter a valid username. This value may contain only " "English letters and numbers")
|
message = _("Enter a valid username. This value may contain only English letters, numbers, and '_', '.', '@', '-'.")
|
||||||
flags = re.ASCII
|
flags = re.ASCII
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from allauth.account.adapter import get_adapter
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib.auth.password_validation import validate_password
|
from django.contrib.auth.password_validation import validate_password
|
||||||
@@ -277,6 +278,11 @@ class UserList(APIView):
|
|||||||
if not all([username, password, email, name]):
|
if not all([username, password, email, name]):
|
||||||
return Response({"detail": "username, password, email, and name are required."}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"detail": "username, password, email, and name are required."}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
try:
|
||||||
|
username = get_adapter().clean_username(username, shallow=True)
|
||||||
|
except DjangoValidationError as e:
|
||||||
|
return Response({"detail": e.messages[0]}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
if User.objects.filter(username=username).exists():
|
if User.objects.filter(username=username).exists():
|
||||||
return Response({"detail": "A user with that username already exists."}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"detail": "A user with that username already exists."}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user