Compare commits

...

28 Commits

Author SHA1 Message Date
semantic-release-bot a3fe375a83 chore(release): 8.1.3 [skip ci]
## [8.1.3](https://github.com/mediacms-io/mediacms/compare/v8.1.2...v8.1.3) (2026-05-19)

### Bug Fixes

* django connection settings ([#1529](https://github.com/mediacms-io/mediacms/issues/1529)) ([e89c4a3](https://github.com/mediacms-io/mediacms/commit/e89c4a3c8523574b5852a434ed67e281b6290584))
* prestart.sh loaddata re-runs on every container restart ([#1502](https://github.com/mediacms-io/mediacms/issues/1502)) ([777b06b](https://github.com/mediacms-io/mediacms/commit/777b06bbebf141e5b1cb27e17533fe65d57eb6cd))
2026-05-19 18:41:29 +00:00
Ayana Oide 777b06bbeb fix: prestart.sh loaddata re-runs on every container restart (#1502) 2026-05-19 21:34:39 +03:00
Markos Gogoulos e89c4a3c85 fix: django connection settings (#1529) 2026-05-19 21:34:18 +03:00
semantic-release-bot 7a02d25d0b chore(release): 8.1.2 [skip ci]
## [8.1.2](https://github.com/mediacms-io/mediacms/compare/v8.1.1...v8.1.2) (2026-05-18)

### Bug Fixes

* remove redundant check ([#1528](https://github.com/mediacms-io/mediacms/issues/1528)) ([c7a673b](https://github.com/mediacms-io/mediacms/commit/c7a673bbbf46efc37621dc4a5109a85fc10e1317))
2026-05-18 11:45:18 +00:00
Markos Gogoulos c7a673bbbf fix: remove redundant check (#1528) 2026-05-18 14:44:40 +03:00
semantic-release-bot b0c0d9a83f chore(release): 8.1.1 [skip ci]
## [8.1.1](https://github.com/mediacms-io/mediacms/compare/v8.1.0...v8.1.1) (2026-05-18)

### Bug Fixes

* x-accell headers on uploaded poster ([#1526](https://github.com/mediacms-io/mediacms/issues/1526)) ([ae63a5a](https://github.com/mediacms-io/mediacms/commit/ae63a5af647c8865b96e6e50dda1ea9d29b5bd0b))
2026-05-18 11:25:57 +00:00
Markos Gogoulos ae63a5af64 fix: x-accell headers on uploaded poster (#1526) 2026-05-18 14:25:23 +03:00
semantic-release-bot 98d5d6af8b chore(release): 8.1.0 [skip ci]
## [8.1.0](https://github.com/mediacms-io/mediacms/compare/v8.0.8...v8.1.0) (2026-05-17)

### Features

* introduce x-accell headers ([9302559](https://github.com/mediacms-io/mediacms/commit/9302559d4bb3e4d0adb299ed37438b04c39e1864))
2026-05-17 07:32:43 +00:00
Markos Gogoulos 9302559d4b feat: introduce x-accell headers 2026-05-17 10:32:13 +03:00
Markos Gogoulos 279cccb980 chore(release): 8.0.8 [skip ci] 2026-05-13 21:15:15 +03:00
semantic-release-bot 25e91e9d5e chore(release): 8.0.8 [skip ci]
## [8.0.8](https://github.com/mediacms-io/mediacms/compare/v8.0.7...v8.0.8) (2026-05-13)

### Bug Fixes

* update documentation and fix smaller issues ([#1520](https://github.com/mediacms-io/mediacms/issues/1520)) ([d6a1151](https://github.com/mediacms-io/mediacms/commit/d6a11514e54b9341ec8a306a259adce6b4199d42))
2026-05-13 18:14:36 +00:00
Markos Gogoulos d6a11514e5 fix: update documentation and fix smaller issues (#1520) 2026-05-13 21:14:02 +03:00
semantic-release-bot c7a1d60d73 chore(release): 8.0.7 [skip ci]
## [8.0.7](https://github.com/mediacms-io/mediacms/compare/v8.0.6...v8.0.7) (2026-05-12)

### Bug Fixes

* bring related items back ([#1515](https://github.com/mediacms-io/mediacms/issues/1515)) ([6ee5bef](https://github.com/mediacms-io/mediacms/commit/6ee5bef6ce31cf849941f65d0817e53b8f03362f))
2026-05-12 11:08:33 +00:00
Markos Gogoulos 6ee5bef6ce fix: bring related items back (#1515) 2026-05-12 14:07:36 +03:00
semantic-release-bot 2e01000559 chore(release): 8.0.6 [skip ci]
## [8.0.6](https://github.com/mediacms-io/mediacms/compare/v8.0.5...v8.0.6) (2026-05-11)

### Bug Fixes

* better place secret key settings ([4f11add](https://github.com/mediacms-io/mediacms/commit/4f11addcfd6657e7e63eed0570b1d4d9bca75698))
2026-05-11 12:26:03 +00:00
Markos Gogoulos 4f11addcfd fix: better place secret key settings 2026-05-11 15:25:23 +03:00
semantic-release-bot b11f2f561c chore(release): 8.0.5 [skip ci]
## [8.0.5](https://github.com/mediacms-io/mediacms/compare/v8.0.4...v8.0.5) (2026-05-11)

### Bug Fixes

* add secret key to gitignore ([b6da9c4](https://github.com/mediacms-io/mediacms/commit/b6da9c4662b3fba234b8dc69700ffa44fced7482))
2026-05-11 11:19:16 +00:00
Markos Gogoulos b6da9c4662 fix: add secret key to gitignore 2026-05-11 14:18:36 +03:00
semantic-release-bot 10c0782fe0 chore(release): 8.0.4 [skip ci]
## [8.0.4](https://github.com/mediacms-io/mediacms/compare/v8.0.3...v8.0.4) (2026-05-11)

### Bug Fixes

* docker compose settings, provide key ([318dad0](https://github.com/mediacms-io/mediacms/commit/318dad0e5d2512d68068c019eb87f942f83318e9))
2026-05-11 11:14:41 +00:00
Markos Gogoulos 318dad0e5d fix: docker compose settings, provide key 2026-05-11 14:13:56 +03:00
semantic-release-bot 09ead87884 chore(release): 8.0.3 [skip ci]
## [8.0.3](https://github.com/mediacms-io/mediacms/compare/v8.0.2...v8.0.3) (2026-05-11)

### Bug Fixes

* secret key ([559977f](https://github.com/mediacms-io/mediacms/commit/559977f9bc74412739784926862b94a558e6fd84))
2026-05-11 10:12:14 +00:00
Markos Gogoulos 559977f9bc fix: secret key 2026-05-11 13:11:22 +03:00
semantic-release-bot 35de017562 chore(release): 8.0.2 [skip ci]
## [8.0.2](https://github.com/mediacms-io/mediacms/compare/v8.0.1...v8.0.2) (2026-05-11)

### Bug Fixes

* provide Bento4 url ([c19c520](https://github.com/mediacms-io/mediacms/commit/c19c5207e907cbd7d0c968d8cd8caaec20706277))
2026-05-11 10:03:56 +00:00
Markos Gogoulos c19c5207e9 fix: provide Bento4 url 2026-05-11 13:03:13 +03:00
semantic-release-bot 0578d096a6 chore(release): 7.7.0 [skip ci]
## [7.7.0](https://github.com/mediacms-io/mediacms/compare/v7.6.0...v7.7.0) (2026-05-11)

### Features

* LTI support and Moodle plugin ([55ab7ff](https://github.com/mediacms-io/mediacms/commit/55ab7ff34fcd05806b845cf958f07841a7cdfc78))
2026-05-11 09:47:49 +00:00
Markos Gogoulos 55ab7ff34f feat: LTI support and Moodle plugin 2026-05-11 12:47:09 +03:00
semantic-release-bot b7427869b6 chore(release): 7.6.0 [skip ci]
## [7.6.0](https://github.com/mediacms-io/mediacms/compare/v7.5.0...v7.6.0) (2026-02-07)

### Features

* Create SECURITY.md ([#1485](https://github.com/mediacms-io/mediacms/issues/1485)) ([11449c2](https://github.com/mediacms-io/mediacms/commit/11449c2187d0f450b86915d88f92595a1825e4cf))
2026-02-07 10:31:40 +00:00
LabPixel 11449c2187 feat: Create SECURITY.md (#1485) 2026-02-07 12:31:10 +02:00
326 changed files with 20382 additions and 4019 deletions
-1
View File
@@ -24,7 +24,6 @@ If applicable, add screenshots to help explain your problem.
**Environment (please complete the following information):**
- OS: [e.g. Ubuntu Linux]
- Installation method: [Docker install, or single server install]
- Browser, if applicable
**Additional context**
+2
View File
@@ -37,3 +37,5 @@ frontend-tools/chapters-editor/client/public/videos/sample-video.mp3
static/chapters_editor/videos/sample-video.mp3
static/video_editor/videos/sample-video.mp3
templates/todo-MS4.md
.secret_key
.secret_key.lock
+1 -2
View File
@@ -9,8 +9,7 @@ repos:
- id: isort
args: ["--profile", "black"]
- repo: https://github.com/psf/black
rev: 23.1.0
rev: 24.10.0
hooks:
- id: black
language_version: python3
additional_dependencies: [ 'click==8.0.4' ]
+79
View File
@@ -1,5 +1,84 @@
# Changelog
## [8.1.3](https://github.com/mediacms-io/mediacms/compare/v8.1.2...v8.1.3) (2026-05-19)
### Bug Fixes
* django connection settings ([#1529](https://github.com/mediacms-io/mediacms/issues/1529)) ([e89c4a3](https://github.com/mediacms-io/mediacms/commit/e89c4a3c8523574b5852a434ed67e281b6290584))
* prestart.sh loaddata re-runs on every container restart ([#1502](https://github.com/mediacms-io/mediacms/issues/1502)) ([777b06b](https://github.com/mediacms-io/mediacms/commit/777b06bbebf141e5b1cb27e17533fe65d57eb6cd))
## [8.1.2](https://github.com/mediacms-io/mediacms/compare/v8.1.1...v8.1.2) (2026-05-18)
### Bug Fixes
* remove redundant check ([#1528](https://github.com/mediacms-io/mediacms/issues/1528)) ([c7a673b](https://github.com/mediacms-io/mediacms/commit/c7a673bbbf46efc37621dc4a5109a85fc10e1317))
## [8.1.1](https://github.com/mediacms-io/mediacms/compare/v8.1.0...v8.1.1) (2026-05-18)
### Bug Fixes
* x-accell headers on uploaded poster ([#1526](https://github.com/mediacms-io/mediacms/issues/1526)) ([ae63a5a](https://github.com/mediacms-io/mediacms/commit/ae63a5af647c8865b96e6e50dda1ea9d29b5bd0b))
## [8.1.0](https://github.com/mediacms-io/mediacms/compare/v8.0.8...v8.1.0) (2026-05-17)
### Features
* introduce x-accell headers ([9302559](https://github.com/mediacms-io/mediacms/commit/9302559d4bb3e4d0adb299ed37438b04c39e1864))
## [8.0.8](https://github.com/mediacms-io/mediacms/compare/v8.0.7...v8.0.8) (2026-05-13)
### Bug Fixes
* update documentation and fix smaller issues ([#1520](https://github.com/mediacms-io/mediacms/issues/1520)) ([d6a1151](https://github.com/mediacms-io/mediacms/commit/d6a11514e54b9341ec8a306a259adce6b4199d42))
## [8.0.7](https://github.com/mediacms-io/mediacms/compare/v8.0.6...v8.0.7) (2026-05-12)
### Bug Fixes
* bring related items back ([#1515](https://github.com/mediacms-io/mediacms/issues/1515)) ([6ee5bef](https://github.com/mediacms-io/mediacms/commit/6ee5bef6ce31cf849941f65d0817e53b8f03362f))
## [8.0.6](https://github.com/mediacms-io/mediacms/compare/v8.0.5...v8.0.6) (2026-05-11)
### Bug Fixes
* better place secret key settings ([4f11add](https://github.com/mediacms-io/mediacms/commit/4f11addcfd6657e7e63eed0570b1d4d9bca75698))
## [8.0.5](https://github.com/mediacms-io/mediacms/compare/v8.0.4...v8.0.5) (2026-05-11)
### Bug Fixes
* add secret key to gitignore ([b6da9c4](https://github.com/mediacms-io/mediacms/commit/b6da9c4662b3fba234b8dc69700ffa44fced7482))
## [8.0.4](https://github.com/mediacms-io/mediacms/compare/v8.0.3...v8.0.4) (2026-05-11)
### Bug Fixes
* docker compose settings, provide key ([318dad0](https://github.com/mediacms-io/mediacms/commit/318dad0e5d2512d68068c019eb87f942f83318e9))
## [8.0.3](https://github.com/mediacms-io/mediacms/compare/v8.0.2...v8.0.3) (2026-05-11)
### Bug Fixes
* secret key ([559977f](https://github.com/mediacms-io/mediacms/commit/559977f9bc74412739784926862b94a558e6fd84))
## [8.0.2](https://github.com/mediacms-io/mediacms/compare/v8.0.1...v8.0.2) (2026-05-11)
### Bug Fixes
* provide Bento4 url ([c19c520](https://github.com/mediacms-io/mediacms/commit/c19c5207e907cbd7d0c968d8cd8caaec20706277))
## [7.7.0](https://github.com/mediacms-io/mediacms/compare/v7.6.0...v7.7.0) (2026-05-11)
### Features
* LTI support and Moodle plugin ([55ab7ff](https://github.com/mediacms-io/mediacms/commit/55ab7ff34fcd05806b845cf958f07841a7cdfc78))
## [7.6.0](https://github.com/mediacms-io/mediacms/compare/v7.5.0...v7.6.0) (2026-02-07)
### Features
* Create SECURITY.md ([#1485](https://github.com/mediacms-io/mediacms/issues/1485)) ([11449c2](https://github.com/mediacms-io/mediacms/commit/11449c2187d0f450b86915d88f92595a1825e4cf))
## [7.5.0](https://github.com/mediacms-io/mediacms/compare/v7.4.0...v7.5.0) (2026-02-06)
### Features
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p ffmpeg-tmp && \
# 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 && \
wget -q --tries=5 --waitretry=10 --timeout=30 https://www.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 && \
-23
View File
@@ -1,23 +0,0 @@
# History
## 3.0.0
### Features
- Updates Python/Django requirements and Dockerfile to use latest 3.11 Python - https://github.com/mediacms-io/mediacms/pull/826/files. This update requires some manual steps, for existing (not new) installations. Check the update section under the [Admin docs](https://github.com/mediacms-io/mediacms/blob/main/docs/admins_docs.md#2-server-installation), either for single server or for Docker Compose installations
- Upgrade postgres on Docker Compose - https://github.com/mediacms-io/mediacms/pull/749
### Fixes
- video player options for HLS - https://github.com/mediacms-io/mediacms/pull/832
- AVI videos not correctly recognised as videos - https://github.com/mediacms-io/mediacms/pull/833
## 2.1.0
### Fixes
- Increase uwsgi buffer-size parameter. This prevents an error by uwsgi with large headers - [#5b60](https://github.com/mediacms-io/mediacms/commit/5b601698a41ad97f08c1830e14b1c18f73ab8315)
- Fix issues with comments. These were not reported on the tracker but it is certain that they would not show comments on media files (non videos but also videos). Unfortunately this reverts work done with Timestamps on comments + Mentions on comments, more on PR [#802](https://github.com/mediacms-io/mediacms/pull/802)
### Features
- Allow tags to contains other characters too, not only English alphabet ones [#801](https://github.com/mediacms-io/mediacms/pull/801)
- Add simple cookie consent code [#799](https://github.com/mediacms-io/mediacms/pull/799)
- Allow password reset & email verify pages on global login required [#790](https://github.com/mediacms-io/mediacms/pull/790)
- Add api_url field to search api [#692](https://github.com/mediacms-io/mediacms/pull/692)
+1
View File
@@ -0,0 +1 @@
Django admin → /admin/lti/ltiplatform/ --> Change to: https://YOUR_MOODLE/filter/mediacms/lti_auth.php
+3 -3
View File
@@ -30,6 +30,7 @@ A demo is available at https://demo.mediacms.io
- **Multiple media types support**: video, audio, image, pdf
- **Multiple media classification options**: categories, tags and custom
- **Multiple media sharing options**: social media share, videos embed code generation
- **Use in LMS**: LTI 1.3 support plus a Moodle plugin for embeding media in LMS
- **Video Trimmer**: trim video, replace, save as new or create segments
- **SAML support**: with ability to add mappings to system roles and groups
- **Easy media searching**: enriched with live search functionality
@@ -42,7 +43,7 @@ A demo is available at https://demo.mediacms.io
- **Multiple transcoding profiles**: sane defaults for multiple dimensions (144p, 240p, 360p, 480p, 720p, 1080p) and multiple profiles (h264, h265, vp9)
- **Adaptive video streaming**: possible through HLS protocol
- **Subtitles/CC**: support for multilingual subtitle files
- **Scalable transcoding**: transcoding through priorities. Experimental support for remote workers
- **Scalable transcoding**: transcoding through priorities.
- **Chunked file uploads**: for pausable/resumable upload of content
- **REST API**: Documented through Swagger
- **Translation**: Most of the CMS is translated to a number of languages
@@ -90,7 +91,6 @@ In order to support automatic transcriptions through Whisper, consider more CPUs
There are two ways to run MediaCMS, through Docker Compose and through installing it on a server via an automation script that installs and configures all needed services. Find the related pages:
- [Single Server](docs/admins_docs.md#2-server-installation) page
- [Docker Compose](docs/admins_docs.md#3-docker-installation) page
A complete guide can be found on the blog post [How to self-host and share your videos in 2021](https://medium.com/@MediaCMS.io/how-to-self-host-and-share-your-videos-in-2021-14067e3b291b).
@@ -108,7 +108,7 @@ There are two ways to run MediaCMS, through Docker Compose and through installin
## Technology
This software uses the following list of awesome technologies: Python, Django, Django Rest Framework, Celery, PostgreSQL, Redis, Nginx, uWSGI, React, Fine Uploader, video.js, FFMPEG, Bento4
This software uses the following list of awesome technologies: Python, Django, Django Rest Framework, Celery, PostgreSQL, Redis, Nginx, Gunicorn, React, Fine Uploader, video.js, FFMPEG, Bento4
## Who is using it
+54
View File
@@ -0,0 +1,54 @@
# Security Policy
Thank you for helping improve the security of MediaCMS.
We take security vulnerabilities seriously and appreciate responsible disclosure.
---
## Reporting a Vulnerability
If you discover a security vulnerability in MediaCMS, **please do not open a public GitHub issue**.
Instead, report it using one of the following methods:
- **GitHub Security Advisories (preferred)**
Use the "Report a vulnerability" feature in this repository.
- **Contact Form**
Submit details via the official contact page:
https://mediacms.io/contact/
Please include as much of the following information as possible:
- Affected version(s)
- Detailed description of the issue
- Steps to reproduce (PoC if available)
- Impact assessment (e.g. RCE, XSS, privilege escalation)
- Any potential mitigations you are aware of
---
## Supported Versions
Security updates are provided for the **latest stable release** of MediaCMS.
Older versions may not receive security patches.
---
## Disclosure Policy
- We aim to acknowledge reports within **7 days**
- We aim to provide a fix or mitigation within **90 days**, depending on severity
- Please allow us time to investigate before any public disclosure
We follow responsible disclosure practices and will coordinate disclosure timelines when appropriate.
---
## Recognition
At this time, MediaCMS does not operate a formal bug bounty program.
However, we are happy to acknowledge valid security reports in release notes or advisories (with your permission).
---
Thank you for helping keep MediaCMS secure.
+12
View File
@@ -3,7 +3,9 @@ from __future__ import absolute_import
import os
from celery import Celery
from celery.signals import worker_process_init
from django.conf import settings
from django.db import connections
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cms.settings")
app = Celery("cms")
@@ -20,3 +22,13 @@ app.conf.task_always_eager = settings.CELERY_TASK_ALWAYS_EAGER
app.conf.worker_prefetch_multiplier = 1
@worker_process_init.connect
def close_db_pool_on_fork(**_):
# psycopg3's ConnectionPool is not fork-safe: children inherit dead sockets
# from the parent's pool and block on getconn() until PoolTimeout. Dispose
# the inherited pool here so each prefork child opens its own on first use.
# NB: plain conn.close() would only putconn() back into the broken pool.
for conn in connections.all():
conn.close_pool()
+1
View File
@@ -24,6 +24,7 @@ INSTALLED_APPS = [
"actions.apps.ActionsConfig",
"rbac.apps.RbacConfig",
"identity_providers.apps.IdentityProvidersConfig",
"lti.apps.LtiConfig",
"debug_toolbar",
"mptt",
"crispy_forms",
+60 -18
View File
@@ -171,8 +171,19 @@ REST_FRAMEWORK = {
}
SECRET_KEY = "2dii4cog7k=5n37$fz)8dst)kg(s3&10)^qa*gv(kk+nv-z&cu"
# TODO: this needs to be changed!
# In docker, deploy/docker/entrypoint.sh ensures the SECRET_KEY env var is
# set (generating .secret_key once on first start if needed). Outside docker,
# either set SECRET_KEY in the environment or create a .secret_key file at the
# project root, e.g.:
# python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())' > .secret_key
SECRET_KEY = os.getenv('SECRET_KEY')
if not SECRET_KEY:
_secret_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), '.secret_key')
if os.path.exists(_secret_path):
with open(_secret_path) as _f:
SECRET_KEY = _f.read().strip()
if not SECRET_KEY:
raise RuntimeError("SECRET_KEY is not set. Set the SECRET_KEY env var or create a .secret_key file at the project root.")
TEMP_DIRECTORY = "/tmp" # Don't use a temp directory inside BASE_DIR!!!
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -191,6 +202,15 @@ THUMBNAIL_UPLOAD_DIR = f"{MEDIA_UPLOAD_DIR}/thumbnails/"
SUBTITLES_UPLOAD_DIR = f"{MEDIA_UPLOAD_DIR}/subtitles/"
HLS_DIR = os.path.join(MEDIA_ROOT, "hls/")
# Protect media files via nginx auth_request
# When True, nginx delegates authorization for /media/<protected>/... to a
# Django endpoint that checks the Media's state and the user's access.
USE_X_ACCEL_REDIRECT = True
# Subdirectories of MEDIA_ROOT that should be gated. "chunks" is intentionally
# omitted (upload state, not playback).
X_ACCEL_PROTECTED_PATHS = ["encoded", "hls", "original"]
X_ACCEL_AUTH_CACHE_SECONDS = 300
FFMPEG_COMMAND = "ffmpeg" # this is the path
FFPROBE_COMMAND = "ffprobe" # this is the path
MP4HLS = "mp4hls"
@@ -255,21 +275,6 @@ CANNOT_ADD_MEDIA_MESSAGE = "User cannot add media, or maximum number of media up
# 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 = ""
# this is used by remote workers to push
# encodings once they are done
# USE_BASIC_HTTP = True
# BASIC_HTTP_USER_PAIR = ('user', 'password')
# specify basic auth user/password pair for use with the
# remote workers, if nginx basic auth is setup
# apache2-utils need be installed
# then run
# htpasswd -c /home/mediacms.io/mediacms/deploy/.htpasswd user
# and set a password
# edit /etc/nginx/sites-enabled/mediacms.io and
# uncomment the two lines related to htpasswd
AUTH_USER_MODEL = "users.User"
LOGIN_REDIRECT_URL = "/"
@@ -300,6 +305,7 @@ INSTALLED_APPS = [
"actions.apps.ActionsConfig",
"rbac.apps.RbacConfig",
"identity_providers.apps.IdentityProvidersConfig",
"lti.apps.LtiConfig",
"debug_toolbar",
"mptt",
"crispy_forms",
@@ -401,7 +407,25 @@ LOGGING = {
},
}
DATABASES = {"default": {"ENGINE": "django.db.backends.postgresql", "NAME": "mediacms", "HOST": "127.0.0.1", "PORT": "5432", "USER": "mediacms", "PASSWORD": "mediacms", "OPTIONS": {'pool': True}}}
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "mediacms",
"HOST": "127.0.0.1",
"PORT": "5432",
"USER": "mediacms",
"PASSWORD": "mediacms",
"OPTIONS": {
"pool": {
"min_size": 2,
"max_size": 8,
"timeout": 10,
"max_lifetime": 30 * 60,
"max_idle": 10 * 60,
}
},
}
}
REDIS_LOCATION = "redis://127.0.0.1:6379/1"
@@ -555,6 +579,7 @@ DJANGO_ADMIN_URL = "admin/"
USE_SAML = False
USE_RBAC = False
USE_IDENTITY_PROVIDERS = False
USE_LTI = False # Enable LTI 1.3 integration
JAZZMIN_UI_TWEAKS = {"theme": "flatly"}
USE_ROUNDED_CORNERS = True
@@ -650,3 +675,20 @@ if USERS_NEEDS_TO_BE_APPROVED:
)
auth_index = MIDDLEWARE.index("django.contrib.auth.middleware.AuthenticationMiddleware")
MIDDLEWARE.insert(auth_index + 1, "cms.middleware.ApprovalMiddleware")
# LTI 1.3 Integration Settings
if USE_LTI:
# Session timeout for LTI launches (seconds)
LTI_SESSION_TIMEOUT = 3600 # 1 hour
# Cookie settings required for iframe embedding from LMS
# IMPORTANT: Requires HTTPS to be enabled!
SESSION_COOKIE_SAMESITE = 'None'
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SAMESITE = 'None'
CSRF_COOKIE_SECURE = True
RELATED_MEDIA_STRATEGY = "no_related"
# Whether LMS course categories appear in the public
# category list.
SHOW_LMS_COURSES_IN_CATEGORIES = False
+1
View File
@@ -25,6 +25,7 @@ urlpatterns = [
re_path(r"^", include("files.urls")),
re_path(r"^", include("users.urls")),
re_path(r"^accounts/", include("allauth.urls")),
re_path(r"^lti/", include("lti.urls")),
re_path(r"^api-auth/", include("rest_framework.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'),
+1 -1
View File
@@ -1 +1 @@
VERSION = "7.5"
VERSION = "8.1.2"
+5
View File
@@ -73,3 +73,8 @@ Can be set through the SAML Configurations tab:
3. **Group Role Mapping**: Maps the role returned by SAML (as set in the SAML Configuration tab) with the role in groups that user will be added
4. **Group mapping**: This creates groups associated with this IDP. Group ids as they come from SAML, associated with MediaCMS groups
5. **Category Mapping**: This maps a group id (from SAML response) with a category in MediaCMS
## More options
USER_SEARCH_FIELD = "name_username_email"
ALLOW_MEDIA_REPLACEMENT = True
+25
View File
@@ -35,4 +35,29 @@ find /home/mediacms.io/mediacms ! \( -path "*.git*" -o -name "package-lock.json"
chmod +x /home/mediacms.io/mediacms/deploy/docker/start.sh /home/mediacms.io/mediacms/deploy/docker/prestart.sh
# Generate or read SECRET_KEY once, shared across all containers via the
# host-mounted project volume. Atomic create-or-read so parallel container
# starts (web + celery_worker + celery_beat + migrations) can't race.
# Uses `mkdir` as the lock primitive (POSIX-atomic, no dependency on flock).
SECRET_KEY_FILE="${SECRET_KEY_FILE:-/home/mediacms.io/mediacms/.secret_key}"
SECRET_KEY_LOCK="${SECRET_KEY_FILE}.lock"
if [ -z "${SECRET_KEY:-}" ]; then
if [ ! -s "$SECRET_KEY_FILE" ]; then
# Spin-acquire the lock. mkdir is atomic; first caller wins, others retry.
while ! mkdir "$SECRET_KEY_LOCK" 2>/dev/null; do
sleep 0.2
done
# Re-check inside the lock: another container may have just written it.
if [ ! -s "$SECRET_KEY_FILE" ]; then
python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())' > "$SECRET_KEY_FILE"
chown www-data:www-data "$SECRET_KEY_FILE"
chmod 600 "$SECRET_KEY_FILE"
echo "entrypoint.sh: generated new SECRET_KEY at $SECRET_KEY_FILE"
fi
rmdir "$SECRET_KEY_LOCK"
fi
export SECRET_KEY="$(cat "$SECRET_KEY_FILE")"
fi
exec "$@"
+9 -2
View File
@@ -2,7 +2,6 @@ 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 = {
@@ -13,7 +12,15 @@ DATABASES = {
"PORT": os.getenv('POSTGRES_PORT', '5432'),
"USER": os.getenv('POSTGRES_USER', 'mediacms'),
"PASSWORD": os.getenv('POSTGRES_PASSWORD', 'mediacms'),
"OPTIONS": {'pool': True},
"OPTIONS": {
"pool": {
"min_size": 2,
"max_size": 8,
"timeout": 10,
"max_lifetime": 30 * 60,
"max_idle": 10 * 60,
}
},
}
}
+31 -4
View File
@@ -1,3 +1,9 @@
# Use existing X-Forwarded-Proto from reverse proxy if present, otherwise use $scheme
map $http_x_forwarded_proto $forwarded_proto {
default $http_x_forwarded_proto;
'' $scheme;
}
server {
listen 80 ;
@@ -9,9 +15,27 @@ server {
location /static {
alias /home/mediacms.io/mediacms/static ;
}
location = /_media_auth {
internal;
proxy_pass http://127.0.0.1:9000/api/v1/media-auth;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header Host $host;
proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $forwarded_proto;
proxy_set_header Cookie $http_cookie;
}
location /media/original {
alias /home/mediacms.io/mediacms/media_files/original;
location ~ ^/media/(encoded|hls|original)/(.*)$ {
auth_request /_media_auth;
alias /home/mediacms.io/mediacms/media_files/$1/$2;
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
}
location /media {
@@ -28,7 +52,10 @@ server {
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
include /etc/nginx/sites-enabled/uwsgi_params;
uwsgi_pass 127.0.0.1:9000;
proxy_pass http://127.0.0.1:9000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $forwarded_proto;
}
}
+4 -5
View File
@@ -6,7 +6,7 @@ ADMIN_PASSWORD=${ADMIN_PASSWORD:-$RANDOM_ADMIN_PASS}
if [ X"$ENABLE_MIGRATIONS" = X"yes" ]; then
echo "Running migrations service"
python manage.py migrate
EXISTING_INSTALLATION=`echo "from users.models import User; print(User.objects.exists())" |python manage.py shell`
EXISTING_INSTALLATION=`echo "from users.models import User; print(User.objects.exists())" |python manage.py shell 2>/dev/null | tail -1`
if [ "$EXISTING_INSTALLATION" = "True" ]; then
echo "Loaddata has already run"
else
@@ -37,7 +37,6 @@ fi
cp deploy/docker/nginx_http_only.conf /etc/nginx/sites-available/default
cp deploy/docker/nginx_http_only.conf /etc/nginx/sites-enabled/default
cp deploy/docker/uwsgi_params /etc/nginx/sites-enabled/uwsgi_params
cp deploy/docker/nginx.conf /etc/nginx/
#### Supervisord Configurations #####
@@ -45,12 +44,12 @@ cp deploy/docker/nginx.conf /etc/nginx/
cp deploy/docker/supervisord/supervisord-debian.conf /etc/supervisor/conf.d/supervisord-debian.conf
if [ X"$ENABLE_UWSGI" = X"yes" ] ; then
echo "Enabling uwsgi app server"
cp deploy/docker/supervisord/supervisord-uwsgi.conf /etc/supervisor/conf.d/supervisord-uwsgi.conf
echo "Enabling gunicorn app server"
cp deploy/docker/supervisord/supervisord-gunicorn.conf /etc/supervisor/conf.d/supervisord-gunicorn.conf
fi
if [ X"$ENABLE_NGINX" = X"yes" ] ; then
echo "Enabling nginx as uwsgi app proxy and media server"
echo "Enabling nginx as gunicorn app proxy and media server"
cp deploy/docker/supervisord/supervisord-nginx.conf /etc/supervisor/conf.d/supervisord-nginx.conf
fi
+1 -1
View File
@@ -11,7 +11,7 @@ else
echo "There is no script $PRE_START_PATH"
fi
# Start Supervisor, with Nginx and uWSGI
# Start Supervisor, with Nginx and Gunicorn
echo "Starting server using supervisord..."
exec /usr/bin/supervisord
@@ -0,0 +1,9 @@
[program:gunicorn]
command=/home/mediacms.io/bin/gunicorn cms.wsgi:application --workers=2 --threads=2 --worker-class=gthread --bind=127.0.0.1:9000 --user=www-data --group=www-data --timeout=120 --keep-alive=5 --max-requests=1000 --max-requests-jitter=50 --access-logfile=- --error-logfile=- --log-level=info --chdir=/home/mediacms.io/mediacms
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
priority=100
startinorder=true
startsecs=0
@@ -1,9 +0,0 @@
[program:uwsgi]
command=/home/mediacms.io/bin/uwsgi --ini /home/mediacms.io/mediacms/deploy/docker/uwsgi.ini
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
priority=100
startinorder=true
startsecs=0
-24
View File
@@ -1,24 +0,0 @@
[uwsgi]
chdir = /home/mediacms.io/mediacms/
virtualenv = /home/mediacms.io
module = cms.wsgi
uid=www-data
gid=www-data
processes = 2
threads = 2
master = true
socket = 127.0.0.1:9000
workers = 2
vacuum = true
hook-master-start = unix_signal:15 gracefully_kill_them_all
need-app = true
die-on-term = true
buffer-size=32768
-16
View File
@@ -1,16 +0,0 @@
uwsgi_param QUERY_STRING $query_string;
uwsgi_param REQUEST_METHOD $request_method;
uwsgi_param CONTENT_TYPE $content_type;
uwsgi_param CONTENT_LENGTH $content_length;
uwsgi_param REQUEST_URI $request_uri;
uwsgi_param PATH_INFO $document_uri;
uwsgi_param DOCUMENT_ROOT $document_root;
uwsgi_param SERVER_PROTOCOL $server_protocol;
uwsgi_param REQUEST_SCHEME $scheme;
uwsgi_param HTTPS $https if_not_empty;
uwsgi_param REMOTE_ADDR $remote_addr;
uwsgi_param REMOTE_PORT $remote_port;
uwsgi_param SERVER_PORT $server_port;
uwsgi_param SERVER_NAME $server_name;
-22
View File
@@ -1,22 +0,0 @@
[Unit]
Description=MediaCMS celery beat
After=network.target
[Service]
Type=simple
User=www-data
Group=www-data
Restart=always
RestartSec=10
WorkingDirectory=/home/mediacms.io/mediacms
Environment=CELERY_BIN="/home/mediacms.io/bin/celery"
Environment=CELERYD_PID_FILE="/home/mediacms.io/mediacms/pids/beat%n.pid"
Environment=CELERYD_LOG_FILE="/home/mediacms.io/mediacms/logs/beat%N.log"
Environment=CELERYD_LOG_LEVEL="INFO"
ExecStart=/bin/sh -c '${CELERY_BIN} -A cms beat --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL}'
ExecStop=/bin/kill -s TERM $MAINPID
[Install]
WantedBy=multi-user.target
-29
View File
@@ -1,29 +0,0 @@
[Unit]
Description=MediaCMS celery long queue
After=network.target
[Service]
Type=forking
User=www-data
Group=www-data
Restart=always
RestartSec=10
WorkingDirectory=/home/mediacms.io/mediacms
Environment=CELERYD_NODES="long1"
Environment=CELERY_QUEUE="long_tasks"
Environment=CELERY_BIN="/home/mediacms.io/bin/celery"
Environment=CELERYD_MULTI="multi"
Environment=CELERYD_OPTS="-Ofair --prefetch-multiplier=1"
Environment=CELERYD_PID_FILE="/home/mediacms.io/mediacms/pids/%n.pid"
Environment=CELERYD_LOG_FILE="/home/mediacms.io/mediacms/logs/%N.log"
Environment=CELERYD_LOG_LEVEL="INFO"
ExecStart=/bin/sh -c '${CELERY_BIN} -A cms multi start ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} -Q ${CELERY_QUEUE}'
ExecStop=/bin/sh -c '${CELERY_BIN} -A cms multi stopwait ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE}'
ExecReload=/bin/sh -c '${CELERY_BIN} -A cms multi restart ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} -Q ${CELERY_QUEUE}'
[Install]
WantedBy=multi-user.target
-39
View File
@@ -1,39 +0,0 @@
[Unit]
Description=MediaCMS celery short queue
After=network.target
[Service]
Type=forking
User=www-data
Group=www-data
Restart=always
RestartSec=10
WorkingDirectory=/home/mediacms.io/mediacms
Environment=CELERYD_NODES="short1 short2"
Environment=CELERY_QUEUE="short_tasks"
# Absolute or relative path to the 'celery' command:
Environment=CELERY_BIN="/home/mediacms.io/bin/celery"
# App instance to use
# comment out this line if you don't use an app
# or fully qualified:
#CELERY_APP="proj.tasks:app"
# How to call manage.py
Environment=CELERYD_MULTI="multi"
# Extra command-line arguments to the worker
Environment=CELERYD_OPTS="--soft-time-limit=300 -c10"
# - %n will be replaced with the first part of the nodename.
# - %I will be replaced with the current child process index
# and is important when using the prefork pool to avoid race conditions.
Environment=CELERYD_PID_FILE="/home/mediacms.io/mediacms/pids/%n.pid"
Environment=CELERYD_LOG_FILE="/home/mediacms.io/mediacms/logs/%N.log"
Environment=CELERYD_LOG_LEVEL="INFO"
ExecStart=/bin/sh -c '${CELERY_BIN} -A cms multi start ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} -Q ${CELERY_QUEUE}'
ExecStop=/bin/sh -c '${CELERY_BIN} -A cms multi stopwait ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE}'
ExecReload=/bin/sh -c '${CELERY_BIN} -A cms multi restart ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} -Q ${CELERY_QUEUE}'
[Install]
WantedBy=multi-user.target
-13
View File
@@ -1,13 +0,0 @@
-----BEGIN DH PARAMETERS-----
MIICCAKCAgEAo3MMiEY/fNbu+usIM0cDi6x8G3JBApv0Lswta4kiyedWT1WN51iQ
9zhOFpmcu6517f/fR9MUdyhVKHxxSqWQTcmTEFtz4P3VLTS/W1N5VbKE2VEMLpIi
wr350aGvV1Er0ujcp5n4O4h0I1tn4/fNyDe7+pHCdwM+hxe8hJ3T0/tKtad4fnIs
WHDjl4f7m7KuFfheiK7Efb8MsT64HDDAYXn+INjtDZrbE5XPw20BqyWkrf07FcPx
8o9GW50Ox7/FYq7jVMI/skEu0BRc8u6uUD9+UOuWUQpdeHeFcvLOgW53Z03XwWuX
RXosUKzBPuGtUDAaKD/HsGW6xmGr2W9yRmu27jKpfYLUb/eWbbnRJwCw04LdzPqv
jmtq02Gioo3lf5H5wYV9IYF6M8+q/slpbttsAcKERimD1273FBRt5VhSugkXWKjr
XDhoXu6vZgj8Opei38qPa8pI1RUFoXHFlCe6WpZQmU8efL8gAMrJr9jUIY8eea1n
u20t5B9ueb9JMjrNafcq6QkKhZLi6fRDDTUyeDvc0dN9R/3Yts97SXfdi1/lX7HS
Ht4zXd5hEkvjo8GcnjsfZpAC39QfHWkDaeUGEqsl3jXjVMfkvoVY51OuokPWZzrJ
M5+wyXNpfGbH67dPk7iHgN7VJvgX0SYscDPTtms50Vk7RwEzLeGuSHMCAQI=
-----END DH PARAMETERS-----
-84
View File
@@ -1,84 +0,0 @@
server {
listen 80 ;
server_name localhost;
gzip on;
access_log /var/log/nginx/mediacms.io.access.log;
error_log /var/log/nginx/mediacms.io.error.log warn;
# # redirect to https if logged in
# if ($http_cookie ~* "sessionid") {
# rewrite ^/(.*)$ https://localhost/$1 permanent;
# }
# # redirect basic forms to https
# location ~ (login|login_form|register|mail_password_form)$ {
# rewrite ^/(.*)$ https://localhost/$1 permanent;
# }
location /static {
alias /home/mediacms.io/mediacms/static ;
}
location /media/original {
alias /home/mediacms.io/mediacms/media_files/original;
}
location /media {
alias /home/mediacms.io/mediacms/media_files ;
}
location / {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
include /etc/nginx/sites-enabled/uwsgi_params;
uwsgi_pass 127.0.0.1:9000;
}
}
server {
listen 443 ssl;
server_name localhost;
ssl_certificate_key /etc/letsencrypt/live/localhost/privkey.pem;
ssl_certificate /etc/letsencrypt/live/localhost/fullchain.pem;
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_ecdh_curve secp521r1:secp384r1;
ssl_prefer_server_ciphers on;
gzip on;
access_log /var/log/nginx/mediacms.io.access.log;
error_log /var/log/nginx/mediacms.io.error.log warn;
location /static {
alias /home/mediacms.io/mediacms/static ;
}
location /media/original {
alias /home/mediacms.io/mediacms/media_files/original;
#auth_basic "auth protected area";
#auth_basic_user_file /home/mediacms.io/mediacms/deploy/local_install/.htpasswd;
}
location /media {
alias /home/mediacms.io/mediacms/media_files ;
}
location / {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
include /etc/nginx/sites-enabled/uwsgi_params;
uwsgi_pass 127.0.0.1:9000;
}
}
@@ -1,58 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIFTjCCBDagAwIBAgISBNOUeDlerH9MkKmHLvZJeMYgMA0GCSqGSIb3DQEBCwUA
MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD
ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0yMDAzMTAxNzUxNDFaFw0y
MDA2MDgxNzUxNDFaMBYxFDASBgNVBAMTC21lZGlhY21zLmlvMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAps5Jn18nW2tq/LYFDgQ1YZGLlpF/B2AAPvvH
3yuD+AcT4skKdZouVL/a5pXrptuYL5lthO9dlcja2tuO2ltYrb7Dp01dAIFaJE8O
DKd+Sv5wr8VWQZykqzMiMBgviml7TBvUHQjvCJg8UwmnN0XSUILCttd6u4qOzS7d
lKMMsKpYzLhElBT0rzhhsWulDiy6aAZbMV95bfR74nIWsBJacy6jx3jvxAuvCtkB
OVdOoVL6BPjDE3SNEk53bAZGIb5A9ri0O5jh/zBFT6tQSjUhAUTkmv9oZP547RnV
fDj+rdvCVk/fE+Jno36mcT183Qd/Ty3fWuqFoM5g/luhnfvWEwIDAQABo4ICYDCC
AlwwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD
AjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTd5EZBt74zu5XxT1uXQs6oM8qOuDAf
BgNVHSMEGDAWgBSoSmpjBH3duubRObemRWXv86jsoTBvBggrBgEFBQcBAQRjMGEw
LgYIKwYBBQUHMAGGImh0dHA6Ly9vY3NwLmludC14My5sZXRzZW5jcnlwdC5vcmcw
LwYIKwYBBQUHMAKGI2h0dHA6Ly9jZXJ0LmludC14My5sZXRzZW5jcnlwdC5vcmcv
MBYGA1UdEQQPMA2CC21lZGlhY21zLmlvMEwGA1UdIARFMEMwCAYGZ4EMAQIBMDcG
CysGAQQBgt8TAQEBMCgwJgYIKwYBBQUHAgEWGmh0dHA6Ly9jcHMubGV0c2VuY3J5
cHQub3JnMIIBBAYKKwYBBAHWeQIEAgSB9QSB8gDwAHYAXqdz+d9WwOe1Nkh90Eng
MnqRmgyEoRIShBh1loFxRVgAAAFwxcnL+AAABAMARzBFAiAb3yeBuW3j9MxcRc0T
icUBvEa/rH7Fv2eB0oQlnZ1exQIhAPf+CtTXmzxoeT/BBiivj4AmGDsq4xWhe/U6
BytYrKLeAHYAB7dcG+V9aP/xsMYdIxXHuuZXfFeUt2ruvGE6GmnTohwAAAFwxcnM
HAAABAMARzBFAiAuP5gKyyaT0LVXxwjYD9zhezvxf4Icx0P9pk75c5ao+AIhAK0+
fSJv+WTXciMT6gA1sk/tuCHuDFAuexSA/6TcRXcVMA0GCSqGSIb3DQEBCwUAA4IB
AQCPCYBU4Q/ro2MUkjDPKGmeqdxQycS4R9WvKTG/nmoahKNg30bnLaDPUcpyMU2k
sPDemdZ7uTGLZ3ZrlIva8DbrnJmrTPf9BMwaM6j+ZV/QhxvKZVIWkLkZrwiVI57X
Ba+rs5IEB4oWJ0EBaeIrzeKG5zLMkRcIdE4Hlhuwu3zGG56c+wmAPuvpIDlYoO6o
W22xRdxoTIHBvkzwonpVYUaRcaIw+48xnllxh1dHO+X69DT45wlF4tKveOUi+L50
4GWJ8Vjv7Fot/WNHEM4Mnmw0jHj9TPkIZKnPNRMdHmJ5CF/FJFDiptOeuzbfohG+
mdvuInb8JDc0XBE99Gf/S4/y
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/
MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow
SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT
GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC
AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF
q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8
SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0
Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA
a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj
/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T
AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG
CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv
bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k
c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw
VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC
ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz
MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu
Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF
AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo
uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/
wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu
X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG
PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6
KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==
-----END CERTIFICATE-----
@@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCmzkmfXydba2r8
tgUOBDVhkYuWkX8HYAA++8ffK4P4BxPiyQp1mi5Uv9rmleum25gvmW2E712VyNra
247aW1itvsOnTV0AgVokTw4Mp35K/nCvxVZBnKSrMyIwGC+KaXtMG9QdCO8ImDxT
Cac3RdJQgsK213q7io7NLt2UowywqljMuESUFPSvOGGxa6UOLLpoBlsxX3lt9Hvi
chawElpzLqPHeO/EC68K2QE5V06hUvoE+MMTdI0STndsBkYhvkD2uLQ7mOH/MEVP
q1BKNSEBROSa/2hk/njtGdV8OP6t28JWT98T4mejfqZxPXzdB39PLd9a6oWgzmD+
W6Gd+9YTAgMBAAECggEADnEJuryYQbf5GUwBAAepP3tEZJLQNqk/HDTcRxwTXuPt
+tKBD1F79WZu40vTjSyx7l0QOFQo/BDZsd0Ubx89fD1p3xA5nxOT5FTb2IifzIpe
4zjokOGo+BGDQjq10vvy6tH1+VWOrGXRwzawvX5UCRhpFz9sptQGLQmDsZy0Oo9B
LtavYVUqsbyqRWlzaclHgbythegIACWkqcalOzOtx+l6TGBRjej+c7URcwYBfr7t
XTAzbP+vnpaJovZyZT1eekr0OLzMpnjx4HvRvzL+NxauRpn6KfabsTfZlk8nrs4I
UdSjeukj1Iz8rGQilHdN/4dVJ3KzrlHVkVTBSjmMUQKBgQDaVXZnhAScfdiKeZbO
rdUAWcnwfkDghtRuAmzHaRM/FhFBEoVhdSbBuu+OUyBnIw/Ra4o2ePuEBcKIUiQO
w2tnE1CY5PPAcjw+OCSpvzy5xxjaqaRbm9BJp3FTeEYGLXERnchPpHg/NpexuF22
QOJ+FrysPyNMxuQp47ZwO9WT3QKBgQDDlSGjq/eeWxemwf7ZqMVlRyqsdJsgnCew
DkC62IGiYCBDfeEmndN+vcA/uzJHYV4iXiqS3aYJCWGaZFMhdIhIn5MgULvO1j5G
u/MxuzaaNPz22FlNCWTLBw4T1HOOvyTL+nLtZDKJ/BHxgHCmur1kiGvvZWrcCthD
afLEmseqrwKBgBuLZKCymxJTHhp6NHhmndSpfzyD8RNibzJhw+90ZiUzV4HqIEGn
Ufhm6Qn/mrroRXqaIpm0saZ6Q4yHMF1cchRS73wahlXlE4yV8KopojOd1pjfhgi4
o5JnOXjaV5s36GfcjATgLvtqm8CkDc6MaQaXP75LSNzKysYuIDoQkmVRAoGAAghF
rja2Pv4BU+lGJarcSj4gEmSvy/nza5/qSka/qhlHnIvtUAJp1TJRkhf24MkBOmgy
Fw6YkBV53ynVt05HsEGAPOC54t9VDFUdpNGmMpoEWuhKnUNQuc9b9RbLEJup3TjA
Avl8kPR+lzzXbtQX7biBLp6mKp0uPB0YubRGCN8CgYA0JMxK0x38Q2x3AQVhOmZh
YubtIa0JqVJhvpweOCFnkq3ebBpLsWYwiLTn86vuD0jupe5M3sxtefjkJmAKd8xY
aBU7QWhjh1fX4mzmggnbjcrIFbkIHsxwMeg567U/4AGxOOUsv9QUn37mqycqRKEn
YfUyYNLM6F3MmQAOs2kaHw==
-----END PRIVATE KEY-----
-13
View File
@@ -1,13 +0,0 @@
[Unit]
Description=MediaCMS uwsgi
[Service]
ExecStart=/home/mediacms.io/bin/uwsgi --ini /home/mediacms.io/mediacms/deploy/local_install/uwsgi.ini
ExecStop=/usr/bin/killall -9 uwsgi
RestartSec=3
#ExecRestart=killall -9 uwsgi; sleep 5; /home/sss/bin/uwsgi --ini /home/sss/wordgames/uwsgi.ini
Restart=always
[Install]
WantedBy=multi-user.target
-7
View File
@@ -1,7 +0,0 @@
/home/mediacms.io/mediacms/logs/*.log {
weekly
missingok
rotate 7
compress
notifempty
}
-38
View File
@@ -1,38 +0,0 @@
user www-data;
worker_processes auto;
pid /run/nginx.pid;
events {
worker_connections 10240;
}
worker_rlimit_nofile 20000; #each connection needs a filehandle (or 2 if you are proxying)
http {
proxy_connect_timeout 75;
proxy_read_timeout 12000;
client_max_body_size 5800M;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 10;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
gzip on;
gzip_disable "msie6";
log_format compression '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" "$gzip_ratio"';
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
-34
View File
@@ -1,34 +0,0 @@
module selinux-mediacms 1.0;
require {
type init_t;
type var_t;
type redis_port_t;
type postgresql_port_t;
type httpd_t;
type httpd_sys_content_t;
type httpd_sys_rw_content_t;
class file { append create execute execute_no_trans getattr ioctl lock open read rename setattr unlink write };
class dir { add_name remove_name rmdir };
class tcp_socket name_connect;
class lnk_file read;
}
#============= httpd_t ==============
allow httpd_t var_t:file { getattr open read };
#============= init_t ==============
allow init_t postgresql_port_t:tcp_socket name_connect;
allow init_t redis_port_t:tcp_socket name_connect;
allow init_t httpd_sys_content_t:dir rmdir;
allow init_t httpd_sys_content_t:file { append create execute execute_no_trans ioctl lock open read rename setattr unlink write };
allow init_t httpd_sys_content_t:lnk_file read;
allow init_t httpd_sys_rw_content_t:dir { add_name remove_name rmdir };
allow init_t httpd_sys_rw_content_t:file { create ioctl lock open read setattr unlink write };
-27
View File
@@ -1,27 +0,0 @@
[uwsgi]
chdir = /home/mediacms.io/mediacms/
virtualenv = /home/mediacms.io
module = cms.wsgi
uid=www-data
gid=www-data
processes = 2
threads = 2
master = true
socket = 127.0.0.1:9000
#socket = /home/mediacms.io/mediacms/deploy/uwsgi.sock
workers = 2
vacuum = true
logto = /home/mediacms.io/mediacms/logs/errorlog.txt
disable-logging = true
buffer-size=32768
-16
View File
@@ -1,16 +0,0 @@
uwsgi_param QUERY_STRING $query_string;
uwsgi_param REQUEST_METHOD $request_method;
uwsgi_param CONTENT_TYPE $content_type;
uwsgi_param CONTENT_LENGTH $content_length;
uwsgi_param REQUEST_URI $request_uri;
uwsgi_param PATH_INFO $document_uri;
uwsgi_param DOCUMENT_ROOT $document_root;
uwsgi_param SERVER_PROTOCOL $server_protocol;
uwsgi_param REQUEST_SCHEME $scheme;
uwsgi_param HTTPS $https if_not_empty;
uwsgi_param REMOTE_ADDR $remote_addr;
uwsgi_param REMOTE_PORT $remote_port;
uwsgi_param SERVER_PORT $server_port;
uwsgi_param SERVER_NAME $server_name;
+1 -65
View File
@@ -2,7 +2,6 @@
## Table of contents
- [1. Welcome](#1-welcome)
- [2. Single Server Installaton](#2-single-server-installation)
- [3. Docker Installation](#3-docker-installation)
- [4. Docker Deployment options](#4-docker-deployment-options)
- [5. Configuration](#5-configuration)
@@ -34,58 +33,6 @@
## 1. Welcome
This page is created for MediaCMS administrators that are responsible for setting up the software, maintaining it and making modifications.
## 2. Single Server Installation
The core dependencies are python3, Django, celery, PostgreSQL, redis, ffmpeg. Any system that can have these dependencies installed, can run MediaCMS. But the install.sh is only tested in Linux Ubuntu 24 and 22 versions.
Installation on an Ubuntu 22/24 system with git utility installed should be completed in a few minutes with the following steps.
Make sure you run it as user root, on a clear system, since the automatic script will install and configure the following services: Celery/PostgreSQL/Redis/Nginx and will override any existing settings.
```bash
mkdir /home/mediacms.io && cd /home/mediacms.io/
git clone https://github.com/mediacms-io/mediacms
cd /home/mediacms.io/mediacms/ && bash ./install.sh
```
The script will ask if you have a URL where you want to deploy MediaCMS, otherwise it will use localhost. If you provide a URL, it will use Let's Encrypt service to install a valid ssl certificate.
### Update
If you've used the above way to install MediaCMS, update with the following:
```bash
cd /home/mediacms.io/mediacms # enter mediacms directory
source /home/mediacms.io/bin/activate # use virtualenv
git pull # update code
pip install -r requirements.txt -U # run pip install to update
python manage.py migrate # run Django migrations
sudo systemctl restart mediacms celery_long celery_short # restart services
```
### Update from version 2 to version 3
Version 3 is using Django 4 and Celery 5, and needs a recent Python 3.x version. If you are updating from an older version, make sure Python is updated first. Version 2 could run on Python 3.6, but version 3 needs Python3.8 and higher.
The syntax for starting Celery has also changed, so you have to copy the celery related systemctl files and restart
```
# cp deploy/local_install/celery_long.service /etc/systemd/system/celery_long.service
# cp deploy/local_install/celery_short.service /etc/systemd/system/celery_short.service
# cp deploy/local_install/celery_beat.service /etc/systemd/system/celery_beat.service
# systemctl daemon-reload
# systemctl start celery_long celery_short celery_beat
```
### Configuration
Checkout the configuration section here.
### Maintenance
Database can be backed up with pg_dump and media_files on /home/mediacms.io/mediacms/media_files include original files and encoded/transcoded versions
## 3. Docker Installation
@@ -220,14 +167,10 @@ Several options are available on `cms/settings.py`, most of the things that are
It is advisable to override any of them by adding it to `local_settings.py` .
In case of a the single server installation, add to `cms/local_settings.py` .
In case of a docker compose installation, add to `deploy/docker/local_settings.py` . This will automatically overwrite `cms/local_settings.py` .
Any change needs restart of MediaCMS in order to take effect.
Single server installation: edit `cms/local_settings.py`, make a change and restart MediaCMS
```bash
#systemctl restart mediacms
```
@@ -795,14 +738,7 @@ Instructions contributed by @alberto98fx
On the [Configuration](https://github.com/mediacms-io/mediacms/blob/main/docs/admins_docs.md#5-configuration) section of this guide we've see how to edit the email settings.
In case we are yet unable to receive email from MediaCMS, the following may help us debug the issue - in most cases it is an issue of setting the correct username, password or TLS option
Enter the Django shell, example if you're using the Single Server installation:
```bash
source /home/mediacms.io/bin/activate
python manage.py shell
```
and inside the shell
Enter the Django shell and inside the shell
```bash
from django.core.mail import EmailMessage
+1 -1
View File
@@ -23,7 +23,7 @@ and will start all services required for MediaCMS, as Celery/Redis for asynchron
For Django, the changes from the image produced by docker-compose.yaml are these:
* Django runs in debug mode, with `python manage.py runserver`
* uwsgi and nginx are not run
* gunicorn and nginx are not run
* Django runs in Debug mode, with Debug Toolbar
* Static files (js/css) are loaded from static/ folder
* corsheaders is installed and configured to allow all origins
+17 -4
View File
@@ -65,6 +65,7 @@ class CategoryAdminForm(forms.ModelForm):
class Meta:
model = Category
# LTI fields will be shown as read-only when USE_LTI is enabled
fields = '__all__'
def clean(self):
@@ -135,7 +136,7 @@ class CategoryAdmin(admin.ModelAdmin):
list_display = ["title", "user", "add_date", "media_count"]
list_filter = []
ordering = ("-add_date",)
readonly_fields = ("user", "media_count")
readonly_fields = ("user", "media_count", "lti_platform", "lti_context_id")
change_form_template = 'admin/files/category/change_form.html'
def get_list_filter(self, request):
@@ -145,6 +146,8 @@ class CategoryAdmin(admin.ModelAdmin):
list_filter.insert(0, "is_rbac_category")
if getattr(settings, 'USE_IDENTITY_PROVIDERS', False):
list_filter.insert(-1, "identity_provider")
if getattr(settings, 'USE_LTI', False):
list_filter.append("is_lms_course")
return list_filter
@@ -154,6 +157,8 @@ class CategoryAdmin(admin.ModelAdmin):
list_display.insert(-1, "is_rbac_category")
if getattr(settings, 'USE_IDENTITY_PROVIDERS', False):
list_display.insert(-1, "identity_provider")
if getattr(settings, 'USE_LTI', False):
list_display.insert(-1, "is_lms_course")
return list_display
@@ -167,6 +172,14 @@ class CategoryAdmin(admin.ModelAdmin):
),
]
additional_fieldsets = []
if getattr(settings, 'USE_LTI', False):
lti_fieldset = [
('LTI Integration', {'fields': ['lti_platform', 'lti_context_id'], 'classes': ['tab'], 'description': 'LTI/LMS integration settings (automatically managed by LTI provisioning)'}),
]
additional_fieldsets.extend(lti_fieldset)
if getattr(settings, 'USE_RBAC', False):
rbac_fieldset = [
('RBAC Settings', {'fields': ['is_rbac_category'], 'classes': ['tab'], 'description': 'Role-Based Access Control settings'}),
@@ -177,9 +190,9 @@ class CategoryAdmin(admin.ModelAdmin):
('RBAC Settings', {'fields': ['is_rbac_category', 'identity_provider'], 'classes': ['tab'], 'description': 'Role-Based Access Control settings'}),
('Group Access', {'fields': ['rbac_groups'], 'description': 'Select the Groups that have access to category'}),
]
return basic_fieldset + rbac_fieldset
else:
return basic_fieldset
additional_fieldsets.extend(rbac_fieldset)
return basic_fieldset + additional_fieldsets
class TagAdmin(admin.ModelAdmin):
+6
View File
@@ -64,4 +64,10 @@ def stuff(request):
if request.user.is_superuser:
ret["DJANGO_ADMIN_URL"] = settings.DJANGO_ADMIN_URL
if getattr(settings, 'USE_LTI', False):
lti_session = request.session.get('lti_session')
if lti_session and request.user.is_authenticated:
ret['lti_session'] = lti_session
return ret
+56 -76
View File
@@ -1,3 +1,5 @@
from pathlib import Path
from crispy_forms.bootstrap import FormActions
from crispy_forms.helper import FormHelper
from crispy_forms.layout import HTML, Field, Layout, Submit
@@ -5,9 +7,11 @@ from django import forms
from django.conf import settings
from .methods import get_next_state, is_mediacms_editor
from .models import MEDIA_STATES, Category, Media, Subtitle
from .models import MEDIA_STATES, Category, Media, MediaPermission, Subtitle
from .widgets import CategoryModalWidget
_PUBLISH_STATE_HTML = (Path(__file__).parent.parent / 'templates/cms/partials/media_publish_state.html').read_text()
class CustomField(Field):
template = 'cms/crispy_custom_field.html'
@@ -116,6 +120,7 @@ class MediaMetadataForm(forms.ModelForm):
class MediaPublishForm(forms.ModelForm):
confirm_state = forms.BooleanField(required=False, initial=False, label="Acknowledge sharing status", help_text="")
shared = forms.BooleanField(required=False, initial=False, label="Shared")
class Meta:
model = Media
@@ -123,16 +128,20 @@ class MediaPublishForm(forms.ModelForm):
widgets = {
"category": CategoryModalWidget(),
"state": forms.RadioSelect(),
}
def __init__(self, user, *args, **kwargs):
self.user = user
self.request = kwargs.pop('request', None)
super(MediaPublishForm, self).__init__(*args, **kwargs)
self.has_custom_permissions = self.instance.permissions.exists() if self.instance.pk else False
self.has_rbac_categories = self.instance.category.filter(is_rbac_category=True).exists() if self.instance.pk else False
self.is_shared = self.has_custom_permissions or self.has_rbac_categories
self.actual_state = self.instance.state if self.instance.pk else None
self.was_shared = self.instance.is_shared if self.instance.pk else False
self.had_explicit_permission = self.instance.permissions.exists() if self.instance.pk else False
is_embed_mode = self._check_embed_mode()
self.fields["shared"].initial = self.was_shared
self.initial["shared"] = self.was_shared
if not is_mediacms_editor(user):
for field in ["featured", "reported_times", "is_reviewed"]:
@@ -146,13 +155,6 @@ class MediaPublishForm(forms.ModelForm):
valid_states.append(self.instance.state)
self.fields["state"].choices = [(state, dict(MEDIA_STATES).get(state, state)) for state in valid_states]
if self.is_shared:
current_choices = list(self.fields["state"].choices)
current_choices.insert(0, ("shared", "Shared"))
self.fields["state"].choices = current_choices
self.fields["state"].initial = "shared"
self.initial["state"] = "shared"
if getattr(settings, 'USE_RBAC', False) and 'category' in self.fields:
if is_mediacms_editor(user):
pass
@@ -169,6 +171,14 @@ class MediaPublishForm(forms.ModelForm):
self.fields['category'].queryset = Category.objects.filter(id__in=combined_category_ids).order_by('title')
# Filter for LMS courses only when in embed mode
if is_embed_mode and 'category' in self.fields:
current_queryset = self.fields['category'].queryset
self.fields['category'].queryset = current_queryset.filter(is_lms_course=True)
self.fields['category'].label = 'Course'
self.fields['category'].help_text = 'Media can be shared with one or more courses'
self.fields['category'].widget.is_lms_mode = True
self.helper = FormHelper()
self.helper.form_tag = True
self.helper.form_class = 'post-form'
@@ -177,7 +187,7 @@ class MediaPublishForm(forms.ModelForm):
self.helper.form_show_errors = False
self.helper.layout = Layout(
CustomField('category'),
CustomField('state'),
HTML(_PUBLISH_STATE_HTML),
CustomField('featured'),
CustomField('reported_times'),
CustomField('is_reviewed'),
@@ -186,83 +196,53 @@ class MediaPublishForm(forms.ModelForm):
self.helper.layout.append(FormActions(Submit('submit', 'Publish Media', css_class='primaryAction')))
def _check_embed_mode(self):
"""Check if the current request is in embed mode"""
if not self.request:
return False
# Check query parameter
mode = self.request.GET.get('mode', '')
if mode == 'lms_embed_mode':
return True
# Check session storage
if self.request.session.get('lms_embed_mode') == 'true':
return True
return False
def clean(self):
cleaned_data = super().clean()
state = cleaned_data.get("state")
categories = cleaned_data.get("category")
shared = cleaned_data.get("shared")
if self.is_shared and state != "shared":
self.fields['confirm_state'].widget = forms.CheckboxInput()
state_index = None
for i, layout_item in enumerate(self.helper.layout):
if isinstance(layout_item, CustomField) and layout_item.fields[0] == 'state':
state_index = i
break
if state_index is not None:
layout_items = list(self.helper.layout)
layout_items.insert(state_index + 1, CustomField('confirm_state'))
self.helper.layout = Layout(*layout_items)
if not cleaned_data.get('confirm_state'):
if state == 'private':
error_parts = []
if self.has_rbac_categories:
rbac_cat_titles = self.instance.category.filter(is_rbac_category=True).values_list('title', flat=True)
error_parts.append(f"shared with users that have access to categories: {', '.join(rbac_cat_titles)}")
if self.has_custom_permissions:
error_parts.append("shared by me with other users (visible in 'Shared by me' page)")
error_message = f"I understand that changing to Private will remove all sharing. Currently this media is {' and '.join(error_parts)}. All this sharing will be removed."
self.add_error('confirm_state', error_message)
else:
error_message = f"I understand that changing to {state.title()} will maintain existing sharing settings."
self.add_error('confirm_state', error_message)
elif state in ['private', 'unlisted']:
custom_permissions = self.instance.permissions.exists()
rbac_categories = categories.filter(is_rbac_category=True).values_list('title', flat=True)
if rbac_categories or custom_permissions:
self.fields['confirm_state'].widget = forms.CheckboxInput()
state_index = None
for i, layout_item in enumerate(self.helper.layout):
if isinstance(layout_item, CustomField) and layout_item.fields[0] == 'state':
state_index = i
break
if state_index is not None:
layout_items = list(self.helper.layout)
layout_items.insert(state_index + 1, CustomField('confirm_state'))
self.helper.layout = Layout(*layout_items)
if not cleaned_data.get('confirm_state'):
if rbac_categories:
error_message = f"I understand that although media state is {state}, the media is also shared with users that have access to categories: {', '.join(rbac_categories)}"
self.add_error('confirm_state', error_message)
if custom_permissions:
error_message = f"I understand that although media state is {state}, the media is also shared by me with other users, that I can see in the 'Shared by me' page"
self.add_error('confirm_state', error_message)
# Convert "shared" state to actual underlying state for saving. we dont keep shared state in DB
if state == "shared":
cleaned_data["state"] = self.actual_state
if self.was_shared and not shared and not cleaned_data.get('confirm_state'):
self.add_error('confirm_state', "I understand that unchecking Shared will remove all existing sharing for this media.")
return cleaned_data
def save(self, *args, **kwargs):
data = self.cleaned_data
state = data.get("state")
shared = data.get("shared")
# If transitioning from shared to private, remove all sharing
if self.is_shared and state == 'private' and data.get('confirm_state'):
# Remove all custom permissions
if shared:
if self.request:
submitted_categories = self.cleaned_data.get('category', [])
submitted_has_rbac = any(cat.is_rbac_category for cat in submitted_categories)
if self.had_explicit_permission or (not self.was_shared and not submitted_has_rbac):
MediaPermission.objects.get_or_create(
media=self.instance,
user=self.request.user,
defaults={'owner_user': self.request.user, 'permission': 'owner'},
)
elif not shared:
self.instance.permissions.all().delete()
# Remove RBAC categories
rbac_cats = self.instance.category.filter(is_rbac_category=True)
self.instance.category.remove(*rbac_cats)
if state != self.initial["state"]:
self.instance.state = get_next_state(self.user, self.initial["state"], self.instance.state)
if state != self.initial.get("state"):
self.instance.state = get_next_state(self.user, self.initial.get("state"), self.instance.state)
media = super(MediaPublishForm, self).save(*args, **kwargs)
+1 -1
View File
@@ -57,4 +57,4 @@ def translate_string(language_code, string):
if not check_language_code(language_code):
return string
return translation_strings[language_code].get(string, string)
return translation_strings[language_code].get(string) or string
+25
View File
@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "حذف الوسائط",
"DOWNLOAD": "تحميل",
"DURATION": "المدة",
"Delete Comments": "",
"Delete Media": "حذف الوسائط",
"Delete media": "حذف الوسائط",
"Disable Comments": "تعطيل التعليقات",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "فشل تغيير المالك. يرجى المحاولة مرة أخرى.",
"Failed to copy media.": "فشل نسخ الوسائط.",
"Failed to create playlist": "فشل إنشاء قائمة التشغيل",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "فشل حذف الوسائط. يرجى المحاولة مرة أخرى.",
"Failed to disable comments.": "فشل تعطيل التعليقات.",
"Failed to disable download.": "فشل تعطيل التنزيل.",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "تصفية المستخدمين الموجودين...",
"Filter playlists...": "تصفية قوائم التشغيل...",
"Filters": "الفلاتر",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "اذهب",
"History": "التاريخ",
"Home": "الرئيسية",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "إدارة التعليقات",
"Manage media": "إدارة الوسائط",
"Manage users": "إدارة المستخدمين",
"Management": "",
"Media": "وسائط",
"Media I own": "الوسائط التي أمتلكها",
"Media was edited": "تم تعديل الوسائط",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "لا توجد نتائج لـ",
"No tags": "لا توجد علامات",
"No users to add": "لا يوجد مستخدمون لإضافتهم",
"Organization": "",
"PLAYLISTS": "قوائم التشغيل",
"PUBLISH STATE": "حالة النشر",
"Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "نشر في",
"Recent uploads": "التحميلات الأخيرة",
"Recommended": "موصى به",
"Record": "",
"Record Screen": "تسجيل الشاشة",
"Record Screen with Audio": "",
"Register": "تسجيل",
"Remove category": "إزالة الفئة",
"Remove from list": "إزالة من القائمة",
"Remove tag": "إزالة العلامة",
"Remove user": "إزالة المستخدم",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "",
"SAVE": "حفظ",
"SEARCH": "بحث",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "تحديد جميع الوسائط",
"Select publish state:": "اختر حالة النشر:",
"Selected": "محدد",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "مشاركة مني",
"Shared with me": "مشاركة معي",
"Sharing": "",
"Sign in": "تسجيل الدخول",
"Sign out": "تسجيل الخروج",
"Sort By": "ترتيب حسب",
"Start Recording": "بدء التسجيل",
"Start uploading media and sharing your work. Media that you upload will show up here.": "ابدأ في تحميل الوسائط ومشاركة عملك. ستظهر الوسائط التي تحملها هنا.",
"Stop Recording": "إيقاف التسجيل",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "إرسال",
"Subtitle was added": "تمت إضافة الترجمة",
"Subtitles": "ترجمات",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "تم تفعيل التعليقات بنجاح",
"Successfully changed owner": "تم تغيير المالك بنجاح",
"Successfully deleted": "تم الحذف بنجاح",
"Successfully deleted comments": "",
"Successfully updated": "تم التحديث بنجاح",
"Successfully updated categories": "تم تحديث الفئات بنجاح",
"Successfully updated playlist membership": "تم تحديث عضوية قائمة التشغيل بنجاح",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "رفع الوسائط",
"Uploads": "التحميلات",
"Users": "المستخدمون",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "عرض الكل",
"Video": "فيديو",
"View all": "عرض الكل",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "مرحباً",
"You are going to copy": "سوف تقوم بالنسخ",
"You are going to delete": "سوف تقوم بالحذف",
"You are going to delete all comments from": "",
"You are going to disable comments to": "سوف تقوم بتعطيل التعليقات لـ",
"You are going to disable download for": "سوف تقوم بتعطيل التنزيل لـ",
"You are going to enable comments to": "سوف تقوم بتفعيل التعليقات لـ",
+25
View File
@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "মিডিয়া মুছুন",
"DOWNLOAD": "ডাউনলোড",
"DURATION": "সময়কাল",
"Delete Comments": "",
"Delete Media": "",
"Delete media": "মিডিয়া মুছুন",
"Disable Comments": "",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "",
"Failed to copy media.": "মিডিয়া কপি করতে ব্যর্থ হয়েছে।",
"Failed to create playlist": "",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "মিডিয়া মুছতে ব্যর্থ হয়েছে। দয়া করে আবার চেষ্টা করুন।",
"Failed to disable comments.": "মন্তব্য নিষ্ক্রিয় করতে ব্যর্থ হয়েছে।",
"Failed to disable download.": "ডাউনলোড নিষ্ক্রিয় করতে ব্যর্থ হয়েছে।",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "",
"Filter playlists...": "",
"Filters": "ফিল্টার",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "যাও",
"History": "ইতিহাস",
"Home": "বাড়ি",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "মন্তব্য পরিচালনা করুন",
"Manage media": "মিডিয়া পরিচালনা করুন",
"Manage users": "ব্যবহারকারীদের পরিচালনা করুন",
"Management": "",
"Media": "মিডিয়া",
"Media I own": "",
"Media was edited": "মিডিয়া সম্পাদিত হয়েছে",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "এর জন্য কোন ফলাফল নেই",
"No tags": "",
"No users to add": "",
"Organization": "",
"PLAYLISTS": "প্লেলিস্ট",
"PUBLISH STATE": "প্রকাশের অবস্থা",
"Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "প্রকাশিত",
"Recent uploads": "সাম্প্রতিক আপলোড",
"Recommended": "প্রস্তাবিত",
"Record": "",
"Record Screen": "স্ক্রিন রেকর্ড করুন",
"Record Screen with Audio": "",
"Register": "নিবন্ধন করুন",
"Remove category": "",
"Remove from list": "",
"Remove tag": "",
"Remove user": "",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "",
"SAVE": "সংরক্ষণ করুন",
"SEARCH": "অনুসন্ধান",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "",
"Select publish state:": "",
"Selected": "",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "আমার দ্বারা শেয়ার করা",
"Shared with me": "আমার সাথে শেয়ার করা",
"Sharing": "",
"Sign in": "সাইন ইন করুন",
"Sign out": "সাইন আউট করুন",
"Sort By": "সাজান",
"Start Recording": "রেকর্ডিং শুরু করুন",
"Start uploading media and sharing your work. Media that you upload will show up here.": "মিডিয়া আপলোড করা এবং আপনার কাজ শেয়ার করা শুরু করুন। আপনি যে মিডিয়া আপলোড করবেন তা এখানে প্রদর্শিত হবে।",
"Stop Recording": "রেকর্ডিং বন্ধ করুন",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "",
"Subtitle was added": "সাবটাইটেল যোগ করা হয়েছে",
"Subtitles": "সাবটাইটেল",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "মন্তব্য সফলভাবে সক্রিয় হয়েছে",
"Successfully changed owner": "",
"Successfully deleted": "সফলভাবে মুছে ফেলা হয়েছে",
"Successfully deleted comments": "",
"Successfully updated": "",
"Successfully updated categories": "",
"Successfully updated playlist membership": "",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "মিডিয়া আপলোড করুন",
"Uploads": "আপলোডসমূহ",
"Users": "",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "সব দেখুন",
"Video": "ভিডিও",
"View all": "সব দেখুন",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "স্বাগতম",
"You are going to copy": "আপনি কপি করতে চলেছেন",
"You are going to delete": "আপনি মুছে ফেলতে চলেছেন",
"You are going to delete all comments from": "",
"You are going to disable comments to": "আপনি মন্তব্য নিষ্ক্রিয় করতে চলেছেন",
"You are going to disable download for": "আপনি ডাউনলোড নিষ্ক্রিয় করতে চলেছেন",
"You are going to enable comments to": "আপনি মন্তব্য সক্রিয় করতে চলেছেন",
+25
View File
@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "SLET MEDIE",
"DOWNLOAD": "HENT",
"DURATION": "VARIGHED",
"Delete Comments": "",
"Delete Media": "Slet Medie",
"Delete media": "Slet medie",
"Disable Comments": "Deaktiver Kommentarer",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "Ændring af ejer mislykkedes. Prøv venligst igen.",
"Failed to copy media.": "Kopiering af medie mislykkedes.",
"Failed to create playlist": "Oprettelse af playliste mislykkedes",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "Sletning af medie mislykkedes. Prøv venligst igen.",
"Failed to disable comments.": "Deaktivering af kommentarer mislykkedes.",
"Failed to disable download.": "Deaktivering af download mislykkedes.",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "Filtrer eksisterende brugere...",
"Filter playlists...": "Filtrer playlister...",
"Filters": "Filtre",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "Vælg",
"History": "Historik",
"Home": "Hjem",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "Administrer kommentarer",
"Manage media": "Administrer medier",
"Manage users": "Administrer brugere",
"Management": "",
"Media": "Medier",
"Media I own": "Medier jeg ejer",
"Media was edited": "Mediet er blevet redigeret",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "Ingen resultater for",
"No tags": "Ingen tags",
"No users to add": "Ingen brugere at tilføje",
"Organization": "",
"PLAYLISTS": "PLAYLISTER",
"PUBLISH STATE": "PUBLICERINGSSTATUS",
"Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "Udgivet på",
"Recent uploads": "Nylige uploads",
"Recommended": "Anbefalet",
"Record": "",
"Record Screen": "Optag skærm",
"Record Screen with Audio": "",
"Register": "Registrer",
"Remove category": "Fjern kategori",
"Remove from list": "Fjern fra liste",
"Remove tag": "Fjern tag",
"Remove user": "Fjern bruger",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "",
"SAVE": "GEM",
"SEARCH": "SØG",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "Vælg alle medier",
"Select publish state:": "Vælg publiceringsstatus:",
"Selected": "Valgt",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "Delt af mig",
"Shared with me": "Delt med mig",
"Sharing": "",
"Sign in": "Log ind",
"Sign out": "Log ud",
"Sort By": "Sorter efter",
"Start Recording": "Start optagelse",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Begynd at uploade medier og dele dit arbejde. Medier, du uploader, vil blive vist her.",
"Stop Recording": "Stop optagelse",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "Indsend",
"Subtitle was added": "Undertekster tilføjet",
"Subtitles": "Undertekster",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "Kommentarer aktiveret med succes",
"Successfully changed owner": "Ejer ændret med succes",
"Successfully deleted": "Slettet med succes",
"Successfully deleted comments": "",
"Successfully updated": "Opdateret med succes",
"Successfully updated categories": "Kategorier opdateret med succes",
"Successfully updated playlist membership": "Playlistemedlemskab opdateret med succes",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "Upload medie",
"Uploads": "Uploads",
"Users": "Brugere",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "SE ALLE",
"Video": "Video",
"View all": "Se alle",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "Velkommen",
"You are going to copy": "Du er ved at kopiere",
"You are going to delete": "Du er ved at slette",
"You are going to delete all comments from": "",
"You are going to disable comments to": "Du er ved at deaktivere kommentarer til",
"You are going to disable download for": "Du er ved at deaktivere download for",
"You are going to enable comments to": "Du er ved at aktivere kommentarer til",
+25
View File
@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "MEDIEN LÖSCHEN",
"DOWNLOAD": "HERUNTERLADEN",
"DURATION": "DAUER",
"Delete Comments": "",
"Delete Media": "Medien löschen",
"Delete media": "Medien löschen",
"Disable Comments": "Kommentare deaktivieren",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "Fehler beim Ändern des Eigentümers. Bitte versuchen Sie es erneut.",
"Failed to copy media.": "Fehler beim Kopieren der Medien.",
"Failed to create playlist": "Fehler beim Erstellen der Playlist",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "Fehler beim Löschen der Medien. Bitte versuchen Sie es erneut.",
"Failed to disable comments.": "Fehler beim Deaktivieren der Kommentare.",
"Failed to disable download.": "Fehler beim Deaktivieren des Downloads.",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "Vorhandene Benutzer filtern...",
"Filter playlists...": "Playlists filtern...",
"Filters": "Filter",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "Los",
"History": "Verlauf",
"Home": "Startseite",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "Kommentare verwalten",
"Manage media": "Medien verwalten",
"Manage users": "Benutzer verwalten",
"Management": "",
"Media": "Medien",
"Media I own": "Medien, die mir gehören",
"Media was edited": "Medien wurden bearbeitet",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "Keine Ergebnisse für",
"No tags": "Keine Tags",
"No users to add": "Keine Benutzer hinzuzufügen",
"Organization": "",
"PLAYLISTS": "PLAYLISTS",
"PUBLISH STATE": "VERÖFFENTLICHUNGSSTATUS",
"Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "Veröffentlicht am",
"Recent uploads": "Neue Uploads",
"Recommended": "Empfohlen",
"Record": "",
"Record Screen": "Bildschirm aufnehmen",
"Record Screen with Audio": "",
"Register": "Registrieren",
"Remove category": "Kategorie entfernen",
"Remove from list": "Aus Liste entfernen",
"Remove tag": "Tag entfernen",
"Remove user": "Benutzer entfernen",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "",
"SAVE": "SPEICHERN",
"SEARCH": "SUCHE",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "Alle Medien auswählen",
"Select publish state:": "Veröffentlichungsstatus auswählen:",
"Selected": "Ausgewählt",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "Von mir geteilt",
"Shared with me": "Mit mir geteilt",
"Sharing": "",
"Sign in": "Anmelden",
"Sign out": "Abmelden",
"Sort By": "Sortieren nach",
"Start Recording": "Aufnahme starten",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Beginnen Sie mit dem Hochladen von Medien und dem Teilen Ihrer Arbeit. Hochgeladene Medien werden hier angezeigt.",
"Stop Recording": "Aufnahme stoppen",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "Absenden",
"Subtitle was added": "Untertitel wurde hinzugefügt",
"Subtitles": "Untertitel",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "Kommentare erfolgreich aktiviert",
"Successfully changed owner": "Eigentümer erfolgreich geändert",
"Successfully deleted": "Erfolgreich gelöscht",
"Successfully deleted comments": "",
"Successfully updated": "Erfolgreich aktualisiert",
"Successfully updated categories": "Kategorien erfolgreich aktualisiert",
"Successfully updated playlist membership": "Playlist-Mitgliedschaft erfolgreich aktualisiert",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "Medien hochladen",
"Uploads": "Uploads",
"Users": "Benutzer",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "ALLE ANZEIGEN",
"Video": "Video",
"View all": "Alle anzeigen",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "Willkommen",
"You are going to copy": "Sie werden kopieren",
"You are going to delete": "Sie werden löschen",
"You are going to delete all comments from": "",
"You are going to disable comments to": "Sie werden Kommentare deaktivieren für",
"You are going to disable download for": "Sie werden Download deaktivieren für",
"You are going to enable comments to": "Sie werden Kommentare aktivieren für",
+25
View File
@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "ΔΙΑΓΡΑΦΗ ΑΡΧΕΙΟΥ",
"DOWNLOAD": "ΚΑΤΕΒΑΣΜΑ",
"DURATION": "ΔΙΑΡΚΕΙΑ",
"Delete Comments": "",
"Delete Media": "Διαγραφή Αρχείου",
"Delete media": "Διαγραφή αρχείου",
"Disable Comments": "Απενεργοποίηση Σχολίων",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "Αποτυχία αλλαγής ιδιοκτήτη. Παρακαλώ δοκιμάστε ξανά.",
"Failed to copy media.": "Αποτυχία αντιγραφής αρχείου.",
"Failed to create playlist": "Αποτυχία δημιουργίας λίστας",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "Αποτυχία διαγραφής αρχείου. Παρακαλώ δοκιμάστε ξανά.",
"Failed to disable comments.": "Αποτυχία απενεργοποίησης σχολίων.",
"Failed to disable download.": "Αποτυχία απενεργοποίησης λήψης.",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "Φιλτράρισμα υπαρχόντων χρηστών...",
"Filter playlists...": "Φιλτράρισμα λιστών...",
"Filters": "Φίλτρα",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "Μετάβαση",
"History": "Ιστορικό",
"Home": "Αρχική",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "Διαχείριση σχολίων",
"Manage media": "Διαχείριση αρχείων",
"Manage users": "Διαχείριση χρηστών",
"Management": "",
"Media": "Αρχεία",
"Media I own": "Δικά μου αρχεία",
"Media was edited": "Το αρχείο επεξεργάστηκε",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "Δεν υπάρχουν αποτελέσματα για",
"No tags": "Δεν υπάρχουν ετικέτες",
"No users to add": "Δεν υπάρχουν χρήστες για προσθήκη",
"Organization": "",
"PLAYLISTS": "ΛΙΣΤΕΣ",
"PUBLISH STATE": "ΚΑΤΑΣΤΑΣΗ ΔΗΜΟΣΙΕΥΣΗΣ",
"Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "Δημοσιεύτηκε στις",
"Recent uploads": "Πρόσφατα ανεβάσματα",
"Recommended": "Προτεινόμενα",
"Record": "",
"Record Screen": "Καταγραφή οθόνης",
"Record Screen with Audio": "",
"Register": "Εγγραφή",
"Remove category": "Αφαίρεση κατηγορίας",
"Remove from list": "Αφαίρεση από λίστα",
"Remove tag": "Αφαίρεση ετικέτας",
"Remove user": "Αφαίρεση χρήστη",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "",
"SAVE": "ΑΠΟΘΗΚΕΥΣΗ",
"SEARCH": "ΑΝΑΖΗΤΗΣΗ",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "Επιλογή όλων των αρχείων",
"Select publish state:": "Επιλέξτε κατάσταση δημοσίευσης:",
"Selected": "Επιλεγμένα",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "Κοινοποιήθηκαν από εμένα",
"Shared with me": "Κοινοποιήθηκαν σε εμένα",
"Sharing": "",
"Sign in": "Σύνδεση",
"Sign out": "Αποσύνδεση",
"Sort By": "Ταξινόμηση",
"Start Recording": "Έναρξη εγγραφής",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Ξεκινήστε να ανεβάζετε αρχεία και να κοινοποιείτε τη δουλειά σας. Τα αρχεία που ανεβάζετε θα εμφανίζονται εδώ.",
"Stop Recording": "Διακοπή εγγραφής",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "Υποβολή",
"Subtitle was added": "Οι υπότιτλοι προστέθηκαν",
"Subtitles": "Υπότιτλοι",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "Τα σχόλια ενεργοποιήθηκαν με επιτυχία",
"Successfully changed owner": "Ο ιδιοκτήτης άλλαξε με επιτυχία",
"Successfully deleted": "Διαγράφηκε με επιτυχία",
"Successfully deleted comments": "",
"Successfully updated": "Ενημερώθηκε με επιτυχία",
"Successfully updated categories": "Οι κατηγορίες ενημερώθηκαν με επιτυχία",
"Successfully updated playlist membership": "Η συμμετοχή στη λίστα ενημερώθηκε με επιτυχία",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "Ανέβασμα αρχείων",
"Uploads": "Ανεβάσματα",
"Users": "Χρήστες",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "ΔΕΣ ΤΑ ΟΛΑ",
"Video": "Βίντεο",
"View all": "Δες τα όλα",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "Καλώς ήρθατε",
"You are going to copy": "Πρόκειται να αντιγράψετε",
"You are going to delete": "Πρόκειται να διαγράψετε",
"You are going to delete all comments from": "",
"You are going to disable comments to": "Πρόκειται να απενεργοποιήσετε τα σχόλια για",
"You are going to disable download for": "Πρόκειται να απενεργοποιήσετε τη λήψη για",
"You are going to enable comments to": "Πρόκειται να ενεργοποιήσετε τα σχόλια για",
+25
View File
@@ -48,6 +48,7 @@ translation_strings = {
"DELETE": "",
"DELETE MEDIA": "",
"Delete media": "",
"Delete Comments": "",
"Delete Media": "",
"Disable Comments": "",
"Disable Download": "",
@@ -71,6 +72,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "",
"Failed to copy media.": "",
"Failed to create playlist": "",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "",
"Failed to disable comments.": "",
"Failed to disable download.": "",
@@ -102,6 +104,9 @@ translation_strings = {
"Filter existing users...": "",
"Filter playlists...": "",
"Filters": "",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "",
"History": "",
"Home": "",
@@ -117,6 +122,7 @@ translation_strings = {
"Loading existing users...": "",
"Loading playlists...": "",
"Loading tags...": "",
"Management": "",
"Manage": "",
"Manage comments": "",
"Manage media": "",
@@ -143,6 +149,7 @@ translation_strings = {
"No results for": "",
"No tags": "",
"No users to add": "",
"Organization": "",
"or": "",
"Pdf": "",
"PLAYLISTS": "",
@@ -162,8 +169,13 @@ translation_strings = {
"Published": "",
"Published on": "",
"Recent uploads": "",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Recommended": "",
"Record": "",
"Record Screen": "",
"Record Screen with Audio": "",
"Register": "",
"Replace": "",
"Remove category": "",
@@ -171,6 +183,7 @@ translation_strings = {
"Remove tag": "",
"Remove user": "",
"results for": "",
"Settings": "",
"SAVE": "",
"SEARCH": "",
"Search": "",
@@ -184,6 +197,12 @@ translation_strings = {
"Selected": "",
"selected": "",
"SHARE": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Sharing": "",
"Share with": "",
"Shared by me": "",
"Shared with me": "",
"SHOW MORE": "",
@@ -194,6 +213,7 @@ translation_strings = {
"Start Recording": "",
"Start uploading media and sharing your work. Media that you upload will show up here.": "",
"Stop Recording": "",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"SUBMIT": "",
"Submit": "",
"Subtitle was added": "",
@@ -201,6 +221,7 @@ translation_strings = {
"Successfully changed owner": "",
"Successfully Copied": "",
"Successfully deleted": "",
"Successfully deleted comments": "",
"Successfully Disabled comments": "",
"Successfully Disabled Download": "",
"Successfully Enabled comments": "",
@@ -240,6 +261,9 @@ translation_strings = {
"UPLOAD MEDIA": "",
"Upload media": "",
"Uploads": "",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"Users": "",
"Video": "",
"view": "",
@@ -252,6 +276,7 @@ translation_strings = {
"yet": "",
"You are going to copy": "",
"You are going to delete": "",
"You are going to delete all comments from": "",
"You are going to disable comments to": "",
"You are going to disable download for": "",
"You are going to enable comments to": "",
+25
View File
@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "ELIMINAR MEDIOS",
"DOWNLOAD": "DESCARGAR",
"DURATION": "DURACIÓN",
"Delete Comments": "",
"Delete Media": "Eliminar Medio",
"Delete media": "Eliminar medios",
"Disable Comments": "Deshabilitar Comentarios",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "Error al cambiar propietario. Por favor, inténtelo de nuevo.",
"Failed to copy media.": "Error al copiar medios.",
"Failed to create playlist": "Error al crear lista de reproducción",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "Error al eliminar medios. Por favor, inténtelo de nuevo.",
"Failed to disable comments.": "Error al deshabilitar comentarios.",
"Failed to disable download.": "Error al deshabilitar descarga.",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "Filtrar usuarios existentes...",
"Filter playlists...": "Filtrar listas de reproducción...",
"Filters": "Filtros",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "Ir",
"History": "Historial",
"Home": "Inicio",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "Gestionar comentarios",
"Manage media": "Gestionar medios",
"Manage users": "Gestionar usuarios",
"Management": "",
"Media": "Medios",
"Media I own": "Medios que poseo",
"Media was edited": "El medio fue editado",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "No hay resultados para",
"No tags": "Sin etiquetas",
"No users to add": "No hay usuarios para agregar",
"Organization": "",
"PLAYLISTS": "LISTAS DE REPRODUCCIÓN",
"PUBLISH STATE": "ESTADO DE PUBLICACIÓN",
"Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "Publicado en",
"Recent uploads": "Subidas recientes",
"Recommended": "Recomendado",
"Record": "",
"Record Screen": "Grabar pantalla",
"Record Screen with Audio": "",
"Register": "Registrarse",
"Remove category": "Eliminar categoría",
"Remove from list": "Eliminar de la lista",
"Remove tag": "Eliminar etiqueta",
"Remove user": "Eliminar usuario",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "",
"SAVE": "GUARDAR",
"SEARCH": "BUSCAR",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "Seleccionar todos los medios",
"Select publish state:": "Seleccionar estado de publicación:",
"Selected": "Seleccionado",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "Compartido por mí",
"Shared with me": "Compartido conmigo",
"Sharing": "",
"Sign in": "Iniciar sesión",
"Sign out": "Cerrar sesión",
"Sort By": "Ordenar por",
"Start Recording": "Iniciar grabación",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Comience a subir medios y compartir su trabajo. Los medios que suba aparecerán aquí.",
"Stop Recording": "Detener grabación",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "Enviar",
"Subtitle was added": "El subtítulo fue agregado",
"Subtitles": "Subtítulos",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "Comentarios habilitados exitosamente",
"Successfully changed owner": "Propietario cambiado exitosamente",
"Successfully deleted": "Eliminado exitosamente",
"Successfully deleted comments": "",
"Successfully updated": "Actualizado exitosamente",
"Successfully updated categories": "Categorías actualizadas exitosamente",
"Successfully updated playlist membership": "Membresía de lista de reproducción actualizada exitosamente",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "Subir medios",
"Uploads": "Subidas",
"Users": "Usuarios",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "VER TODO",
"Video": "Video",
"View all": "Ver todo",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "Bienvenido",
"You are going to copy": "Vas a copiar",
"You are going to delete": "Vas a eliminar",
"You are going to delete all comments from": "",
"You are going to disable comments to": "Vas a deshabilitar comentarios de",
"You are going to disable download for": "Vas a deshabilitar descarga de",
"You are going to enable comments to": "Vas a habilitar comentarios de",
+25
View File
@@ -49,6 +49,7 @@ translation_strings = {
"DELETE MEDIA": "SUPPRIMER LE MÉDIA",
"DOWNLOAD": "TÉLÉCHARGER",
"DURATION": "DURÉE",
"Delete Comments": "",
"Delete Media": "Supprimer le média",
"Delete media": "Supprimer le média",
"Disable Comments": "Désactiver les commentaires",
@@ -71,6 +72,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "Échec du changement de propriétaire. Veuillez réessayer.",
"Failed to copy media.": "Échec de la copie du média.",
"Failed to create playlist": "Échec de la création de la playlist",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "Échec de la suppression du média. Veuillez réessayer.",
"Failed to disable comments.": "Échec de la désactivation des commentaires.",
"Failed to disable download.": "Échec de la désactivation du téléchargement.",
@@ -102,6 +104,9 @@ translation_strings = {
"Filter existing users...": "Filtrer les utilisateurs existants...",
"Filter playlists...": "Filtrer les playlists...",
"Filters": "Filtres",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "Aller",
"History": "Historique",
"Home": "Accueil",
@@ -122,6 +127,7 @@ translation_strings = {
"Manage comments": "Gérer les commentaires",
"Manage media": "Gérer les médias",
"Manage users": "Gérer les utilisateurs",
"Management": "",
"Media": "Média",
"Media I own": "Médias que je possède",
"Media was edited": "Le média a été modifié",
@@ -138,6 +144,7 @@ translation_strings = {
"No results for": "Aucun résultat pour",
"No tags": "Aucun tag",
"No users to add": "Aucun utilisateur à ajouter",
"Organization": "",
"PLAYLISTS": "PLAYLISTS",
"PUBLISH STATE": "ÉTAT DE PUBLICATION",
"Pdf": "PDF",
@@ -157,12 +164,17 @@ translation_strings = {
"Published on": "Publié le",
"Recent uploads": "Téléchargements récents",
"Recommended": "Recommandé",
"Record": "",
"Record Screen": "Enregistrer l'écran",
"Record Screen with Audio": "",
"Register": "S'inscrire",
"Remove category": "Supprimer la catégorie",
"Remove from list": "Supprimer de la liste",
"Remove tag": "Supprimer le tag",
"Remove user": "Supprimer l'utilisateur",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "",
"SAVE": "ENREGISTRER",
"SEARCH": "RECHERCHER",
@@ -179,14 +191,22 @@ translation_strings = {
"Select all media": "Sélectionner tous les médias",
"Select publish state:": "Sélectionner l'état de publication:",
"Selected": "Sélectionné",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "Partagé par moi",
"Shared with me": "Partagé avec moi",
"Sharing": "",
"Sign in": "Se connecter",
"Sign out": "Se déconnecter",
"Sort By": "Trier par",
"Start Recording": "Commencer l'enregistrement",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Commencez à télécharger des médias et à partager votre travail. Les médias que vous téléchargez apparaîtront ici.",
"Stop Recording": "Arrêter l'enregistrement",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "Soumettre",
"Subtitle was added": "Le sous-titre a été ajouté",
"Subtitles": "Sous-titres",
@@ -197,6 +217,7 @@ translation_strings = {
"Successfully Enabled comments": "Commentaires activés avec succès",
"Successfully changed owner": "Propriétaire changé avec succès",
"Successfully deleted": "Supprimé avec succès",
"Successfully deleted comments": "",
"Successfully updated": "Mis à jour avec succès",
"Successfully updated categories": "Catégories mises à jour avec succès",
"Successfully updated playlist membership": "Adhésion à la playlist mise à jour avec succès",
@@ -233,6 +254,9 @@ translation_strings = {
"Upload media": "Télécharger des médias",
"Uploads": "Téléchargements",
"Users": "Utilisateurs",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "VOIR TOUT",
"Video": "Vidéo",
"View all": "Voir tout",
@@ -241,6 +265,7 @@ translation_strings = {
"Welcome": "Bienvenue",
"You are going to copy": "Vous allez copier",
"You are going to delete": "Vous allez supprimer",
"You are going to delete all comments from": "",
"You are going to disable comments to": "Vous allez désactiver les commentaires de",
"You are going to disable download for": "Vous allez désactiver le téléchargement de",
"You are going to enable comments to": "Vous allez activer les commentaires de",
+25
View File
@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "מחק מדיה",
"DOWNLOAD": "הורד",
"DURATION": "משך",
"Delete Comments": "",
"Delete Media": "",
"Delete media": "מחק מדיה",
"Disable Comments": "",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "",
"Failed to copy media.": "העתקת המדיה נכשלה.",
"Failed to create playlist": "",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "מחיקת המדיה נכשלה. אנא נסה שוב.",
"Failed to disable comments.": "ביטול התגובות נכשל.",
"Failed to disable download.": "ביטול ההורדה נכשל.",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "",
"Filter playlists...": "",
"Filters": "מסננים",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "בצע",
"History": "היסטוריה",
"Home": "דף הבית",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "ניהול תגובות",
"Manage media": "ניהול מדיה",
"Manage users": "ניהול משתמשים",
"Management": "",
"Media": "מדיה",
"Media I own": "",
"Media was edited": "המדיה נערכה",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "אין תוצאות עבור",
"No tags": "",
"No users to add": "",
"Organization": "",
"PLAYLISTS": "פלייליסטים",
"PUBLISH STATE": "מצב פרסום",
"Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "פורסם בתאריך",
"Recent uploads": "העלאות אחרונות",
"Recommended": "מומלץ",
"Record": "",
"Record Screen": "הקלטת מסך",
"Record Screen with Audio": "",
"Register": "הרשמה",
"Remove category": "",
"Remove from list": "",
"Remove tag": "",
"Remove user": "",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "",
"SAVE": "שמור",
"SEARCH": "חפש",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "",
"Select publish state:": "",
"Selected": "",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "שותף על ידי",
"Shared with me": "שותף איתי",
"Sharing": "",
"Sign in": "התחבר",
"Sign out": "התנתק",
"Sort By": "מיין לפי",
"Start Recording": "התחל הקלטה",
"Start uploading media and sharing your work. Media that you upload will show up here.": "התחל להעלות מדיה ולשתף את עבודתך. המדיה שתעלה תופיע כאן.",
"Stop Recording": "עצור הקלטה",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "",
"Subtitle was added": "הכתובית נוספה",
"Subtitles": "כתוביות",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "התגובות הופעלו בהצלחה",
"Successfully changed owner": "",
"Successfully deleted": "נמחק בהצלחה",
"Successfully deleted comments": "",
"Successfully updated": "",
"Successfully updated categories": "",
"Successfully updated playlist membership": "",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "העלה מדיה",
"Uploads": "העלאות",
"Users": "",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "הצג הכל",
"Video": "וידאו",
"View all": "הצג הכל",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "ברוך הבא",
"You are going to copy": "אתה עומד להעתיק",
"You are going to delete": "אתה עומד למחוק",
"You are going to delete all comments from": "",
"You are going to disable comments to": "אתה עומד לבטל תגובות ל",
"You are going to disable download for": "אתה עומד לבטל הורדה עבור",
"You are going to enable comments to": "אתה עומד להפעיל תגובות ל",
+25
View File
@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "मीडिया हटाएं",
"DOWNLOAD": "डाउनलोड करें",
"DURATION": "अवधि",
"Delete Comments": "",
"Delete Media": "मीडिया हटाएं",
"Delete media": "मीडिया हटाएं",
"Disable Comments": "टिप्पणियां अक्षम करें",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "स्वामी बदलने में विफल। कृपया पुनः प्रयास करें।",
"Failed to copy media.": "मीडिया कॉपी करने में विफल।",
"Failed to create playlist": "प्लेलिस्ट बनाने में विफल",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "मीडिया हटाने में विफल। कृपया पुनः प्रयास करें।",
"Failed to disable comments.": "टिप्पणियों को अक्षम करने में विफल।",
"Failed to disable download.": "डाउनलोड अक्षम करने में विफल।",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "मौजूदा उपयोगकर्ताओं को फ़िल्टर करें...",
"Filter playlists...": "प्लेलिस्ट फ़िल्टर करें...",
"Filters": "फ़िल्टर",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "जाएं",
"History": "इतिहास",
"Home": "मुख्य पृष्ठ",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "टिप्पणियाँ प्रबंधित करें",
"Manage media": "मीडिया प्रबंधित करें",
"Manage users": "उपयोगकर्ताओं को प्रबंधित करें",
"Management": "",
"Media": "मीडिया",
"Media I own": "मेरे स्वामित्व वाली मीडिया",
"Media was edited": "मीडिया संपादित किया गया था",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "के लिए कोई परिणाम नहीं",
"No tags": "कोई टैग नहीं",
"No users to add": "जोड़ने के लिए कोई उपयोगकर्ता नहीं",
"Organization": "",
"PLAYLISTS": "प्लेलिस्ट",
"PUBLISH STATE": "प्रकाशन स्थिति",
"Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "पर प्रकाशित",
"Recent uploads": "हाल के अपलोड",
"Recommended": "अनुशंसित",
"Record": "",
"Record Screen": "स्क्रीन रिकॉर्ड करें",
"Record Screen with Audio": "",
"Register": "पंजीकरण करें",
"Remove category": "श्रेणी हटाएं",
"Remove from list": "सूची से हटाएं",
"Remove tag": "टैग हटाएं",
"Remove user": "उपयोगकर्ता हटाएं",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "",
"SAVE": "सहेजें",
"SEARCH": "खोजें",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "सभी मीडिया चुनें",
"Select publish state:": "प्रकाशन स्थिति चुनें:",
"Selected": "चयनित",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "मेरे द्वारा साझा किया गया",
"Shared with me": "मेरे साथ साझा किया गया",
"Sharing": "",
"Sign in": "साइन इन करें",
"Sign out": "साइन आउट करें",
"Sort By": "इसके अनुसार क्रमबद्ध करें",
"Start Recording": "रिकॉर्डिंग प्रारंभ करें",
"Start uploading media and sharing your work. Media that you upload will show up here.": "मीडिया अपलोड करना और अपना काम साझा करना शुरू करें। आपके द्वारा अपलोड किया गया मीडिया यहां दिखाई देगा।",
"Stop Recording": "रिकॉर्डिंग रोकें",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "प्रस्तुत करें",
"Subtitle was added": "उपशीर्षक जोड़ा गया",
"Subtitles": "उपशीर्षक",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "टिप्पणियां सफलतापूर्वक सक्षम की गईं",
"Successfully changed owner": "स्वामी सफलतापूर्वक बदला गया",
"Successfully deleted": "सफलतापूर्वक हटाया गया",
"Successfully deleted comments": "",
"Successfully updated": "सफलतापूर्वक अपडेट किया गया",
"Successfully updated categories": "श्रेणियां सफलतापूर्वक अपडेट की गईं",
"Successfully updated playlist membership": "प्लेलिस्ट सदस्यता सफलतापूर्वक अपडेट की गई",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "मीडिया अपलोड करें",
"Uploads": "अपलोड",
"Users": "उपयोगकर्ता",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "सभी देखें",
"Video": "वीडियो",
"View all": "सभी देखें",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "स्वागत है",
"You are going to copy": "आप कॉपी करने जा रहे हैं",
"You are going to delete": "आप हटाने जा रहे हैं",
"You are going to delete all comments from": "",
"You are going to disable comments to": "आप टिप्पणियों को अक्षम करने जा रहे हैं",
"You are going to disable download for": "आप डाउनलोड को अक्षम करने जा रहे हैं",
"You are going to enable comments to": "आप टिप्पणियों को सक्षम करने जा रहे हैं",
+25
View File
@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "HAPUS MEDIA",
"DOWNLOAD": "UNDUH",
"DURATION": "DURASI",
"Delete Comments": "",
"Delete Media": "Hapus Media",
"Delete media": "Hapus media",
"Disable Comments": "Nonaktifkan Komentar",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "Gagal mengganti pemilik. Silakan coba lagi.",
"Failed to copy media.": "Gagal menyalin media.",
"Failed to create playlist": "Gagal membuat daftar putar",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "Gagal menghapus media. Silakan coba lagi.",
"Failed to disable comments.": "Gagal menonaktifkan komentar.",
"Failed to disable download.": "Gagal menonaktifkan unduhan.",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "Filter pengguna yang ada...",
"Filter playlists...": "Filter daftar putar...",
"Filters": "Filter",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "Pergi",
"History": "Riwayat",
"Home": "Beranda",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "Kelola komentar",
"Manage media": "Kelola media",
"Manage users": "Kelola pengguna",
"Management": "",
"Media": "Media",
"Media I own": "Media yang saya miliki",
"Media was edited": "Media telah diedit",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "Tidak ada hasil untuk",
"No tags": "Tidak ada tag",
"No users to add": "Tidak ada pengguna untuk ditambahkan",
"Organization": "",
"PLAYLISTS": "DAFTAR PUTAR",
"PUBLISH STATE": "STATUS PUBLIKASI",
"Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "Diterbitkan pada",
"Recent uploads": "Unggahan terbaru",
"Recommended": "Direkomendasikan",
"Record": "",
"Record Screen": "Rekam Layar",
"Record Screen with Audio": "",
"Register": "Daftar",
"Remove category": "Hapus kategori",
"Remove from list": "Hapus dari daftar",
"Remove tag": "Hapus tag",
"Remove user": "Hapus pengguna",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "",
"SAVE": "SIMPAN",
"SEARCH": "CARI",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "Pilih semua media",
"Select publish state:": "Pilih status publikasi:",
"Selected": "Dipilih",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "Dibagikan oleh saya",
"Shared with me": "Dibagikan dengan saya",
"Sharing": "",
"Sign in": "Masuk",
"Sign out": "Keluar",
"Sort By": "Urutkan Berdasarkan",
"Start Recording": "Mulai Merekam",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Mulai mengunggah media dan berbagi karya Anda. Media yang Anda unggah akan muncul di sini.",
"Stop Recording": "Hentikan Perekaman",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "Kirim",
"Subtitle was added": "Subtitle telah ditambahkan",
"Subtitles": "Subtitel",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "Komentar berhasil diaktifkan",
"Successfully changed owner": "Berhasil mengganti pemilik",
"Successfully deleted": "Berhasil dihapus",
"Successfully deleted comments": "",
"Successfully updated": "Berhasil diperbarui",
"Successfully updated categories": "Kategori berhasil diperbarui",
"Successfully updated playlist membership": "Keanggotaan daftar putar berhasil diperbarui",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "Unggah media",
"Uploads": "Unggahan",
"Users": "Pengguna",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "LIHAT SEMUA",
"Video": "Video",
"View all": "Lihat semua",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "Selamat datang",
"You are going to copy": "Anda akan menyalin",
"You are going to delete": "Anda akan menghapus",
"You are going to delete all comments from": "",
"You are going to disable comments to": "Anda akan menonaktifkan komentar untuk",
"You are going to disable download for": "Anda akan menonaktifkan unduhan untuk",
"You are going to enable comments to": "Anda akan mengaktifkan komentar untuk",
+25
View File
@@ -49,6 +49,7 @@ translation_strings = {
"DELETE MEDIA": "ELIMINA MEDIA",
"DOWNLOAD": "SCARICA",
"DURATION": "DURATA",
"Delete Comments": "",
"Delete Media": "Elimina Media",
"Delete media": "Elimina media",
"Disable Comments": "Disabilita Commenti",
@@ -71,6 +72,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "Impossibile cambiare proprietario. Riprova.",
"Failed to copy media.": "Impossibile copiare il media.",
"Failed to create playlist": "Impossibile creare playlist",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "Impossibile eliminare il media. Riprova.",
"Failed to disable comments.": "Impossibile disabilitare i commenti.",
"Failed to disable download.": "Impossibile disabilitare il download.",
@@ -102,6 +104,9 @@ translation_strings = {
"Filter existing users...": "Filtra utenti esistenti...",
"Filter playlists...": "Filtra playlist...",
"Filters": "Filtri",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "Vai",
"History": "Cronologia",
"Home": "Home",
@@ -122,6 +127,7 @@ translation_strings = {
"Manage comments": "Gestisci i commenti",
"Manage media": "Gestisci i media",
"Manage users": "Gestisci gli utenti",
"Management": "",
"Media": "Media",
"Media I own": "Media di mia proprietà",
"Media was edited": "Il media è stato modificato",
@@ -138,6 +144,7 @@ translation_strings = {
"No results for": "Nessun risultato per",
"No tags": "Nessun tag",
"No users to add": "Nessun utente da aggiungere",
"Organization": "",
"PLAYLISTS": "PLAYLIST",
"PUBLISH STATE": "STATO DI PUBBLICAZIONE",
"Pdf": "PDF",
@@ -157,12 +164,17 @@ translation_strings = {
"Published on": "Pubblicato il",
"Recent uploads": "Caricamenti recenti",
"Recommended": "Raccomandati",
"Record": "",
"Record Screen": "Registra schermo",
"Record Screen with Audio": "",
"Register": "Registrati",
"Remove category": "Rimuovi categoria",
"Remove from list": "Rimuovi dalla lista",
"Remove tag": "Rimuovi tag",
"Remove user": "Rimuovi utente",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "",
"SAVE": "SALVA",
"SEARCH": "CERCA",
@@ -179,14 +191,22 @@ translation_strings = {
"Select all media": "Seleziona tutti i media",
"Select publish state:": "Seleziona stato di pubblicazione:",
"Selected": "Selezionato",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "Condiviso da me",
"Shared with me": "Condiviso con me",
"Sharing": "",
"Sign in": "Login",
"Sign out": "Logout",
"Sort By": "Ordina per",
"Start Recording": "Inizia registrazione",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Inizia a caricare media e condividere il tuo lavoro. I media caricati appariranno qui.",
"Stop Recording": "Interrompi registrazione",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "Invia",
"Subtitle was added": "I sottotitoli sono stati aggiunti",
"Subtitles": "Sottotitoli",
@@ -197,6 +217,7 @@ translation_strings = {
"Successfully Enabled comments": "Commenti abilitati con successo",
"Successfully changed owner": "Proprietario cambiato con successo",
"Successfully deleted": "Eliminato con successo",
"Successfully deleted comments": "",
"Successfully updated": "Aggiornato con successo",
"Successfully updated categories": "Categorie aggiornate con successo",
"Successfully updated playlist membership": "Appartenenza alla playlist aggiornata con successo",
@@ -233,6 +254,9 @@ translation_strings = {
"Upload media": "Carica i media",
"Uploads": "Caricamenti",
"Users": "Utenti",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "MOSTRA TUTTI",
"Video": "Video",
"View all": "Mostra tutti",
@@ -241,6 +265,7 @@ translation_strings = {
"Welcome": "Benvenuto",
"You are going to copy": "Stai per copiare",
"You are going to delete": "Stai per eliminare",
"You are going to delete all comments from": "",
"You are going to disable comments to": "Stai per disabilitare i commenti di",
"You are going to disable download for": "Stai per disabilitare il download di",
"You are going to enable comments to": "Stai per abilitare i commenti di",
+25
View File
@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "メディアを削除",
"DOWNLOAD": "ダウンロード",
"DURATION": "期間",
"Delete Comments": "",
"Delete Media": "メディアを削除",
"Delete media": "メディアを削除",
"Disable Comments": "コメントを無効化",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "所有者の変更に失敗しました。もう一度お試しください。",
"Failed to copy media.": "メディアのコピーに失敗しました。",
"Failed to create playlist": "プレイリストの作成に失敗しました",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "メディアの削除に失敗しました。もう一度お試しください。",
"Failed to disable comments.": "コメントの無効化に失敗しました。",
"Failed to disable download.": "ダウンロードの無効化に失敗しました。",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "既存ユーザーをフィルター...",
"Filter playlists...": "プレイリストをフィルター...",
"Filters": "フィルター",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "行く",
"History": "履歴",
"Home": "ホーム",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "コメントを管理",
"Manage media": "メディアを管理",
"Manage users": "ユーザーを管理",
"Management": "",
"Media": "メディア",
"Media I own": "自分が所有するメディア",
"Media was edited": "メディアが編集されました",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "の結果はありません",
"No tags": "タグなし",
"No users to add": "追加するユーザーなし",
"Organization": "",
"PLAYLISTS": "プレイリスト",
"PUBLISH STATE": "公開状態",
"Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "公開日",
"Recent uploads": "最近のアップロード",
"Recommended": "おすすめ",
"Record": "",
"Record Screen": "画面を録画",
"Record Screen with Audio": "",
"Register": "登録",
"Remove category": "カテゴリーを削除",
"Remove from list": "リストから削除",
"Remove tag": "タグを削除",
"Remove user": "ユーザーを削除",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "",
"SAVE": "保存",
"SEARCH": "検索",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "すべてのメディアを選択",
"Select publish state:": "公開状態を選択:",
"Selected": "選択済み",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "自分が共有",
"Shared with me": "共有されたもの",
"Sharing": "",
"Sign in": "サインイン",
"Sign out": "サインアウト",
"Sort By": "並び替え",
"Start Recording": "録画開始",
"Start uploading media and sharing your work. Media that you upload will show up here.": "メディアをアップロードして作品を共有しましょう。アップロードしたメディアはここに表示されます。",
"Stop Recording": "録画停止",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "送信",
"Subtitle was added": "字幕が追加されました",
"Subtitles": "字幕",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "コメントが正常に有効化されました",
"Successfully changed owner": "所有者が正常に変更されました",
"Successfully deleted": "正常に削除されました",
"Successfully deleted comments": "",
"Successfully updated": "正常に更新されました",
"Successfully updated categories": "カテゴリーが正常に更新されました",
"Successfully updated playlist membership": "プレイリストメンバーシップが正常に更新されました",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "メディアをアップロード",
"Uploads": "アップロード",
"Users": "ユーザー",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "すべて表示",
"Video": "ビデオ",
"View all": "すべて表示",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "ようこそ",
"You are going to copy": "コピーします",
"You are going to delete": "削除します",
"You are going to delete all comments from": "",
"You are going to disable comments to": "コメントを無効化します",
"You are going to disable download for": "ダウンロードを無効化します",
"You are going to enable comments to": "コメントを有効化します",
+25
View File
@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "미디어 삭제",
"DOWNLOAD": "다운로드",
"DURATION": "재생 시간",
"Delete Comments": "",
"Delete Media": "미디어 삭제",
"Delete media": "미디어 삭제",
"Disable Comments": "댓글 비활성화",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "소유자 변경에 실패했습니다. 다시 시도해주세요.",
"Failed to copy media.": "미디어 복사에 실패했습니다.",
"Failed to create playlist": "재생 목록 만들기 실패",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "미디어 삭제에 실패했습니다. 다시 시도해주세요.",
"Failed to disable comments.": "댓글 비활성화에 실패했습니다.",
"Failed to disable download.": "다운로드 비활성화에 실패했습니다.",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "기존 사용자 필터링...",
"Filter playlists...": "재생 목록 필터링...",
"Filters": "필터",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "이동",
"History": "기록",
"Home": "",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "댓글 관리",
"Manage media": "미디어 관리",
"Manage users": "사용자 관리",
"Management": "",
"Media": "미디어",
"Media I own": "내가 소유한 미디어",
"Media was edited": "미디어가 편집되었습니다",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "결과 없음",
"No tags": "태그 없음",
"No users to add": "추가할 사용자 없음",
"Organization": "",
"PLAYLISTS": "재생 목록",
"PUBLISH STATE": "게시 상태",
"Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "게시일",
"Recent uploads": "최근 업로드",
"Recommended": "추천",
"Record": "",
"Record Screen": "화면 녹화",
"Record Screen with Audio": "",
"Register": "등록",
"Remove category": "카테고리 제거",
"Remove from list": "목록에서 제거",
"Remove tag": "태그 제거",
"Remove user": "사용자 제거",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "",
"SAVE": "저장",
"SEARCH": "검색",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "모든 미디어 선택",
"Select publish state:": "게시 상태 선택:",
"Selected": "선택됨",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "내가 공유함",
"Shared with me": "나와 공유됨",
"Sharing": "",
"Sign in": "로그인",
"Sign out": "로그아웃",
"Sort By": "정렬",
"Start Recording": "녹화 시작",
"Start uploading media and sharing your work. Media that you upload will show up here.": "미디어를 업로드하고 작업을 공유하세요. 업로드한 미디어가 여기에 표시됩니다.",
"Stop Recording": "녹화 중지",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "제출",
"Subtitle was added": "자막이 추가되었습니다",
"Subtitles": "자막",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "댓글이 활성화되었습니다",
"Successfully changed owner": "소유자가 변경되었습니다",
"Successfully deleted": "삭제 성공",
"Successfully deleted comments": "",
"Successfully updated": "업데이트 성공",
"Successfully updated categories": "카테고리가 업데이트되었습니다",
"Successfully updated playlist membership": "재생 목록 멤버십이 업데이트되었습니다",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "미디어 업로드",
"Uploads": "업로드",
"Users": "사용자",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "모두 보기",
"Video": "비디오",
"View all": "모두 보기",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "환영합니다",
"You are going to copy": "복사하려고 합니다",
"You are going to delete": "삭제하려고 합니다",
"You are going to delete all comments from": "",
"You are going to disable comments to": "댓글을 비활성화하려고 합니다",
"You are going to disable download for": "다운로드를 비활성화하려고 합니다",
"You are going to enable comments to": "댓글을 활성화하려고 합니다",
+25
View File
@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "MEDIA VERWIJDEREN",
"DOWNLOAD": "DOWNLOADEN",
"DURATION": "DUUR",
"Delete Comments": "",
"Delete Media": "Media verwijderen",
"Delete media": "Media verwijderen",
"Disable Comments": "Reacties uitschakelen",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "Eigenaar wijzigen mislukt. Probeer het opnieuw.",
"Failed to copy media.": "Media kopiëren mislukt.",
"Failed to create playlist": "Afspeellijst maken mislukt",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "Media verwijderen mislukt. Probeer het opnieuw.",
"Failed to disable comments.": "Reacties uitschakelen mislukt.",
"Failed to disable download.": "Download uitschakelen mislukt.",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "Filter bestaande gebruikers...",
"Filter playlists...": "Filter afspeellijsten...",
"Filters": "Filters",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "Ga",
"History": "Geschiedenis",
"Home": "Home",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "Reacties beheren",
"Manage media": "Media beheren",
"Manage users": "Gebruikers beheren",
"Management": "",
"Media": "Media",
"Media I own": "Media die ik bezit",
"Media was edited": "Media is bewerkt",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "Geen resultaten voor",
"No tags": "Geen tags",
"No users to add": "Geen gebruikers om toe te voegen",
"Organization": "",
"PLAYLISTS": "AFSPEELLIJSTEN",
"PUBLISH STATE": "PUBLICATIESTATUS",
"Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "Gepubliceerd op",
"Recent uploads": "Recente uploads",
"Recommended": "Aanbevolen",
"Record": "",
"Record Screen": "Scherm opnemen",
"Record Screen with Audio": "",
"Register": "Registreren",
"Remove category": "Categorie verwijderen",
"Remove from list": "Verwijderen uit lijst",
"Remove tag": "Tag verwijderen",
"Remove user": "Gebruiker verwijderen",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "",
"SAVE": "OPSLAAN",
"SEARCH": "ZOEKEN",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "Alle media selecteren",
"Select publish state:": "Selecteer publicatiestatus:",
"Selected": "Geselecteerd",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "Gedeeld door mij",
"Shared with me": "Gedeeld met mij",
"Sharing": "",
"Sign in": "Inloggen",
"Sign out": "Uitloggen",
"Sort By": "Sorteer op",
"Start Recording": "Opname starten",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Begin met het uploaden van media en het delen van uw werk. Media die u uploadt, verschijnt hier.",
"Stop Recording": "Opname stoppen",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "Indienen",
"Subtitle was added": "Ondertitel is toegevoegd",
"Subtitles": "Ondertitels",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "Reacties succesvol ingeschakeld",
"Successfully changed owner": "Eigenaar succesvol gewijzigd",
"Successfully deleted": "Succesvol verwijderd",
"Successfully deleted comments": "",
"Successfully updated": "Succesvol bijgewerkt",
"Successfully updated categories": "Categorieën succesvol bijgewerkt",
"Successfully updated playlist membership": "Afspeellijstlidmaatschap succesvol bijgewerkt",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "Media uploaden",
"Uploads": "Uploads",
"Users": "Gebruikers",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "BEKIJK ALLES",
"Video": "Video",
"View all": "Bekijk alles",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "Welkom",
"You are going to copy": "Je gaat kopiëren",
"You are going to delete": "Je gaat verwijderen",
"You are going to delete all comments from": "",
"You are going to disable comments to": "Je gaat reacties uitschakelen voor",
"You are going to disable download for": "Je gaat download uitschakelen voor",
"You are going to enable comments to": "Je gaat reacties inschakelen voor",
+25
View File
@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "EXCLUIR MÍDIA",
"DOWNLOAD": "BAIXAR",
"DURATION": "DURAÇÃO",
"Delete Comments": "",
"Delete Media": "Excluir mídia",
"Delete media": "Excluir mídia",
"Disable Comments": "Desativar comentários",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "Falha ao mudar proprietário. Por favor, tente novamente.",
"Failed to copy media.": "Falha ao copiar mídia.",
"Failed to create playlist": "Falha ao criar playlist",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "Falha ao excluir mídia. Por favor, tente novamente.",
"Failed to disable comments.": "Falha ao desativar comentários.",
"Failed to disable download.": "Falha ao desativar download.",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "Filtrar usuários existentes...",
"Filter playlists...": "Filtrar playlists...",
"Filters": "Filtros",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "Ir",
"History": "Histórico",
"Home": "Início",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "Gerenciar comentários",
"Manage media": "Gerenciar mídia",
"Manage users": "Gerenciar usuários",
"Management": "",
"Media": "Mídia",
"Media I own": "Mídia que possuo",
"Media was edited": "Mídia foi editada",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "Nenhum resultado para",
"No tags": "Nenhuma tag",
"No users to add": "Nenhum usuário para adicionar",
"Organization": "",
"PLAYLISTS": "PLAYLISTS",
"PUBLISH STATE": "ESTADO DE PUBLICAÇÃO",
"Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "Publicado em",
"Recent uploads": "Uploads recentes",
"Recommended": "Recomendado",
"Record": "",
"Record Screen": "Gravar tela",
"Record Screen with Audio": "",
"Register": "Registrar",
"Remove category": "Remover categoria",
"Remove from list": "Remover da lista",
"Remove tag": "Remover tag",
"Remove user": "Remover usuário",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "",
"SAVE": "SALVAR",
"SEARCH": "PESQUISAR",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "Selecionar todas as mídias",
"Select publish state:": "Selecionar estado de publicação:",
"Selected": "Selecionado",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "Compartilhado por mim",
"Shared with me": "Compartilhado comigo",
"Sharing": "",
"Sign in": "Entrar",
"Sign out": "Sair",
"Sort By": "Ordenar por",
"Start Recording": "Iniciar gravação",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Comece a fazer upload de mídia e compartilhar seu trabalho. A mídia que você fizer upload aparecerá aqui.",
"Stop Recording": "Parar gravação",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "Enviar",
"Subtitle was added": "Legenda foi adicionada",
"Subtitles": "Legendas",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "Comentários ativados com sucesso",
"Successfully changed owner": "Proprietário alterado com sucesso",
"Successfully deleted": "Excluído com sucesso",
"Successfully deleted comments": "",
"Successfully updated": "Atualizado com sucesso",
"Successfully updated categories": "Categorias atualizadas com sucesso",
"Successfully updated playlist membership": "Associação da playlist atualizada com sucesso",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "Carregar mídia",
"Uploads": "Uploads",
"Users": "Usuários",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "VER TODOS",
"Video": "Vídeo",
"View all": "Ver todos",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "Bem-vindo",
"You are going to copy": "Você vai copiar",
"You are going to delete": "Você vai excluir",
"You are going to delete all comments from": "",
"You are going to disable comments to": "Você vai desativar comentários de",
"You are going to disable download for": "Você vai desativar download de",
"You are going to enable comments to": "Você vai ativar comentários de",
+25
View File
@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "УДАЛИТЬ МЕДИА",
"DOWNLOAD": "СКАЧАТЬ",
"DURATION": "ДЛИТЕЛЬНОСТЬ",
"Delete Comments": "",
"Delete Media": "Удалить медиа",
"Delete media": "Удалить медиа",
"Disable Comments": "Отключить комментарии",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "Не удалось изменить владельца. Пожалуйста, попробуйте снова.",
"Failed to copy media.": "Не удалось скопировать медиа.",
"Failed to create playlist": "Не удалось создать плейлист",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "Не удалось удалить медиа. Пожалуйста, попробуйте снова.",
"Failed to disable comments.": "Не удалось отключить комментарии.",
"Failed to disable download.": "Не удалось отключить загрузку.",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "Фильтровать существующих пользователей...",
"Filter playlists...": "Фильтровать плейлисты...",
"Filters": "Фильтры",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "Перейти",
"History": "История",
"Home": "Главная",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "Управление комментариями",
"Manage media": "Управление медиа",
"Manage users": "Управление пользователями",
"Management": "",
"Media": "Медиа",
"Media I own": "Медиа, которыми я владею",
"Media was edited": "Медиа было отредактировано",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "Нет результатов для",
"No tags": "Нет тегов",
"No users to add": "Нет пользователей для добавления",
"Organization": "",
"PLAYLISTS": "ПЛЕЙЛИСТЫ",
"PUBLISH STATE": "СОСТОЯНИЕ ПУБЛИКАЦИИ",
"Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "Опубликовано",
"Recent uploads": "Недавние загрузки",
"Recommended": "Рекомендуемое",
"Record": "",
"Record Screen": "Запись экрана",
"Record Screen with Audio": "",
"Register": "Регистрация",
"Remove category": "Удалить категорию",
"Remove from list": "Удалить из списка",
"Remove tag": "Удалить тег",
"Remove user": "Удалить пользователя",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "",
"SAVE": "СОХРАНИТЬ",
"SEARCH": "ПОИСК",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "Выбрать все медиа",
"Select publish state:": "Выберите состояние публикации:",
"Selected": "Выбрано",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "Мной поделено",
"Shared with me": "Поделено со мной",
"Sharing": "",
"Sign in": "Войти",
"Sign out": "Выйти",
"Sort By": "Сортировать по",
"Start Recording": "Начать запись",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Начните загружать медиа и делиться своей работой. Загруженные медиа появятся здесь.",
"Stop Recording": "Остановить запись",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "Отправить",
"Subtitle was added": "Субтитры были добавлены",
"Subtitles": "Субтитры",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "Комментарии успешно включены",
"Successfully changed owner": "Владелец успешно изменен",
"Successfully deleted": "Успешно удалено",
"Successfully deleted comments": "",
"Successfully updated": "Успешно обновлено",
"Successfully updated categories": "Категории успешно обновлены",
"Successfully updated playlist membership": "Членство в плейлисте успешно обновлено",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "Загрузить медиа",
"Uploads": "Загрузки",
"Users": "Пользователи",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "ПОКАЗАТЬ ВСЕ",
"Video": "Видео",
"View all": "Показать все",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "Добро пожаловать",
"You are going to copy": "Вы собираетесь скопировать",
"You are going to delete": "Вы собираетесь удалить",
"You are going to delete all comments from": "",
"You are going to disable comments to": "Вы собираетесь отключить комментарии для",
"You are going to disable download for": "Вы собираетесь отключить загрузку для",
"You are going to enable comments to": "Вы собираетесь включить комментарии для",
+25
View File
@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "IZBRIŠI MEDIJ",
"DOWNLOAD": "PRENESI",
"DURATION": "TRAJANJE",
"Delete Comments": "",
"Delete Media": "Izbriši Medij",
"Delete media": "Izbriši medij",
"Disable Comments": "Onemogoči Komentarje",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "Spreminjanje lastnika ni uspelo. Prosim poskusite ponovno.",
"Failed to copy media.": "Kopiranje medija ni uspelo.",
"Failed to create playlist": "Ustvarjanje seznama predvajanja ni uspelo",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "Brisanje medija ni uspelo. Prosim poskusite ponovno.",
"Failed to disable comments.": "Onemogočanje komentarjev ni uspelo.",
"Failed to disable download.": "Onemogočanje prenosa ni uspelo.",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "Filtriraj obstoječe uporabnike...",
"Filter playlists...": "Filtriraj sezname predvajanja...",
"Filters": "Filtri",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "Pojdi",
"History": "Zgodovina",
"Home": "Domov",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "Upravljaj komentarje",
"Manage media": "Upravljaj medije",
"Manage users": "Upravljaj uporabnike",
"Management": "",
"Media": "Mediji",
"Media I own": "Mediji, ki jih posedujam",
"Media was edited": "Medij je bil urejen",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "Ni rezultatov za",
"No tags": "Brez oznak",
"No users to add": "Ni uporabnikov za dodajanje",
"Organization": "",
"PLAYLISTS": "SEZNAMI PREDVAJANJA",
"PUBLISH STATE": "STANJE OBJAVE",
"Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "Objavljeno",
"Recent uploads": "Nedavne naložitve",
"Recommended": "Priporočeno",
"Record": "",
"Record Screen": "Snemanje zaslona",
"Record Screen with Audio": "",
"Register": "Registracija",
"Remove category": "Odstrani kategorijo",
"Remove from list": "Odstrani s seznama",
"Remove tag": "Odstrani oznako",
"Remove user": "Odstrani uporabnika",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "",
"SAVE": "SHRANI",
"SEARCH": "ISKANJE",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "Izberi vse medije",
"Select publish state:": "Izberi stanje objave:",
"Selected": "Izbrano",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "Deljeno z moje strani",
"Shared with me": "Deljeno z mano",
"Sharing": "",
"Sign in": "Prijava",
"Sign out": "Odjava",
"Sort By": "Razvrsti po",
"Start Recording": "Začni snemanje",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Začnite nalagati medije in deliti svoje delo. Mediji, ki jih naložite, bodo prikazani tukaj.",
"Stop Recording": "Ustavi snemanje",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "Pošlji",
"Subtitle was added": "Podnapisi so bili dodani",
"Subtitles": "Podnapisi",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "Komentarji uspešno omogočeni",
"Successfully changed owner": "Lastnik uspešno spremenjen",
"Successfully deleted": "Uspešno izbrisano",
"Successfully deleted comments": "",
"Successfully updated": "Uspešno posodobljeno",
"Successfully updated categories": "Kategorije uspešno posodobljene",
"Successfully updated playlist membership": "Članstvo seznama predvajanja uspešno posodobljeno",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "Naloži medij",
"Uploads": "Naloženi",
"Users": "Uporabniki",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "PRIKAŽI VSE",
"Video": "Video",
"View all": "Prikaži vse",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "Dobrodošli",
"You are going to copy": "Kopirate",
"You are going to delete": "Brišete",
"You are going to delete all comments from": "",
"You are going to disable comments to": "Onemogočate komentarje za",
"You are going to disable download for": "Onemogočate prenos za",
"You are going to enable comments to": "Omogočate komentarje za",
+25
View File
@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "MEDYAYI SİL",
"DOWNLOAD": "İNDİR",
"DURATION": "SÜRE",
"Delete Comments": "",
"Delete Media": "Medyayı Sil",
"Delete media": "Medyayı sil",
"Disable Comments": "Yorumları Devre Dışı Bırak",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "Sahip değiştirilemedi. Lütfen tekrar deneyin.",
"Failed to copy media.": "Medya kopyalanamadı.",
"Failed to create playlist": "Çalma listesi oluşturulamadı",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "Medya silinemedi. Lütfen tekrar deneyin.",
"Failed to disable comments.": "Yorumlar devre dışı bırakılamadı.",
"Failed to disable download.": "İndirme devre dışı bırakılamadı.",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "Mevcut kullanıcıları filtrele...",
"Filter playlists...": "Çalma listelerini filtrele...",
"Filters": "Filtreler",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "Git",
"History": "Geçmiş",
"Home": "Ana Sayfa",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "Yorumları yönet",
"Manage media": "Medyayı yönet",
"Manage users": "Kullanıcıları yönet",
"Management": "",
"Media": "Medya",
"Media I own": "Sahip olduğum medya",
"Media was edited": "Medya düzenlendi",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "Sonuç bulunamadı",
"No tags": "Etiket yok",
"No users to add": "Eklenecek kullanıcı yok",
"Organization": "",
"PLAYLISTS": "ÇALMA LİSTELERİ",
"PUBLISH STATE": "YAYINLANMA DURUMU",
"Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "Yayınlanma tarihi",
"Recent uploads": "Son yüklemeler",
"Recommended": "Önerilen",
"Record": "",
"Record Screen": "Ekranı Kaydet",
"Record Screen with Audio": "",
"Register": "Kayıt Ol",
"Remove category": "Kategoriyi kaldır",
"Remove from list": "Listeden kaldır",
"Remove tag": "Etiketi kaldır",
"Remove user": "Kullanıcıyı kaldır",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "",
"SAVE": "KAYDET",
"SEARCH": "ARA",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "Tüm medyayı seç",
"Select publish state:": "Yayınlanma durumunu seç:",
"Selected": "Seçildi",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "Paylaştıklarım",
"Shared with me": "Benimle paylaşılanlar",
"Sharing": "",
"Sign in": "Giriş Yap",
"Sign out": "Çıkış Yap",
"Sort By": "Sırala",
"Start Recording": "Kaydı Başlat",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Medya yüklemeye ve çalışmanızı paylaşmaya başlayın. Yüklediğiniz medya burada görünecektir.",
"Stop Recording": "Kaydı Durdur",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "Gönder",
"Subtitle was added": "Alt yazı eklendi",
"Subtitles": "Altyazılar",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "Yorumlar başarıyla etkinleştirildi",
"Successfully changed owner": "Sahip başarıyla değiştirildi",
"Successfully deleted": "Başarıyla silindi",
"Successfully deleted comments": "",
"Successfully updated": "Başarıyla güncellendi",
"Successfully updated categories": "Kategoriler başarıyla güncellendi",
"Successfully updated playlist membership": "Çalma listesi üyeliği başarıyla güncellendi",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "Medya yükle",
"Uploads": "Yüklemeler",
"Users": "Kullanıcılar",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "HEPSİNİ GÖR",
"Video": "Video",
"View all": "Hepsini gör",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "Hoş geldiniz",
"You are going to copy": "Kopyalayacaksınız",
"You are going to delete": "Sileceksiniz",
"You are going to delete all comments from": "",
"You are going to disable comments to": "Yorumları devre dışı bırakacaksınız",
"You are going to disable download for": "İndirmeyi devre dışı bırakacaksınız",
"You are going to enable comments to": "Yorumları etkinleştireceksiniz",
+25
View File
@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "میڈیا حذف کریں",
"DOWNLOAD": "ڈاؤن لوڈ",
"DURATION": "دورانیہ",
"Delete Comments": "",
"Delete Media": "میڈیا حذف کریں",
"Delete media": "میڈیا حذف کریں",
"Disable Comments": "تبصرے غیر فعال کریں",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "مالک تبدیل کرنے میں ناکام۔ براہ کرم دوبارہ کوشش کریں۔",
"Failed to copy media.": "میڈیا کاپی کرنے میں ناکام۔",
"Failed to create playlist": "پلے لسٹ بنانے میں ناکام",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "میڈیا حذف کرنے میں ناکام۔ براہ کرم دوبارہ کوشش کریں۔",
"Failed to disable comments.": "تبصرے غیر فعال کرنے میں ناکام۔",
"Failed to disable download.": "ڈاؤن لوڈ غیر فعال کرنے میں ناکام۔",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "موجودہ صارفین فلٹر کریں...",
"Filter playlists...": "پلے لسٹس فلٹر کریں...",
"Filters": "فلٹرز",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "جائیں",
"History": "تاریخ",
"Home": "ہوم",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "تبصرے منظم کریں",
"Manage media": "میڈیا منظم کریں",
"Manage users": "صارفین منظم کریں",
"Management": "",
"Media": "میڈیا",
"Media I own": "",
"Media was edited": "میڈیا ترمیم کیا گیا",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "کے لئے کوئی نتائج نہیں",
"No tags": "کوئی ٹیگز نہیں",
"No users to add": "شامل کرنے کے لیے کوئی صارف نہیں",
"Organization": "",
"PLAYLISTS": "پلے لسٹس",
"PUBLISH STATE": "اشاعت کی حالت",
"Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "پر شائع ہوا",
"Recent uploads": "حالیہ اپ لوڈز",
"Recommended": "تجویز کردہ",
"Record": "",
"Record Screen": "اسکرین ریکارڈ کریں",
"Record Screen with Audio": "",
"Register": "رجسٹر کریں",
"Remove category": "قسم ہٹائیں",
"Remove from list": "فہرست سے ہٹائیں",
"Remove tag": "ٹیگ ہٹائیں",
"Remove user": "صارف ہٹائیں",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "",
"SAVE": "محفوظ کریں",
"SEARCH": "تلاش کریں",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "تمام میڈیا منتخب کریں",
"Select publish state:": "اشاعت کی حالت منتخب کریں:",
"Selected": "منتخب شدہ",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "میری طرف سے شیئر کیا گیا",
"Shared with me": "میرے ساتھ شیئر کیا گیا",
"Sharing": "",
"Sign in": "سائن ان کریں",
"Sign out": "سائن آؤٹ کریں",
"Sort By": "ترتیب دیں",
"Start Recording": "ریکارڈنگ شروع کریں",
"Start uploading media and sharing your work. Media that you upload will show up here.": "میڈیا اپ لوڈ کرنا اور اپنا کام شیئر کرنا شروع کریں۔ آپ جو میڈیا اپ لوڈ کرتے ہیں وہ یہاں ظاہر ہوگا۔",
"Stop Recording": "ریکارڈنگ روکیں",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "جمع کرائیں",
"Subtitle was added": "سب ٹائٹل شامل کیا گیا",
"Subtitles": "سب ٹائٹلز",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "تبصرے کامیابی سے فعال ہو گئے",
"Successfully changed owner": "مالک کامیابی سے تبدیل ہو گیا",
"Successfully deleted": "کامیابی سے حذف ہو گیا",
"Successfully deleted comments": "",
"Successfully updated": "کامیابی سے اپ ڈیٹ ہو گیا",
"Successfully updated categories": "اقسام کامیابی سے اپ ڈیٹ ہو گئیں",
"Successfully updated playlist membership": "پلے لسٹ ممبرشپ کامیابی سے اپ ڈیٹ ہو گئی",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "میڈیا اپ لوڈ کریں",
"Uploads": "اپ لوڈز",
"Users": "صارفین",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "سب دیکھیں",
"Video": "ویڈیو",
"View all": "سب دیکھیں",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "خوش آمدید",
"You are going to copy": "آپ کاپی کرنے جا رہے ہیں",
"You are going to delete": "آپ حذف کرنے جا رہے ہیں",
"You are going to delete all comments from": "",
"You are going to disable comments to": "آپ تبصرے غیر فعال کرنے جا رہے ہیں",
"You are going to disable download for": "آپ ڈاؤن لوڈ غیر فعال کرنے جا رہے ہیں",
"You are going to enable comments to": "آپ تبصرے فعال کرنے جا رہے ہیں",
+25
View File
@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "删除媒体",
"DOWNLOAD": "下载",
"DURATION": "时长",
"Delete Comments": "",
"Delete Media": "",
"Delete media": "删除媒体",
"Disable Comments": "",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "",
"Failed to copy media.": "复制媒体失败。",
"Failed to create playlist": "",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "删除媒体失败。请重试。",
"Failed to disable comments.": "禁用评论失败。",
"Failed to disable download.": "禁用下载失败。",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "",
"Filter playlists...": "",
"Filters": "筛选",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "",
"History": "历史",
"Home": "主页",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "管理评论",
"Manage media": "管理媒体",
"Manage users": "管理用户",
"Management": "",
"Media": "媒体",
"Media I own": "",
"Media was edited": "媒体已编辑",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "没有结果",
"No tags": "",
"No users to add": "",
"Organization": "",
"PLAYLISTS": "播放列表",
"PUBLISH STATE": "发布状态",
"Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "发布于",
"Recent uploads": "最近上传",
"Recommended": "推荐",
"Record": "",
"Record Screen": "录制屏幕",
"Record Screen with Audio": "",
"Register": "注册",
"Remove category": "",
"Remove from list": "",
"Remove tag": "",
"Remove user": "",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "",
"SAVE": "保存",
"SEARCH": "搜索",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "",
"Select publish state:": "",
"Selected": "",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "我分享的",
"Shared with me": "分享给我的",
"Sharing": "",
"Sign in": "登录",
"Sign out": "登出",
"Sort By": "排序方式",
"Start Recording": "开始录制",
"Start uploading media and sharing your work. Media that you upload will show up here.": "开始上传媒体并分享您的作品。您上传的媒体将显示在这里。",
"Stop Recording": "停止录制",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "",
"Subtitle was added": "字幕已添加",
"Subtitles": "字幕",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "评论已成功启用",
"Successfully changed owner": "",
"Successfully deleted": "删除成功",
"Successfully deleted comments": "",
"Successfully updated": "",
"Successfully updated categories": "",
"Successfully updated playlist membership": "",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "上传媒体",
"Uploads": "上传",
"Users": "",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "查看全部",
"Video": "视频",
"View all": "查看全部",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "欢迎",
"You are going to copy": "您将复制",
"You are going to delete": "您将删除",
"You are going to delete all comments from": "",
"You are going to disable comments to": "您将禁用评论",
"You are going to disable download for": "您将禁用下载",
"You are going to enable comments to": "您将启用评论",
+25
View File
@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "刪除影片",
"DOWNLOAD": "下載",
"DURATION": "時長",
"Delete Comments": "",
"Delete Media": "",
"Delete media": "刪除媒體",
"Disable Comments": "",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "",
"Failed to copy media.": "複製媒體失敗。",
"Failed to create playlist": "",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "刪除媒體失敗。請再試一次。",
"Failed to disable comments.": "停用留言失敗。",
"Failed to disable download.": "停用下載失敗。",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "",
"Filter playlists...": "",
"Filters": "篩選器",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "執行",
"History": "觀看紀錄",
"Home": "首頁",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "留言管理",
"Manage media": "媒體管理",
"Manage users": "使用者管理",
"Management": "",
"Media": "媒體",
"Media I own": "",
"Media was edited": "媒體已更新",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "查無相關結果:",
"No tags": "",
"No users to add": "",
"Organization": "",
"PLAYLISTS": "播放清單",
"PUBLISH STATE": "發布狀態",
"Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "發布日期為",
"Recent uploads": "最近上傳",
"Recommended": "推薦內容",
"Record": "",
"Record Screen": "螢幕錄製",
"Record Screen with Audio": "",
"Register": "註冊",
"Remove category": "",
"Remove from list": "",
"Remove tag": "",
"Remove user": "",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "",
"SAVE": "儲存",
"SEARCH": "搜尋",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "",
"Select publish state:": "",
"Selected": "",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "我分享的",
"Shared with me": "與我分享",
"Sharing": "",
"Sign in": "登入",
"Sign out": "登出",
"Sort By": "排序方式",
"Start Recording": "開始錄製",
"Start uploading media and sharing your work. Media that you upload will show up here.": "開始上傳媒體並分享您的作品。您上傳的媒體將顯示在此處。",
"Stop Recording": "停止錄製",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "",
"Subtitle was added": "字幕已新增",
"Subtitles": "字幕",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "成功啟用留言",
"Successfully changed owner": "",
"Successfully deleted": "成功刪除",
"Successfully deleted comments": "",
"Successfully updated": "",
"Successfully updated categories": "",
"Successfully updated playlist membership": "",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "上傳媒體",
"Uploads": "上傳內容",
"Users": "",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "查看全部",
"Video": "影片",
"View all": "瀏覽全部",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "歡迎",
"You are going to copy": "您即將複製",
"You are going to delete": "您即將刪除",
"You are going to delete all comments from": "",
"You are going to disable comments to": "您即將停用留言",
"You are going to disable download for": "您即將停用下載",
"You are going to enable comments to": "您即將啟用留言",
+10
View File
@@ -965,3 +965,13 @@ def get_alphanumeric_only(string):
"""
string = "".join([char for char in string if char.isalnum()])
return string.lower()
def get_alphanumeric_and_spaces(string):
"""Returns a query that contains only alphanumeric characters and spaces
This include characters other than the English alphabet too
"""
string = "".join([char for char in string if char.isalnum() or char.isspace()])
# Replace multiple spaces with single space and strip
string = " ".join(string.split())
return string
+4 -2
View File
@@ -120,9 +120,11 @@ class MediaList(APIView):
operation_description='Delete media for MediaCMS managers and reviewers',
)
def delete(self, request, format=None):
if not is_mediacms_manager(request.user):
return Response({"detail": "bad permissions"}, status=status.HTTP_403_FORBIDDEN)
tokens = request.GET.get("tokens")
if tokens:
tokens = tokens.split(",")
tokens = [t for t in tokens.split(",") if t][:50]
Media.objects.filter(friendly_token__in=tokens).delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -177,7 +179,7 @@ class CommentList(APIView):
def delete(self, request, format=None):
comment_ids = request.GET.get("comment_ids")
if comment_ids:
comments = comment_ids.split(",")
comments = [c for c in comment_ids.split(",") if c][:50]
Comment.objects.filter(uid__in=comments).delete()
return Response(status=status.HTTP_204_NO_CONTENT)
+15 -9
View File
@@ -238,6 +238,8 @@ def show_related_media(media, request=None, limit=100):
return show_related_media_calculated(media, request, limit)
elif settings.RELATED_MEDIA_STRATEGY == "author":
return show_related_media_author(media, request, limit)
elif settings.RELATED_MEDIA_STRATEGY == "no_related":
return []
return show_related_media_content(media, request, limit)
@@ -451,17 +453,21 @@ def kill_ffmpeg_process(filepath):
filepath: Path to the file being processed by ffmpeg
Returns:
subprocess.CompletedProcess: Result of the kill command
bool: True if the lookup ran, False if input was unusable
"""
if not filepath:
if not filepath or not isinstance(filepath, str):
return False
cmd = "ps aux|grep 'ffmpeg'|grep %s|grep -v grep |awk '{print $2}'" % filepath
result = subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
pid = result.stdout.decode("utf-8").strip()
if pid:
cmd = "kill -9 %s" % pid
result = subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
return result
try:
ps = subprocess.run(["ps", "aux"], stdout=subprocess.PIPE, check=False)
except OSError:
return False
for line in ps.stdout.decode("utf-8", "replace").splitlines():
if "ffmpeg" not in line or filepath not in line or "grep" in line:
continue
parts = line.split()
if len(parts) > 1 and parts[1].isdigit():
subprocess.run(["kill", "-9", parts[1]], check=False)
return True
def copy_video(original_media, copy_encodings=True, title_suffix="(Trimmed)"):
@@ -0,0 +1,22 @@
# Generated by Django 5.2.6 on 2025-12-29 16:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('files', '0014_alter_subtitle_options_and_more'),
]
operations = [
migrations.AddField(
model_name='category',
name='is_lms_course',
field=models.BooleanField(db_index=True, default=False, help_text='Whether this category represents an LMS course'),
),
migrations.AddField(
model_name='category',
name='lti_context_id',
field=models.CharField(blank=True, db_index=True, help_text='LTI context ID from platform', max_length=255),
),
]
@@ -0,0 +1,21 @@
# Generated by Django 5.2.6 on 2025-12-29 16:15
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('files', '0015_category_is_lms_course_category_lti_context_id'),
('lti', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='category',
name='lti_platform',
field=models.ForeignKey(
blank=True, help_text='LTI Platform if this is an LTI course', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='categories', to='lti.ltiplatform'
),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.2.6 on 2026-04-21 15:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('files', '0016_category_lti_platform'),
]
operations = [
migrations.AddField(
model_name='mediapermission',
name='source',
field=models.CharField(choices=[('lti_embed', 'LTI Embed'), ('explicit', 'Explicit')], default='explicit', max_length=32),
),
]
+26
View File
@@ -0,0 +1,26 @@
# Generated by Django 5.2.6 on 2026-04-27 17:43
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('files', '0017_mediapermission_source'),
]
operations = [
migrations.CreateModel(
name='EmbedMediaCourse',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='embedded_media', to='files.category')),
('media', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='embed_courses', to='files.media')),
],
options={
'unique_together': {('media', 'category')},
},
),
]
+1 -1
View File
@@ -3,7 +3,7 @@ from .category import Category, Tag # noqa: F401
from .comment import Comment # noqa: F401
from .encoding import EncodeProfile, Encoding # noqa: F401
from .license import License # noqa: F401
from .media import Media, MediaPermission # noqa: F401
from .media import EmbedMediaCourse, Media, MediaPermission # noqa: F401
from .page import Page, TinyMCEMedia # noqa: F401
from .playlist import Playlist, PlaylistMedia # noqa: F401
from .rating import Rating, RatingCategory # noqa: F401
+8 -1
View File
@@ -47,6 +47,13 @@ class Category(models.Model):
verbose_name='IDP Config Name',
)
# LTI/LMS integration fields
is_lms_course = models.BooleanField(default=False, db_index=True, help_text='Whether this category represents an LMS course')
lti_platform = models.ForeignKey('lti.LTIPlatform', blank=True, null=True, on_delete=models.SET_NULL, related_name='categories', help_text='LTI Platform if this is an LTI course')
lti_context_id = models.CharField(max_length=255, blank=True, db_index=True, help_text='LTI context ID from platform')
def __str__(self):
return self.title
@@ -137,7 +144,7 @@ class Tag(models.Model):
return True
def save(self, *args, **kwargs):
self.title = helpers.get_alphanumeric_only(self.title)
self.title = helpers.get_alphanumeric_and_spaces(self.title)
self.title = self.title[:100]
super(Tag, self).save(*args, **kwargs)
-4
View File
@@ -6,7 +6,6 @@ from django.core.files import File
from django.db import models
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from django.urls import reverse
from .. import helpers
from .utils import (
@@ -136,9 +135,6 @@ class Encoding(models.Model):
def __str__(self):
return f"{self.profile.name}-{self.media.title}"
def get_absolute_url(self):
return reverse("api_get_encoding", kwargs={"encoding_id": self.id})
@receiver(post_save, sender=Encoding)
def encoding_file_save(sender, instance, created, **kwargs):
+45 -26
View File
@@ -10,7 +10,7 @@ from django.conf import settings
from django.contrib.postgres.indexes import GinIndex
from django.contrib.postgres.search import SearchVectorField
from django.core.files import File
from django.db import models
from django.db import models, transaction
from django.db.models import Func, Value
from django.db.models.signals import m2m_changed, post_delete, post_save, pre_delete
from django.dispatch import receiver
@@ -352,20 +352,11 @@ class Media(models.Model):
# first get anything interesting out of the media
# that needs to be search able
a_tags = b_tags = ""
a_tags = ""
if self.id:
a_tags = " ".join([tag.title for tag in self.tags.all()])
b_tags = " ".join([tag.title.replace("-", " ") for tag in self.tags.all()])
items = [
self.title,
self.user.username,
self.user.email,
self.user.name,
self.description,
a_tags,
b_tags,
]
items = [self.friendly_token, self.title, self.user.username, self.user.email, self.user.name, self.description, a_tags]
for subtitle in self.subtitles.all():
items.append(subtitle.subtitle_text)
@@ -545,7 +536,9 @@ class Media(models.Model):
from .. import tasks
tasks.produce_sprite_from_video.delay(self.friendly_token)
# Defer until the surrounding transaction commits so the worker can
# actually find the Media row. Runs immediately if not in a tx.
transaction.on_commit(lambda token=self.friendly_token: tasks.produce_sprite_from_video.delay(token))
return True
def encode(self, profiles=[], force=True, chunkize=True):
@@ -568,9 +561,8 @@ class Media(models.Model):
profiles.remove(profile)
encoding = Encoding(media=self, profile=profile)
encoding.save()
enc_url = settings.SSL_FRONTEND_HOST + encoding.get_absolute_url()
tasks.encode_media.apply_async(
args=[self.friendly_token, profile.id, encoding.id, enc_url],
args=[self.friendly_token, profile.id, encoding.id],
kwargs={"force": force},
priority=0,
)
@@ -584,13 +576,12 @@ class Media(models.Model):
continue
encoding = Encoding(media=self, profile=profile)
encoding.save()
enc_url = settings.SSL_FRONTEND_HOST + encoding.get_absolute_url()
if profile.resolution in settings.MINIMUM_RESOLUTIONS_TO_ENCODE:
priority = 9
else:
priority = 0
tasks.encode_media.apply_async(
args=[self.friendly_token, profile.id, encoding.id, enc_url],
args=[self.friendly_token, profile.id, encoding.id],
kwargs={"force": force},
priority=priority,
)
@@ -739,15 +730,6 @@ class Media(models.Model):
ep["updated_time"] = encoding.update_date
return ep
@property
def categories_info(self):
"""Property used on serializers"""
ret = []
for cat in self.category.all():
ret.append({"title": cat.title, "url": cat.get_absolute_url()})
return ret
@property
def tags_info(self):
"""Property used on serializers"""
@@ -974,6 +956,12 @@ class Media(models.Model):
return chapter_data.chapter_data
return data
@property
def is_shared(self):
if not self.pk:
return False
return self.permissions.exists() or self.category.filter(is_rbac_category=True).exists()
class MediaPermission(models.Model):
"""Model to store user permissions for media"""
@@ -984,10 +972,18 @@ class MediaPermission(models.Model):
("owner", "Owner"),
)
SOURCE_LTI_EMBED = 'lti_embed'
SOURCE_EXPLICIT = 'explicit'
SOURCE_CHOICES = (
(SOURCE_LTI_EMBED, 'LTI Embed'),
(SOURCE_EXPLICIT, 'Explicit'),
)
owner_user = models.ForeignKey('users.User', on_delete=models.CASCADE, related_name='granted_permissions')
user = models.ForeignKey('users.User', on_delete=models.CASCADE)
media = models.ForeignKey('Media', on_delete=models.CASCADE, related_name='permissions')
permission = models.CharField(max_length=20, choices=PERMISSION_CHOICES)
source = models.CharField(max_length=32, choices=SOURCE_CHOICES, default=SOURCE_EXPLICIT)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
@@ -997,6 +993,29 @@ class MediaPermission(models.Model):
return f"{self.user.username} - {self.media.title} ({self.permission})"
class EmbedMediaCourse(models.Model):
"""
Records that a user shared a media item into a course during an LTI session.
This is a pure audit/tracking table used by the course cleanup bulk action to
identify which MediaPermission records were created via LTI embedding and should
be removed when the course is cleaned up.
It does NOT add the media to the category (Media.category M2M is untouched),
so no m2m_changed signals fire and no category counts are affected.
"""
media = models.ForeignKey('Media', on_delete=models.CASCADE, related_name='embed_courses')
category = models.ForeignKey('Category', on_delete=models.CASCADE, related_name='embedded_media')
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ('media', 'category')
def __str__(self):
return f"{self.media.title} in {self.category.title}"
@receiver(post_save, sender=Media)
def media_save(sender, instance, created, **kwargs):
# media_file path is not set correctly until mode is saved
+15 -6
View File
@@ -18,13 +18,22 @@ class VideoChapterData(models.Model):
data = []
if self.data and isinstance(self.data, list):
for item in self.data:
if item.get("startTime") and item.get("endTime") and item.get("chapterTitle"):
chapter_item = {
'startTime': item.get("startTime"),
'endTime': item.get("endTime"),
'chapterTitle': item.get("chapterTitle"),
if not isinstance(item, dict):
continue
start_time = item.get("startTime")
end_time = item.get("endTime")
chapter_title = item.get("chapterTitle")
if start_time is None or end_time is None or not chapter_title:
continue
if not isinstance(start_time, (int, float, str)) or not isinstance(end_time, (int, float, str)):
continue
data.append(
{
'startTime': start_time,
'endTime': end_time,
'chapterTitle': chapter_title,
}
data.append(chapter_item)
)
return data
+23 -2
View File
@@ -98,7 +98,27 @@ class MediaSerializer(serializers.ModelSerializer):
self.fields['category'].queryset = non_rbac_categories.union(rbac_categories)
class SingleMediaSerializer(serializers.ModelSerializer):
class CategoriesInfoMixin(serializers.Serializer):
categories_info = serializers.SerializerMethodField()
def get_categories_info(self, obj):
request = self.context.get("request")
user = getattr(request, "user", None)
if user and user.is_authenticated:
accessible_rbac = set(user.get_rbac_categories_as_member().values_list("pk", flat=True))
else:
accessible_rbac = set()
ret = []
for cat in obj.category.all():
if cat.is_rbac_category and cat.pk not in accessible_rbac:
continue
ret.append({"title": cat.title, "url": cat.get_absolute_url(), "is_lms_course": cat.is_lms_course})
return ret
class SingleMediaSerializer(CategoriesInfoMixin, serializers.ModelSerializer):
user = serializers.ReadOnlyField(source="user.username")
url = serializers.SerializerMethodField()
is_shared = serializers.SerializerMethodField()
@@ -177,7 +197,7 @@ class SingleMediaSerializer(serializers.ModelSerializer):
)
class MediaSearchSerializer(serializers.ModelSerializer):
class MediaSearchSerializer(CategoriesInfoMixin, serializers.ModelSerializer):
url = serializers.SerializerMethodField()
api_url = serializers.SerializerMethodField()
@@ -226,6 +246,7 @@ class CategorySerializer(serializers.ModelSerializer):
"media_count",
"user",
"thumbnail_url",
"is_lms_course",
)
+2 -5
View File
@@ -171,8 +171,7 @@ def chunkize_media(self, friendly_token, profiles, force=True):
continue
encoding = Encoding(media=media, profile=profile)
encoding.save()
enc_url = settings.SSL_FRONTEND_HOST + encoding.get_absolute_url()
encode_media.delay(friendly_token, profile.id, encoding.id, enc_url, force=force)
encode_media.delay(friendly_token, profile.id, encoding.id, force=force)
return False
chunks = [os.path.join(cwd, ch) for ch in chunks]
@@ -202,13 +201,12 @@ def chunkize_media(self, friendly_token, profiles, force=True):
)
encoding.save()
enc_url = settings.SSL_FRONTEND_HOST + encoding.get_absolute_url()
if profile.resolution in settings.MINIMUM_RESOLUTIONS_TO_ENCODE:
priority = 0
else:
priority = 9
encode_media.apply_async(
args=[friendly_token, profile.id, encoding.id, enc_url],
args=[friendly_token, profile.id, encoding.id],
kwargs={"force": force, "chunk": True, "chunk_file_path": chunk},
priority=priority,
)
@@ -246,7 +244,6 @@ def encode_media(
friendly_token,
profile_id,
encoding_id,
encoding_url,
force=True,
chunk=False,
chunk_file_path="",
+6 -5
View File
@@ -61,12 +61,11 @@ urlpatterns = [
views.MediaDetail.as_view(),
name="api_get_media",
),
re_path(
r"^api/v1/media/encoding/(?P<encoding_id>[\w]*)$",
views.EncodingDetail.as_view(),
name="api_get_encoding",
),
re_path(r"^api/v1/search$", views.MediaSearch.as_view()),
re_path(
rf"^api/v1/media/{friendly_token}/share$",
views.media_share,
),
re_path(
rf"^api/v1/media/{friendly_token}/actions$",
views.MediaActions.as_view(),
@@ -80,6 +79,7 @@ urlpatterns = [
views.trim_video,
),
re_path(r"^api/v1/categories$", views.CategoryList.as_view()),
re_path(r"^api/v1/categories/contributor$", views.CategoryListContributor.as_view()),
re_path(r"^api/v1/tags$", views.TagList.as_view()),
re_path(r"^api/v1/comments$", views.CommentList.as_view()),
re_path(
@@ -106,6 +106,7 @@ urlpatterns = [
re_path(r"^api/v1/tasks$", views.TasksList.as_view()),
re_path(r"^api/v1/tasks/$", views.TasksList.as_view()),
re_path(r"^api/v1/tasks/(?P<friendly_token>[\w|\W]*)$", views.TaskDetail.as_view()),
re_path(r"^api/v1/media-auth$", views.media_auth, name="media_auth"),
re_path(r"^manage/comments$", views.manage_comments, name="manage_comments"),
re_path(r"^manage/media$", views.manage_media, name="manage_media"),
re_path(r"^manage/users$", views.manage_users, name="manage_users"),
+4 -2
View File
@@ -1,14 +1,16 @@
# Import all views for backward compatibility
from .auth import custom_login_view, saml_metadata # noqa: F401
from .categories import CategoryList, TagList # noqa: F401
from .categories import CategoryList, CategoryListContributor, TagList # noqa: F401
from .comments import CommentDetail, CommentList # noqa: F401
from .encoding import EncodeProfileList, EncodingDetail # noqa: F401
from .encoding import EncodeProfileList # noqa: F401
from .media import MediaActions # noqa: F401
from .media import MediaBulkUserActions # noqa: F401
from .media import MediaDetail # noqa: F401
from .media import MediaList # noqa: F401
from .media import MediaSearch # noqa: F401
from .media import media_share # noqa: F401
from .media_auth import media_auth # noqa: F401
from .pages import about # noqa: F401
from .pages import add_subtitle # noqa: F401
from .pages import approval_required # noqa: F401
+29 -7
View File
@@ -23,17 +23,18 @@ class CategoryList(APIView):
},
)
def get(self, request, format=None):
base_filters = {}
if not is_mediacms_editor(request.user):
base_filters = {"is_rbac_category": False}
base_queryset = Category.objects.prefetch_related("user")
categories = base_queryset.filter(**base_filters)
show_lms = getattr(settings, 'SHOW_LMS_COURSES_IN_CATEGORIES', True)
categories = Category.objects.prefetch_related("user")
if not show_lms:
categories = categories.filter(is_lms_course=False)
if not is_mediacms_editor(request.user):
categories = categories.filter(is_rbac_category=False)
if getattr(settings, 'USE_RBAC', False) and request.user.is_authenticated:
rbac_categories = request.user.get_rbac_categories_as_member()
if not show_lms:
rbac_categories = rbac_categories.filter(is_lms_course=False)
categories = categories.union(rbac_categories)
categories = categories.order_by("title")
@@ -43,6 +44,27 @@ class CategoryList(APIView):
return Response(ret)
class CategoryListContributor(APIView):
"""List LMS courses where the user has contributor access"""
@swagger_auto_schema(
tags=['Categories'],
operation_summary='Lists LMS courses for Contributors',
operation_description='Lists LMS courses where the user has contributor access',
responses={
200: openapi.Response('response description', CategorySerializer),
},
)
def get(self, request, format=None):
if not request.user.is_authenticated:
return Response([])
categories = request.user.get_rbac_categories_as_contributor().filter(is_lms_course=True)
serializer = CategorySerializer(categories.order_by("title"), many=True, context={"request": request})
return Response(serializer.data)
class TagList(APIView):
"""List tags"""
+6 -3
View File
@@ -75,8 +75,11 @@ class CommentDetail(APIView):
try:
media = Media.objects.select_related("user").get(friendly_token=friendly_token)
self.check_object_permissions(self.request, media)
if media.state == "private" and self.request.user != media.user:
return Response({"detail": "media is private"}, status=status.HTTP_400_BAD_REQUEST)
if media.state == "private":
user = self.request.user
has_access = user.is_authenticated and (user.has_member_access_to_media(media) or is_mediacms_editor(user))
if not has_access:
return Response({"detail": "media is private"}, status=status.HTTP_400_BAD_REQUEST)
return media
except PermissionDenied:
return Response({"detail": "bad permissions"}, status=status.HTTP_400_BAD_REQUEST)
@@ -97,7 +100,7 @@ class CommentDetail(APIView):
media = self.get_object(friendly_token)
if isinstance(media, Response):
return media
comments = media.comments.filter().prefetch_related("user")
comments = media.comments.filter().prefetch_related("user").order_by("-add_date")
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
paginator = pagination_class()
page = paginator.paginate_queryset(comments, request)
+1 -158
View File
@@ -1,168 +1,11 @@
from django.conf import settings
from drf_yasg.utils import swagger_auto_schema
from rest_framework import permissions, status
from rest_framework.parsers import (
FileUploadParser,
FormParser,
JSONParser,
MultiPartParser,
)
from rest_framework.response import Response
from rest_framework.views import APIView
from ..helpers import produce_ffmpeg_commands
from ..models import EncodeProfile, Encoding
from ..models import EncodeProfile
from ..serializers import EncodeProfileSerializer
class EncodingDetail(APIView):
"""Experimental. This View is used by remote workers
Needs heavy testing and documentation.
"""
permission_classes = (permissions.IsAdminUser,)
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)
@swagger_auto_schema(auto_schema=None)
def post(self, request, encoding_id):
ret = {}
force = request.data.get("force", False)
task_id = request.data.get("task_id", False)
action = request.data.get("action", "")
chunk = request.data.get("chunk", False)
chunk_file_path = request.data.get("chunk_file_path", "")
encoding_status = request.data.get("status", "")
progress = request.data.get("progress", "")
commands = request.data.get("commands", "")
logs = request.data.get("logs", "")
retries = request.data.get("retries", "")
worker = request.data.get("worker", "")
temp_file = request.data.get("temp_file", "")
total_run_time = request.data.get("total_run_time", "")
if action == "start":
try:
encoding = Encoding.objects.get(id=encoding_id)
media = encoding.media
profile = encoding.profile
except BaseException:
Encoding.objects.filter(id=encoding_id).delete()
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
# TODO: break chunk True/False logic here
if (
Encoding.objects.filter(
media=media,
profile=profile,
chunk=chunk,
chunk_file_path=chunk_file_path,
).count()
> 1 # noqa
and force is False # noqa
):
Encoding.objects.filter(id=encoding_id).delete()
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
else:
Encoding.objects.filter(
media=media,
profile=profile,
chunk=chunk,
chunk_file_path=chunk_file_path,
).exclude(id=encoding.id).delete()
encoding.status = "running"
if task_id:
encoding.task_id = task_id
encoding.save()
if chunk:
original_media_path = chunk_file_path
original_media_md5sum = encoding.md5sum
original_media_url = settings.SSL_FRONTEND_HOST + encoding.media_chunk_url
else:
original_media_path = media.media_file.path
original_media_md5sum = media.md5sum
original_media_url = settings.SSL_FRONTEND_HOST + media.original_media_url
ret["original_media_url"] = original_media_url
ret["original_media_path"] = original_media_path
ret["original_media_md5sum"] = original_media_md5sum
# generating the commands here, and will replace these with temporary
# files created on the remote server
tf = "TEMP_FILE_REPLACE"
tfpass = "TEMP_FPASS_FILE_REPLACE"
ffmpeg_commands = produce_ffmpeg_commands(
original_media_path,
media.media_info,
resolution=profile.resolution,
codec=profile.codec,
output_filename=tf,
pass_file=tfpass,
chunk=chunk,
)
if not ffmpeg_commands:
encoding.delete()
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
ret["duration"] = media.duration
ret["ffmpeg_commands"] = ffmpeg_commands
ret["profile_extension"] = profile.extension
return Response(ret, status=status.HTTP_201_CREATED)
elif action == "update_fields":
try:
encoding = Encoding.objects.get(id=encoding_id)
except BaseException:
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
to_update = ["size", "update_date"]
if encoding_status:
encoding.status = encoding_status
to_update.append("status")
if progress:
encoding.progress = progress
to_update.append("progress")
if logs:
encoding.logs = logs
to_update.append("logs")
if commands:
encoding.commands = commands
to_update.append("commands")
if task_id:
encoding.task_id = task_id
to_update.append("task_id")
if total_run_time:
encoding.total_run_time = total_run_time
to_update.append("total_run_time")
if worker:
encoding.worker = worker
to_update.append("worker")
if temp_file:
encoding.temp_file = temp_file
to_update.append("temp_file")
if retries:
encoding.retries = retries
to_update.append("retries")
try:
encoding.save(update_fields=to_update)
except BaseException:
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
return Response({"status": "success"}, status=status.HTTP_201_CREATED)
@swagger_auto_schema(auto_schema=None)
def put(self, request, encoding_id, format=None):
encoding_file = request.data["file"]
encoding = Encoding.objects.filter(id=encoding_id).first()
if not encoding:
return Response(
{"detail": "encoding does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
encoding.media_file = encoding_file
encoding.save()
return Response({"detail": "ok"}, status=status.HTTP_201_CREATED)
class EncodeProfileList(APIView):
"""List encode profiles"""
+215 -23
View File
@@ -2,8 +2,11 @@ from datetime import datetime, timedelta
from django.conf import settings
from django.contrib.postgres.search import SearchQuery
from django.db.models import Count, Q
from django.db.models import Count, F, Prefetch, Q, prefetch_related_objects
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework import permissions, status
@@ -35,6 +38,8 @@ from ..methods import (
)
from ..models import (
Category,
Comment,
EmbedMediaCourse,
EncodeProfile,
Media,
MediaPermission,
@@ -112,6 +117,8 @@ class MediaList(APIView):
upload_date = params.get('upload_date', '').strip()
duration = params.get('duration', '').strip()
publish_state = params.get('publish_state', '').strip()
shared_user = params.get('shared_user', '').strip()
shared_group = params.get('shared_group', '').strip()
query = params.get("q", "").strip().lower()
parsed_combined = False
@@ -152,6 +159,7 @@ class MediaList(APIView):
gte = datetime(year, 1, 1)
already_sorted = False
include_sharing_info = False
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
if show_param == "recommended":
@@ -164,26 +172,41 @@ class MediaList(APIView):
if not self.request.user.is_authenticated:
media = Media.objects.none()
else:
media = Media.objects.filter(permissions__owner_user=self.request.user).prefetch_related("user", "tags").distinct()
base_queryset = Media.objects.prefetch_related("user", "tags")
conditions = Q(permissions__owner_user=self.request.user)
if getattr(settings, 'USE_RBAC', False):
rbac_categories = request.user.get_rbac_categories_as_contributor()
conditions |= Q(category__in=rbac_categories, user=self.request.user)
media = base_queryset.filter(conditions).distinct()
include_sharing_info = True
elif show_param == "shared_with_me":
if not self.request.user.is_authenticated:
media = Media.objects.none()
else:
base_queryset = Media.objects.prefetch_related("user", "tags")
# Build OR conditions similar to _get_media_queryset
conditions = Q(permissions__user=request.user)
exclude_lti_embed = request.GET.get('exclude_lti_embed') == '1'
# in LTI, if this is set, show only media that are shared explicitly with user
if exclude_lti_embed:
conditions = Q(permissions__user=request.user, permissions__source=MediaPermission.SOURCE_EXPLICIT)
else:
conditions = Q(permissions__user=request.user)
if getattr(settings, 'USE_RBAC', False):
rbac_categories = request.user.get_rbac_categories_as_member()
conditions |= Q(category__in=rbac_categories)
media = base_queryset.filter(conditions).distinct()
media = base_queryset.filter(conditions).exclude(user=request.user).distinct()
include_sharing_info = True
elif author_param:
user_queryset = User.objects.all()
user = get_object_or_404(user_queryset, username=author_param)
if self.request.user == user or is_mediacms_editor(self.request.user):
media = Media.objects.filter(user=user).prefetch_related("user", "tags")
if self.request.user == user:
include_sharing_info = True
else:
media = self._get_media_queryset(request, user)
already_sorted = True
@@ -234,6 +257,12 @@ class MediaList(APIView):
elif publish_state in ['private', 'public', 'unlisted']:
media = media.filter(state=publish_state)
if shared_user and include_sharing_info:
media = media.filter(permissions__user__username=shared_user).distinct()
if shared_group and include_sharing_info:
media = media.filter(category__is_rbac_category=True, category__rbac_groups__name=shared_group).distinct()
if not already_sorted:
media = media.order_by(f"{ordering}{sort_by}")
@@ -243,6 +272,16 @@ class MediaList(APIView):
page = paginator.paginate_queryset(media, request)
prefetch_related_objects(page, 'tags')
if include_sharing_info:
# this is the data for the Shared with me/by me pages on 'my media'
prefetch_related_objects(
page,
Prefetch('permissions', queryset=MediaPermission.objects.select_related('user').exclude(user=request.user)),
Prefetch('category', queryset=Category.objects.filter(is_rbac_category=True).prefetch_related('rbac_groups'), to_attr='rbac_categories_prefetched'),
)
serializer = MediaSerializer(page, many=True, context={"request": request})
tags_set = set()
@@ -253,6 +292,19 @@ class MediaList(APIView):
response = paginator.get_paginated_response(serializer.data)
response.data['tags'] = tags
if include_sharing_info:
shared_users = {}
shared_groups = {}
for media_obj in page:
for perm in media_obj.permissions.all():
shared_users[perm.user.username] = {"username": perm.user.username, "name": perm.user.name or perm.user.username}
for cat in getattr(media_obj, 'rbac_categories_prefetched', []):
for group in cat.rbac_groups.all():
shared_groups[group.name] = {"name": group.name}
response.data['shared_users'] = list(shared_users.values())
response.data['shared_groups'] = list(shared_groups.values())
return response
@swagger_auto_schema(
@@ -295,6 +347,7 @@ class MediaBulkUserActions(APIView):
enum=[
"enable_comments",
"disable_comments",
"delete_comments",
"delete_media",
"enable_download",
"disable_download",
@@ -313,6 +366,7 @@ class MediaBulkUserActions(APIView):
"remove_from_category",
"add_tags",
"remove_tags",
"course_cleanup",
],
),
'playlist_ids': openapi.Schema(
@@ -360,12 +414,15 @@ class MediaBulkUserActions(APIView):
media_ids = request.data.get('media_ids', [])
action = request.data.get('action')
if not media_ids:
return Response({"detail": "media_ids is required"}, status=status.HTTP_400_BAD_REQUEST)
if not action:
return Response({"detail": "action is required"}, status=status.HTTP_400_BAD_REQUEST)
if action == "course_cleanup":
return self._handle_course_cleanup(request, media_ids)
if not media_ids:
return Response({"detail": "media_ids is required"}, status=status.HTTP_400_BAD_REQUEST)
media = Media.objects.filter(user=request.user, friendly_token__in=media_ids)
if not media:
@@ -379,6 +436,10 @@ class MediaBulkUserActions(APIView):
media.update(enable_comments=False)
return Response({"detail": f"Comments disabled for {media.count()} media items"})
elif action == "delete_comments":
deleted_count, _ = Comment.objects.filter(media__in=media).delete()
return Response({"detail": f"{deleted_count} comments deleted"})
elif action == "delete_media":
count = media.count()
media.delete()
@@ -454,7 +515,16 @@ class MediaBulkUserActions(APIView):
m.save(update_fields=["state", "listable"])
return Response({"detail": f"State updated to {state} for {media.count()} media items"})
remove_sharing = request.data.get('remove_sharing', False)
if remove_sharing:
MediaPermission.objects.filter(media__in=media).delete()
for m in media:
rbac_cats = m.category.filter(is_rbac_category=True)
if rbac_cats.exists():
m.category.remove(*rbac_cats)
return Response({"detail": f"State updated to {state}"})
elif action == "change_owner":
owner = request.data.get('owner')
@@ -492,8 +562,9 @@ class MediaBulkUserActions(APIView):
users = (
MediaPermission.objects.filter(media__in=media, permission=ownership_type)
.exclude(user=request.user)
.values('user__name', 'user__username')
.annotate(media_count=Count('media', distinct=True))
.annotate(media_count=Count('media'))
.filter(media_count=media_count)
)
@@ -561,7 +632,13 @@ class MediaBulkUserActions(APIView):
elif action == "category_membership":
media_count = media.count()
results = list(Category.objects.filter(media__in=media).values('title', 'uid').annotate(media_count=Count('media', distinct=True)).filter(media_count=media_count))
# Categories where ALL selected media are members via the M2M relation
m2m_uids = set(Category.objects.filter(media__in=media).annotate(selected_count=Count('media', distinct=True)).filter(selected_count=media_count).values_list('uid', flat=True))
# Categories where ANY selected media has an EmbedMediaCourse record
embed_uids = set(EmbedMediaCourse.objects.filter(media__in=media).values_list('category__uid', flat=True))
results = list(Category.objects.filter(uid__in=m2m_uids | embed_uids).values('title', 'uid'))
return Response({'results': results})
@@ -574,12 +651,33 @@ class MediaBulkUserActions(APIView):
elif action == "add_to_category":
category_uids = request.data.get('category_uids', [])
if not category_uids:
return Response({"detail": "category_uids is required for add_to_category action"}, status=status.HTTP_400_BAD_REQUEST)
lti_context_id = request.data.get('lti_context_id')
if not category_uids and not lti_context_id:
return Response({"detail": "category_uids or lti_context_id is required for add_to_category action"}, status=status.HTTP_400_BAD_REQUEST)
categories = Category.objects.none()
# Prioritize category_uids
if category_uids:
requested = Category.objects.filter(uid__in=category_uids)
allowed_ids = [cat.id for cat in requested if not cat.is_rbac_category or request.user.has_contributor_access_to_category(cat)]
categories = Category.objects.filter(id__in=allowed_ids)
elif lti_context_id:
# Filter categories by lti_context_id and ensure they ARE RBAC categories
potential_categories = Category.objects.filter(lti_context_id=lti_context_id, is_rbac_category=True)
# Check user access (must have contributor access)
valid_category_ids = []
for cat in potential_categories:
if request.user.has_contributor_access_to_category(cat):
valid_category_ids.append(cat.id)
if valid_category_ids:
categories = Category.objects.filter(id__in=valid_category_ids)
categories = Category.objects.filter(uid__in=category_uids)
if not categories:
return Response({"detail": "No matching categories found"}, status=status.HTTP_400_BAD_REQUEST)
return Response({"detail": "No matching categories found or access denied"}, status=status.HTTP_400_BAD_REQUEST)
added_count = 0
for category in categories:
@@ -595,9 +693,11 @@ class MediaBulkUserActions(APIView):
if not category_uids:
return Response({"detail": "category_uids is required for remove_from_category action"}, status=status.HTTP_400_BAD_REQUEST)
categories = Category.objects.filter(uid__in=category_uids)
requested = Category.objects.filter(uid__in=category_uids)
allowed_ids = [cat.id for cat in requested if not cat.is_rbac_category or request.user.has_contributor_access_to_category(cat)]
categories = Category.objects.filter(id__in=allowed_ids)
if not categories:
return Response({"detail": "No matching categories found"}, status=status.HTTP_400_BAD_REQUEST)
return Response({"detail": "No matching categories found or access denied"}, status=status.HTTP_400_BAD_REQUEST)
removed_count = 0
for category in categories:
@@ -605,6 +705,9 @@ class MediaBulkUserActions(APIView):
if m.category.filter(uid=category.uid).exists():
m.category.remove(category)
removed_count += 1
EmbedMediaCourse.objects.filter(media=m, category=category).delete()
if not m.category.filter(is_rbac_category=True).exists() and not m.permissions.exclude(user=m.user).exists():
m.permissions.filter(user=m.user).delete()
return Response({"detail": f"Removed {removed_count} media items from {categories.count()} categories"})
@@ -647,6 +750,72 @@ class MediaBulkUserActions(APIView):
else:
return Response({"detail": f"Unknown action: {action}"}, status=status.HTTP_400_BAD_REQUEST)
def _handle_course_cleanup(self, request, media_ids):
category_uids = request.data.get('category_uids', [])
remove_permissions = request.data.get('remove_permissions', False)
remove_comments = request.data.get('remove_comments', False)
apply_to_all = request.data.get('apply_to_all', False)
if not category_uids:
return Response({"detail": "category_uids is required"}, status=status.HTTP_400_BAD_REQUEST)
categories = Category.objects.filter(uid__in=category_uids)
if not categories.exists():
return Response({"detail": "No matching categories found"}, status=status.HTTP_400_BAD_REQUEST)
valid_categories = [cat for cat in categories if request.user.has_contributor_access_to_category(cat)]
if not valid_categories:
return Response({"detail": "No contributor access to specified categories"}, status=status.HTTP_403_FORBIDDEN)
has_media = bool(media_ids)
selected_media = Media.objects.filter(user=request.user, friendly_token__in=media_ids) if has_media else Media.objects.none()
for category in valid_categories:
# All users who are members of any group linked to this category
group_users = User.objects.filter(rbac_groups__in=category.rbac_groups.all()).distinct()
# Get media explicitly embedded into this course via LTI
embed_qs = EmbedMediaCourse.objects.filter(category=category)
embedded_media_ids = list(embed_qs.values_list('media_id', flat=True))
all_course_media = Media.objects.filter(category=category)
if has_media:
selected_embedded = embed_qs.filter(media__in=selected_media)
selected_embedded_media_ids = list(selected_embedded.values_list('media_id', flat=True))
if remove_permissions:
MediaPermission.objects.filter(media__in=selected_media, user__in=group_users).exclude(user=F('media__user')).delete()
MediaPermission.objects.filter(media_id__in=selected_embedded_media_ids).exclude(user=F('media__user')).delete()
if remove_comments:
Comment.objects.filter(media__in=selected_media).delete()
if apply_to_all:
other_course_media = all_course_media.exclude(friendly_token__in=media_ids)
other_embedded = embed_qs.exclude(media__in=selected_media)
other_embedded_media_ids = list(other_embedded.values_list('media_id', flat=True))
if remove_permissions:
MediaPermission.objects.filter(media__in=other_course_media, user__in=group_users).exclude(user=F('media__user')).delete()
MediaPermission.objects.filter(media_id__in=other_embedded_media_ids).exclude(user=F('media__user')).delete()
if remove_comments:
Comment.objects.filter(media__in=other_course_media).delete()
for m in other_course_media:
m.category.remove(category)
for m in selected_media:
m.category.remove(category)
else:
if remove_permissions:
MediaPermission.objects.filter(media__in=all_course_media, user__in=group_users).exclude(user=F('media__user')).delete()
MediaPermission.objects.filter(media_id__in=embedded_media_ids).exclude(user=F('media__user')).delete()
if remove_comments:
Comment.objects.filter(media__in=all_course_media).delete()
if embedded_media_ids:
Comment.objects.filter(media_id__in=embedded_media_ids).delete()
for m in all_course_media:
m.category.remove(category)
return Response({"detail": "Course cleanup completed successfully"})
class MediaDetail(APIView):
"""
@@ -697,12 +866,9 @@ class MediaDetail(APIView):
return media
serializer = SingleMediaSerializer(media, context={"request": request})
if media.state == "private":
related_media = []
else:
related_media = show_related_media(media, request=request, limit=100)
related_media_serializer = MediaSerializer(related_media, many=True, context={"request": request})
related_media = related_media_serializer.data
related_media = show_related_media(media, request=request, limit=100)
related_media_serializer = MediaSerializer(related_media, many=True, context={"request": request})
related_media = related_media_serializer.data
ret = serializer.data
# update rattings info with user specific ratings
@@ -1073,3 +1239,29 @@ class MediaSearch(APIView):
page = paginator.paginate_queryset(media, request)
serializer = MediaSearchSerializer(page, many=True, context={"request": request})
return paginator.get_paginated_response(serializer.data)
@csrf_exempt
@require_POST
def media_share(request, friendly_token):
"""Mark a media item as shared when the owner embeds it via the LTI plugin."""
if not request.user.is_authenticated:
return HttpResponse(status=401)
media = get_object_or_404(Media, friendly_token=friendly_token)
if media.user != request.user:
return HttpResponse(status=403)
MediaPermission.objects.get_or_create(
media=media,
user=request.user,
defaults={'owner_user': request.user, 'permission': 'owner'},
)
courseid = request.POST.get('courseid')
if courseid:
category = Category.objects.filter(lti_context_id=str(courseid), is_rbac_category=True).first()
if category:
EmbedMediaCourse.objects.get_or_create(media=media, category=category)
return HttpResponse(status=200)
+125
View File
@@ -0,0 +1,125 @@
import re
from urllib.parse import unquote
from django.conf import settings
from django.core.cache import cache
from django.db.models import Q
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_GET
from ..methods import is_mediacms_editor
from ..models import Media
UID_RE = re.compile(r"[0-9a-f]{32}")
THUMBNAILS_PREFIX = "original/thumbnails/"
def _ttl():
return getattr(settings, "X_ACCEL_AUTH_CACHE_SECONDS", 300)
def _extract_uid(uri):
if not uri:
return None
match = UID_RE.search(uri)
return match.group(0) if match else None
def _relpath_from_uri(uri):
path = unquote(uri.split("?", 1)[0])
media_url = settings.MEDIA_URL
if path.startswith(media_url):
return path[len(media_url) :]
return None
def _lookup_uid_by_path(relpath):
path_key = f"xaccel:path:{relpath}"
cached = cache.get(path_key)
if cached is not None:
return cached or None
parts = relpath.split("/", 4)
if len(parts) < 5 or parts[2] != "user":
cache.set(path_key, "", _ttl())
return None
username = parts[3]
row = Media.objects.filter(user__username=username).filter(Q(uploaded_thumbnail=relpath) | Q(uploaded_poster=relpath)).values("uid").first()
uid_hex = row["uid"].hex if row else ""
cache.set(path_key, uid_hex, _ttl())
return uid_hex or None
def _lookup_state(uid):
"""Return (state, owner_id) for a uid, or (None, None) if missing.
Cached on uid alone since state/ownership do not depend on the requester.
Uses .values() rather than .only() because Media.__init__ touches deferred
file fields, which would otherwise recurse via refresh_from_db.
"""
state_key = f"xaccel:state:{uid}"
cached = cache.get(state_key)
if cached is not None:
return cached
row = Media.objects.filter(uid=uid).values("state", "user_id").first()
value = (row["state"], row["user_id"]) if row else (None, None)
cache.set(state_key, value, _ttl())
return value
def _decide(uid, user):
state, owner_id = _lookup_state(uid)
if state is None:
return False
if state in ("public", "unlisted"):
return True
# private
if not user.is_authenticated:
return False
if owner_id == user.id:
return True
if is_mediacms_editor(user):
return True
# RBAC / MediaPermission path needs a full Media instance.
try:
media = Media.objects.get(uid=uid)
except Media.DoesNotExist:
return False
return user.has_member_access_to_media(media)
@csrf_exempt
@require_GET
def media_auth(request):
"""Authorize a protected media request from nginx auth_request.
nginx passes the original request URI in the X-Original-URI header. The
Media.uid (32 hex chars, no dashes) is embedded somewhere in that URI for
every protected path. No uid => deny. Unknown uid => deny.
"""
if not getattr(settings, "USE_X_ACCEL_REDIRECT", True):
return HttpResponse(status=204)
uri = request.META.get("HTTP_X_ORIGINAL_URI", "")
uid = _extract_uid(uri)
if not uid:
# User-uploaded thumbnails/posters don't have the uid in the filename.
# Fall back to a per-path lookup, scoped to /original/thumbnails/.
relpath = _relpath_from_uri(uri)
if relpath and relpath.startswith(THUMBNAILS_PREFIX):
uid = _lookup_uid_by_path(relpath)
if not uid:
return HttpResponse(status=403)
user = request.user
cache_key = f"xaccel:auth:{uid}:{user.id if user.is_authenticated else 'anon'}"
cached = cache.get(cache_key)
if cached is None:
allowed = _decide(uid, user)
cache.set(cache_key, allowed, _ttl())
else:
allowed = cached
return HttpResponse(status=204 if allowed else 403)
+72 -19
View File
@@ -1,5 +1,6 @@
import json
import os
import re
from django.conf import settings
from django.contrib import messages
@@ -7,6 +8,7 @@ from django.contrib.auth.decorators import login_required
from django.core.mail import EmailMessage
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import render
from django.utils.html import mark_safe, strip_tags
from django.views.decorators.csrf import csrf_exempt
from cms.version import VERSION
@@ -24,7 +26,7 @@ from ..forms import (
WhisperSubtitlesForm,
)
from ..frontend_translations import translate_string
from ..helpers import get_alphanumeric_only
from ..helpers import get_alphanumeric_and_spaces
from ..methods import (
can_transcribe_video,
create_video_trim_request,
@@ -243,6 +245,31 @@ def history(request):
return render(request, "cms/history.html", context)
_TIMESTAMP_RE = re.compile(r'^(?:(\d+):)?([0-5]?\d):([0-5]?\d)(?:\.(\d{1,3}))?$')
def _timestamp_to_seconds(value):
"""Parse 'HH:MM:SS.mmm', 'MM:SS.mmm', etc., or a numeric value, into float seconds.
Returns None if the value can't be parsed.
"""
if isinstance(value, bool):
return None
if isinstance(value, (int, float)):
return float(value)
if not isinstance(value, str):
return None
match = _TIMESTAMP_RE.match(value.strip())
if not match:
return None
hours = int(match.group(1)) if match.group(1) else 0
minutes = int(match.group(2))
seconds = int(match.group(3))
millis_str = match.group(4) or '0'
millis = int(millis_str.ljust(3, '0'))
return hours * 3600 + minutes * 60 + seconds + millis / 1000.0
@csrf_exempt
@login_required
def video_chapters(request, friendly_token):
@@ -262,20 +289,39 @@ def video_chapters(request, friendly_token):
data = request_data.get("chapters")
if data is None:
return JsonResponse({'success': False, 'error': 'Request must contain "chapters" array'}, status=400)
if not isinstance(data, list):
return JsonResponse({'success': False, 'error': '"chapters" must be an array'}, status=400)
if len(data) > 200:
return JsonResponse({'success': False, 'error': 'Too many chapters (max 200)'}, status=400)
chapters = []
for _, chapter_data in enumerate(data):
start_time = chapter_data.get('startTime')
end_time = chapter_data.get('endTime')
for chapter_data in data:
if not isinstance(chapter_data, dict):
continue
raw_start = chapter_data.get('startTime')
raw_end = chapter_data.get('endTime')
chapter_title = chapter_data.get('chapterTitle')
if start_time and end_time and chapter_title:
chapters.append(
{
'startTime': start_time,
'endTime': end_time,
'chapterTitle': chapter_title,
}
)
start_seconds = _timestamp_to_seconds(raw_start)
end_seconds = _timestamp_to_seconds(raw_end)
if start_seconds is None or end_seconds is None:
continue
if start_seconds < 0 or end_seconds < 0 or start_seconds >= end_seconds:
continue
if not isinstance(chapter_title, str) or not chapter_title.strip():
continue
chapter_title = strip_tags(chapter_title).strip()[:500]
if not chapter_title:
continue
chapters.append(
{
'startTime': raw_start if isinstance(raw_start, str) else start_seconds,
'endTime': raw_end if isinstance(raw_end, str) else end_seconds,
'chapterTitle': chapter_title,
}
)
except Exception as e: # noqa
return JsonResponse({'success': False, 'error': 'Request data must be a list of video chapters with startTime, endTime, chapterTitle'}, status=400)
@@ -310,8 +356,8 @@ def edit_media(request):
media.tags.remove(tag)
if form.cleaned_data.get("new_tags"):
for tag in form.cleaned_data.get("new_tags").split(","):
tag = get_alphanumeric_only(tag)
tag = tag[:99]
tag = get_alphanumeric_and_spaces(tag)
tag = tag[:100]
if tag:
try:
tag = Tag.objects.get(title=tag)
@@ -350,13 +396,13 @@ def publish_media(request):
return HttpResponseRedirect(media.get_absolute_url())
if request.method == "POST":
form = MediaPublishForm(request.user, request.POST, request.FILES, instance=media)
form = MediaPublishForm(request.user, request.POST, request.FILES, instance=media, request=request)
if form.is_valid():
media = form.save()
messages.add_message(request, messages.INFO, translate_string(request.LANGUAGE_CODE, "Media was edited"))
return HttpResponseRedirect(media.get_absolute_url())
else:
form = MediaPublishForm(request.user, instance=media)
form = MediaPublishForm(request.user, instance=media, request=request)
return render(
request,
@@ -449,11 +495,18 @@ def edit_chapters(request):
if not (is_mediacms_editor(request.user) or request.user.has_contributor_access_to_media(media)):
return HttpResponseRedirect("/")
chapters = media.chapter_data
_html_escapes = str.maketrans({'<': r'\u003C', '>': r'\u003E', '&': r'\u0026'})
chapters_json = mark_safe(json.dumps(media.chapter_data).translate(_html_escapes))
return render(
request,
"cms/edit_chapters.html",
{"media_object": media, "add_subtitle_url": media.add_subtitle_url, "media_file_path": helpers.url_from_path(media.media_file.path), "media_id": media.friendly_token, "chapters": chapters},
{
"media_object": media,
"add_subtitle_url": media.add_subtitle_url,
"media_file_path": helpers.url_from_path(media.media_file.path),
"media_id": media.friendly_token,
"chapters": chapters_json,
},
)
@@ -547,7 +600,7 @@ def embed_media(request):
media = Media.objects.values("title").filter(friendly_token=friendly_token).first()
if not media:
return HttpResponseRedirect("/")
return HttpResponse('This media no longer exists', status=404, content_type='text/plain; charset=utf-8')
context = {}
context["media"] = friendly_token
+20 -4
View File
@@ -3,6 +3,8 @@ import json
from django import forms
from django.utils.safestring import mark_safe
from .models import Category
class CategoryModalWidget(forms.SelectMultiple):
"""Two-panel category selector with modal"""
@@ -12,28 +14,42 @@ class CategoryModalWidget(forms.SelectMultiple):
js = ('js/category_modal.js',)
def render(self, name, value, attrs=None, renderer=None):
is_lms_mode = getattr(self, 'is_lms_mode', False)
# Get all categories as JSON
categories = []
for opt_value, opt_label in self.choices:
if opt_value: # Skip empty choice
categories.append({'id': str(opt_value), 'title': str(opt_label)})
# Extract the actual ID value from ModelChoiceIteratorValue if needed
category_id = opt_value.value if hasattr(opt_value, 'value') else opt_value
# Get is_lms_course info from the Category object
try:
cat_obj = Category.objects.get(id=category_id)
categories.append({'id': str(category_id), 'title': str(opt_label), 'is_lms_course': cat_obj.is_lms_course})
except Category.DoesNotExist:
categories.append({'id': str(category_id), 'title': str(opt_label), 'is_lms_course': False})
all_categories_json = json.dumps(categories)
selected_ids_json = json.dumps([str(v) for v in (value or [])])
lms_mode_json = json.dumps(is_lms_mode)
search_placeholder = "Search courses..." if is_lms_mode else "Search categories..."
selected_header = "Selected Courses" if is_lms_mode else "Selected Categories"
html = f'''<div class="category-widget" data-name="{name}">
<div class="category-content">
<div class="category-panel">
<input type="text" class="category-search" placeholder="Search categories...">
<input type="text" class="category-search" placeholder="{search_placeholder}">
<div class="category-list scrollable" data-panel="left"></div>
</div>
<div class="category-panel">
<h3>Selected Categories</h3>
<h3>{selected_header}</h3>
<div class="category-list scrollable" data-panel="right"></div>
</div>
</div>
<div class="hidden-inputs"></div>
<script type="application/json" class="category-data">{{"all":{all_categories_json},"selected":{selected_ids_json}}}</script>
<script type="application/json" class="category-data">{{"all":{all_categories_json},"selected":{selected_ids_json},"lms_mode":{lms_mode_json}}}</script>
</div>'''
return mark_safe(html)
@@ -14,6 +14,7 @@ class EmbedInfoOverlay extends Component {
this.authorThumbnail = options.authorThumbnail || '';
this.videoTitle = options.videoTitle || 'Video';
this.videoUrl = options.videoUrl || '';
this.titleTarget = options.titleTarget || '_blank';
this.showTitle = options.showTitle !== undefined ? options.showTitle : true;
this.showRelated = options.showRelated !== undefined ? options.showRelated : true;
this.showUserAvatar = options.showUserAvatar !== undefined ? options.showUserAvatar : true;
@@ -140,7 +141,7 @@ class EmbedInfoOverlay extends Component {
if (this.videoUrl && this.linkTitle) {
const titleLink = document.createElement('a');
titleLink.href = this.videoUrl;
titleLink.target = '_blank';
titleLink.target = this.titleTarget;
titleLink.rel = 'noopener noreferrer';
titleLink.textContent = this.videoTitle;
titleLink.title = this.videoTitle;
@@ -223,7 +223,8 @@ function VideoJSPlayer({ videoId = 'default-video', showTitle = true, showRelate
const finalShowRelated = getOption(showRelated, 'showRelated', true);
const finalShowUserAvatar = getOption(showUserAvatar, 'showUserAvatar', true);
const finalLinkTitle = getOption(linkTitle, 'linkTitle', true);
const finalTimestamp = getOption(urlTimestamp, 'urlTimestamp', null);
// urlTimestamp: prop wins if set, otherwise fall back to window.MEDIA_DATA on both main and embed players
const finalTimestamp = urlTimestamp ?? mediaData?.urlTimestamp ?? null;
// Utility function to detect touch devices
const isTouchDevice = useMemo(() => {
@@ -2216,12 +2217,26 @@ function VideoJSPlayer({ videoId = 'default-video', showTitle = true, showRelate
// BEGIN: Add Embed Info Overlay Component (for embed player only)
if (isEmbedPlayer) {
let overlayVideoUrl = currentVideo.url;
let overlayTitleTarget = '_blank';
const parentMediaBase = mediaData?.parentMediaBase;
if (parentMediaBase) {
const token = new URLSearchParams(window.location.search).get('m');
if (token) {
const sep = parentMediaBase.includes('?') ? '&' : '?';
overlayVideoUrl = `${parentMediaBase}${sep}token=${token}`;
overlayTitleTarget = '_blank';
}
}
customComponents.current.embedInfoOverlay = new EmbedInfoOverlay(playerRef.current, {
authorName: currentVideo.author_name,
authorProfile: currentVideo.author_profile,
authorThumbnail: currentVideo.author_thumbnail,
videoTitle: currentVideo.title,
videoUrl: currentVideo.url,
videoUrl: overlayVideoUrl,
titleTarget: overlayTitleTarget,
showTitle: finalShowTitle,
showRelated: finalShowRelated,
showUserAvatar: finalShowUserAvatar,
+37 -35
View File
@@ -3,50 +3,52 @@ import { createRoot } from 'react-dom/client';
import VideoJS from './VideoJS.jsx';
// Mount the components when the DOM is ready
const mountComponents = () => {
// Mount main video player
const rootContainerMainNew = document.getElementById('video-js-root-main');
if (rootContainerMainNew && !rootContainerMainNew.hasChildNodes()) {
const rootMain = createRoot(rootContainerMainNew);
rootMain.render(
<StrictMode>
<VideoJS videoId="video-main" />
</StrictMode>
);
}
// Track root instances keyed by container id.
// Each entry: { root, container } so we can detect if the DOM element was replaced.
const roots = {};
// Mount embed video player
const rootContainerEmbedNew = document.getElementById('video-js-root-embed');
if (rootContainerEmbedNew && !rootContainerEmbedNew.hasChildNodes()) {
const rootEmbed = createRoot(rootContainerEmbedNew);
rootEmbed.render(
<StrictMode>
<VideoJS videoId="video-embed" />
</StrictMode>
);
const mountComponents = () => {
const containers = [
{ id: 'video-js-root-main', videoId: 'video-main' },
{ id: 'video-js-root-embed', videoId: 'video-embed' },
];
for (const { id, videoId } of containers) {
const container = document.getElementById(id);
if (!container) continue;
const existing = roots[id];
if (existing && existing.container === container) {
// Same DOM node re-render with latest MEDIA_DATA.
existing.root.render(
<StrictMode>
<VideoJS videoId={videoId} />
</StrictMode>
);
} else {
// First mount, or container was replaced (SPA navigation).
if (existing) {
existing.root.unmount();
}
const root = createRoot(container);
root.render(
<StrictMode>
<VideoJS videoId={videoId} />
</StrictMode>
);
roots[id] = { root, container };
}
}
};
// Expose the mounting function globally for manual triggering
// Expose globally so VideoJSEmbed can trigger a re-mount after MEDIA_DATA is updated.
window.triggerVideoJSMount = mountComponents;
// Listen for custom events to trigger mounting
document.addEventListener('triggerVideoJSMount', () => {
mountComponents();
});
document.addEventListener('triggerVideoJSMount', mountComponents);
// Initial mount
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', mountComponents);
} else {
mountComponents();
}
// Also periodically check for new containers (as a fallback)
setInterval(() => {
const embedContainer = document.getElementById('video-js-root-embed');
if (embedContainer && !embedContainer.hasChildNodes()) {
mountComponents();
}
}, 1000);
+247 -203
View File
@@ -10,7 +10,7 @@
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.24"
"@babel/code-frame@^7.27.1":
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.27.1":
version "7.27.1"
resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz"
integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==
@@ -24,7 +24,7 @@
resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz"
integrity sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==
"@babel/core@^7.27.4":
"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.21.3", "@babel/core@^7.27.4":
version "7.27.4"
resolved "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz"
integrity sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==
@@ -160,7 +160,7 @@
debug "^4.3.1"
globals "^11.1.0"
"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.27.6":
"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.21.3", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.27.6":
version "7.27.6"
resolved "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz"
integrity sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==
@@ -168,116 +168,11 @@
"@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.27.1"
"@esbuild/android-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622"
integrity sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==
"@esbuild/android-arm@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz#fedb265bc3a589c84cc11f810804f234947c3682"
integrity sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==
"@esbuild/android-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz#35cf419c4cfc8babe8893d296cd990e9e9f756f2"
integrity sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==
"@esbuild/darwin-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz"
integrity sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==
"@esbuild/darwin-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz#d70d5790d8bf475556b67d0f8b7c5bdff053d85d"
integrity sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==
"@esbuild/freebsd-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz#98755cd12707f93f210e2494d6a4b51b96977f54"
integrity sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==
"@esbuild/freebsd-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz#c1eb2bff03915f87c29cece4c1a7fa1f423b066e"
integrity sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==
"@esbuild/linux-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz#bad4238bd8f4fc25b5a021280c770ab5fc3a02a0"
integrity sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==
"@esbuild/linux-arm@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz#3e617c61f33508a27150ee417543c8ab5acc73b0"
integrity sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==
"@esbuild/linux-ia32@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz#699391cccba9aee6019b7f9892eb99219f1570a7"
integrity sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==
"@esbuild/linux-loong64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz#e6fccb7aac178dd2ffb9860465ac89d7f23b977d"
integrity sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==
"@esbuild/linux-mips64el@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz#eeff3a937de9c2310de30622a957ad1bd9183231"
integrity sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==
"@esbuild/linux-ppc64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz#2f7156bde20b01527993e6881435ad79ba9599fb"
integrity sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==
"@esbuild/linux-riscv64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz#6628389f210123d8b4743045af8caa7d4ddfc7a6"
integrity sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==
"@esbuild/linux-s390x@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz#255e81fb289b101026131858ab99fba63dcf0071"
integrity sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==
"@esbuild/linux-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz#c7690b3417af318a9b6f96df3031a8865176d338"
resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz"
integrity sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==
"@esbuild/netbsd-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz#30e8cd8a3dded63975e2df2438ca109601ebe0d1"
integrity sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==
"@esbuild/openbsd-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz#7812af31b205055874c8082ea9cf9ab0da6217ae"
integrity sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==
"@esbuild/sunos-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz#d5c275c3b4e73c9b0ecd38d1ca62c020f887ab9d"
integrity sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==
"@esbuild/win32-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz#73bc7f5a9f8a77805f357fab97f290d0e4820ac9"
integrity sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==
"@esbuild/win32-ia32@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz#ec93cbf0ef1085cc12e71e0d661d20569ff42102"
integrity sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==
"@esbuild/win32-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d"
integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==
"@eslint-community/eslint-utils@^4.2.0":
version "4.7.0"
resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz"
@@ -333,7 +228,7 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@eslint/js@9.29.0", "@eslint/js@^9.25.0":
"@eslint/js@^9.25.0", "@eslint/js@9.29.0":
version "9.29.0"
resolved "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz"
integrity sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==
@@ -411,74 +306,19 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@parcel/watcher-android-arm64@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz#507f836d7e2042f798c7d07ad19c3546f9848ac1"
integrity sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==
"@parcel/watcher-darwin-arm64@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz#3d26dce38de6590ef79c47ec2c55793c06ad4f67"
integrity sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==
"@parcel/watcher-darwin-x64@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz#99f3af3869069ccf774e4ddfccf7e64fd2311ef8"
integrity sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==
"@parcel/watcher-freebsd-x64@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz#14d6857741a9f51dfe51d5b08b7c8afdbc73ad9b"
integrity sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==
"@parcel/watcher-linux-arm-glibc@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz#43c3246d6892381db473bb4f663229ad20b609a1"
integrity sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==
"@parcel/watcher-linux-arm-musl@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz#663750f7090bb6278d2210de643eb8a3f780d08e"
integrity sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==
"@parcel/watcher-linux-arm64-glibc@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz#ba60e1f56977f7e47cd7e31ad65d15fdcbd07e30"
integrity sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==
"@parcel/watcher-linux-arm64-musl@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz#f7fbcdff2f04c526f96eac01f97419a6a99855d2"
integrity sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==
"@parcel/watcher-linux-x64-glibc@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz#4d2ea0f633eb1917d83d483392ce6181b6a92e4e"
resolved "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz"
integrity sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==
"@parcel/watcher-linux-x64-musl@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz#277b346b05db54f55657301dd77bdf99d63606ee"
resolved "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz"
integrity sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==
"@parcel/watcher-win32-arm64@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz#7e9e02a26784d47503de1d10e8eab6cceb524243"
integrity sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==
"@parcel/watcher-win32-ia32@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz#2d0f94fa59a873cdc584bf7f6b1dc628ddf976e6"
integrity sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==
"@parcel/watcher-win32-x64@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz#ae52693259664ba6f2228fa61d7ee44b64ea0947"
integrity sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==
"@parcel/watcher@^2.4.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.5.1.tgz#342507a9cfaaf172479a882309def1e991fb1200"
resolved "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz"
integrity sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==
dependencies:
detect-libc "^1.0.3"
@@ -505,6 +345,98 @@
resolved "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.11.tgz"
integrity sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==
"@rollup/pluginutils@^5.2.0":
version "5.3.0"
resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz"
integrity sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==
dependencies:
"@types/estree" "^1.0.0"
estree-walker "^2.0.2"
picomatch "^4.0.2"
"@svgr/babel-plugin-add-jsx-attribute@8.0.0":
version "8.0.0"
resolved "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz"
integrity sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==
"@svgr/babel-plugin-remove-jsx-attribute@8.0.0":
version "8.0.0"
resolved "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz"
integrity sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==
"@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0":
version "8.0.0"
resolved "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz"
integrity sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==
"@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0":
version "8.0.0"
resolved "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz"
integrity sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==
"@svgr/babel-plugin-svg-dynamic-title@8.0.0":
version "8.0.0"
resolved "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz"
integrity sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==
"@svgr/babel-plugin-svg-em-dimensions@8.0.0":
version "8.0.0"
resolved "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz"
integrity sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==
"@svgr/babel-plugin-transform-react-native-svg@8.1.0":
version "8.1.0"
resolved "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz"
integrity sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==
"@svgr/babel-plugin-transform-svg-component@8.0.0":
version "8.0.0"
resolved "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz"
integrity sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==
"@svgr/babel-preset@8.1.0":
version "8.1.0"
resolved "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz"
integrity sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==
dependencies:
"@svgr/babel-plugin-add-jsx-attribute" "8.0.0"
"@svgr/babel-plugin-remove-jsx-attribute" "8.0.0"
"@svgr/babel-plugin-remove-jsx-empty-expression" "8.0.0"
"@svgr/babel-plugin-replace-jsx-attribute-value" "8.0.0"
"@svgr/babel-plugin-svg-dynamic-title" "8.0.0"
"@svgr/babel-plugin-svg-em-dimensions" "8.0.0"
"@svgr/babel-plugin-transform-react-native-svg" "8.1.0"
"@svgr/babel-plugin-transform-svg-component" "8.0.0"
"@svgr/core@*", "@svgr/core@^8.1.0":
version "8.1.0"
resolved "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz"
integrity sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==
dependencies:
"@babel/core" "^7.21.3"
"@svgr/babel-preset" "8.1.0"
camelcase "^6.2.0"
cosmiconfig "^8.1.3"
snake-case "^3.0.4"
"@svgr/hast-util-to-babel-ast@8.0.0":
version "8.0.0"
resolved "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz"
integrity sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==
dependencies:
"@babel/types" "^7.21.3"
entities "^4.4.0"
"@svgr/plugin-jsx@^8.1.0":
version "8.1.0"
resolved "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz"
integrity sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==
dependencies:
"@babel/core" "^7.21.3"
"@svgr/babel-preset" "8.1.0"
"@svgr/hast-util-to-babel-ast" "8.0.0"
svg-parser "^2.0.4"
"@types/babel__core@^7.20.5":
version "7.20.5"
resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz"
@@ -538,7 +470,7 @@
dependencies:
"@babel/types" "^7.20.7"
"@types/estree@^1.0.6":
"@types/estree@^1.0.0", "@types/estree@^1.0.6":
version "1.0.8"
resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz"
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
@@ -553,7 +485,7 @@
resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz"
integrity sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==
"@types/react@^19.1.2":
"@types/react@^19.0.0", "@types/react@^19.1.2":
version "19.1.8"
resolved "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz"
integrity sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==
@@ -613,7 +545,7 @@ acorn-jsx@^5.3.2:
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
acorn@^8.15.0:
"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.15.0:
version "8.15.0"
resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz"
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
@@ -665,12 +597,12 @@ brace-expansion@^1.1.7:
braces@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
dependencies:
fill-range "^7.1.1"
browserslist@^4.24.0:
browserslist@^4.24.0, "browserslist@>= 4.21.0":
version "4.25.0"
resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz"
integrity sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==
@@ -685,6 +617,11 @@ callsites@^3.0.0:
resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
camelcase@^6.2.0:
version "6.3.0"
resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz"
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
caniuse-lite@^1.0.30001718:
version "1.0.30001723"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001723.tgz"
@@ -700,7 +637,7 @@ chalk@^4.0.0:
chokidar@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30"
resolved "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz"
integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==
dependencies:
readdirp "^4.0.1"
@@ -732,6 +669,16 @@ cookie@^1.0.1:
resolved "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz"
integrity sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==
cosmiconfig@^8.1.3:
version "8.3.6"
resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz"
integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==
dependencies:
import-fresh "^3.3.0"
js-yaml "^4.1.0"
parse-json "^5.2.0"
path-type "^4.0.0"
cross-spawn@^7.0.6:
version "7.0.6"
resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz"
@@ -760,7 +707,7 @@ deep-is@^0.1.3:
detect-libc@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz"
integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==
dom-walk@^0.1.0:
@@ -768,11 +715,31 @@ dom-walk@^0.1.0:
resolved "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz"
integrity sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==
dot-case@^3.0.4:
version "3.0.4"
resolved "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz"
integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==
dependencies:
no-case "^3.0.4"
tslib "^2.0.3"
electron-to-chromium@^1.5.160:
version "1.5.168"
resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.168.tgz"
integrity sha512-RUNQmFLNIWVW6+z32EJQ5+qx8ci6RGvdtDC0Ls+F89wz6I2AthpXF0w0DIrn2jpLX0/PU9ZCo+Qp7bg/EckJmA==
entities@^4.4.0:
version "4.5.0"
resolved "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz"
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
error-ex@^1.3.1:
version "1.3.4"
resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz"
integrity sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==
dependencies:
is-arrayish "^0.2.1"
esbuild@^0.18.10:
version "0.18.20"
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz"
@@ -839,7 +806,7 @@ eslint-visitor-keys@^4.2.1:
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz"
integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
eslint@^9.25.0:
"eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", eslint@^9.25.0, eslint@>=8.40:
version "9.29.0"
resolved "https://registry.npmjs.org/eslint/-/eslint-9.29.0.tgz"
integrity sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==
@@ -908,6 +875,11 @@ estraverse@^5.1.0, estraverse@^5.2.0:
resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz"
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
estree-walker@^2.0.2:
version "2.0.2"
resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
esutils@^2.0.2:
version "2.0.3"
resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz"
@@ -937,7 +909,7 @@ file-entry-cache@^8.0.0:
fill-range@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz"
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
dependencies:
to-regex-range "^5.0.1"
@@ -963,11 +935,6 @@ flatted@^3.2.9:
resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz"
integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==
fsevents@~2.3.2:
version "2.3.3"
resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
gensync@^1.0.0-beta.2:
version "1.0.0-beta.2"
resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz"
@@ -980,7 +947,7 @@ glob-parent@^6.0.2:
dependencies:
is-glob "^4.0.3"
global@4.4.0, global@^4.3.1, global@^4.4.0, global@~4.4.0:
global@^4.3.1, global@^4.4.0, global@~4.4.0, global@4.4.0:
version "4.4.0"
resolved "https://registry.npmjs.org/global/-/global-4.4.0.tgz"
integrity sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==
@@ -1015,10 +982,10 @@ ignore@^5.2.0:
immutable@^5.0.2:
version "5.1.3"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.3.tgz#e6486694c8b76c37c063cca92399fa64098634d4"
resolved "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz"
integrity sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==
import-fresh@^3.2.1:
import-fresh@^3.2.1, import-fresh@^3.3.0:
version "3.3.1"
resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz"
integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==
@@ -1031,6 +998,11 @@ imurmurhash@^0.1.4:
resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz"
integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==
is-arrayish@^0.2.1:
version "0.2.1"
resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz"
integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz"
@@ -1050,7 +1022,7 @@ is-glob@^4.0.0, is-glob@^4.0.3:
is-number@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
isexe@^2.0.0:
@@ -1080,6 +1052,11 @@ json-buffer@3.0.1:
resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz"
integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
json-parse-even-better-errors@^2.3.0:
version "2.3.1"
resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz"
integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
json-schema-traverse@^0.4.1:
version "0.4.1"
resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz"
@@ -1110,6 +1087,11 @@ levn@^0.4.1:
prelude-ls "^1.2.1"
type-check "~0.4.0"
lines-and-columns@^1.1.6:
version "1.2.4"
resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz"
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
locate-path@^6.0.0:
version "6.0.0"
resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz"
@@ -1122,6 +1104,13 @@ lodash.merge@^4.6.2:
resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
lower-case@^2.0.2:
version "2.0.2"
resolved "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz"
integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==
dependencies:
tslib "^2.0.3"
lru-cache@^5.1.1:
version "5.1.1"
resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz"
@@ -1140,7 +1129,7 @@ m3u8-parser@^7.2.0:
micromatch@^4.0.5:
version "4.0.8"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz"
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
dependencies:
braces "^3.0.3"
@@ -1175,7 +1164,7 @@ ms@^2.1.3:
resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
mux.js@7.1.0, mux.js@^7.0.1:
mux.js@^7.0.1, mux.js@7.1.0:
version "7.1.0"
resolved "https://registry.npmjs.org/mux.js/-/mux.js-7.1.0.tgz"
integrity sha512-NTxawK/BBELJrYsZThEulyUMDVlLizKdxyAsMuzoCD1eFj97BVaA8D/CvKsKu6FOLYkFojN5CbM9h++ZTZtknA==
@@ -1193,9 +1182,17 @@ natural-compare@^1.4.0:
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
no-case@^3.0.4:
version "3.0.4"
resolved "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz"
integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==
dependencies:
lower-case "^2.0.2"
tslib "^2.0.3"
node-addon-api@^7.0.0:
version "7.1.1"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558"
resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz"
integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==
node-releases@^2.0.19:
@@ -1236,6 +1233,16 @@ parent-module@^1.0.0:
dependencies:
callsites "^3.0.0"
parse-json@^5.2.0:
version "5.2.0"
resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz"
integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==
dependencies:
"@babel/code-frame" "^7.0.0"
error-ex "^1.3.1"
json-parse-even-better-errors "^2.3.0"
lines-and-columns "^1.1.6"
path-exists@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz"
@@ -1246,6 +1253,11 @@ path-key@^3.1.0:
resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz"
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
path-type@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
picocolors@^1.1.1:
version "1.1.1"
resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz"
@@ -1253,9 +1265,14 @@ picocolors@^1.1.1:
picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
picomatch@^4.0.2:
version "4.0.3"
resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz"
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
pkcs7@^1.0.4:
version "1.0.4"
resolved "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz"
@@ -1279,7 +1296,7 @@ prelude-ls@^1.2.1:
prettier@^3.6.0:
version "3.6.2"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.6.2.tgz#ccda02a1003ebbb2bfda6f83a074978f608b9393"
resolved "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz"
integrity sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==
process@^0.11.10:
@@ -1292,7 +1309,7 @@ punycode@^2.1.0:
resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz"
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
react-dom@^19.1.0:
react-dom@^19.1.0, react-dom@>=18:
version "19.1.0"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz"
integrity sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==
@@ -1319,14 +1336,14 @@ react-router@7.6.2:
cookie "^1.0.1"
set-cookie-parser "^2.6.0"
react@^19.1.0:
react@^19.1.0, react@>=18:
version "19.1.0"
resolved "https://registry.npmjs.org/react/-/react-19.1.0.tgz"
integrity sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==
readdirp@^4.0.1:
version "4.1.2"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d"
resolved "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz"
integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==
resolve-from@^4.0.0:
@@ -1334,17 +1351,17 @@ resolve-from@^4.0.0:
resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz"
integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
rollup@^3.27.1:
rollup@^1.20.0||^2.0.0||^3.0.0||^4.0.0, rollup@^3.27.1:
version "3.29.5"
resolved "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz"
integrity sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==
optionalDependencies:
fsevents "~2.3.2"
sass@^1.89.2:
version "1.89.2"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.89.2.tgz#a771716aeae774e2b529f72c0ff2dfd46c9de10e"
integrity sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==
sass@*, sass@^1.89.2:
version "1.93.2"
resolved "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz"
integrity sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==
dependencies:
chokidar "^4.0.0"
immutable "^5.0.2"
@@ -1379,7 +1396,15 @@ shebang-regex@^3.0.0:
resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.1:
snake-case@^3.0.4:
version "3.0.4"
resolved "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz"
integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==
dependencies:
dot-case "^3.0.4"
tslib "^2.0.3"
source-map-js@^1.2.1, "source-map-js@>=0.6.2 <2.0.0":
version "1.2.1"
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
@@ -1396,13 +1421,23 @@ supports-color@^7.1.0:
dependencies:
has-flag "^4.0.0"
svg-parser@^2.0.4:
version "2.0.4"
resolved "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz"
integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz"
integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
dependencies:
is-number "^7.0.0"
tslib@^2.0.3:
version "2.8.1"
resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz"
@@ -1425,7 +1460,7 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
"video.js@^7 || ^8", video.js@^8.23.3:
"video.js@^7 || ^8", video.js@^8, video.js@^8.23.3:
version "8.23.3"
resolved "https://registry.npmjs.org/video.js/-/video.js-8.23.3.tgz"
integrity sha512-Toe0VLlDZcUhiaWfcePS1OEdT3ATfktm0hk/PELfD7zUoPDHeT+cJf/wZmCy5M5eGVwtGUg25RWPCj1L/1XufA==
@@ -1462,7 +1497,16 @@ videojs-vtt.js@0.15.5:
dependencies:
global "^4.3.1"
vite@^4.5.14:
vite-plugin-svgr@^4.5.0:
version "4.5.0"
resolved "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-4.5.0.tgz"
integrity sha512-W+uoSpmVkSmNOGPSsDCWVW/DDAyv+9fap9AZXBvWiQqrboJ08j2vh0tFxTD/LjwqwAd3yYSVJgm54S/1GhbdnA==
dependencies:
"@rollup/pluginutils" "^5.2.0"
"@svgr/core" "^8.1.0"
"@svgr/plugin-jsx" "^8.1.0"
"vite@^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0", vite@^4.5.14, vite@>=2.6.0:
version "4.5.14"
resolved "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz"
integrity sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==
@@ -54,7 +54,7 @@
.category-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
align-items: flex-start;
padding: 20px 24px;
border-bottom: 1px solid #e0e0e0;
@@ -74,6 +74,19 @@
}
}
.category-modal-subtitle {
display: flex;
align-items: center;
gap: 8px;
margin-top: 6px;
font-size: 13px;
color: #777;
.dark_theme & {
color: #aaa;
}
}
.category-modal-close {
background: none;
border: none;
@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
import './BulkActionCategoryModal.scss';
import { translateString } from '../utils/helpers/';
import { inEmbeddedApp } from '../utils/helpers/embeddedApp';
interface Category {
title: string;
@@ -24,6 +25,7 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
onError,
csrfToken,
}) => {
const isLmsMode = inEmbeddedApp();
const [existingCategories, setExistingCategories] = useState<Category[]>([]);
const [allCategories, setAllCategories] = useState<Category[]>([]);
const [categoriesToAdd, setCategoriesToAdd] = useState<Category[]>([]);
@@ -66,20 +68,27 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
const existingData = await existingResponse.json();
const existing = existingData.results || [];
// Fetch all categories
const allResponse = await fetch('/api/v1/categories');
// Fetch all categories (or LMS courses only in embed mode)
const categoriesUrl = isLmsMode
? '/api/v1/categories/contributor?lms_courses_only=true'
: '/api/v1/categories';
const allResponse = await fetch(categoriesUrl);
if (!allResponse.ok) {
throw new Error(translateString('Failed to fetch all categories'));
throw new Error(isLmsMode ? translateString('Failed to fetch courses') : translateString('Failed to fetch all categories'));
}
const allData = await allResponse.json();
const all = allData.results || allData;
setExistingCategories(existing);
// In LMS mode, filter existing to only show LMS course categories
const allUids = new Set(all.map((c: Category) => c.uid));
const filteredExisting = isLmsMode ? existing.filter((c: Category) => allUids.has(c.uid)) : existing;
setExistingCategories(filteredExisting);
setAllCategories(all);
} catch (error) {
console.error('Error fetching categories:', error);
onError(translateString('Failed to load categories'));
onError(isLmsMode ? translateString('Failed to load courses') : translateString('Failed to load categories'));
} finally {
setIsLoading(false);
}
@@ -126,7 +135,7 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
});
if (!addResponse.ok) {
throw new Error(translateString('Failed to add categories'));
throw new Error(isLmsMode ? translateString('Failed to add courses') : translateString('Failed to add categories'));
}
}
@@ -147,15 +156,15 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
});
if (!removeResponse.ok) {
throw new Error(translateString('Failed to remove categories'));
throw new Error(isLmsMode ? translateString('Failed to remove courses') : translateString('Failed to remove categories'));
}
}
onSuccess(translateString('Successfully updated categories'));
onSuccess(isLmsMode ? translateString('Successfully updated courses') : translateString('Successfully updated categories'));
onCancel();
} catch (error) {
console.error('Error processing categories:', error);
onError(translateString('Failed to update categories. Please try again.'));
onError(isLmsMode ? translateString('Failed to update courses. Please try again.') : translateString('Failed to update categories. Please try again.'));
} finally {
setIsProcessing(false);
}
@@ -184,7 +193,14 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
<div className="category-modal-overlay">
<div className="category-modal">
<div className="category-modal-header">
<h2>{translateString('Add / Remove from Categories')}</h2>
<div>
<h2>{isLmsMode ? translateString('Share with Course') : translateString('Add / Remove from Categories')}</h2>
{isLmsMode && (
<div className="category-modal-subtitle">
<span>{translateString('Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)')}</span>
</div>
)}
</div>
<button className="category-modal-close" onClick={onCancel}>
×
</button>
@@ -192,14 +208,14 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
<div className="category-modal-content">
<div className="category-panel">
<h3>{translateString('Categories')}</h3>
<h3>{isLmsMode ? translateString('Courses') : translateString('Categories')}</h3>
{isLoading ? (
<div className="loading-message">{translateString('Loading categories...')}</div>
<div className="loading-message">{isLmsMode ? translateString('Loading courses...') : translateString('Loading categories...')}</div>
) : (
<div className="category-list scrollable">
{leftPanelCategories.length === 0 ? (
<div className="empty-message">{translateString('All categories already added')}</div>
<div className="empty-message">{isLmsMode ? translateString('All courses already added') : translateString('All categories already added')}</div>
) : (
leftPanelCategories.map((category) => (
<div
@@ -227,11 +243,11 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
</h3>
{isLoading ? (
<div className="loading-message">{translateString('Loading categories...')}</div>
<div className="loading-message">{isLmsMode ? translateString('Loading courses...') : translateString('Loading categories...')}</div>
) : (
<div className="category-list scrollable">
{rightPanelCategories.length === 0 ? (
<div className="empty-message">{translateString('No categories')}</div>
<div className="empty-message">{isLmsMode ? translateString('No courses') : translateString('No categories')}</div>
) : (
rightPanelCategories.map((category) => {
const isExisting = existingCategories.some((c) => c.uid === category.uid);
@@ -251,7 +267,7 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
removeCategoryFromAddList(category);
}
}}
title={isMarkedForRemoval ? translateString('Undo removal') : isExisting ? translateString('Remove category') : translateString('Remove from list')}
title={isMarkedForRemoval ? translateString('Undo removal') : isExisting ? (isLmsMode ? translateString('Remove course') : translateString('Remove category')) : translateString('Remove from list')}
>
{isMarkedForRemoval ? '↺' : '×'}
</button>
@@ -47,7 +47,7 @@ export const BulkActionChangeOwnerModal: React.FC<BulkActionChangeOwnerModalProp
}
try {
const response = await fetch(`/api/v1/users?name=${encodeURIComponent(name)}`);
const response = await fetch(`/api/v1/users?name=${encodeURIComponent(name)}&exclude_self=True`);
if (!response.ok) {
throw new Error(translateString('Failed to search users'));
}
@@ -0,0 +1,33 @@
.course-cleanup-options {
margin-top: 16px;
display: flex;
flex-direction: column;
gap: 10px;
}
.course-cleanup-checkbox {
display: flex;
align-items: flex-start;
gap: 8px;
font-size: 13px;
color: #555;
cursor: pointer;
line-height: 1.4;
.dark_theme & {
color: #ccc;
}
input[type='checkbox'] {
flex-shrink: 0;
margin-top: 2px;
width: 15px;
height: 15px;
cursor: pointer;
accent-color: var(--default-theme-color, #009933);
}
span {
flex: 1;
}
}
@@ -0,0 +1,237 @@
import React, { useState, useEffect } from 'react';
import './BulkActionCategoryModal.scss';
import './BulkActionCourseCleanupModal.scss';
import { translateString } from '../utils/helpers/';
interface Course {
title: string;
uid: string;
}
interface BulkActionCourseCleanupModalProps {
isOpen: boolean;
selectedMediaIds: string[];
onCancel: () => void;
onSuccess: (message: string) => void;
onError: (message: string) => void;
csrfToken: string;
}
export const BulkActionCourseCleanupModal: React.FC<BulkActionCourseCleanupModalProps> = ({
isOpen,
selectedMediaIds,
onCancel,
onSuccess,
onError,
csrfToken,
}) => {
const hasMediaSelected = selectedMediaIds.length > 0;
const [availableCourses, setAvailableCourses] = useState<Course[]>([]);
const [coursesToCleanup, setCoursesToCleanup] = useState<Course[]>([]);
const [removePermissions, setRemovePermissions] = useState(false);
const [removeComments, setRemoveComments] = useState(false);
const [applyToAll, setApplyToAll] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
useEffect(() => {
if (isOpen) {
fetchCourses();
} else {
setAvailableCourses([]);
setCoursesToCleanup([]);
setRemovePermissions(false);
setRemoveComments(false);
setApplyToAll(false);
}
}, [isOpen, selectedMediaIds.join(',')]);
const fetchCourses = async () => {
setIsLoading(true);
try {
const contributorResponse = await fetch('/api/v1/categories/contributor?lms_courses_only=true');
if (!contributorResponse.ok) throw new Error('Failed to fetch courses');
const contributorData = await contributorResponse.json();
const allContributorCourses: Course[] = contributorData.results || contributorData;
if (hasMediaSelected) {
const membershipResponse = await fetch('/api/v1/media/user/bulk_actions', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
body: JSON.stringify({ action: 'category_membership', media_ids: selectedMediaIds }),
});
if (!membershipResponse.ok) throw new Error('Failed to fetch media categories');
const membershipData = await membershipResponse.json();
const mediaCategoryUids = new Set((membershipData.results || []).map((c: Course) => c.uid));
setAvailableCourses(allContributorCourses.filter((c) => mediaCategoryUids.has(c.uid)));
} else {
setAvailableCourses(allContributorCourses);
}
} catch (error) {
onError(translateString('Failed to load courses'));
} finally {
setIsLoading(false);
}
};
const addCourseToCleanup = (course: Course) => {
if (!coursesToCleanup.some((c) => c.uid === course.uid)) {
setCoursesToCleanup((prev) => [...prev, course]);
setAvailableCourses((prev) => prev.filter((c) => c.uid !== course.uid));
}
};
const removeCourseFromCleanup = (course: Course) => {
setCoursesToCleanup((prev) => prev.filter((c) => c.uid !== course.uid));
setAvailableCourses((prev) => [...prev, course]);
};
const handleProceed = async () => {
if (coursesToCleanup.length === 0) return;
setIsProcessing(true);
try {
const response = await fetch('/api/v1/media/user/bulk_actions', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
body: JSON.stringify({
action: 'course_cleanup',
media_ids: selectedMediaIds,
category_uids: coursesToCleanup.map((c) => c.uid),
remove_permissions: removePermissions,
remove_comments: removeComments,
apply_to_all: applyToAll,
}),
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.detail || 'Failed');
}
onSuccess(translateString('Course cleanup completed successfully'));
onCancel();
} catch (error: any) {
onError(error.message || translateString('Course cleanup failed. Please try again.'));
} finally {
setIsProcessing(false);
}
};
if (!isOpen) return null;
return (
<div className="category-modal-overlay">
<div className="category-modal">
<div className="category-modal-header">
<div>
<h2>{translateString('Course Cleanup')}</h2>
<div className="category-modal-subtitle">
<span>
{translateString(
'Cleanup irrelevant course content such as old user permissions and course tags. Add course to the right column and select what to cleanup under the Column. The cleanup can apply to the media you have selected only or to all media in the course, if that option is selected.'
)}
</span>
</div>
</div>
<button className="category-modal-close" onClick={onCancel}>
×
</button>
</div>
<div className="category-modal-content">
<div className="category-panel">
<h3>{translateString('Courses available')}</h3>
{isLoading ? (
<div className="loading-message">{translateString('Loading courses...')}</div>
) : (
<div className="category-list scrollable">
{availableCourses.length === 0 ? (
<div className="empty-message">{translateString('No courses available')}</div>
) : (
availableCourses.map((course) => (
<div
key={course.uid}
className="category-item clickable"
onClick={() => addCourseToCleanup(course)}
>
<span>{course.title}</span>
<button className="add-btn">+</button>
</div>
))
)}
</div>
)}
</div>
<div className="category-panel">
<h3>{translateString('Courses to cleanup')}</h3>
{isLoading ? (
<div className="loading-message">{translateString('Loading courses...')}</div>
) : (
<>
<div className="category-list scrollable">
{coursesToCleanup.length === 0 ? (
<div className="empty-message">{translateString('No courses selected')}</div>
) : (
coursesToCleanup.map((course) => (
<div key={course.uid} className="category-item">
<span>{course.title}</span>
<button
className="remove-btn"
onClick={() => removeCourseFromCleanup(course)}
title={translateString('Remove from list')}
>
×
</button>
</div>
))
)}
</div>
<div className="course-cleanup-options">
<label className="course-cleanup-checkbox">
<input
type="checkbox"
checked={removePermissions}
onChange={(e) => setRemovePermissions(e.target.checked)}
/>
<span>{translateString('Remove present course permissions for all course members')}</span>
</label>
<label className="course-cleanup-checkbox">
<input
type="checkbox"
checked={removeComments}
onChange={(e) => setRemoveComments(e.target.checked)}
/>
<span>{translateString('Remove Comments')}</span>
</label>
{hasMediaSelected && (
<label className="course-cleanup-checkbox">
<input
type="checkbox"
checked={applyToAll}
onChange={(e) => setApplyToAll(e.target.checked)}
/>
<span>{translateString('Apply cleanup to all media shared in the course')}</span>
</label>
)}
</div>
</>
)}
</div>
</div>
<div className="category-modal-footer">
<button className="category-btn category-btn-cancel" onClick={onCancel} disabled={isProcessing}>
{translateString('Cancel')}
</button>
<button
className="category-btn category-btn-proceed"
onClick={handleProceed}
disabled={isProcessing || coursesToCleanup.length === 0}
>
{isProcessing ? translateString('Processing...') : translateString('Proceed')}
</button>
</div>
</div>
</div>
);
};
@@ -54,7 +54,7 @@
.permission-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
align-items: flex-start;
padding: 20px 24px;
border-bottom: 1px solid #e0e0e0;
@@ -102,6 +102,19 @@
}
}
.permission-modal-subtitle {
display: flex;
align-items: center;
gap: 8px;
margin-top: 6px;
font-size: 13px;
color: #777;
.dark_theme & {
color: #aaa;
}
}
.permission-modal-content {
display: flex;
gap: 24px;
@@ -226,7 +226,42 @@ export const BulkActionPermissionModal: React.FC<BulkActionPermissionModalProps>
<div className="permission-modal-overlay">
<div className="permission-modal">
<div className="permission-modal-header">
<h2>{translateString('Manage')} {permissionLabel}</h2>
<div>
<h2>{translateString('Share with')} {permissionLabel}</h2>
{permissionType === 'viewer' && (
<div className="permission-modal-subtitle">
<span>{translateString('Give users viewer permissions to your media by adding them to the below list.')}</span>
<span
className="info-tooltip"
title={translateString("Users can view your media via: My Media > Shared with Me > particular media > ...")}
>
i
</span>
</div>
)}
{permissionType === 'editor' && (
<div className="permission-modal-subtitle">
<span>{translateString('Give users editor permissions to your media by adding them to the below list.')}</span>
<span
className="info-tooltip"
title={translateString("Users can edit your media via: My Media > Shared with Me > particular media > Edit...")}
>
i
</span>
</div>
)}
{permissionType === 'owner' && (
<div className="permission-modal-subtitle">
<span>{translateString('Give users owner permissions to your media, except for deleting the media, by adding them to the below list.')}</span>
<span
className="info-tooltip"
title={translateString("Users can manage your media via: My Media > Shared with Me > particular media > ...")}
>
i
</span>
</div>
)}
</div>
<button className="permission-modal-close" onClick={onCancel}>
×
</button>
@@ -284,6 +319,21 @@ export const BulkActionPermissionModal: React.FC<BulkActionPermissionModalProps>
?
</span>
)}
{permissionType === 'viewer' && (
<span className="info-tooltip" title={translateString('Remove users from the list to remove viewer permissions')}>
i
</span>
)}
{permissionType === 'editor' && (
<span className="info-tooltip" title={translateString('Remove users from the list to remove editor permissions')}>
i
</span>
)}
{permissionType === 'owner' && (
<span className="info-tooltip" title={translateString('Remove users from the list to remove owner permissions')}>
i
</span>
)}
</h3>
<div className="search-box">
<input
@@ -159,6 +159,56 @@
}
}
.shared-selector {
display: flex;
flex-direction: column;
gap: 6px;
&-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 500;
color: #333;
cursor: pointer;
user-select: none;
input[type='checkbox'] {
width: 16px;
height: 16px;
cursor: pointer;
&:disabled {
cursor: not-allowed;
opacity: 0.5;
}
}
.dark_theme & {
color: #fff;
}
}
&-note {
margin: 0;
font-size: 12px;
color: #777;
&--warn {
color: #b45309;
}
.dark_theme & {
color: #aaa;
&--warn {
color: #f59e0b;
}
}
}
}
.publish-state-modal-footer {
display: flex;
justify-content: flex-end;

Some files were not shown because too many files have changed in this diff Show More