Compare commits

...

30 Commits
v4.1 ... v4.5.1

Author SHA1 Message Date
Markos Gogoulos
d1fda05fdc fix: flake8 2025-03-09 20:50:07 +02:00
Markos Gogoulos
a02e0a8a66 fix: flake8 2025-03-09 20:48:09 +02:00
Markos Gogoulos
21f76dbb6e feat: playlist optimizations (#1216) 2025-03-09 20:44:04 +02:00
Markos Gogoulos
50e9f3103f feat: better support for subtitles (#1215) 2025-03-09 20:29:26 +02:00
Markos Gogoulos
0b9a203123 revert head changes 2025-02-13 20:31:19 +02:00
Sven-Thorsten Dietrich
5cbd815496 fix: Fix Docker WARN: FromAsCasing (#1196)
Fixes: L27 'as' and 'FROM' keywords' casing do not match

Signed-off-by: Sven-Thorsten Dietrich <thebigcorporation@gmail.com>
2025-02-13 13:57:14 +02:00
Markos Gogoulos
3a8cacc847 feat: Bulk fixes (#1195)
remove ckeditor - not in use
add more strict default password validators
set Django admin as configurable URL
add nginx HSTS and CSP headers
enable moving from private to unlisted in the PORTAL_WORKFLOW private
on default comments listing, show only comments for public media
in case of a private media, dont expose any unneeded metadata
2025-02-13 13:41:53 +02:00
Markos Gogoulos
5402ee7bc5 fix: crispy forms (#1194) 2025-02-12 14:27:27 +02:00
Markos Gogoulos
a6a2b50c8d remove redundant message 2025-02-10 22:25:24 +02:00
Markos Gogoulos
23e48a8bb7 remove redundant message 2025-02-10 22:24:42 +02:00
Markos Gogoulos
313cd9cbc6 fix issue with static files in dev 2025-02-10 22:04:14 +02:00
Markos Gogoulos
0392dbe1ed feat: lib updates and more (#1187)
This PR performs the following changes:

- update python in Dockerfile
- updates all python libraries (including Django)
- makes md5sum calculation redundant by default
- updates Postgresql used in Docker. For new installations this is ok. For existing ones this is backwards incompatible, and restore through pg_dump+pg_restore has to be performed in order to utilize Postgres 17 (since its not compatible with previous versions and will complain)
- fixes issues with HLS, and adds test to ensure they won't happen again
- allows . and @ in usernames
2025-02-10 11:34:00 +02:00
Markos Gogoulos
a7562c244e fix: black (#1164) 2025-01-22 20:16:21 +02:00
Sonu Kumar
d2ee12087c feat: Re-Arranged Subtitles by first letter of language (#863) 2025-01-22 19:25:47 +02:00
aleensd
6db01932e1 feat: fullscreen images slideshow (#1120) 2025-01-22 19:02:01 +02:00
Markos Gogoulos
53d8215346 feat: comment URL (#1163) 2025-01-22 14:55:43 +02:00
David
1b960b28f8 feat: Update insert login_required middleware to allow auth middleware (#1082) 2025-01-22 14:54:25 +02:00
Markos Gogoulos
02d9188aa1 feat: increase task limit (#1161) 2025-01-22 11:22:07 +02:00
Meng Sen
8d9a4618f0 feat: support for reading environment variables on Docker 2024-11-22 18:35:47 +02:00
aleensd
cf93a77802 feat: integrate react-pdf-viewer for PDF display (#1112) 2024-11-21 12:15:25 +02:00
Markos Gogoulos
5a1e4f25ed fix: issue with create_hls 2024-11-20 15:04:27 +02:00
Markos Gogoulos
9fc7597e73 fix: GH action 2024-11-20 13:47:29 +02:00
Markos Gogoulos
9b3e0250d4 fix: GH action 2024-11-20 13:36:58 +02:00
Markos Gogoulos
1384471745 fix: flake8 2024-11-20 13:29:37 +02:00
Markos Gogoulos
29b362c8ce fix: flake8 2024-11-20 13:27:57 +02:00
Markos Gogoulos
b8ee2e9fb8 fix: pre-commit 2024-11-20 13:17:25 +02:00
Markos Gogoulos
99be0f07dd feat: edit slideshow_url results 2024-11-18 15:48:02 +02:00
Markos Gogoulos
27d1660192 feat: provide slideshow media for images (#1108) 2024-11-14 12:22:23 +02:00
Nathan Hyde
98adb22205 Fixes mediacms-io/mediacms#1046 - submition => submission (#1094) 2024-10-30 19:49:26 +02:00
yatesdr
673ddeb5bd feat: Set option for allowing only certain domains to register (#1086) 2024-10-19 14:51:20 +03:00
83 changed files with 34620 additions and 11367 deletions

View File

@@ -13,10 +13,10 @@ jobs:
uses: actions/checkout@v1
- name: Build the Stack
run: docker-compose -f docker-compose-dev.yaml build
run: docker compose -f docker-compose-dev.yaml build
- name: Start containers
run: docker-compose -f docker-compose-dev.yaml up -d
run: docker compose -f docker-compose-dev.yaml up -d
- name: List containers
run: docker ps
@@ -26,10 +26,10 @@ jobs:
shell: bash
- name: Run Django Tests
run: docker-compose -f docker-compose-dev.yaml exec --env TESTING=True -T web pytest
run: docker compose -f docker-compose-dev.yaml exec --env TESTING=True -T web pytest
# Run with coverage, saves report on htmlcov dir
# run: docker-compose -f docker-compose-dev.yaml exec --env TESTING=True -T web pytest --cov --cov-report=html --cov-config=.coveragerc
- name: Tear down the Stack
run: docker-compose -f docker-compose-dev.yaml down
run: docker compose -f docker-compose-dev.yaml down

View File

@@ -1,38 +0,0 @@
name: Semantic Release
on:
push:
branches:
- main
permissions:
contents: write
issues: write
jobs:
semantic-release:
runs-on: ubuntu-latest
environment: dev
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "lts/*"
- name: Install Dependencies
run: npm clean-install
- name: Verify the integrity of provenance attestations and registry signatures for installed dependencies
run: npm audit signatures
- name: Run Semantic Release
run: npx semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,70 +1,88 @@
FROM python:3.11.4-bookworm AS compile-image
FROM python:3.13-bookworm AS build-image
SHELL ["/bin/bash", "-c"]
# Set up virtualenv
ENV VIRTUAL_ENV=/home/mediacms.io
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
ENV PIP_NO_CACHE_DIR=1
RUN mkdir -p /home/mediacms.io/mediacms/{logs} && cd /home/mediacms.io && python3 -m venv $VIRTUAL_ENV
# Install dependencies:
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . /home/mediacms.io/mediacms
WORKDIR /home/mediacms.io/mediacms
RUN wget -q http://zebulon.bok.net/Bento4/binaries/Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip && \
unzip Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip -d ../bento4 && \
mv ../bento4/Bento4-SDK-1-6-0-637.x86_64-unknown-linux/* ../bento4/ && \
rm -rf ../bento4/Bento4-SDK-1-6-0-637.x86_64-unknown-linux && \
rm -rf ../bento4/docs && \
rm Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip
############ RUNTIME IMAGE ############
FROM python:3.11.4-bookworm as runtime-image
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
# See: https://github.com/celery/celery/issues/6285#issuecomment-715316219
ENV CELERY_APP='cms'
# Use these to toggle which processes supervisord should run
ENV ENABLE_UWSGI='yes'
ENV ENABLE_NGINX='yes'
ENV ENABLE_CELERY_BEAT='yes'
ENV ENABLE_CELERY_SHORT='yes'
ENV ENABLE_CELERY_LONG='yes'
ENV ENABLE_MIGRATIONS='yes'
# Set up virtualenv
ENV VIRTUAL_ENV=/home/mediacms.io
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
COPY --chown=www-data:www-data --from=compile-image /home/mediacms.io /home/mediacms.io
RUN apt-get update -y && apt-get -y upgrade && apt-get install --no-install-recommends \
supervisor nginx imagemagick procps wget xz-utils -y && \
# Install system dependencies needed for downloading and extracting
RUN apt-get update -y && \
apt-get install -y --no-install-recommends wget xz-utils unzip && \
rm -rf /var/lib/apt/lists/* && \
apt-get purge --auto-remove && \
apt-get clean
# Install ffmpeg
RUN wget -q https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz && \
mkdir -p ffmpeg-tmp && \
tar -xf ffmpeg-release-amd64-static.tar.xz --strip-components 1 -C ffmpeg-tmp && \
cp -v ffmpeg-tmp/ffmpeg ffmpeg-tmp/ffprobe ffmpeg-tmp/qt-faststart /usr/local/bin && \
rm -rf ffmpeg-tmp ffmpeg-release-amd64-static.tar.xz
# Install Bento4 in the specified location
RUN mkdir -p /home/mediacms.io/bento4 && \
wget -q http://zebulon.bok.net/Bento4/binaries/Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip && \
unzip Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip -d /home/mediacms.io/bento4 && \
mv /home/mediacms.io/bento4/Bento4-SDK-1-6-0-637.x86_64-unknown-linux/* /home/mediacms.io/bento4/ && \
rm -rf /home/mediacms.io/bento4/Bento4-SDK-1-6-0-637.x86_64-unknown-linux && \
rm -rf /home/mediacms.io/bento4/docs && \
rm Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip
############ RUNTIME IMAGE ############
FROM python:3.13-bookworm AS runtime_image
SHELL ["/bin/bash", "-c"]
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
ENV CELERY_APP='cms'
ENV VIRTUAL_ENV=/home/mediacms.io
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
# Install runtime system dependencies
RUN apt-get update -y && \
apt-get -y upgrade && \
apt-get install --no-install-recommends supervisor nginx imagemagick procps -y && \
rm -rf /var/lib/apt/lists/* && \
apt-get purge --auto-remove && \
apt-get clean
# Copy ffmpeg and Bento4 from build image
COPY --from=build-image /usr/local/bin/ffmpeg /usr/local/bin/ffmpeg
COPY --from=build-image /usr/local/bin/ffprobe /usr/local/bin/ffprobe
COPY --from=build-image /usr/local/bin/qt-faststart /usr/local/bin/qt-faststart
COPY --from=build-image /home/mediacms.io/bento4 /home/mediacms.io/bento4
# Set up virtualenv
RUN mkdir -p /home/mediacms.io/mediacms/{logs} && \
cd /home/mediacms.io && \
python3 -m venv $VIRTUAL_ENV
# Install Python dependencies
COPY requirements.txt requirements-dev.txt ./
ARG DEVELOPMENT_MODE=False
RUN pip install --no-cache-dir -r requirements.txt && \
if [ "$DEVELOPMENT_MODE" = "True" ]; then \
echo "Installing development dependencies..." && \
pip install --no-cache-dir -r requirements-dev.txt; \
fi
# Copy application files
COPY . /home/mediacms.io/mediacms
WORKDIR /home/mediacms.io/mediacms
# required for sprite thumbnail generation for large video files
COPY deploy/docker/policy.xml /etc/ImageMagick-6/policy.xml
# Set process control environment variables
ENV ENABLE_UWSGI='yes' \
ENABLE_NGINX='yes' \
ENABLE_CELERY_BEAT='yes' \
ENABLE_CELERY_SHORT='yes' \
ENABLE_CELERY_LONG='yes' \
ENABLE_MIGRATIONS='yes'
EXPOSE 9000 80
RUN chmod +x ./deploy/docker/entrypoint.sh
ENTRYPOINT ["./deploy/docker/entrypoint.sh"]
CMD ["./deploy/docker/start.sh"]
CMD ["./deploy/docker/start.sh"]

View File

@@ -1,73 +0,0 @@
FROM python:3.11.4-bookworm AS compile-image
SHELL ["/bin/bash", "-c"]
# Set up virtualenv
ENV VIRTUAL_ENV=/home/mediacms.io
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
ENV PIP_NO_CACHE_DIR=1
RUN mkdir -p /home/mediacms.io/mediacms/{logs} && cd /home/mediacms.io && python3 -m venv $VIRTUAL_ENV
# Install dependencies:
COPY requirements.txt .
COPY requirements-dev.txt .
RUN pip install -r requirements.txt
RUN pip install -r requirements-dev.txt
COPY . /home/mediacms.io/mediacms
WORKDIR /home/mediacms.io/mediacms
RUN wget -q http://zebulon.bok.net/Bento4/binaries/Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip && \
unzip Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip -d ../bento4 && \
mv ../bento4/Bento4-SDK-1-6-0-637.x86_64-unknown-linux/* ../bento4/ && \
rm -rf ../bento4/Bento4-SDK-1-6-0-637.x86_64-unknown-linux && \
rm -rf ../bento4/docs && \
rm Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip
############ RUNTIME IMAGE ############
FROM python:3.11.4-bookworm as runtime-image
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
# See: https://github.com/celery/celery/issues/6285#issuecomment-715316219
ENV CELERY_APP='cms'
# Use these to toggle which processes supervisord should run
ENV ENABLE_UWSGI='yes'
ENV ENABLE_NGINX='yes'
ENV ENABLE_CELERY_BEAT='yes'
ENV ENABLE_CELERY_SHORT='yes'
ENV ENABLE_CELERY_LONG='yes'
ENV ENABLE_MIGRATIONS='yes'
# Set up virtualenv
ENV VIRTUAL_ENV=/home/mediacms.io
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
COPY --chown=www-data:www-data --from=compile-image /home/mediacms.io /home/mediacms.io
RUN apt-get update -y && apt-get -y upgrade && apt-get install --no-install-recommends \
supervisor nginx imagemagick procps wget xz-utils -y && \
rm -rf /var/lib/apt/lists/* && \
apt-get purge --auto-remove && \
apt-get clean
RUN wget -q https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz && \
mkdir -p ffmpeg-tmp && \
tar -xf ffmpeg-release-amd64-static.tar.xz --strip-components 1 -C ffmpeg-tmp && \
cp -v ffmpeg-tmp/ffmpeg ffmpeg-tmp/ffprobe ffmpeg-tmp/qt-faststart /usr/local/bin && \
rm -rf ffmpeg-tmp ffmpeg-release-amd64-static.tar.xz
WORKDIR /home/mediacms.io/mediacms
EXPOSE 9000 80
RUN chmod +x ./deploy/docker/entrypoint.sh
ENTRYPOINT ["./deploy/docker/entrypoint.sh"]
CMD ["./deploy/docker/start.sh"]

View File

@@ -10,6 +10,10 @@ admin-shell:
fi
build-frontend:
docker-compose -f docker-compose-dev.yaml exec frontend npm run dist
docker compose -f docker-compose-dev.yaml exec frontend npm run dist
cp -r frontend/dist/static/* static/
docker-compose -f docker-compose-dev.yaml restart web
docker compose -f docker-compose-dev.yaml restart web
test:
docker compose -f docker-compose-dev.yaml exec --env TESTING=True -T web pytest

View File

@@ -23,9 +23,9 @@ INSTALLED_APPS = [
'debug_toolbar',
'mptt',
'crispy_forms',
"crispy_bootstrap5",
'uploader.apps.UploaderConfig',
'djcelery_email',
'ckeditor',
'drf_yasg',
'corsheaders',
]
@@ -41,9 +41,10 @@ MIDDLEWARE = [
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'debug_toolbar.middleware.DebugToolbarMiddleware',
"allauth.account.middleware.AccountMiddleware",
]
DEBUG = True
CORS_ORIGIN_ALLOW_ALL = True
STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static/'),)
STATIC_ROOT = None
STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static'),)
STATIC_ROOT = os.path.join(BASE_DIR, 'static_collected')

View File

@@ -111,7 +111,7 @@ TIME_TO_ACTION_ANONYMOUS = 10 * 60
# django-allauth settings
ACCOUNT_SESSION_REMEMBER = True
ACCOUNT_AUTHENTICATION_METHOD = "username_email"
ACCOUNT_LOGIN_METHODS = {"username", "email"}
ACCOUNT_EMAIL_REQUIRED = True # new users need to specify email
ACCOUNT_EMAIL_VERIFICATION = "optional" # 'mandatory' 'none'
ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
@@ -123,13 +123,15 @@ ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE = False
ACCOUNT_USERNAME_REQUIRED = True
ACCOUNT_LOGIN_ON_PASSWORD_RESET = True
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = 1
ACCOUNT_LOGIN_ATTEMPTS_LIMIT = 20
ACCOUNT_LOGIN_ATTEMPTS_TIMEOUT = 5
# registration won't be open, might also consider to remove links for register
USERS_CAN_SELF_REGISTER = True
RESTRICTED_DOMAINS_FOR_USER_REGISTRATION = ["xxx.com", "emaildomainwhatever.com"]
# Comma separated list of domains: ["organization.com", "private.organization.com", "org2.com"]
# Empty list disables.
ALLOWED_DOMAINS_FOR_USER_REGISTRATION = []
# django rest settings
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
@@ -226,11 +228,11 @@ POST_UPLOAD_AUTHOR_MESSAGE_UNLISTED_NO_COMMENTARY = ""
CANNOT_ADD_MEDIA_MESSAGE = ""
# mp4hls command, part of Bendo4
# mp4hls command, part of Bento4
MP4HLS_COMMAND = "/home/mediacms.io/mediacms/Bento4-SDK-1-6-0-637.x86_64-unknown-linux/bin/mp4hls"
# highly experimental, related with remote workers
ADMIN_TOKEN = "c2b8e1838b6128asd333ddc5e24"
ADMIN_TOKEN = ""
# this is used by remote workers to push
# encodings once they are done
# USE_BASIC_HTTP = True
@@ -245,35 +247,6 @@ ADMIN_TOKEN = "c2b8e1838b6128asd333ddc5e24"
# uncomment the two lines related to htpasswd
CKEDITOR_CONFIGS = {
"default": {
"toolbar": "Custom",
"width": "100%",
"toolbar_Custom": [
["Styles"],
["Format"],
["Bold", "Italic", "Underline"],
["HorizontalRule"],
[
"NumberedList",
"BulletedList",
"-",
"Outdent",
"Indent",
"-",
"JustifyLeft",
"JustifyCenter",
"JustifyRight",
"JustifyBlock",
],
["Link", "Unlink"],
["Image"],
["RemoveFormat", "Source"],
],
}
}
AUTH_USER_MODEL = "users.User"
LOGIN_REDIRECT_URL = "/"
@@ -302,9 +275,9 @@ INSTALLED_APPS = [
"debug_toolbar",
"mptt",
"crispy_forms",
"crispy_bootstrap5",
"uploader.apps.UploaderConfig",
"djcelery_email",
"ckeditor",
"drf_yasg",
]
@@ -318,6 +291,7 @@ MIDDLEWARE = [
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"debug_toolbar.middleware.DebugToolbarMiddleware",
"allauth.account.middleware.AccountMiddleware",
]
ROOT_URLCONF = "cms.urls"
@@ -345,11 +319,15 @@ WSGI_APPLICATION = "cms.wsgi.application"
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
"OPTIONS": {
"user_attributes": ("username", "email", "first_name", "last_name"),
"max_similarity": 0.7,
},
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
"OPTIONS": {
"min_length": 5,
"min_length": 7,
},
},
{
@@ -484,14 +462,14 @@ else:
if GLOBAL_LOGIN_REQUIRED:
# this should go after the AuthenticationMiddleware middleware
MIDDLEWARE.insert(5, "login_required.middleware.LoginRequiredMiddleware")
MIDDLEWARE.insert(6, "login_required.middleware.LoginRequiredMiddleware")
LOGIN_REQUIRED_IGNORE_PATHS = [
r'/accounts/login/$',
r'/accounts/logout/$',
r'/accounts/signup/$',
r'/accounts/password/.*/$',
r'/accounts/confirm-email/.*/$',
r'/api/v[0-9]+/',
# r'/api/v[0-9]+/',
]
# if True, only show original, don't perform any action on videos
@@ -534,4 +512,21 @@ LANGUAGE_CODE = 'en' # default language
SPRITE_NUM_SECS = 10
# number of seconds for sprite image.
# If you plan to change this, you must also follow the instructions on admin_docs.md
# to change the equivalent value in ./frontend/src/static/js/components/media-viewer/VideoViewer/index.js and then re-build frontend
# to change the equivalent value in ./frontend/src/static/js/components/media-viewer/VideoViewer/index.js and then re-build frontend
# how many images will be shown on the slideshow
SLIDESHOW_ITEMS = 30
# this calculation is redundant most probably, setting as an option
CALCULATE_MD5SUM = False
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
CRISPY_TEMPLATE_PACK = "bootstrap5"
# allow option to override the default admin url
# keep the trailing slash
DJANGO_ADMIN_URL = "admin/"
# CSRF_COOKIE_SECURE = True
# SESSION_COOKIE_SECURE = True
PYSUBS_COMMAND = "pysubs2"

View File

@@ -1,4 +1,5 @@
import debug_toolbar
from django.conf import settings
from django.conf.urls import include
from django.contrib import admin
from django.urls import path, re_path
@@ -25,7 +26,7 @@ urlpatterns = [
re_path(r"^", include("users.urls")),
re_path(r"^accounts/", include("allauth.urls")),
re_path(r"^api-auth/", include("rest_framework.urls")),
path("admin/", admin.site.urls),
path(settings.DJANGO_ADMIN_URL, admin.site.urls),
re_path(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
re_path(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
path('docs/api/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),

View File

@@ -7,6 +7,7 @@ ln -sf /dev/stdout /var/log/nginx/mediacms.io.access.log && ln -sf /dev/stderr /
cp /home/mediacms.io/mediacms/deploy/docker/local_settings.py /home/mediacms.io/mediacms/cms/local_settings.py
mkdir -p /home/mediacms.io/mediacms/{logs,media_files/hls}
touch /home/mediacms.io/mediacms/logs/debug.log

View File

@@ -1,17 +1,18 @@
FRONTEND_HOST = 'http://localhost'
PORTAL_NAME = 'MediaCMS'
SECRET_KEY = 'ma!s3^b-cw!f#7s6s0m3*jx77a@riw(7701**(r=ww%w!2+yk2'
POSTGRES_HOST = 'db'
REDIS_LOCATION = "redis://redis:6379/1"
import os
FRONTEND_HOST = os.getenv('FRONTEND_HOST', 'http://localhost')
PORTAL_NAME = os.getenv('PORTAL_NAME', 'MediaCMS')
SECRET_KEY = os.getenv('SECRET_KEY', 'ma!s3^b-cw!f#7s6s0m3*jx77a@riw(7701**(r=ww%w!2+yk2')
REDIS_LOCATION = os.getenv('REDIS_LOCATION', 'redis://redis:6379/1')
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "mediacms",
"HOST": POSTGRES_HOST,
"PORT": "5432",
"USER": "mediacms",
"PASSWORD": "mediacms",
"NAME": os.getenv('POSTGRES_NAME', 'mediacms'),
"HOST": os.getenv('POSTGRES_HOST', 'db'),
"PORT": os.getenv('POSTGRES_PORT', '5432'),
"USER": os.getenv('POSTGRES_USER', 'mediacms'),
"PASSWORD": os.getenv('POSTGRES_PASSWORD', 'mediacms'),
}
}
@@ -31,4 +32,4 @@ CELERY_RESULT_BACKEND = BROKER_URL
MP4HLS_COMMAND = "/home/mediacms.io/bento4/bin/mp4hls"
DEBUG = False
DEBUG = os.getenv('DEBUG', 'False') == 'True'

99
deploy/docker/policy.xml Normal file
View File

@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policymap [
<!ELEMENT policymap (policy)*>
<!ATTLIST policymap xmlns CDATA #FIXED ''>
<!ELEMENT policy EMPTY>
<!ATTLIST policy xmlns CDATA #FIXED '' domain NMTOKEN #REQUIRED
name NMTOKEN #IMPLIED pattern CDATA #IMPLIED rights NMTOKEN #IMPLIED
stealth NMTOKEN #IMPLIED value CDATA #IMPLIED>
]>
<!--
Configure ImageMagick policies.
Domains include system, delegate, coder, filter, path, or resource.
Rights include none, read, write, execute and all. Use | to combine them,
for example: "read | write" to permit read from, or write to, a path.
Use a glob expression as a pattern.
Suppose we do not want users to process MPEG video images:
<policy domain="delegate" rights="none" pattern="mpeg:decode" />
Here we do not want users reading images from HTTP:
<policy domain="coder" rights="none" pattern="HTTP" />
The /repository file system is restricted to read only. We use a glob
expression to match all paths that start with /repository:
<policy domain="path" rights="read" pattern="/repository/*" />
Lets prevent users from executing any image filters:
<policy domain="filter" rights="none" pattern="*" />
Any large image is cached to disk rather than memory:
<policy domain="resource" name="area" value="1GP"/>
Use the default system font unless overwridden by the application:
<policy domain="system" name="font" value="/usr/share/fonts/favorite.ttf"/>
Define arguments for the memory, map, area, width, height and disk resources
with SI prefixes (.e.g 100MB). In addition, resource policies are maximums
for each instance of ImageMagick (e.g. policy memory limit 1GB, -limit 2GB
exceeds policy maximum so memory limit is 1GB).
Rules are processed in order. Here we want to restrict ImageMagick to only
read or write a small subset of proven web-safe image types:
<policy domain="delegate" rights="none" pattern="*" />
<policy domain="filter" rights="none" pattern="*" />
<policy domain="coder" rights="none" pattern="*" />
<policy domain="coder" rights="read|write" pattern="{GIF,JPEG,PNG,WEBP}" />
-->
<policymap>
<!-- <policy domain="resource" name="temporary-path" value="/tmp"/> -->
<policy domain="resource" name="memory" value="1GiB"/>
<policy domain="resource" name="map" value="30GiB"/>
<policy domain="resource" name="width" value="16MP"/>
<policy domain="resource" name="height" value="16MP"/>
<!-- <policy domain="resource" name="list-length" value="128"/> -->
<policy domain="resource" name="area" value="40GP"/>
<policy domain="resource" name="disk" value="100GiB"/>
<!-- <policy domain="resource" name="file" value="768"/> -->
<!-- <policy domain="resource" name="thread" value="4"/> -->
<!-- <policy domain="resource" name="throttle" value="0"/> -->
<!-- <policy domain="resource" name="time" value="3600"/> -->
<!-- <policy domain="coder" rights="none" pattern="MVG" /> -->
<!-- <policy domain="module" rights="none" pattern="{PS,PDF,XPS}" /> -->
<!-- <policy domain="path" rights="none" pattern="@*" /> -->
<!-- <policy domain="cache" name="memory-map" value="anonymous"/> -->
<!-- <policy domain="cache" name="synchronize" value="True"/> -->
<!-- <policy domain="cache" name="shared-secret" value="passphrase" stealth="true"/>
<!-- <policy domain="system" name="max-memory-request" value="256MiB"/> -->
<!-- <policy domain="system" name="shred" value="2"/> -->
<!-- <policy domain="system" name="precision" value="6"/> -->
<!-- <policy domain="system" name="font" value="/path/to/font.ttf"/> -->
<!-- <policy domain="system" name="pixel-cache-memory" value="anonymous"/> -->
<!-- <policy domain="system" name="shred" value="2"/> -->
<!-- <policy domain="system" name="precision" value="6"/> -->
<!-- not needed due to the need to use explicitly by mvg: -->
<!-- <policy domain="delegate" rights="none" pattern="MVG" /> -->
<!-- use curl -->
<policy domain="delegate" rights="none" pattern="URL" />
<policy domain="delegate" rights="none" pattern="HTTPS" />
<policy domain="delegate" rights="none" pattern="HTTP" />
<!-- in order to avoid to get image with password text -->
<policy domain="path" rights="none" pattern="@*"/>
<!-- disable ghostscript format types -->
<policy domain="coder" rights="none" pattern="PS" />
<policy domain="coder" rights="none" pattern="PS2" />
<policy domain="coder" rights="none" pattern="PS3" />
<policy domain="coder" rights="none" pattern="EPS" />
<policy domain="coder" rights="none" pattern="PDF" />
<policy domain="coder" rights="none" pattern="XPS" />
</policymap>

View File

@@ -49,7 +49,7 @@ server {
ssl_dhparam /etc/nginx/dhparams/dhparams.pem;
ssl_protocols TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_ecdh_curve secp521r1:secp384r1;
ssl_prefer_server_ciphers on;

View File

@@ -4,13 +4,23 @@ services:
migrations:
build:
context: .
dockerfile: ./Dockerfile-dev
dockerfile: ./Dockerfile
args:
- DEVELOPMENT_MODE=True
image: mediacms/mediacms-dev:latest
volumes:
- ./:/home/mediacms.io/mediacms/
command: "python manage.py migrate"
command: "./deploy/docker/prestart.sh"
environment:
DEVELOPMENT_MODE: "True"
DEVELOPMENT_MODE: True
ENABLE_UWSGI: 'no'
ENABLE_NGINX: 'no'
ENABLE_CELERY_SHORT: 'no'
ENABLE_CELERY_LONG: 'no'
ENABLE_CELERY_BEAT: 'no'
ADMIN_USER: 'admin'
ADMIN_EMAIL: 'admin@localhost'
ADMIN_PASSWORD: 'admin'
restart: on-failure
depends_on:
redis:
@@ -30,16 +40,10 @@ services:
depends_on:
- web
web:
build:
context: .
dockerfile: ./Dockerfile-dev
image: mediacms/mediacms-dev:latest
command: "python manage.py runserver 0.0.0.0:80"
environment:
DEVELOPMENT_MODE: "True"
ADMIN_USER: 'admin'
ADMIN_PASSWORD: 'admin'
ADMIN_EMAIL: 'admin@localhost'
DEVELOPMENT_MODE: True
ports:
- "80:80"
volumes:
@@ -47,7 +51,7 @@ services:
depends_on:
- migrations
db:
image: postgres:15.2-alpine
image: postgres:17.2-alpine
volumes:
- ../postgres_data:/var/lib/postgresql/data/
restart: always
@@ -80,5 +84,6 @@ services:
ENABLE_NGINX: 'no'
ENABLE_CELERY_BEAT: 'no'
ENABLE_MIGRATIONS: 'no'
DEVELOPMENT_MODE: True
depends_on:
- web

View File

@@ -68,7 +68,7 @@ services:
depends_on:
- migrations
db:
image: postgres:15.2-alpine
image: postgres:17.2-alpine
volumes:
- ../postgres_data/:/var/lib/postgresql/data/
restart: always

View File

@@ -70,7 +70,7 @@ services:
depends_on:
- migrations
db:
image: postgres:15.2-alpine
image: postgres:17.2-alpine
volumes:
- ../postgres_data/:/var/lib/postgresql/data/
restart: always

View File

@@ -90,7 +90,7 @@ services:
depends_on:
- migrations
db:
image: postgres:15.2-alpine
image: postgres:17.2-alpine
volumes:
- ../postgres_data:/var/lib/postgresql/data/
restart: always

View File

@@ -66,7 +66,7 @@ services:
depends_on:
- migrations
db:
image: postgres:15.2-alpine
image: postgres:17.2-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
restart: always

View File

@@ -62,7 +62,7 @@ services:
depends_on:
- migrations
db:
image: postgres:15.2-alpine
image: postgres:17.2-alpine
volumes:
- ../postgres_data:/var/lib/postgresql/data/
restart: always

View File

@@ -355,13 +355,22 @@ ADMIN_EMAIL_LIST = ['info@mediacms.io']
### 5.13 Disallow user registrations from specific domains
set domains that are not valid for registration via this variable:
Set domains that are not valid for registration via this variable:
```
RESTRICTED_DOMAINS_FOR_USER_REGISTRATION = [
'xxx.com', 'emaildomainwhatever.com']
```
Alternatively, allow only permitted domains to register. This can be useful if you're using mediacms as a private service within an organization, and want to give free registration for those in the org, but deny registration from all other domains. Setting this option bans all domains NOT in the list from registering. Default is a blank list, which is ignored. To disable, set to a blank list.
```
ALLOWED_DOMAINS_FOR_USER_REGISTRATION = [
"private.com",
"vod.private.com",
"my.favorite.domain",
"test.private.com"]
```
### 5.14 Require a review by MediaCMS editors/managers/admins
set value
@@ -842,7 +851,6 @@ After this command is run, translate the string to the language you want. If the
### 20.5 Add a new language and translate
To add a new language: add the language in settings.py, then add the file in `files/frontend-translations/`. Make sure you copy the initial strings by copying `files/frontend-translations/en.py` to it.
## 21. How to change the video frames on videos
By default while watching a video you can hover and see the small images named sprites that are extracted every 10 seconds of a video. You can change this number to something smaller by performing the following:

View File

@@ -1,6 +1,15 @@
from django.contrib import admin
from .models import Category, Comment, EncodeProfile, Encoding, Language, Media, Subtitle, Tag
from .models import (
Category,
Comment,
EncodeProfile,
Encoding,
Language,
Media,
Subtitle,
Tag,
)
class CommentAdmin(admin.ModelAdmin):

View File

@@ -15,7 +15,7 @@ class VideoEncodingError(Exception):
RE_TIMECODE = re.compile(r"time=(\d+:\d+:\d+.\d+)")
console_encoding = locale.getdefaultlocale()[1] or "UTF-8"
console_encoding = locale.getlocale()[1] or "UTF-8"
class FFmpegBackend(object):

View File

@@ -34,5 +34,7 @@ def stuff(request):
ret["RSS_URL"] = "/rss"
ret["TRANSLATION"] = get_translation(request.LANGUAGE_CODE)
ret["REPLACEMENTS"] = get_translation_strings(request.LANGUAGE_CODE)
if request.user.is_superuser:
ret["DJANGO_ADMIN_URL"] = settings.DJANGO_ADMIN_URL
return ret

View File

@@ -68,6 +68,8 @@ class SubtitleForm(forms.ModelForm):
def __init__(self, media_item, *args, **kwargs):
super(SubtitleForm, self).__init__(*args, **kwargs)
self.instance.media = media_item
self.fields["subtitle_file"].help_text = "SubRip (.srt) and WebVTT (.vtt) are supported file formats."
self.fields["subtitle_file"].label = "Subtitle or Closed Caption File"
def save(self, *args, **kwargs):
self.instance.user = self.instance.media.user
@@ -75,6 +77,14 @@ class SubtitleForm(forms.ModelForm):
return media
class EditSubtitleForm(forms.Form):
subtitle = forms.CharField(widget=forms.Textarea, required=True)
def __init__(self, subtitle, *args, **kwargs):
super(EditSubtitleForm, self).__init__(*args, **kwargs)
self.fields["subtitle"].initial = subtitle.subtitle_file.read().decode("utf-8")
class ContactForm(forms.Form):
from_email = forms.EmailField(required=True)
name = forms.CharField(required=False)

View File

@@ -32,7 +32,6 @@ for translation_file in files:
replacement_strings[language_code] = tr_module.replacement_strings
def get_translation(language_code):
# get list of translations per language
if not check_language_code(language_code):

View File

@@ -18,7 +18,10 @@ class Command(BaseCommand):
files = os.listdir(translations_dir)
files = [f for f in files if f.endswith('.py') and f not in ('__init__.py', 'en.py')]
# Import the original English translations
from files.frontend_translations.en import replacement_strings, translation_strings
from files.frontend_translations.en import (
replacement_strings,
translation_strings,
)
for file in files:
file_path = os.path.join(translations_dir, file)
@@ -44,12 +47,12 @@ class Command(BaseCommand):
with open(file_path, 'w') as f:
f.write("translation_strings = {\n")
for key, value in translation_strings_wip.items():
f.write(f' "{key}": "{value}",\n')
f.write(f' "{key}": "{value}",\n') # noqa
f.write("}\n\n")
f.write("replacement_strings = {\n")
for key, value in replacement_strings_wip.items():
f.write(f' "{key}": "{value}",\n')
f.write(f' "{key}": "{value}",\n') # noqa
f.write("}\n")
self.stdout.write(self.style.SUCCESS(f'Processed {file}'))

View File

@@ -119,12 +119,16 @@ def get_next_state(user, current_state, next_state):
if next_state not in ["public", "private", "unlisted"]:
next_state = settings.PORTAL_WORKFLOW # get default state
if is_mediacms_editor(user):
# allow any transition
return next_state
if settings.PORTAL_WORKFLOW == "private":
next_state = "private"
if next_state in ["private", "unlisted"]:
next_state = next_state
else:
next_state = current_state
if settings.PORTAL_WORKFLOW == "unlisted":
# don't allow to make media public in this case

View File

@@ -780,6 +780,36 @@ class Media(models.Model):
return helpers.url_from_path(self.poster.path)
return None
@property
def slideshow_items(self):
slideshow_items = getattr(settings, "SLIDESHOW_ITEMS", 30)
if self.media_type != "image":
items = []
else:
qs = Media.objects.filter(listable=True, user=self.user, media_type="image").exclude(id=self.id).order_by('id')[:slideshow_items]
items = [
{
"poster_url": item.poster_url,
"url": item.get_absolute_url(),
"thumbnail_url": item.thumbnail_url,
"title": item.title,
"original_media_url": item.original_media_url,
}
for item in qs
]
items.insert(
0,
{
"poster_url": self.poster_url,
"url": self.get_absolute_url(),
"thumbnail_url": self.thumbnail_url,
"title": self.title,
"original_media_url": self.original_media_url,
},
)
return items
@property
def subtitles_info(self):
"""Property used on serializers
@@ -787,7 +817,9 @@ class Media(models.Model):
"""
ret = []
for subtitle in self.subtitles.all():
# Retrieve all subtitles and sort by the first letter of their associated language's title
sorted_subtitles = sorted(self.subtitles.all(), key=lambda s: s.language.title[0])
for subtitle in sorted_subtitles:
ret.append(
{
"src": helpers.url_from_path(subtitle.subtitle_file.path),
@@ -1178,9 +1210,36 @@ class Subtitle(models.Model):
user = models.ForeignKey("users.User", on_delete=models.CASCADE)
class Meta:
ordering = ["language__title"]
def __str__(self):
return "{0}-{1}".format(self.media.title, self.language.title)
def get_absolute_url(self):
return f"{reverse('edit_subtitle')}?id={self.id}"
@property
def url(self):
return self.get_absolute_url()
def convert_to_srt(self):
input_path = self.subtitle_file.path
with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as tmpdirname:
pysub = settings.PYSUBS_COMMAND
cmd = [pysub, input_path, "--to", "vtt", "-o", tmpdirname]
stdout = helpers.run_command(cmd)
list_of_files = os.listdir(tmpdirname)
if list_of_files:
subtitles_file = os.path.join(tmpdirname, list_of_files[0])
cmd = ["cp", subtitles_file, input_path]
stdout = helpers.run_command(cmd) # noqa
else:
raise Exception("Could not convert to srt")
return True
class RatingCategory(models.Model):
"""Rating Category
@@ -1253,7 +1312,7 @@ class Playlist(models.Model):
@property
def media_count(self):
return self.media.count()
return self.media.filter(listable=True).count()
def get_absolute_url(self, api=False):
if api:
@@ -1300,7 +1359,7 @@ class Playlist(models.Model):
@property
def thumbnail_url(self):
pm = self.playlistmedia_set.first()
pm = self.playlistmedia_set.filter(media__listable=True).first()
if pm and pm.media.thumbnail:
return helpers.url_from_path(pm.media.thumbnail.path)
return None

View File

@@ -145,6 +145,7 @@ class SingleMediaSerializer(serializers.ModelSerializer):
"ratings_info",
"add_subtitle_url",
"allow_download",
"slideshow_items",
)

View File

@@ -23,7 +23,17 @@ from users.models import User
from .backends import FFmpegBackend
from .exceptions import VideoEncodingError
from .helpers import calculate_seconds, create_temp_file, get_file_name, get_file_type, media_file_info, produce_ffmpeg_commands, produce_friendly_token, rm_file, run_command
from .helpers import (
calculate_seconds,
create_temp_file,
get_file_name,
get_file_type,
media_file_info,
produce_ffmpeg_commands,
produce_friendly_token,
rm_file,
run_command,
)
from .methods import list_tasks, notify_users, pre_save_action
from .models import Category, EncodeProfile, Encoding, Media, Rating, Tag
@@ -38,7 +48,7 @@ ERRORS_LIST = [
]
@task(name="chunkize_media", bind=True, queue="short_tasks", soft_time_limit=60 * 30)
@task(name="chunkize_media", bind=True, queue="short_tasks", soft_time_limit=60 * 30 * 4)
def chunkize_media(self, friendly_token, profiles, force=True):
"""Break media in chunks and start encoding tasks"""
@@ -376,25 +386,13 @@ def produce_sprite_from_video(friendly_token):
output_name = tmpdirname + "/sprites.jpg"
fps = getattr(settings, 'SPRITE_NUM_SECS', 10)
ffmpeg_cmd = [
settings.FFMPEG_COMMAND,
"-i", media.media_file.path,
"-f", "image2",
"-vf", f"fps=1/{fps}, scale=160:90",
tmpdir_image_files
]
ffmpeg_cmd = [settings.FFMPEG_COMMAND, "-i", media.media_file.path, "-f", "image2", "-vf", f"fps=1/{fps}, scale=160:90", tmpdir_image_files] # noqa
run_command(ffmpeg_cmd)
image_files = [f for f in os.listdir(tmpdirname) if f.startswith("img") and f.endswith(".jpg")]
image_files = sorted(image_files, key=lambda x: int(re.search(r'\d+', x).group()))
image_files = [os.path.join(tmpdirname, f) for f in image_files]
cmd_convert = [
"convert",
*image_files, # image files, unpacked into the list
"-append",
output_name
]
run_command(cmd_convert)
cmd_convert = ["convert", *image_files, "-append", output_name] # image files, unpacked into the list
ret = run_command(cmd_convert) # noqa
if os.path.exists(output_name) and get_file_type(output_name) == "image":
with open(output_name, "rb") as f:
@@ -403,8 +401,8 @@ def produce_sprite_from_video(friendly_token):
content=myfile,
name=get_file_name(media.media_file.path) + "sprites.jpg",
)
except BaseException:
pass
except Exception as e:
print(e)
return True
@@ -429,26 +427,27 @@ def create_hls(friendly_token):
p = media.uid.hex
output_dir = os.path.join(settings.HLS_DIR, p)
encodings = media.encodings.filter(profile__extension="mp4", status="success", chunk=False, profile__codec="h264")
if encodings:
existing_output_dir = None
if os.path.exists(output_dir):
existing_output_dir = output_dir
output_dir = os.path.join(settings.HLS_DIR, p + produce_friendly_token())
files = " ".join([f.media_file.path for f in encodings if f.media_file])
cmd = [
settings.MP4HLS_COMMAND,
'--segment-duration=4',
f'--output-dir={output_dir}',
files
]
files = [f.media_file.path for f in encodings if f.media_file]
cmd = [settings.MP4HLS_COMMAND, '--segment-duration=4', f'--output-dir={output_dir}', *files]
run_command(cmd)
if existing_output_dir:
# override content with -T !
cmd = "cp -rT {0} {1}".format(output_dir, existing_output_dir)
cmd = ["cp", "-rT", output_dir, existing_output_dir]
run_command(cmd)
shutil.rmtree(output_dir)
try:
shutil.rmtree(output_dir)
except: # noqa
# this was breaking in some cases where it was already deleted
# because create_hls was running multiple times
pass
output_dir = existing_output_dir
pp = os.path.join(output_dir, "master.m3u8")
if os.path.exists(pp):

View File

@@ -7,5 +7,4 @@ register = template.Library()
@register.filter
def custom_translate(string, lang_code):
return translate_string(lang_code, string)

View File

@@ -12,6 +12,7 @@ urlpatterns = [
re_path(r"^about", views.about, name="about"),
re_path(r"^setlanguage", views.setlanguage, name="setlanguage"),
re_path(r"^add_subtitle", views.add_subtitle, name="add_subtitle"),
re_path(r"^edit_subtitle", views.edit_subtitle, name="edit_subtitle"),
re_path(r"^categories$", views.categories, name="categories"),
re_path(r"^contact$", views.contact, name="contact"),
re_path(r"^edit", views.edit_media, name="edit_media"),

View File

@@ -6,23 +6,33 @@ from django.contrib.auth.decorators import login_required
from django.contrib.postgres.search import SearchQuery
from django.core.mail import EmailMessage
from django.db.models import Q
from django.http import HttpResponseRedirect
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from drf_yasg import openapi as openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework import permissions, status
from rest_framework.exceptions import PermissionDenied
from rest_framework.parsers import FileUploadParser, FormParser, JSONParser, MultiPartParser
from rest_framework.parsers import (
FileUploadParser,
FormParser,
JSONParser,
MultiPartParser,
)
from rest_framework.response import Response
from rest_framework.settings import api_settings
from rest_framework.views import APIView
from actions.models import USER_MEDIA_ACTIONS, MediaAction
from cms.custom_pagination import FastPaginationWithoutCount
from cms.permissions import IsAuthorizedToAdd, IsAuthorizedToAddComment, IsUserOrEditor, user_allowed_to_upload
from cms.permissions import (
IsAuthorizedToAdd,
IsAuthorizedToAddComment,
IsUserOrEditor,
user_allowed_to_upload,
)
from users.models import User
from .forms import ContactForm, MediaForm, SubtitleForm
from .forms import ContactForm, EditSubtitleForm, MediaForm, SubtitleForm
from .frontend_translations import translate_string
from .helpers import clean_query, get_alphanumeric_only, produce_ffmpeg_commands
from .methods import (
@@ -36,7 +46,17 @@ from .methods import (
show_related_media,
update_user_ratings,
)
from .models import Category, Comment, EncodeProfile, Encoding, Media, Playlist, PlaylistMedia, Tag
from .models import (
Category,
Comment,
EncodeProfile,
Encoding,
Media,
Playlist,
PlaylistMedia,
Subtitle,
Tag,
)
from .serializers import (
CategorySerializer,
CommentSerializer,
@@ -86,12 +106,68 @@ def add_subtitle(request):
form = SubtitleForm(media, request.POST, request.FILES)
if form.is_valid():
subtitle = form.save()
messages.add_message(request, messages.INFO, translate_string(request.LANGUAGE_CODE, "Subtitle was added"))
new_subtitle = Subtitle.objects.filter(id=subtitle.id).first()
try:
new_subtitle.convert_to_srt()
messages.add_message(request, messages.INFO, "Subtitle was added!")
return HttpResponseRedirect(subtitle.media.get_absolute_url())
except: # noqa: E722
new_subtitle.delete()
error_msg = "Invalid subtitle format. Use SubRip (.srt) or WebVTT (.vtt) files."
form.add_error("subtitle_file", error_msg)
return HttpResponseRedirect(subtitle.media.get_absolute_url())
else:
form = SubtitleForm(media_item=media)
return render(request, "cms/add_subtitle.html", {"form": form})
subtitles = media.subtitles.all()
context = {"media": media, "form": form, "subtitles": subtitles}
return render(request, "cms/add_subtitle.html", context)
@login_required
def edit_subtitle(request):
subtitle_id = request.GET.get("id", "").strip()
action = request.GET.get("action", "").strip()
if not subtitle_id:
return HttpResponseRedirect("/")
subtitle = Subtitle.objects.filter(id=subtitle_id).first()
if not subtitle:
return HttpResponseRedirect("/")
if not (request.user == subtitle.user or is_mediacms_editor(request.user) or is_mediacms_manager(request.user)):
return HttpResponseRedirect("/")
context = {"subtitle": subtitle, "action": action}
if action == "download":
response = HttpResponse(subtitle.subtitle_file.read(), content_type="text/vtt")
filename = subtitle.subtitle_file.name.split("/")[-1]
if not filename.endswith(".vtt"):
filename = f"{filename}.vtt"
response["Content-Disposition"] = f"attachment; filename={filename}" # noqa
return response
if request.method == "GET":
form = EditSubtitleForm(subtitle)
context["form"] = form
elif request.method == "POST":
confirm = request.GET.get("confirm", "").strip()
if confirm == "true":
messages.add_message(request, messages.INFO, "Subtitle was deleted")
redirect_url = subtitle.media.get_absolute_url()
subtitle.delete()
return HttpResponseRedirect(redirect_url)
form = EditSubtitleForm(subtitle, request.POST)
subtitle_text = form.data["subtitle"]
with open(subtitle.subtitle_file.path, "w") as ff:
ff.write(subtitle_text)
messages.add_message(request, messages.INFO, "Subtitle was edited")
return HttpResponseRedirect(subtitle.media.get_absolute_url())
return render(request, "cms/edit_subtitle.html", context)
def categories(request):
@@ -656,6 +732,9 @@ class MediaActions(APIView):
def get(self, request, friendly_token, format=None):
# show date and reason for each time media was reported
media = self.get_object(friendly_token)
if not (request.user == media.user or is_mediacms_editor(request.user) or is_mediacms_manager(request.user)):
return Response({"detail": "not allowed"}, status=status.HTTP_400_BAD_REQUEST)
if isinstance(media, Response):
return media
@@ -909,9 +988,10 @@ class PlaylistDetail(APIView):
serializer = PlaylistDetailSerializer(playlist, context={"request": request})
playlist_media = PlaylistMedia.objects.filter(playlist=playlist).prefetch_related("media__user")
playlist_media = PlaylistMedia.objects.filter(playlist=playlist, media__state="public").prefetch_related("media__user")
playlist_media = [c.media for c in playlist_media]
playlist_media_serializer = MediaSerializer(playlist_media, many=True, context={"request": request})
ret = serializer.data
ret["playlist_media"] = playlist_media_serializer.data
@@ -1176,7 +1256,7 @@ class CommentList(APIView):
def get(self, request, format=None):
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
paginator = pagination_class()
comments = Comment.objects.filter()
comments = Comment.objects.filter(media__state="public").order_by("-add_date")
comments = comments.prefetch_related("user")
comments = comments.prefetch_related("media")
params = self.request.query_params

44387
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -49,6 +49,10 @@
"react-mentions": "^4.3.1",
"sortablejs": "^1.13.0",
"timeago.js": "^4.0.2",
"url-parse": "^1.5.1"
"url-parse": "^1.5.1",
"pdfjs-dist": "^3.4.120",
"@react-pdf-viewer/core": "^3.9.0",
"@react-pdf-viewer/default-layout": "^3.12.0"
}
}

View File

@@ -0,0 +1,53 @@
import React, { useEffect, useRef, useState } from 'react';
import './ToolTip.scss';
function Tooltip({ children, content, title, position = 'right', classNames = '' }) {
const [active, setActive] = useState(false);
const [tooltipDimensions, setTooltipDimensions] = useState({
height: 0,
width: 0,
});
const popUpRef = useRef(null);
const showTip = () => {
setActive(true);
};
const hideTip = () => {
setActive(false);
};
useEffect(() => {
if (popUpRef.current) {
setTooltipDimensions({
height: popUpRef.current.clientHeight || 0,
width: popUpRef.current.clientWidth || 0,
});
}
}, [active]);
const tooltipPositionStyles = {
right: { left: '100%', marginLeft: '10px', top: '-50%' },
left: { right: '100%', marginRight: '10px', top: '-50%' },
top: { left: '50%', top: `-${tooltipDimensions.height + 10}px`, transform: 'translateX(-50%)' },
center: { top: '50%', left: '50%', translate: 'x-[-50%]' },
'bottom-left': { left: `-${tooltipDimensions.width - 20}px`, top: '100%', marginTop: '10px' },
};
return (
<div onMouseEnter={showTip} onMouseLeave={hideTip}>
<div
ref={popUpRef}
className={`tooltip-box ${active ? 'show' : 'hide'} ${classNames}`}
style={tooltipPositionStyles[position]}
>
{title && <div className="tooltip-title">{title}</div>}
<div className="tooltip-content">{content}</div>
</div>
{children}
</div>
);
}
export default Tooltip;

View File

@@ -0,0 +1,31 @@
.tooltip-box {
position: absolute;
padding: 10px;
z-index: 100;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
opacity: 0;
transform: translateY(-10px);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.tooltip-box.show {
opacity: 1;
transform: translateY(0);
}
.tooltip-box.hide {
opacity: 0;
transform: translateY(-10px);
}
.tooltip-title {
color: #333;
font-size: 14px;
margin-bottom: 5px;
}
.tooltip-content {
color: #666;
font-size: 12px;
}

View File

@@ -505,7 +505,7 @@ export default function CommentsList(props) {
function onCommentSubmitFail() {
// FIXME: Without delay creates conflict [ Uncaught Error: Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch. ].
setTimeout(
() => PageActions.addNotification(commentsText.ucfirstSingle + ' submition failed', 'commentSubmitFail'),
() => PageActions.addNotification(commentsText.ucfirstSingle + ' submission failed', 'commentSubmitFail'),
100
);
}

View File

@@ -69,13 +69,18 @@ a.item-thumb {
}
}
.item.pdf-item &,
.item.attachment-item & {
.item.pdf-item & {
&:before {
content: '\e415';
}
}
.item.attachment-item & {
&:before {
content: '\e24d';
}
}
.item.playlist-item & {
&:before {

View File

@@ -522,6 +522,7 @@
display: block;
img {
cursor: pointer;
position: relative;
display: block;
max-width: 100%;
@@ -530,6 +531,135 @@
}
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.slideshow-container {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: auto;
max-width: 90%;
}
.slideshow-image img {
display: block;
width: auto;
height: auto;
max-width: 100%; /* Ensure image doesn't exceed container width */
max-height: 90vh;
border-radius: 0;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
transition: transform 60s ease-in-out, opacity 60 ease-in-out;
}
.slideshow-title {
margin-top: 10px;
text-align: start;
font-size: 16px;
font-weight: bold;
color: #bdb6b6;
z-index: 1200;
}
.arrow {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
width: 40px;
height: 40px;
top: 50%;
transform: translateY(-50%);
border: none;
color: white;
font-size: 2rem;
background-color: rgba(0, 0, 0, 0.2);
cursor: pointer;
padding: 10px;
border-radius: 50%;
z-index: 1000;
transition: background-color 0.2s ease, transform 0.2s ease;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
}
.arrow:hover {
background: rgba(92, 78, 78, 0.6);
transform: translateY(-50%) scale(1.1);
}
.arrow.left {
left: 10px;
}
.arrow.right {
right: 10px;
}
.thumbnail-navigation {
position: fixed;
display: flex;
align-items: center;
justify-content: center;
margin-top: 20px;
gap: 10px;
bottom: 10%;
left: 50%;
transform: translateX(-50%);
}
.thumbnail-container {
display: flex;
gap: 10px;
overflow-x: auto;
scroll-behavior: smooth;
max-width: 80%;
padding: 10px 0;
scrollbar-width: none; /* Hide scrollbar for Firefox */
}
.thumbnail-container.center-thumbnails {
display: flex;
justify-content: center;
overflow: visible; /* No scrollbars for fewer thumbnails */
}
.thumbnail-container::-webkit-scrollbar {
display: none; /* Hide scrollbar for Chrome/Safari */
}
.thumbnail {
width: 60px;
height: 60px;
object-fit: cover;
border: 2px solid transparent;
border-radius: 5px;
cursor: pointer;
transition: transform 0.3s ease;
}
.thumbnail.active {
border-color: white;
}
.thumbnail:hover {
transform: scale(1.1);
}
.viewer-container .player-container {
@media screen and (min-width: 480px) {
border-radius: 10px;
@@ -537,7 +667,6 @@
}
.viewer-container .player-container.audio-player-container {
@media screen and (min-width: 480px) {
padding-top: 0.75 * 56.25%;
}
@@ -551,6 +680,26 @@
}
}
.viewer-container .pdf-container {
overflow-y: auto;
display: flex;
justify-content: center;
align-items: center;
width: 100%; // Default width for mobile
height: 400px; // Default height for mobile
@media (min-width: 768px) and (max-width: 1023px) { // Tablets
width: 90%;
height: 600px;
}
@media (min-width: 1024px) { // Desktop
width: 85%;
height: 900px;
}
}
.viewer-container .player-container.viewer-pdf-container,
.viewer-container .player-container.viewer-attachment-container {
background-color: var(--item-thumb-bg-color);

View File

@@ -1,8 +1,10 @@
import React, { useContext, useEffect, useState } from 'react';
import { SiteContext } from '../../utils/contexts/';
import { MediaPageStore } from '../../utils/stores/';
import { SpinnerLoader } from '../_shared';
import Tooltip from '../_shared/ToolTip';
export default function ImageViewer(props) {
export default function ImageViewer() {
const site = useContext(SiteContext);
let initialImage = getImageUrl();
@@ -11,6 +13,12 @@ export default function ImageViewer(props) {
initialImage = initialImage ? initialImage : '';
const [image, setImage] = useState(initialImage);
const [slideshowItems, setSlideshowItems] = useState([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [currentIndex, setCurrentIndex] = useState(0);
const [isImgLoading, setIsImgLoading] = useState(true);
const thumbnailRef = React.useRef();
function onImageLoad() {
setImage(getImageUrl());
@@ -19,34 +27,142 @@ export default function ImageViewer(props) {
function getImageUrl() {
const media_data = MediaPageStore.get('media-data');
let imgUrl = 'string' === typeof media_data.poster_url ? media_data.poster_url.trim() : '';
if ('' === imgUrl) {
imgUrl = 'string' === typeof media_data.thumbnail_url ? media_data.thumbnail_url.trim() : '';
}
if ('' === imgUrl) {
imgUrl =
'string' === typeof MediaPageStore.get('media-original-url')
? MediaPageStore.get('media-original-url').trim()
: '';
}
if ('' === imgUrl) {
return '#';
}
let imgUrl =
media_data.poster_url?.trim() ||
media_data.thumbnail_url?.trim() ||
MediaPageStore.get('media-original-url')?.trim() ||
'#';
return site.url + '/' + imgUrl.replace(/^\//g, '');
}
const fetchSlideShowItems = () => {
const media_data = MediaPageStore.get('media-data');
const items = media_data.slideshow_items;
if (Array.isArray(items)) {
setSlideshowItems(items);
}
};
useEffect(() => {
if (image) {
fetchSlideShowItems();
}
}, [image]);
useEffect(() => {
MediaPageStore.on('loaded_image_data', onImageLoad);
return () => MediaPageStore.removeListener('loaded_image_data', onImageLoad);
}, []);
useEffect(() => {
if (!isModalOpen) return;
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [isModalOpen, slideshowItems]);
const handleKeyDown = (event) => {
if (event.key === 'ArrowRight') handleNext();
if (event.key === 'ArrowLeft') handlePrevious();
if (event.key === 'Escape') onClose();
};
const onClose = () => setIsModalOpen(false);
const handleNext = () => {
setIsImgLoading(true);
setCurrentIndex((prevIndex) => (prevIndex + 1) % slideshowItems.length);
};
const handlePrevious = () => {
setIsImgLoading(true);
setCurrentIndex((prevIndex) => (prevIndex - 1 + slideshowItems.length) % slideshowItems.length);
};
const handleDotClick = (index) => {
setIsImgLoading(true);
setCurrentIndex(index);
};
const handleImageClick = (index) => {
const mediaPageUrl = site.url + slideshowItems[index]?.url;
window.location.href = mediaPageUrl;
};
const scrollThumbnails = (direction) => {
if (thumbnailRef.current) {
const scrollAmount = 10;
if (direction === 'left') {
thumbnailRef.current.scrollBy({ left: -scrollAmount, behavior: 'smooth' });
} else if (direction === 'right') {
thumbnailRef.current.scrollBy({ left: scrollAmount, behavior: 'smooth' });
}
}
};
return !image ? null : (
<div className="viewer-image-container">
<img src={image} alt={MediaPageStore.get('media-data').title || null} />
<Tooltip content={'load full-image'} position="center">
<img src={image} alt={MediaPageStore.get('media-data').title || null} onClick={() => setIsModalOpen(true)} />
</Tooltip>
{isModalOpen && slideshowItems && (
<div className="modal-overlay" onClick={() => setIsModalOpen(false)}>
<div className="slideshow-container" onClick={(e) => e.stopPropagation()}>
{!isImgLoading && (
<button className="arrow left" onClick={handlePrevious} aria-label="Previous slide">
&#8249;
</button>
)}
<div className="slideshow-image">
{isImgLoading && <SpinnerLoader size="large" />}
<img
src={site.url + '/' + slideshowItems[currentIndex]?.original_media_url}
alt={`Slide ${currentIndex + 1}`}
onClick={() => handleImageClick(currentIndex)}
onLoad={() => setIsImgLoading(false)}
onError={() => setIsImgLoading(false)}
style={{ display: isImgLoading ? 'none' : 'block' }}
/>
{!isImgLoading && <div className="slideshow-title">{slideshowItems[currentIndex]?.title}</div>}
</div>
{!isImgLoading && (
<button className="arrow right" onClick={handleNext} aria-label="Next slide">
&#8250;
</button>
)}
<div className="thumbnail-navigation">
{slideshowItems.length > 5 && (
<button className="arrow left" onClick={() => scrollThumbnails('left')} aria-label="Scroll left">
&#8249;
</button>
)}
<div
className={`thumbnail-container ${slideshowItems.length <= 5 ? 'center-thumbnails' : ''}`}
ref={thumbnailRef}
>
{slideshowItems.map((item, index) => (
<img
key={index}
src={site.url + '/' + item.thumbnail_url}
alt={`Thumbnail ${index + 1}`}
className={`thumbnail ${currentIndex === index ? 'active' : ''}`}
onClick={() => handleDotClick(index)}
/>
))}
</div>
{slideshowItems.length > 5 && (
<button className="arrow right" onClick={() => scrollThumbnails('right')} aria-label="Scroll right">
&#8250;
</button>
)}
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -1,15 +1,18 @@
import React from 'react';
import { Worker, Viewer } from '@react-pdf-viewer/core';
import { defaultLayoutPlugin } from '@react-pdf-viewer/default-layout';
export default function PdfViewer() {
import '@react-pdf-viewer/core/lib/styles/index.css'
import '@react-pdf-viewer/default-layout/lib/styles/index.css';
export default function PdfViewer({ fileUrl }) {
const defaultLayoutPluginInstance = defaultLayoutPlugin();
return (
<div className="player-container viewer-pdf-container">
<div className="player-container-inner">
<span>
<span>
<i className="material-icons">insert_drive_file</i>
</span>
</span>
<div className='pdf-container'>
<Worker workerUrl="https://unpkg.com/pdfjs-dist@3.4.120/build/pdf.worker.min.js">
<Viewer fileUrl={fileUrl} plugins={[defaultLayoutPluginInstance]} />
</Worker>
</div>
</div>
);
}

View File

@@ -7,6 +7,8 @@ import ImageViewer from '../components/media-viewer/ImageViewer';
import PdfViewer from '../components/media-viewer/PdfViewer';
import VideoViewer from '../components/media-viewer/VideoViewer';
import { _VideoMediaPage } from './_VideoMediaPage';
import { formatInnerLink } from '../utils/helpers';
import {SiteContext} from '../utils/contexts/';
if (window.MediaCMS.site.devEnv) {
const extractUrlParams = () => {
@@ -52,7 +54,8 @@ export class MediaPage extends _VideoMediaPage {
case 'image':
return <ImageViewer />;
case 'pdf':
return <PdfViewer />;
const pdf_url = formatInnerLink(MediaPageStore.get('media-original-url'), SiteContext._currentValue.url);
return <PdfViewer fileUrl={pdf_url} />;
}
return <AttachmentViewer />;

View File

@@ -46,6 +46,11 @@ if (window.MediaCMS.site.devEnv) {
}
function PlayAllLink(props) {
if (!props.media || !props.media.length) {
return <span>{props.children}</span>;
}
let playAllUrl = props.media[0].url;
if (window.MediaCMS.site.devEnv && -1 < playAllUrl.indexOf('view?')) {

View File

@@ -1,16 +1,15 @@
-r requirements.txt
rpdb
tqdm
ipython
flake8
pylint
pep8
django-silk
pre-commit
pytest-cov
pytest-django
pytest-factoryboy
Faker
django-cors-headers
rpdb==0.2.0
tqdm==4.67.1
ipython==8.32.0
flake8==7.1.1
pylint==3.3.4
pep8==1.7.1
django-silk==5.3.2
pytest-cov==6.0.0
pytest-django==4.9.0
pytest-factoryboy==2.7.0
Faker==35.2.0
django-cors-headers==4.7.0

View File

@@ -1,21 +1,22 @@
Django==4.2.2
djangorestframework==3.14.0
django-allauth==0.54.0
psycopg==3.1.9
uwsgi==2.0.21
django-redis==5.3.0
celery==5.3.1
drf-yasg==1.21.6
Pillow==9.5.0
django-imagekit==4.1.0
markdown==3.4.3
django-filter==23.2
Django==5.1.6
djangorestframework==3.15.2
django-allauth==65.4.1
psycopg==3.2.4
uwsgi==2.0.28
django-redis==5.4.0
celery==5.4.0
drf-yasg==1.21.8
Pillow==11.1.0
django-imagekit==5.0.0
markdown==3.7
django-filter==24.3
filetype==1.2.0
django-mptt==0.14.0
django-crispy-forms==1.13.0
requests==2.31.0
django-mptt==0.16.0
crispy-bootstrap5==2024.10
requests==2.32.3
django-celery-email==3.0.0
m3u8==3.5.0
django-ckeditor==6.6.1
django-debug-toolbar==4.1.0
m3u8==6.0.0
django-debug-toolbar==5.0.1
django-login-required-middleware==0.9.0
pre-commit==4.1.0
pysubs2==1.8.0

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -11,6 +11,45 @@ object-assign
* @license MIT
*/
/*!
* The buffer module from node.js, for the browser.
*
* @author Feross Aboukhadijeh <https://feross.org>
* @license MIT
*/
/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */
/**
* @licstart The following is the entire license notice for the
* JavaScript code in this page
*
* Copyright 2023 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @licend The above is the entire license notice for the
* JavaScript code in this page
*/
/**
* A React component to view a PDF document
*
* @see https://react-pdf-viewer.dev
* @license https://react-pdf-viewer.dev/license
* @copyright 2019-2023 Nguyen Huu Phuoc <me@phuoc.ng>
*/
/** @license React v0.20.2
* scheduler.production.min.js
*

View File

@@ -1 +1 @@
!function(){"use strict";var n,e={6814:function(n,e,r){(0,r(2541).X)()}},r={};function t(n){var o=r[n];if(void 0!==o)return o.exports;var u=r[n]={exports:{}};return e[n].call(u.exports,u,u.exports,t),u.exports}t.m=e,n=[],t.O=function(e,r,o,u){if(!r){var i=1/0;for(a=0;a<n.length;a++){r=n[a][0],o=n[a][1],u=n[a][2];for(var f=!0,c=0;c<r.length;c++)(!1&u||i>=u)&&Object.keys(t.O).every((function(n){return t.O[n](r[c])}))?r.splice(c--,1):(f=!1,u<i&&(i=u));f&&(n.splice(a--,1),e=o())}return e}u=u||0;for(var a=n.length;a>0&&n[a-1][2]>u;a--)n[a]=n[a-1];n[a]=[r,o,u]},t.n=function(n){var e=n&&n.__esModule?function(){return n.default}:function(){return n};return t.d(e,{a:e}),e},t.d=function(n,e){for(var r in e)t.o(e,r)&&!t.o(n,r)&&Object.defineProperty(n,r,{enumerable:!0,get:e[r]})},t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(n){if("object"==typeof window)return window}}(),t.o=function(n,e){return Object.prototype.hasOwnProperty.call(n,e)},t.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},t.j=443,function(){var n={443:0};t.O.j=function(e){return 0===n[e]};var e=function(e,r){var o,u,i=r[0],f=r[1],c=r[2],a=0;for(o in f)t.o(f,o)&&(t.m[o]=f[o]);if(c)var l=c(t);for(e&&e(r);a<i.length;a++)u=i[a],t.o(n,u)&&n[u]&&n[u][0](),n[i[a]]=0;return t.O(l)},r=self.webpackChunkmediacms_frontend=self.webpackChunkmediacms_frontend||[];r.forEach(e.bind(null,0)),r.push=e.bind(null,r.push.bind(r))}();var o=t.O(void 0,[431],(function(){return t(6814)}));o=t.O(o)}();
!function(){"use strict";var n,e={66814:function(n,e,r){(0,r(92541).X)()}},r={};function t(n){var o=r[n];if(void 0!==o)return o.exports;var u=r[n]={exports:{}};return e[n].call(u.exports,u,u.exports,t),u.exports}t.m=e,n=[],t.O=function(e,r,o,u){if(!r){var i=1/0;for(a=0;a<n.length;a++){r=n[a][0],o=n[a][1],u=n[a][2];for(var f=!0,c=0;c<r.length;c++)(!1&u||i>=u)&&Object.keys(t.O).every((function(n){return t.O[n](r[c])}))?r.splice(c--,1):(f=!1,u<i&&(i=u));f&&(n.splice(a--,1),e=o())}return e}u=u||0;for(var a=n.length;a>0&&n[a-1][2]>u;a--)n[a]=n[a-1];n[a]=[r,o,u]},t.n=function(n){var e=n&&n.__esModule?function(){return n.default}:function(){return n};return t.d(e,{a:e}),e},t.d=function(n,e){for(var r in e)t.o(e,r)&&!t.o(n,r)&&Object.defineProperty(n,r,{enumerable:!0,get:e[r]})},t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(n){if("object"==typeof window)return window}}(),t.o=function(n,e){return Object.prototype.hasOwnProperty.call(n,e)},t.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},t.j=443,function(){var n={443:0};t.O.j=function(e){return 0===n[e]};var e=function(e,r){var o,u,i=r[0],f=r[1],c=r[2],a=0;for(o in f)t.o(f,o)&&(t.m[o]=f[o]);if(c)var l=c(t);for(e&&e(r);a<i.length;a++)u=i[a],t.o(n,u)&&n[u]&&n[u][0](),n[i[a]]=0;return t.O(l)},r=self.webpackChunkmediacms_frontend=self.webpackChunkmediacms_frontend||[];r.forEach(e.bind(null,0)),r.push=e.bind(null,r.push.bind(r))}();var o=t.O(void 0,[431],(function(){return t(66814)}));o=t.O(o)}();

View File

@@ -1 +1 @@
!function(){"use strict";var n,e={2772:function(n,e,r){(0,r(2541).X)()}},r={};function t(n){var o=r[n];if(void 0!==o)return o.exports;var u=r[n]={exports:{}};return e[n].call(u.exports,u,u.exports,t),u.exports}t.m=e,n=[],t.O=function(e,r,o,u){if(!r){var i=1/0;for(a=0;a<n.length;a++){r=n[a][0],o=n[a][1],u=n[a][2];for(var f=!0,c=0;c<r.length;c++)(!1&u||i>=u)&&Object.keys(t.O).every((function(n){return t.O[n](r[c])}))?r.splice(c--,1):(f=!1,u<i&&(i=u));f&&(n.splice(a--,1),e=o())}return e}u=u||0;for(var a=n.length;a>0&&n[a-1][2]>u;a--)n[a]=n[a-1];n[a]=[r,o,u]},t.n=function(n){var e=n&&n.__esModule?function(){return n.default}:function(){return n};return t.d(e,{a:e}),e},t.d=function(n,e){for(var r in e)t.o(e,r)&&!t.o(n,r)&&Object.defineProperty(n,r,{enumerable:!0,get:e[r]})},t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(n){if("object"==typeof window)return window}}(),t.o=function(n,e){return Object.prototype.hasOwnProperty.call(n,e)},t.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},t.j=841,function(){var n={841:0};t.O.j=function(e){return 0===n[e]};var e=function(e,r){var o,u,i=r[0],f=r[1],c=r[2],a=0;for(o in f)t.o(f,o)&&(t.m[o]=f[o]);if(c)var l=c(t);for(e&&e(r);a<i.length;a++)u=i[a],t.o(n,u)&&n[u]&&n[u][0](),n[i[a]]=0;return t.O(l)},r=self.webpackChunkmediacms_frontend=self.webpackChunkmediacms_frontend||[];r.forEach(e.bind(null,0)),r.push=e.bind(null,r.push.bind(r))}();var o=t.O(void 0,[431],(function(){return t(2772)}));o=t.O(o)}();
!function(){"use strict";var n,e={2772:function(n,e,r){(0,r(92541).X)()}},r={};function t(n){var o=r[n];if(void 0!==o)return o.exports;var u=r[n]={exports:{}};return e[n].call(u.exports,u,u.exports,t),u.exports}t.m=e,n=[],t.O=function(e,r,o,u){if(!r){var i=1/0;for(a=0;a<n.length;a++){r=n[a][0],o=n[a][1],u=n[a][2];for(var f=!0,c=0;c<r.length;c++)(!1&u||i>=u)&&Object.keys(t.O).every((function(n){return t.O[n](r[c])}))?r.splice(c--,1):(f=!1,u<i&&(i=u));f&&(n.splice(a--,1),e=o())}return e}u=u||0;for(var a=n.length;a>0&&n[a-1][2]>u;a--)n[a]=n[a-1];n[a]=[r,o,u]},t.n=function(n){var e=n&&n.__esModule?function(){return n.default}:function(){return n};return t.d(e,{a:e}),e},t.d=function(n,e){for(var r in e)t.o(e,r)&&!t.o(n,r)&&Object.defineProperty(n,r,{enumerable:!0,get:e[r]})},t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(n){if("object"==typeof window)return window}}(),t.o=function(n,e){return Object.prototype.hasOwnProperty.call(n,e)},t.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},t.j=841,function(){var n={841:0};t.O.j=function(e){return 0===n[e]};var e=function(e,r){var o,u,i=r[0],f=r[1],c=r[2],a=0;for(o in f)t.o(f,o)&&(t.m[o]=f[o]);if(c)var l=c(t);for(e&&e(r);a<i.length;a++)u=i[a],t.o(n,u)&&n[u]&&n[u][0](),n[i[a]]=0;return t.O(l)},r=self.webpackChunkmediacms_frontend=self.webpackChunkmediacms_frontend||[];r.forEach(e.bind(null,0)),r.push=e.bind(null,r.push.bind(r))}();var o=t.O(void 0,[431],(function(){return t(2772)}));o=t.O(o)}();

View File

@@ -1 +1 @@
!function(){"use strict";var n,e={9980:function(n,e,r){(0,r(2541).X)()}},r={};function t(n){var o=r[n];if(void 0!==o)return o.exports;var u=r[n]={exports:{}};return e[n].call(u.exports,u,u.exports,t),u.exports}t.m=e,n=[],t.O=function(e,r,o,u){if(!r){var i=1/0;for(a=0;a<n.length;a++){r=n[a][0],o=n[a][1],u=n[a][2];for(var f=!0,c=0;c<r.length;c++)(!1&u||i>=u)&&Object.keys(t.O).every((function(n){return t.O[n](r[c])}))?r.splice(c--,1):(f=!1,u<i&&(i=u));f&&(n.splice(a--,1),e=o())}return e}u=u||0;for(var a=n.length;a>0&&n[a-1][2]>u;a--)n[a]=n[a-1];n[a]=[r,o,u]},t.n=function(n){var e=n&&n.__esModule?function(){return n.default}:function(){return n};return t.d(e,{a:e}),e},t.d=function(n,e){for(var r in e)t.o(e,r)&&!t.o(n,r)&&Object.defineProperty(n,r,{enumerable:!0,get:e[r]})},t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(n){if("object"==typeof window)return window}}(),t.o=function(n,e){return Object.prototype.hasOwnProperty.call(n,e)},t.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},t.j=348,function(){var n={348:0};t.O.j=function(e){return 0===n[e]};var e=function(e,r){var o,u,i=r[0],f=r[1],c=r[2],a=0;for(o in f)t.o(f,o)&&(t.m[o]=f[o]);if(c)var l=c(t);for(e&&e(r);a<i.length;a++)u=i[a],t.o(n,u)&&n[u]&&n[u][0](),n[i[a]]=0;return t.O(l)},r=self.webpackChunkmediacms_frontend=self.webpackChunkmediacms_frontend||[];r.forEach(e.bind(null,0)),r.push=e.bind(null,r.push.bind(r))}();var o=t.O(void 0,[431],(function(){return t(9980)}));o=t.O(o)}();
!function(){"use strict";var n,e={49980:function(n,e,r){(0,r(92541).X)()}},r={};function t(n){var o=r[n];if(void 0!==o)return o.exports;var u=r[n]={exports:{}};return e[n].call(u.exports,u,u.exports,t),u.exports}t.m=e,n=[],t.O=function(e,r,o,u){if(!r){var i=1/0;for(a=0;a<n.length;a++){r=n[a][0],o=n[a][1],u=n[a][2];for(var f=!0,c=0;c<r.length;c++)(!1&u||i>=u)&&Object.keys(t.O).every((function(n){return t.O[n](r[c])}))?r.splice(c--,1):(f=!1,u<i&&(i=u));f&&(n.splice(a--,1),e=o())}return e}u=u||0;for(var a=n.length;a>0&&n[a-1][2]>u;a--)n[a]=n[a-1];n[a]=[r,o,u]},t.n=function(n){var e=n&&n.__esModule?function(){return n.default}:function(){return n};return t.d(e,{a:e}),e},t.d=function(n,e){for(var r in e)t.o(e,r)&&!t.o(n,r)&&Object.defineProperty(n,r,{enumerable:!0,get:e[r]})},t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(n){if("object"==typeof window)return window}}(),t.o=function(n,e){return Object.prototype.hasOwnProperty.call(n,e)},t.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},t.j=348,function(){var n={348:0};t.O.j=function(e){return 0===n[e]};var e=function(e,r){var o,u,i=r[0],f=r[1],c=r[2],a=0;for(o in f)t.o(f,o)&&(t.m[o]=f[o]);if(c)var l=c(t);for(e&&e(r);a<i.length;a++)u=i[a],t.o(n,u)&&n[u]&&n[u][0](),n[i[a]]=0;return t.O(l)},r=self.webpackChunkmediacms_frontend=self.webpackChunkmediacms_frontend||[];r.forEach(e.bind(null,0)),r.push=e.bind(null,r.push.bind(r))}();var o=t.O(void 0,[431],(function(){return t(49980)}));o=t.O(o)}();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -17,7 +17,6 @@
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}" />
{% endif %}
<button class="primaryAction" type="submit">Sign In</button>
<a class="button secondaryAction" href="{% url 'account_reset_password' %}">Forgot Password?</a>
</form>
</div>

View File

@@ -140,7 +140,6 @@
{% csrf_token %}
{{ form.as_p }}
<input type="hidden" name="next" value="{{ redirect_url }}" />
<a class="button secondaryAction" href="{% url 'account_reset_password' %}">Forgot Password?</a>
<button class="primaryAction" type="submit">Sign In</button>
</form>

View File

@@ -7,11 +7,23 @@
<div class="user-action-form-wrap">
<div class="user-action-form-inner">
<h1>Add subtitle</h1>
Media: <a href="{{media.get_absolute_url}}">{{media.title}}</a>
<form enctype="multipart/form-data" action="" method="post" class="post-form">
{% csrf_token %}
{{ form|crispy }}
<button class="primaryAction" type="submit">Add</button>
</form>
{% if subtitles %}
<h3>View/Edit Existing Subtitles</h3>
<ul>
{% for subtitle in subtitles %}
<li><a href="{{subtitle.url}}">{{subtitle.language.title}}</a></li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
{% endblock innercontent %}

View File

@@ -7,8 +7,6 @@
{% block headermeta %}{% endblock headermeta %}
{% block innercontent %}
<script type="text/javascript" src="{% static "ckeditor/ckeditor-init.js" %}"></script>
<script type="text/javascript" src="{% static "ckeditor/ckeditor/ckeditor.js" %}"></script>
<div class="user-action-form-wrap">
<div class="user-action-form-inner">

View File

@@ -0,0 +1,49 @@
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% block headtitle %}Edit subtitle - {{PORTAL_NAME}}{% endblock headtitle %}
{% block headermeta %}{% endblock headermeta %}
{% block innercontent %}
{% if action == 'delete' %}
<div class="user-action-form-wrap">
<h1>Confirm deletion</h1>
<div class="user-action-form-inner">
are you sure you want to delete the subtitle?
<form action="{{subtitle.url}}&action=delete&confirm=true" method="post" class="post-form">
{% csrf_token %}
<button class="secondaryAction" type="submit">DELETE SUBTITLE</button>
</form>
</div>
</div>
{% else %}
<div class="user-action-form-wrap">
<h1>Edit {{subtitle.language.title}} subtitle</h1>
<div class="user-action-form-inner">
Media: <a href="{{subtitle.media.get_absolute_url}}">{{subtitle.media.title}}</a>
<form action="" method="post" class="post-form">
{% csrf_token %}
{{ form|crispy }}
<button class="primaryAction" style="margin-right: 10px" type="submit">SAVE</button>
<button class="primaryAction" style="margin-right: 10px" type="button" onclick="window.location.href='{{subtitle.url}}&action=download';">DOWNLOAD</button>
<button class="primaryAction" type="button" onclick="window.location.href='{{subtitle.url}}&action=delete';">DELETE</button>
</form>
</div>
</div>
{% endif %}
{% endblock innercontent %}

View File

@@ -20,100 +20,105 @@
<meta property="og:type" content="website">
{% endif %}
{% if media_object.media_type == "video" %}
{% if media_object.state != "private" %}
<meta property="og:image" content="{{FRONTEND_HOST}}{{media_object.poster_url}}">
{% if media_object.media_type == "video" %}
<meta name="twitter:card" content="summary_large_image">
<meta property="og:image" content="{{FRONTEND_HOST}}{{media_object.poster_url}}">
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "VideoObject",
"name": "{{media_object.title}} - {{PORTAL_NAME}}",
"url": "{{FRONTEND_HOST}}{{media_object.get_absolute_url}}",
"description": "{% if media_object.summary %}{{media_object.summary}}{% else %}{{media_object.description}}{% endif %}",
"thumbnailUrl": [
"{{FRONTEND_HOST}}{{media_object.poster_url}}"
],
"uploadDate": "{{media_object.add_date}}",
"dateModified": "{{media_object.edit_date}}",
"embedUrl": "{{FRONTEND_HOST}}/embed?m={{media}}",
"duration": "T{{media_object.duration}}S",
"potentialAction": {
"@type": "ViewAction",
"target": "{{FRONTEND_HOST}}{{media_object.get_absolute_url}}"
<meta name="twitter:card" content="summary_large_image">
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "VideoObject",
"name": "{{media_object.title}} - {{PORTAL_NAME}}",
"url": "{{FRONTEND_HOST}}{{media_object.get_absolute_url}}",
"description": "{% if media_object.summary %}{{media_object.summary}}{% else %}{{media_object.description}}{% endif %}",
"thumbnailUrl": [
"{{FRONTEND_HOST}}{{media_object.poster_url}}"
],
"uploadDate": "{{media_object.add_date}}",
"dateModified": "{{media_object.edit_date}}",
"embedUrl": "{{FRONTEND_HOST}}/embed?m={{media}}",
"duration": "T{{media_object.duration}}S",
"potentialAction": {
"@type": "ViewAction",
"target": "{{FRONTEND_HOST}}{{media_object.get_absolute_url}}"
}
}
}
</script>
</script>
{% elif media_object.media_type == "audio" %}
{% elif media_object.media_type == "audio" %}
<meta property="og:image" content="{{FRONTEND_HOST}}{{media_object.poster_url}}">
<meta property="og:image" content="{{FRONTEND_HOST}}{{media_object.poster_url}}">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:card" content="summary_large_image">
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "AudioObject",
"name": "{{media_object.title}} - {{PORTAL_NAME}}",
"url": "{{FRONTEND_HOST}}{{media_object.get_absolute_url}}",
"description": "{% if media_object.summary %}{{media_object.summary}}{% else %}{{media_object.description}}{% endif %}",
"uploadDate": "{{media_object.add_date}}",
"dateModified": "{{media_object.edit_date}}",
"duration": "T{{media_object.duration}}S",
"potentialAction": {
"@type": "ViewAction",
"target": "{{FRONTEND_HOST}}{{media_object.get_absolute_url}}"
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "AudioObject",
"name": "{{media_object.title}} - {{PORTAL_NAME}}",
"url": "{{FRONTEND_HOST}}{{media_object.get_absolute_url}}",
"description": "{% if media_object.summary %}{{media_object.summary}}{% else %}{{media_object.description}}{% endif %}",
"uploadDate": "{{media_object.add_date}}",
"dateModified": "{{media_object.edit_date}}",
"duration": "T{{media_object.duration}}S",
"potentialAction": {
"@type": "ViewAction",
"target": "{{FRONTEND_HOST}}{{media_object.get_absolute_url}}"
}
}
}
</script>
</script>
{% elif media_object.media_type == "image" %}
{% elif media_object.media_type == "image" %}
<meta property="og:image" content="{{FRONTEND_HOST}}{{media_object.original_media_url}}">
<meta property="og:image" content="{{FRONTEND_HOST}}{{media_object.original_media_url}}">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:card" content="summary_large_image">
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "ImageObject",
"name": "{{media_object.title}} - {{PORTAL_NAME}}",
"url": "{{FRONTEND_HOST}}{{media_object.get_absolute_url}}",
"description": "{% if media_object.summary %}{{media_object.summary}}{% else %}{{media_object.description}}{% endif %}",
"uploadDate": "{{media_object.add_date}}",
"dateModified": "{{media_object.edit_date}}",
"potentialAction": {
"@type": "ViewAction",
"target": "{{FRONTEND_HOST}}{{media_object.get_absolute_url}}"
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "ImageObject",
"name": "{{media_object.title}} - {{PORTAL_NAME}}",
"url": "{{FRONTEND_HOST}}{{media_object.get_absolute_url}}",
"description": "{% if media_object.summary %}{{media_object.summary}}{% else %}{{media_object.description}}{% endif %}",
"uploadDate": "{{media_object.add_date}}",
"dateModified": "{{media_object.edit_date}}",
"potentialAction": {
"@type": "ViewAction",
"target": "{{FRONTEND_HOST}}{{media_object.get_absolute_url}}"
}
}
}
</script>
</script>
{% else %}
<meta name="twitter:card" content="summary">
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "MediaObject",
"name": "{{media_object.title}} - {{PORTAL_NAME}}",
"url": "{{FRONTEND_HOST}}{{media_object.get_absolute_url}}",
"description": "{% if media_object.summary %}{{media_object.summary}}{% else %}{{media_object.description}}{% endif %}",
"uploadDate": "{{media_object.add_date}}",
"dateModified": "{{media_object.edit_date}}",
"potentialAction": {
"@type": "ViewAction",
"target": "{{FRONTEND_HOST}}{{media_object.get_absolute_url}}"
}
}
</script>
{% endif %}
{% else %}
<meta name="twitter:card" content="summary">
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "MediaObject",
"name": "{{media_object.title}} - {{PORTAL_NAME}}",
"url": "{{FRONTEND_HOST}}{{media_object.get_absolute_url}}",
"description": "{% if media_object.summary %}{{media_object.summary}}{% else %}{{media_object.description}}{% endif %}",
"uploadDate": "{{media_object.add_date}}",
"dateModified": "{{media_object.edit_date}}",
"potentialAction": {
"@type": "ViewAction",
"target": "{{FRONTEND_HOST}}{{media_object.get_absolute_url}}"
}
}
</script>
{% endif %}
{% endblock headermeta %}
{% block topimports %}

View File

@@ -3,8 +3,6 @@
{% block headtitle %}Edit profile - {% endblock headtitle %}
{% block innercontent %}
<script type="text/javascript" src="{% static "ckeditor/ckeditor-init.js" %}"></script>
<script type="text/javascript" src="{% static "ckeditor/ckeditor/ckeditor.js" %}"></script>
<div class="user-action-form-wrap">
<div class="user-action-form-inner">

View File

@@ -22,7 +22,7 @@ MediaCMS.url = {
editChannel: "{{user.default_channel_edit_url}}",
changePassword: "/accounts/password/change/",
/* Administration pages */
{% if IS_MEDIACMS_ADMIN %}admin: '/admin',{% endif %}
{% if IS_MEDIACMS_ADMIN %}admin: '/{{DJANGO_ADMIN_URL}}',{% endif %}
/* Management pages */
{% if IS_MEDIACMS_EDITOR %}manageMedia: "/manage/media",{% endif %}
{% if IS_MEDIACMS_MANAGER %}manageUsers: "/manage/users",{% endif %}

View File

@@ -35,13 +35,14 @@ class TestX(TestCase):
client.post('/fu/upload/', {'qqfile': fp, 'qqfilename': 'medium_video.mp4', 'qquuid': str(uuid.uuid4())})
self.assertEqual(Media.objects.all().count(), 3, "Problem with file upload")
# by default the portal_workflow is public, so anything uploaded gets public
self.assertEqual(Media.objects.filter(state='public').count(), 3, "Expected all media to be public, as per the default portal workflow")
self.assertEqual(Media.objects.filter(media_type='video', encoding_status='success').count(), 2, "Encoding did not finish well")
self.assertEqual(Media.objects.filter(media_type='video').count(), 2, "Media identification failed")
self.assertEqual(Media.objects.filter(media_type='image').count(), 1, "Media identification failed")
self.assertEqual(Media.objects.filter(user=self.user).count(), 3, "User assignment failed")
medium_video = Media.objects.get(title="medium_video.mp4")
self.assertEqual(len(medium_video.hls_info), 11, "Problem with HLS info")
# using the provided EncodeProfiles, these two files should produce 9 Encoding objects.
# if new EncodeProfiles are added and enabled, this will break!

View File

@@ -10,6 +10,10 @@ class MyAccountAdapter(DefaultAccountAdapter):
return settings.SSL_FRONTEND_HOST + url
def clean_email(self, email):
if hasattr(settings, "ALLOWED_DOMAINS_FOR_USER_REGISTRATION") and settings.ALLOWED_DOMAINS_FOR_USER_REGISTRATION:
if email.split("@")[1] not in settings.ALLOWED_DOMAINS_FOR_USER_REGISTRATION:
raise ValidationError("Domain is not in the permitted list")
if email.split("@")[1] in settings.RESTRICTED_DOMAINS_FOR_USER_REGISTRATION:
raise ValidationError("Domain is restricted from registering")
return email

View File

@@ -93,16 +93,16 @@ class LoginSerializer(serializers.Serializer):
username = data.get('username', None)
password = data.get('password', None)
if settings.ACCOUNT_AUTHENTICATION_METHOD == 'username' and not username:
if settings.ACCOUNT_LOGIN_METHODS == {"username"} and not username:
raise serializers.ValidationError('username is required to log in.')
else:
username_or_email = username
if settings.ACCOUNT_AUTHENTICATION_METHOD == 'email' and not email:
if settings.ACCOUNT_LOGIN_METHODS == {"email"} and not email:
raise serializers.ValidationError('email is required to log in.')
else:
username_or_email = email
if settings.ACCOUNT_AUTHENTICATION_METHOD == 'username_email' and not (username or email):
if settings.ACCOUNT_LOGIN_METHODS == {"username", "email"} and not (username or email):
raise serializers.ValidationError('username or email is required to log in.')
else:
username_or_email = username or email

View File

@@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _
@deconstructible
class ASCIIUsernameValidator(validators.RegexValidator):
regex = r"^[\w]+$"
regex = r"^[\w.@]+$"
message = _("Enter a valid username. This value may contain only " "English letters and numbers")
flags = re.ASCII