mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-12-06 04:22:30 -05:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1fda05fdc | ||
|
|
a02e0a8a66 | ||
|
|
21f76dbb6e | ||
|
|
50e9f3103f | ||
|
|
0b9a203123 | ||
|
|
5cbd815496 | ||
|
|
3a8cacc847 | ||
|
|
5402ee7bc5 | ||
|
|
a6a2b50c8d | ||
|
|
23e48a8bb7 | ||
|
|
313cd9cbc6 | ||
|
|
0392dbe1ed | ||
|
|
a7562c244e | ||
|
|
d2ee12087c | ||
|
|
6db01932e1 | ||
|
|
53d8215346 | ||
|
|
1b960b28f8 | ||
|
|
02d9188aa1 | ||
|
|
8d9a4618f0 | ||
|
|
cf93a77802 | ||
|
|
5a1e4f25ed | ||
|
|
9fc7597e73 | ||
|
|
9b3e0250d4 | ||
|
|
1384471745 | ||
|
|
29b362c8ce | ||
|
|
b8ee2e9fb8 | ||
|
|
99be0f07dd | ||
|
|
27d1660192 | ||
|
|
98adb22205 | ||
|
|
673ddeb5bd |
8
.github/workflows/python.yml
vendored
8
.github/workflows/python.yml
vendored
@@ -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
|
||||
|
||||
38
.github/workflows/semantic-release.yaml
vendored
38
.github/workflows/semantic-release.yaml
vendored
@@ -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 }}
|
||||
122
Dockerfile
122
Dockerfile
@@ -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"]
|
||||
@@ -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"]
|
||||
8
Makefile
8
Makefile
@@ -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
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
99
deploy/docker/policy.xml
Normal 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>
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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}'))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -145,6 +145,7 @@ class SingleMediaSerializer(serializers.ModelSerializer):
|
||||
"ratings_info",
|
||||
"add_subtitle_url",
|
||||
"allow_download",
|
||||
"slideshow_items",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -7,5 +7,4 @@ register = template.Library()
|
||||
|
||||
@register.filter
|
||||
def custom_translate(string, lang_code):
|
||||
|
||||
return translate_string(lang_code, string)
|
||||
|
||||
@@ -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"),
|
||||
|
||||
100
files/views.py
100
files/views.py
@@ -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
44387
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
53
frontend/src/static/js/components/_shared/ToolTip.jsx
Normal file
53
frontend/src/static/js/components/_shared/ToolTip.jsx
Normal 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;
|
||||
31
frontend/src/static/js/components/_shared/ToolTip.scss
Normal file
31
frontend/src/static/js/components/_shared/ToolTip.scss
Normal 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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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">
|
||||
‹
|
||||
</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">
|
||||
›
|
||||
</button>
|
||||
)}
|
||||
<div className="thumbnail-navigation">
|
||||
{slideshowItems.length > 5 && (
|
||||
<button className="arrow left" onClick={() => scrollThumbnails('left')} aria-label="Scroll left">
|
||||
‹
|
||||
</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">
|
||||
›
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 />;
|
||||
|
||||
@@ -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?')) {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
*
|
||||
|
||||
@@ -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)}();
|
||||
@@ -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)}();
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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">
|
||||
|
||||
49
templates/cms/edit_subtitle.html
Normal file
49
templates/cms/edit_subtitle.html
Normal 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 %}
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user