mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-03-10 15:07:23 -04:00
Compare commits
227 Commits
v7.4.0
...
feat-lti-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45774fbc8c | ||
|
|
5ac0515d05 | ||
|
|
492e3366c0 | ||
|
|
3f1ca5d02f | ||
|
|
8bc09b69f7 | ||
|
|
7b8e4975ab | ||
|
|
df8701a515 | ||
|
|
2c1103e906 | ||
|
|
1c50dcdff8 | ||
|
|
b8fee7d06a | ||
|
|
1df6e0c10d | ||
|
|
dc328cd33c | ||
|
|
b39789c2c4 | ||
|
|
86fa084391 | ||
|
|
533a30b6cb | ||
|
|
cdbed4bc1d | ||
|
|
58d336478c | ||
|
|
d28fc114f3 | ||
|
|
0056e1ed8f | ||
|
|
2423e9517e | ||
|
|
699f4bd09d | ||
|
|
6071921908 | ||
|
|
33da2242b4 | ||
|
|
21ddd04165 | ||
|
|
96755af3b2 | ||
|
|
17526aeecd | ||
|
|
23c5c544ce | ||
|
|
ae9d614e84 | ||
|
|
e044187973 | ||
|
|
b5a76c53e1 | ||
|
|
86348f1747 | ||
|
|
c6be65edd6 | ||
|
|
4656ba1176 | ||
|
|
7e7cd10549 | ||
|
|
d5919e10af | ||
|
|
f16b61644a | ||
|
|
c486574eb0 | ||
|
|
364d22dd8e | ||
|
|
209cede39c | ||
|
|
9b8f63881a | ||
|
|
82fb2665e6 | ||
|
|
abd22426d7 | ||
|
|
86f4484c36 | ||
|
|
f226c2fe1e | ||
|
|
98111234f8 | ||
|
|
467e273ff5 | ||
|
|
166fb5c00b | ||
|
|
d3f722611d | ||
|
|
fae5634729 | ||
|
|
5cdcef2205 | ||
|
|
f6e44f8343 | ||
|
|
1b3a58bc60 | ||
|
|
befc1f0efa | ||
|
|
6075514ffa | ||
|
|
9c7cc293ce | ||
|
|
f7c675596f | ||
|
|
36d815c0cf | ||
|
|
156e0bddd6 | ||
|
|
637e9eabea | ||
|
|
e12f361935 | ||
|
|
c4d569e7b0 | ||
|
|
24d15d2639 | ||
|
|
1c0805748d | ||
|
|
8e7f1ef15d | ||
|
|
d00b4ed3c5 | ||
|
|
7126cf1cbd | ||
|
|
33135f81e9 | ||
|
|
cfd305d563 | ||
|
|
d4bad33592 | ||
|
|
7c0ea60d0e | ||
|
|
c6726d1b13 | ||
|
|
7c04cc30fb | ||
|
|
f28ce5990b | ||
|
|
27828d798e | ||
|
|
ba53467033 | ||
|
|
a77761ec35 | ||
|
|
4d07ae64c6 | ||
|
|
1a96ac0703 | ||
|
|
9e0290ae49 | ||
|
|
610a22935f | ||
|
|
81e3325687 | ||
|
|
16f7b010ab | ||
|
|
68cb21ff8a | ||
|
|
2ddc3b24d7 | ||
|
|
63e96b327e | ||
|
|
48537515cb | ||
|
|
e6db138d11 | ||
|
|
2f2d32f0db | ||
|
|
f4d3439246 | ||
|
|
7fe9891942 | ||
|
|
9eb8a1ad62 | ||
|
|
23ee0dc7cc | ||
|
|
e5be39f392 | ||
|
|
f0c084fa53 | ||
|
|
571bfcc4ce | ||
|
|
c04380af47 | ||
|
|
97741f780e | ||
|
|
78cce0eb10 | ||
|
|
472b3029c4 | ||
|
|
343f1e7009 | ||
|
|
8c78b67b0c | ||
|
|
29fc7fb861 | ||
|
|
b03a33d93e | ||
|
|
64472be406 | ||
|
|
cc0f4d4645 | ||
|
|
095e4d2cb4 | ||
|
|
5c8978453e | ||
|
|
83189076e4 | ||
|
|
ca6dbf3740 | ||
|
|
8646bd70dc | ||
|
|
1f493c8e15 | ||
|
|
e11cb7ea6e | ||
|
|
3131e76ef7 | ||
|
|
809cdccc42 | ||
|
|
ed36240f45 | ||
|
|
77bafff6f6 | ||
|
|
f6252f4f77 | ||
|
|
764580287f | ||
|
|
ce6c9a0a3c | ||
|
|
1ced023a07 | ||
|
|
981fec296c | ||
|
|
40cd7916e7 | ||
|
|
bcef59c3a9 | ||
|
|
e93c8225c4 | ||
|
|
5c3c33ca84 | ||
|
|
7a954e7a3d | ||
|
|
8610df0c2b | ||
|
|
8ab9030d14 | ||
|
|
15c8dec041 | ||
|
|
9af4686bd4 | ||
|
|
bcc8a0858c | ||
|
|
549b672d48 | ||
|
|
abe950f1da | ||
|
|
5fecda02d6 | ||
|
|
3c6f8c102c | ||
|
|
2d28520cd4 | ||
|
|
4bd56da2d8 | ||
|
|
fdfa857794 | ||
|
|
2c1f27c0be | ||
|
|
2f0bbd2533 | ||
|
|
54336f6c31 | ||
|
|
37e21f7ebf | ||
|
|
3deee80dd0 | ||
|
|
2e57164831 | ||
|
|
de0c16729b | ||
|
|
2c0bba1427 | ||
|
|
54a8e41f6d | ||
|
|
78fb19b464 | ||
|
|
8e5e7991b7 | ||
|
|
5cf435eca0 | ||
|
|
5026ce73da | ||
|
|
8b2ebe2415 | ||
|
|
8df320e134 | ||
|
|
8c8f737460 | ||
|
|
995faedb08 | ||
|
|
bde300b4bd | ||
|
|
fd5c0a2908 | ||
|
|
9c145da2e2 | ||
|
|
e9e5d44c3e | ||
|
|
a624c2e5b8 | ||
|
|
748d3b39ba | ||
|
|
ddc6bf9e67 | ||
|
|
aa7dbfe534 | ||
|
|
5cc72357c6 | ||
|
|
01b061a47b | ||
|
|
fbc78e7944 | ||
|
|
9e7a8afdda | ||
|
|
5572a67019 | ||
|
|
610590972f | ||
|
|
bdf7d3c2d0 | ||
|
|
a47bf5a3f8 | ||
|
|
38caea3c7c | ||
|
|
30491bf420 | ||
|
|
d0ebe19c2a | ||
|
|
59be9f16c0 | ||
|
|
a2d898c54e | ||
|
|
9733d53c0b | ||
|
|
70e2c67f3d | ||
|
|
77721d9c0e | ||
|
|
06bc64b2c4 | ||
|
|
b9899476b9 | ||
|
|
107750406e | ||
|
|
ae4ae5a07e | ||
|
|
f346a5604c | ||
|
|
56026a1a96 | ||
|
|
a88413ce14 | ||
|
|
9dab3ad858 | ||
|
|
dfe7e8fab0 | ||
|
|
1181d16ab9 | ||
|
|
d032ee3baa | ||
|
|
93f66d206b | ||
|
|
0585513439 | ||
|
|
9667e6b0ad | ||
|
|
f56948a4a2 | ||
|
|
8b3e76b554 | ||
|
|
dc417de628 | ||
|
|
35cd56c85c | ||
|
|
f0b2451815 | ||
|
|
7696251394 | ||
|
|
b95725660b | ||
|
|
d6bf98b30e | ||
|
|
3baa8ef7d7 | ||
|
|
45246eac4f | ||
|
|
9685c1b5d4 | ||
|
|
20a1da22bb | ||
|
|
f9a94321ad | ||
|
|
f85299a600 | ||
|
|
29ab2a715b | ||
|
|
43ce685f08 | ||
|
|
8c682a76af | ||
|
|
ec6b6daa81 | ||
|
|
cf90169240 | ||
|
|
fb3f377e27 | ||
|
|
f5f9a7beac | ||
|
|
726a5b74a1 | ||
|
|
40c31f295a | ||
|
|
1d77293afc | ||
|
|
5c702387ca | ||
|
|
0001f370a9 | ||
|
|
af71d4c906 | ||
|
|
eb7503125d | ||
|
|
f897d0ba2b | ||
|
|
545cca154e | ||
|
|
ef4ff9cb1d | ||
|
|
3a40fc6d88 | ||
|
|
f67d2a4d78 | ||
|
|
295578dae2 |
@@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## [7.5.0](https://github.com/mediacms-io/mediacms/compare/v7.4.0...v7.5.0) (2026-02-06)
|
||||
|
||||
### Features
|
||||
|
||||
* bump version ([36d815c](https://github.com/mediacms-io/mediacms/commit/36d815c0cfbe21d3136541d410d545742b9ebecd))
|
||||
|
||||
## [7.4.0](https://github.com/mediacms-io/mediacms/compare/v7.3.0...v7.4.0) (2026-02-06)
|
||||
|
||||
### Features
|
||||
|
||||
23
HISTORY.md
23
HISTORY.md
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -24,6 +24,7 @@ INSTALLED_APPS = [
|
||||
"actions.apps.ActionsConfig",
|
||||
"rbac.apps.RbacConfig",
|
||||
"identity_providers.apps.IdentityProvidersConfig",
|
||||
"lti.apps.LtiConfig",
|
||||
"debug_toolbar",
|
||||
"mptt",
|
||||
"crispy_forms",
|
||||
|
||||
@@ -300,6 +300,7 @@ INSTALLED_APPS = [
|
||||
"actions.apps.ActionsConfig",
|
||||
"rbac.apps.RbacConfig",
|
||||
"identity_providers.apps.IdentityProvidersConfig",
|
||||
"lti.apps.LtiConfig",
|
||||
"debug_toolbar",
|
||||
"mptt",
|
||||
"crispy_forms",
|
||||
@@ -555,6 +556,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 +652,18 @@ 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
|
||||
# SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"
|
||||
# Consider using cached_db for reliability if sessions are lost between many LTI launches
|
||||
|
||||
@@ -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 @@
|
||||
VERSION = "7.7"
|
||||
VERSION = "7.ki"
|
||||
|
||||
@@ -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 ;
|
||||
|
||||
@@ -28,7 +34,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
9
deploy/docker/supervisord/supervisord-gunicorn.conf
Normal file
9
deploy/docker/supervisord/supervisord-gunicorn.conf
Normal file
@@ -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
|
||||
@@ -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
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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-----
|
||||
@@ -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-----
|
||||
@@ -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
|
||||
@@ -1,7 +0,0 @@
|
||||
/home/mediacms.io/mediacms/logs/*.log {
|
||||
weekly
|
||||
missingok
|
||||
rotate 7
|
||||
compress
|
||||
notifempty
|
||||
}
|
||||
@@ -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/*;
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -127,6 +127,7 @@ class MediaPublishForm(forms.ModelForm):
|
||||
|
||||
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
|
||||
@@ -169,6 +170,16 @@ 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 self.request and 'category' in self.fields:
|
||||
is_embed_mode = self._check_embed_mode()
|
||||
if is_embed_mode:
|
||||
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'
|
||||
@@ -186,6 +197,22 @@ 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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
21
files/migrations/0016_category_lti_platform.py
Normal file
21
files/migrations/0016_category_lti_platform.py
Normal file
@@ -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'
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -745,7 +736,7 @@ class Media(models.Model):
|
||||
|
||||
ret = []
|
||||
for cat in self.category.all():
|
||||
ret.append({"title": cat.title, "url": cat.get_absolute_url()})
|
||||
ret.append({"title": cat.title, "url": cat.get_absolute_url(), "is_lms_course": cat.is_lms_course})
|
||||
return ret
|
||||
|
||||
@property
|
||||
|
||||
@@ -226,6 +226,7 @@ class CategorySerializer(serializers.ModelSerializer):
|
||||
"media_count",
|
||||
"user",
|
||||
"thumbnail_url",
|
||||
"is_lms_course",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -80,6 +80,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(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 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 .media import MediaActions # noqa: F401
|
||||
|
||||
@@ -43,6 +43,49 @@ class CategoryList(APIView):
|
||||
return Response(ret)
|
||||
|
||||
|
||||
class CategoryListContributor(APIView):
|
||||
"""List categories where user has contributor access"""
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
name='lms_courses_only',
|
||||
type=openapi.TYPE_BOOLEAN,
|
||||
in_=openapi.IN_QUERY,
|
||||
description='Filter to show only LMS courses (categories with is_lms_course=True)',
|
||||
),
|
||||
],
|
||||
tags=['Categories'],
|
||||
operation_summary='Lists Categories for Contributors',
|
||||
operation_description='Lists all categories 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 = Category.objects.none()
|
||||
|
||||
# Filter for LMS courses only if requested
|
||||
lms_courses_only = request.GET.get('lms_courses_only', '').lower() in ['true', '1', 'yes']
|
||||
if lms_courses_only:
|
||||
categories = categories.filter(is_lms_course=True)
|
||||
else:
|
||||
categories = Category.objects.filter(is_rbac_category=False).prefetch_related("user")
|
||||
|
||||
# Get RBAC categories where user has contributor access
|
||||
if getattr(settings, 'USE_RBAC', False):
|
||||
rbac_categories = request.user.get_rbac_categories_as_contributor()
|
||||
categories = categories.union(rbac_categories)
|
||||
|
||||
categories = categories.order_by("title")
|
||||
|
||||
serializer = CategorySerializer(categories, many=True, context={"request": request})
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class TagList(APIView):
|
||||
"""List tags"""
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@ class MediaList(APIView):
|
||||
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()
|
||||
elif author_param:
|
||||
user_queryset = User.objects.all()
|
||||
user = get_object_or_404(user_queryset, username=author_param)
|
||||
@@ -574,12 +574,31 @@ 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:
|
||||
categories = Category.objects.filter(uid__in=category_uids)
|
||||
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:
|
||||
@@ -697,12 +716,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
|
||||
|
||||
@@ -24,7 +24,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,
|
||||
@@ -310,8 +310,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 +350,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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,7 @@ 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>
|
||||
<h2>{isLmsMode ? translateString('Share with Course') : translateString('Add / Remove from Categories')}</h2>
|
||||
<button className="category-modal-close" onClick={onCancel}>
|
||||
×
|
||||
</button>
|
||||
@@ -192,14 +201,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 +236,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 +260,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>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import './BulkActionsDropdown.scss';
|
||||
import { translateString } from '../utils/helpers/';
|
||||
import { inEmbeddedApp } from '../utils/helpers/embeddedApp';
|
||||
|
||||
interface BulkActionsDropdownProps {
|
||||
selectedCount: number;
|
||||
@@ -12,7 +13,7 @@ const BULK_ACTIONS = [
|
||||
{ value: 'add-remove-coeditors', label: translateString('Add / Remove Co-Editors'), enabled: true },
|
||||
{ value: 'add-remove-coowners', label: translateString('Add / Remove Co-Owners'), enabled: true },
|
||||
{ value: 'add-remove-playlist', label: translateString('Add to / Remove from Playlist'), enabled: true },
|
||||
{ value: 'add-remove-category', label: translateString('Add to / Remove from Category'), enabled: true },
|
||||
{ value: 'add-remove-category', label: inEmbeddedApp() ? translateString('Share with Course') : translateString('Add to / Remove from Category'), enabled: true },
|
||||
{ value: 'add-remove-tags', label: translateString('Add / Remove Tags'), enabled: true },
|
||||
{ value: 'enable-comments', label: translateString('Enable Comments'), enabled: true },
|
||||
{ value: 'disable-comments', label: translateString('Disable Comments'), enabled: true },
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { translateString } from '../utils/helpers/';
|
||||
import { translateString, inSelectMediaEmbedMode } from '../utils/helpers/';
|
||||
|
||||
interface MediaListHeaderProps {
|
||||
title?: string;
|
||||
@@ -11,10 +11,12 @@ interface MediaListHeaderProps {
|
||||
|
||||
export const MediaListHeader: React.FC<MediaListHeaderProps> = (props) => {
|
||||
const viewAllText = props.viewAllText || translateString('VIEW ALL');
|
||||
const isSelectMediaMode = inSelectMediaEmbedMode();
|
||||
|
||||
return (
|
||||
<div className={(props.className ? props.className + ' ' : '') + 'media-list-header'} style={props.style}>
|
||||
<h2>{props.title}</h2>
|
||||
{props.viewAllLink ? (
|
||||
{!isSelectMediaMode && props.viewAllLink ? (
|
||||
<h3>
|
||||
{' '}
|
||||
<a href={props.viewAllLink} title={viewAllText}>
|
||||
|
||||
@@ -1,18 +1,50 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useMediaItem } from '../../utils/hooks/';
|
||||
import { PositiveInteger, PositiveIntegerOrZero } from '../../utils/helpers/';
|
||||
import { PositiveInteger, PositiveIntegerOrZero, inSelectMediaEmbedMode } from '../../utils/helpers/';
|
||||
import { MediaItemThumbnailLink, itemClassname } from './includes/items/';
|
||||
import { Item } from './Item';
|
||||
|
||||
export function MediaItem(props) {
|
||||
const type = props.type;
|
||||
const isSelectMediaMode = inSelectMediaEmbedMode();
|
||||
|
||||
const [titleComponent, descriptionComponent, thumbnailUrl, UnderThumbWrapper, editMediaComponent, metaComponents, viewMediaComponent] =
|
||||
const [titleComponentOrig, descriptionComponent, thumbnailUrl, UnderThumbWrapperOrig, editMediaComponent, metaComponents, viewMediaComponent] =
|
||||
useMediaItem({ ...props, type });
|
||||
|
||||
// In embed mode, override components to remove links
|
||||
const ItemTitle = ({ title }) => (
|
||||
<h3>
|
||||
<span>{title}</span>
|
||||
</h3>
|
||||
);
|
||||
|
||||
const ItemMain = ({ children }) => <div className="item-main">{children}</div>;
|
||||
|
||||
const titleComponent = isSelectMediaMode
|
||||
? () => <ItemTitle title={props.title} />
|
||||
: titleComponentOrig;
|
||||
|
||||
const UnderThumbWrapper = isSelectMediaMode ? ItemMain : UnderThumbWrapperOrig;
|
||||
|
||||
function thumbnailComponent() {
|
||||
if (isSelectMediaMode) {
|
||||
// In embed mode, render thumbnail without link
|
||||
const thumbStyle = thumbnailUrl ? { backgroundImage: "url('" + thumbnailUrl + "')" } : null;
|
||||
return (
|
||||
<div
|
||||
key="item-thumb"
|
||||
className={'item-thumb' + (!thumbnailUrl ? ' no-thumb' : '')}
|
||||
style={thumbStyle}
|
||||
>
|
||||
{thumbnailUrl ? (
|
||||
<div key="item-type-icon" className="item-type-icon">
|
||||
<div></div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <MediaItemThumbnailLink src={thumbnailUrl} title={props.title} link={props.link} />;
|
||||
}
|
||||
|
||||
@@ -25,11 +57,13 @@ export function MediaItem(props) {
|
||||
const finalClassname = containerClassname +
|
||||
(props.showSelection ? ' with-selection' : '') +
|
||||
(props.isSelected ? ' selected' : '') +
|
||||
(props.hasAnySelection ? ' has-any-selection' : '');
|
||||
(props.hasAnySelection || isSelectMediaMode ? ' has-any-selection' : '');
|
||||
|
||||
const handleItemClick = (e) => {
|
||||
// If there's any selection active, clicking the item should toggle selection
|
||||
if (props.hasAnySelection && props.onCheckboxChange) {
|
||||
const isSelectMediaMode = inSelectMediaEmbedMode();
|
||||
|
||||
// In select media mode or if there's any selection active, clicking the item should toggle selection
|
||||
if ((isSelectMediaMode || props.hasAnySelection) && props.onCheckboxChange) {
|
||||
// Check if clicking on the checkbox itself, edit icon, or view icon
|
||||
if (e.target.closest('.item-selection-checkbox') ||
|
||||
e.target.closest('.item-edit-icon') ||
|
||||
@@ -59,16 +93,24 @@ export function MediaItem(props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{editMediaComponent()}
|
||||
{viewMediaComponent()}
|
||||
{!isSelectMediaMode && editMediaComponent()}
|
||||
{!isSelectMediaMode && viewMediaComponent()}
|
||||
|
||||
{thumbnailComponent()}
|
||||
|
||||
<UnderThumbWrapper title={props.title} link={props.link}>
|
||||
{titleComponent()}
|
||||
{metaComponents()}
|
||||
{descriptionComponent()}
|
||||
</UnderThumbWrapper>
|
||||
{isSelectMediaMode ? (
|
||||
<UnderThumbWrapper>
|
||||
{titleComponent()}
|
||||
{metaComponents()}
|
||||
{descriptionComponent()}
|
||||
</UnderThumbWrapper>
|
||||
) : (
|
||||
<UnderThumbWrapper title={props.title} link={props.link}>
|
||||
{titleComponent()}
|
||||
{metaComponents()}
|
||||
{descriptionComponent()}
|
||||
</UnderThumbWrapper>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useMediaItem } from '../../utils/hooks/';
|
||||
import { PositiveIntegerOrZero } from '../../utils/helpers/';
|
||||
import { PositiveIntegerOrZero, inSelectMediaEmbedMode } from '../../utils/helpers/';
|
||||
import { MediaDurationInfo } from '../../utils/classes/';
|
||||
import { MediaPlaylistOptions } from '../media-playlist-options/MediaPlaylistOptions';
|
||||
import { MediaItemDuration, MediaItemPlaylistIndex, itemClassname } from './includes/items/';
|
||||
@@ -9,10 +9,26 @@ import { MediaItem } from './MediaItem';
|
||||
|
||||
export function MediaItemAudio(props) {
|
||||
const type = props.type;
|
||||
const isSelectMediaMode = inSelectMediaEmbedMode();
|
||||
|
||||
const [titleComponent, descriptionComponent, thumbnailUrl, UnderThumbWrapper, editMediaComponent, metaComponents, viewMediaComponent] =
|
||||
const [titleComponentOrig, descriptionComponent, thumbnailUrl, UnderThumbWrapperOrig, editMediaComponent, metaComponents, viewMediaComponent] =
|
||||
useMediaItem({ ...props, type });
|
||||
|
||||
// In embed mode, override components to remove links
|
||||
const ItemTitle = ({ title }) => (
|
||||
<h3>
|
||||
<span>{title}</span>
|
||||
</h3>
|
||||
);
|
||||
|
||||
const ItemMain = ({ children }) => <div className="item-main">{children}</div>;
|
||||
|
||||
const titleComponent = isSelectMediaMode
|
||||
? () => <ItemTitle title={props.title} />
|
||||
: titleComponentOrig;
|
||||
|
||||
const UnderThumbWrapper = isSelectMediaMode ? ItemMain : UnderThumbWrapperOrig;
|
||||
|
||||
const _MediaDurationInfo = new MediaDurationInfo();
|
||||
|
||||
_MediaDurationInfo.update(props.duration);
|
||||
@@ -22,6 +38,21 @@ export function MediaItemAudio(props) {
|
||||
const durationISO8601 = _MediaDurationInfo.ISO8601();
|
||||
|
||||
function thumbnailComponent() {
|
||||
if (isSelectMediaMode) {
|
||||
// In embed mode, render thumbnail without link
|
||||
return (
|
||||
<div
|
||||
key="item-thumb"
|
||||
className={'item-thumb' + (!thumbnailUrl ? ' no-thumb' : '')}
|
||||
style={!thumbnailUrl ? null : { backgroundImage: "url('" + thumbnailUrl + "')" }}
|
||||
>
|
||||
{props.inPlaylistView ? null : (
|
||||
<MediaItemDuration ariaLabel={duration} time={durationISO8601} text={durationStr} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const attr = {
|
||||
key: 'item-thumb',
|
||||
href: props.link,
|
||||
@@ -68,11 +99,11 @@ export function MediaItemAudio(props) {
|
||||
const finalClassname = containerClassname +
|
||||
(props.showSelection ? ' with-selection' : '') +
|
||||
(props.isSelected ? ' selected' : '') +
|
||||
(props.hasAnySelection ? ' has-any-selection' : '');
|
||||
(props.hasAnySelection || isSelectMediaMode ? ' has-any-selection' : '');
|
||||
|
||||
const handleItemClick = (e) => {
|
||||
// If there's any selection active, clicking the item should toggle selection
|
||||
if (props.hasAnySelection && props.onCheckboxChange) {
|
||||
// In embed mode or if there's any selection active, clicking the item should toggle selection
|
||||
if ((isSelectMediaMode || props.hasAnySelection) && props.onCheckboxChange) {
|
||||
// Check if clicking on the checkbox itself, edit icon, or view icon
|
||||
if (e.target.closest('.item-selection-checkbox') ||
|
||||
e.target.closest('.item-edit-icon') ||
|
||||
@@ -104,16 +135,24 @@ export function MediaItemAudio(props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{editMediaComponent()}
|
||||
{viewMediaComponent()}
|
||||
{!isSelectMediaMode && editMediaComponent()}
|
||||
{!isSelectMediaMode && viewMediaComponent()}
|
||||
|
||||
{thumbnailComponent()}
|
||||
|
||||
<UnderThumbWrapper title={props.title} link={props.link}>
|
||||
{titleComponent()}
|
||||
{metaComponents()}
|
||||
{descriptionComponent()}
|
||||
</UnderThumbWrapper>
|
||||
{isSelectMediaMode ? (
|
||||
<UnderThumbWrapper>
|
||||
{titleComponent()}
|
||||
{metaComponents()}
|
||||
{descriptionComponent()}
|
||||
</UnderThumbWrapper>
|
||||
) : (
|
||||
<UnderThumbWrapper title={props.title} link={props.link}>
|
||||
{titleComponent()}
|
||||
{metaComponents()}
|
||||
{descriptionComponent()}
|
||||
</UnderThumbWrapper>
|
||||
)}
|
||||
|
||||
{playlistOptionsComponent()}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useMediaItem } from '../../utils/hooks/';
|
||||
import { PositiveIntegerOrZero } from '../../utils/helpers/';
|
||||
import { PositiveIntegerOrZero, inSelectMediaEmbedMode } from '../../utils/helpers/';
|
||||
import { MediaDurationInfo } from '../../utils/classes/';
|
||||
import { MediaPlaylistOptions } from '../media-playlist-options/MediaPlaylistOptions.jsx';
|
||||
import { MediaItemVideoPlayer, MediaItemDuration, MediaItemVideoPreviewer, MediaItemPlaylistIndex, itemClassname } from './includes/items/';
|
||||
@@ -9,10 +9,26 @@ import { MediaItem } from './MediaItem';
|
||||
|
||||
export function MediaItemVideo(props) {
|
||||
const type = props.type;
|
||||
const isSelectMediaMode = inSelectMediaEmbedMode();
|
||||
|
||||
const [titleComponent, descriptionComponent, thumbnailUrl, UnderThumbWrapper, editMediaComponent, metaComponents, viewMediaComponent] =
|
||||
const [titleComponentOrig, descriptionComponent, thumbnailUrl, UnderThumbWrapperOrig, editMediaComponent, metaComponents, viewMediaComponent] =
|
||||
useMediaItem({ ...props, type });
|
||||
|
||||
// In embed mode, override components to remove links
|
||||
const ItemTitle = ({ title }) => (
|
||||
<h3>
|
||||
<span>{title}</span>
|
||||
</h3>
|
||||
);
|
||||
|
||||
const ItemMain = ({ children }) => <div className="item-main">{children}</div>;
|
||||
|
||||
const titleComponent = isSelectMediaMode
|
||||
? () => <ItemTitle title={props.title} />
|
||||
: titleComponentOrig;
|
||||
|
||||
const UnderThumbWrapper = isSelectMediaMode ? ItemMain : UnderThumbWrapperOrig;
|
||||
|
||||
const _MediaDurationInfo = new MediaDurationInfo();
|
||||
|
||||
_MediaDurationInfo.update(props.duration);
|
||||
@@ -26,6 +42,24 @@ export function MediaItemVideo(props) {
|
||||
}
|
||||
|
||||
function thumbnailComponent() {
|
||||
if (isSelectMediaMode) {
|
||||
// In select media mode, render thumbnail without link
|
||||
return (
|
||||
<div
|
||||
key="item-thumb"
|
||||
className={'item-thumb' + (!thumbnailUrl ? ' no-thumb' : '')}
|
||||
style={!thumbnailUrl ? null : { backgroundImage: "url('" + thumbnailUrl + "')" }}
|
||||
>
|
||||
{props.inPlaylistView ? null : (
|
||||
<MediaItemDuration ariaLabel={duration} time={durationISO8601} text={durationStr} />
|
||||
)}
|
||||
{props.inPlaylistView || props.inPlaylistPage ? null : (
|
||||
<MediaItemVideoPreviewer url={props.preview_thumbnail} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const attr = {
|
||||
key: 'item-thumb',
|
||||
href: props.link,
|
||||
@@ -75,11 +109,11 @@ export function MediaItemVideo(props) {
|
||||
const finalClassname = containerClassname +
|
||||
(props.showSelection ? ' with-selection' : '') +
|
||||
(props.isSelected ? ' selected' : '') +
|
||||
(props.hasAnySelection ? ' has-any-selection' : '');
|
||||
(props.hasAnySelection || isSelectMediaMode ? ' has-any-selection' : '');
|
||||
|
||||
const handleItemClick = (e) => {
|
||||
// If there's any selection active, clicking the item should toggle selection
|
||||
if (props.hasAnySelection && props.onCheckboxChange) {
|
||||
// In select media mode or if there's any selection active, clicking the item should toggle selection
|
||||
if ((isSelectMediaMode || props.hasAnySelection) && props.onCheckboxChange) {
|
||||
// Check if clicking on the checkbox itself, edit icon, or view icon
|
||||
if (e.target.closest('.item-selection-checkbox') ||
|
||||
e.target.closest('.item-edit-icon') ||
|
||||
@@ -111,19 +145,27 @@ export function MediaItemVideo(props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{editMediaComponent()}
|
||||
{viewMediaComponent()}
|
||||
{!isSelectMediaMode && editMediaComponent()}
|
||||
{!isSelectMediaMode && viewMediaComponent()}
|
||||
|
||||
{props.hasMediaViewer ? videoViewerComponent() : thumbnailComponent()}
|
||||
|
||||
<UnderThumbWrapper title={props.title} link={props.link}>
|
||||
{titleComponent()}
|
||||
{metaComponents()}
|
||||
{descriptionComponent()}
|
||||
</UnderThumbWrapper>
|
||||
</div>
|
||||
{isSelectMediaMode ? (
|
||||
<UnderThumbWrapper>
|
||||
{titleComponent()}
|
||||
{metaComponents()}
|
||||
{descriptionComponent()}
|
||||
</UnderThumbWrapper>
|
||||
) : (
|
||||
<UnderThumbWrapper title={props.title} link={props.link}>
|
||||
{titleComponent()}
|
||||
{metaComponents()}
|
||||
{descriptionComponent()}
|
||||
</UnderThumbWrapper>
|
||||
)}
|
||||
|
||||
{playlistOptionsComponent()}
|
||||
{playlistOptionsComponent()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,6 +79,10 @@ function EditMediaButton(props) {
|
||||
link = '/edit-media.html';
|
||||
}
|
||||
|
||||
if (link && inEmbeddedApp()) {
|
||||
link += '&mode=lms_embed_mode';
|
||||
}
|
||||
|
||||
return (
|
||||
<a href={link} rel="nofollow" title={translateString('Edit media')} className="edit-media-icon">
|
||||
<i className="material-icons">edit</i>
|
||||
@@ -94,11 +98,18 @@ export default function ViewerInfoContent(props) {
|
||||
!PageStore.get('config-enabled').taxonomies.tags || PageStore.get('config-enabled').taxonomies.tags.enabled
|
||||
? metafield(MediaPageStore.get('media-tags'))
|
||||
: [];
|
||||
let mediaCategories = MediaPageStore.get('media-categories');
|
||||
|
||||
// Filter to show only LMS courses when in embed mode
|
||||
if (inEmbeddedApp()) {
|
||||
mediaCategories = mediaCategories.filter(cat => cat.is_lms_course === true);
|
||||
}
|
||||
|
||||
const categoriesContent = PageStore.get('config-options').pages.media.categoriesWithTitle
|
||||
? []
|
||||
: !PageStore.get('config-enabled').taxonomies.categories ||
|
||||
PageStore.get('config-enabled').taxonomies.categories.enabled
|
||||
? metafield(MediaPageStore.get('media-categories'))
|
||||
? metafield(mediaCategories)
|
||||
: [];
|
||||
|
||||
let summary = MediaPageStore.get('media-summary');
|
||||
@@ -220,9 +231,13 @@ export default function ViewerInfoContent(props) {
|
||||
<MediaMetaField
|
||||
value={categoriesContent}
|
||||
title={
|
||||
1 < categoriesContent.length
|
||||
? translateString('Categories')
|
||||
: translateString('Category')
|
||||
inEmbeddedApp()
|
||||
? (1 < categoriesContent.length
|
||||
? translateString('Courses')
|
||||
: translateString('Course'))
|
||||
: (1 < categoriesContent.length
|
||||
? translateString('Categories')
|
||||
: translateString('Category'))
|
||||
}
|
||||
id="categories"
|
||||
/>
|
||||
@@ -274,7 +289,7 @@ export default function ViewerInfoContent(props) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!inEmbeddedApp() && <CommentsList />}
|
||||
<CommentsList />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { VideoViewerActions } from '../../../utils/actions/';
|
||||
import { SiteContext, SiteConsumer } from '../../../utils/contexts/';
|
||||
import { PageStore, MediaPageStore, VideoViewerStore } from '../../../utils/stores/';
|
||||
import { addClassname, removeClassname, formatInnerLink } from '../../../utils/helpers/';
|
||||
import { addClassname, removeClassname, formatInnerLink, inEmbeddedApp } from '../../../utils/helpers/';
|
||||
import { BrowserCache, UpNextLoaderView, MediaDurationInfo } from '../../../utils/classes/';
|
||||
import {
|
||||
orderedSupportedVideoFormats,
|
||||
@@ -176,11 +176,13 @@ export default class VideoViewer extends React.PureComponent {
|
||||
topLeftHtml = document.createElement('div');
|
||||
topLeftHtml.setAttribute('class', 'media-links-top-left');
|
||||
|
||||
const linkTarget = inEmbeddedApp() || window.location.href.indexOf('lms_embed_mode') > -1 ? '_self' : '_blank';
|
||||
|
||||
if (titleLink) {
|
||||
titleLink.setAttribute('class', 'title-link');
|
||||
titleLink.setAttribute('href', this.props.data.url);
|
||||
titleLink.setAttribute('title', this.props.data.title);
|
||||
titleLink.setAttribute('target', '_blank');
|
||||
titleLink.setAttribute('target', linkTarget);
|
||||
titleLink.innerHTML = this.props.data.title;
|
||||
}
|
||||
|
||||
@@ -191,7 +193,7 @@ export default class VideoViewer extends React.PureComponent {
|
||||
formatInnerLink(this.props.data.author_profile, this.props.siteUrl)
|
||||
);
|
||||
userThumbLink.setAttribute('title', this.props.data.author_name);
|
||||
userThumbLink.setAttribute('target', '_blank');
|
||||
userThumbLink.setAttribute('target', linkTarget);
|
||||
userThumbLink.setAttribute(
|
||||
'style',
|
||||
'background-image:url(' +
|
||||
|
||||
@@ -656,30 +656,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.fixed-nav {
|
||||
.profile-info-nav-wrap {
|
||||
padding-bottom: $_authorPage-navHeight;
|
||||
}
|
||||
|
||||
.profile-nav {
|
||||
z-index: 3;
|
||||
position: fixed;
|
||||
top: var(--header-height);
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.visible-sidebar & {
|
||||
padding-left: var(--sidebar-width);
|
||||
}
|
||||
|
||||
.sliding-sidebar & {
|
||||
transition-property: padding-left;
|
||||
transition-duration: 0.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-main {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { LinksContext, MemberContext, SiteContext } from '../../utils/contexts/'
|
||||
import { PageStore, ProfilePageStore } from '../../utils/stores/';
|
||||
import { PageActions, ProfilePageActions } from '../../utils/actions/';
|
||||
import { CircleIconButton, PopupMain } from '../_shared';
|
||||
import { translateString } from '../../utils/helpers/';
|
||||
import { translateString, inEmbeddedApp, inSelectMediaEmbedMode, isSelectMediaMode } from '../../utils/helpers/';
|
||||
|
||||
class ProfileSearchBar extends React.PureComponent {
|
||||
constructor(props) {
|
||||
@@ -372,30 +372,47 @@ class NavMenuInlineTabs extends React.PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const isSelectMediaMode = inSelectMediaEmbedMode();
|
||||
|
||||
// Append action=select_media to links when in select mode
|
||||
const mediaLink = isSelectMediaMode
|
||||
? `${LinksContext._currentValue.profile.media}${LinksContext._currentValue.profile.media.includes('?') ? '&' : '?'}action=select_media`
|
||||
: LinksContext._currentValue.profile.media;
|
||||
|
||||
const sharedByMeLink = isSelectMediaMode
|
||||
? `${LinksContext._currentValue.profile.shared_by_me}${LinksContext._currentValue.profile.shared_by_me.includes('?') ? '&' : '?'}action=select_media`
|
||||
: LinksContext._currentValue.profile.shared_by_me;
|
||||
|
||||
const sharedWithMeLink = isSelectMediaMode
|
||||
? `${LinksContext._currentValue.profile.shared_with_me}${LinksContext._currentValue.profile.shared_with_me.includes('?') ? '&' : '?'}action=select_media`
|
||||
: LinksContext._currentValue.profile.shared_with_me;
|
||||
|
||||
return (
|
||||
<nav ref="tabsNav" className="profile-nav items-list-outer list-inline list-slider">
|
||||
<div className="profile-nav-inner items-list-outer">
|
||||
{this.state.displayPrev ? this.previousBtn : null}
|
||||
|
||||
<ul className="items-list-wrap" ref="itemsListWrap">
|
||||
<InlineTab
|
||||
id="about"
|
||||
isActive={'about' === this.props.type}
|
||||
label={translateString('About')}
|
||||
link={LinksContext._currentValue.profile.about}
|
||||
/>
|
||||
{!isSelectMediaMode && !inEmbeddedApp() ? (
|
||||
<InlineTab
|
||||
id="about"
|
||||
isActive={'about' === this.props.type}
|
||||
label={translateString('About')}
|
||||
link={LinksContext._currentValue.profile.about}
|
||||
/>
|
||||
) : null}
|
||||
<InlineTab
|
||||
id="media"
|
||||
isActive={'media' === this.props.type}
|
||||
label={translateString(this.userIsAuthor ? 'Media I own' : 'Media')}
|
||||
link={LinksContext._currentValue.profile.media}
|
||||
link={mediaLink}
|
||||
/>
|
||||
{this.userIsAuthor ? (
|
||||
<InlineTab
|
||||
id="shared_by_me"
|
||||
isActive={'shared_by_me' === this.props.type}
|
||||
label={translateString('Shared by me')}
|
||||
link={LinksContext._currentValue.profile.shared_by_me}
|
||||
link={sharedByMeLink}
|
||||
/>
|
||||
) : null}
|
||||
{this.userIsAuthor ? (
|
||||
@@ -403,11 +420,11 @@ class NavMenuInlineTabs extends React.PureComponent {
|
||||
id="shared_with_me"
|
||||
isActive={'shared_with_me' === this.props.type}
|
||||
label={translateString('Shared with me')}
|
||||
link={LinksContext._currentValue.profile.shared_with_me}
|
||||
link={sharedWithMeLink}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{MemberContext._currentValue.can.saveMedia ? (
|
||||
{!isSelectMediaMode && MemberContext._currentValue.can.saveMedia ? (
|
||||
<InlineTab
|
||||
id="playlists"
|
||||
isActive={'playlists' === this.props.type}
|
||||
@@ -606,12 +623,6 @@ export default function ProfilePagesHeader(props) {
|
||||
const profilePageHeaderRef = useRef(null);
|
||||
const profileNavRef = useRef(null);
|
||||
|
||||
const [fixedNav, setFixedNav] = useState(false);
|
||||
|
||||
const positions = {
|
||||
profileNavTop: 0,
|
||||
};
|
||||
|
||||
const userIsAdmin = !MemberContext._currentValue.is.anonymous && MemberContext._currentValue.is.admin;
|
||||
const userIsAuthor =
|
||||
!MemberContext._currentValue.is.anonymous &&
|
||||
@@ -623,18 +634,6 @@ export default function ProfilePagesHeader(props) {
|
||||
userIsAuthor ||
|
||||
(!MemberContext._currentValue.is.anonymous && MemberContext._currentValue.can.deleteProfile);
|
||||
|
||||
function updateProfileNavTopPosition() {
|
||||
positions.profileHeaderTop = profilePageHeaderRef.current.offsetTop;
|
||||
positions.profileNavTop =
|
||||
positions.profileHeaderTop +
|
||||
profilePageHeaderRef.current.offsetHeight -
|
||||
profileNavRef.current.refs.tabsNav.offsetHeight;
|
||||
}
|
||||
|
||||
function updateFixedNavPosition() {
|
||||
setFixedNav(positions.profileHeaderTop + window.scrollY > positions.profileNavTop);
|
||||
}
|
||||
|
||||
function cancelProfileRemoval() {
|
||||
popupContentRef.current.toggle();
|
||||
}
|
||||
@@ -669,42 +668,26 @@ export default function ProfilePagesHeader(props) {
|
||||
}
|
||||
}
|
||||
|
||||
function onWindowResize() {
|
||||
updateProfileNavTopPosition();
|
||||
updateFixedNavPosition();
|
||||
}
|
||||
|
||||
function onWindowScroll() {
|
||||
updateFixedNavPosition();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (userCanDeleteProfile) {
|
||||
ProfilePageStore.on('profile_delete', onProfileDelete);
|
||||
ProfilePageStore.on('profile_delete_fail', onProfileDeleteFail);
|
||||
}
|
||||
|
||||
PageStore.on('resize', onWindowResize);
|
||||
PageStore.on('changed_page_sidebar_visibility', onWindowResize);
|
||||
PageStore.on('window_scroll', onWindowScroll);
|
||||
|
||||
updateProfileNavTopPosition();
|
||||
updateFixedNavPosition();
|
||||
|
||||
return () => {
|
||||
if (userCanDeleteProfile) {
|
||||
ProfilePageStore.removeListener('profile_delete', onProfileDelete);
|
||||
ProfilePageStore.removeListener('profile_delete_fail', onProfileDeleteFail);
|
||||
}
|
||||
|
||||
PageStore.removeListener('resize', onWindowResize);
|
||||
PageStore.removeListener('changed_page_sidebar_visibility', onWindowResize);
|
||||
PageStore.removeListener('window_scroll', onWindowScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={profilePageHeaderRef} className={'profile-page-header' + (fixedNav ? ' fixed-nav' : '')}>
|
||||
<div
|
||||
ref={profilePageHeaderRef}
|
||||
className={'profile-page-header'}
|
||||
{...(isSelectMediaMode() ? { 'data-action': 'select_media' } : {})}
|
||||
>
|
||||
{!props.hideChannelBanner && (
|
||||
<span className="profile-banner-wrap">
|
||||
{props.author.banner_thumbnail_url ? (
|
||||
@@ -768,7 +751,7 @@ export default function ProfilePagesHeader(props) {
|
||||
)}
|
||||
|
||||
<div className="profile-info-nav-wrap">
|
||||
{props.author.thumbnail_url || props.author.name ? (
|
||||
{!inEmbeddedApp() && (props.author.thumbnail_url || props.author.name) ? (
|
||||
<div className="profile-info">
|
||||
<div className="profile-info-inner">
|
||||
<div>
|
||||
|
||||
@@ -3,14 +3,14 @@ import { ApiUrlConsumer } from '../utils/contexts/';
|
||||
import { MediaListWrapper } from '../components/MediaListWrapper';
|
||||
import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListAsync.jsx';
|
||||
import { Page } from './Page';
|
||||
import { translateString } from '../utils/helpers/';
|
||||
import { translateString, inEmbeddedApp } from '../utils/helpers/';
|
||||
|
||||
interface CategoriesPageProps {
|
||||
id?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export const CategoriesPage: React.FC<CategoriesPageProps> = ({ id = 'categories', title = translateString('Categories') }) => (
|
||||
export const CategoriesPage: React.FC<CategoriesPageProps> = ({ id = 'categories', title = inEmbeddedApp() ? translateString('Courses') : translateString('Categories') }) => (
|
||||
<Page id={id}>
|
||||
<ApiUrlConsumer>
|
||||
{(apiUrl) => (
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { ApiUrlContext, LinksConsumer, MemberContext } from '../utils/contexts';
|
||||
import { PageStore, ProfilePageStore } from '../utils/stores';
|
||||
import { ProfilePageActions, PageActions } from '../utils/actions';
|
||||
import { inEmbeddedApp, translateString } from '../utils/helpers/';
|
||||
import { inEmbeddedApp, inSelectMediaEmbedMode, translateString } from '../utils/helpers/';
|
||||
import { MediaListWrapper } from '../components/MediaListWrapper';
|
||||
import ProfilePagesHeader from '../components/profile-page/ProfilePagesHeader';
|
||||
import ProfilePagesContent from '../components/profile-page/ProfilePagesContent';
|
||||
@@ -202,13 +202,45 @@ export class ProfileMediaPage extends Page {
|
||||
}
|
||||
|
||||
handleMediaSelection(mediaId, isSelected) {
|
||||
const isSelectMediaMode = inSelectMediaEmbedMode();
|
||||
|
||||
this.setState((prevState) => {
|
||||
const newSelectedMedia = new Set(prevState.selectedMedia);
|
||||
if (isSelected) {
|
||||
newSelectedMedia.add(mediaId);
|
||||
const newSelectedMedia = new Set();
|
||||
|
||||
// In select media mode, only allow single selection
|
||||
if (isSelectMediaMode) {
|
||||
if (isSelected) {
|
||||
newSelectedMedia.add(mediaId);
|
||||
console.log('Selected media item:', mediaId);
|
||||
|
||||
// Send postMessage to parent window (Moodle TinyMCE plugin)
|
||||
if (window.parent !== window) {
|
||||
// Construct the embed URL
|
||||
const baseUrl = window.location.origin;
|
||||
const embedUrl = `${baseUrl}/embed?m=${mediaId}`;
|
||||
|
||||
// Send message in the format expected by the Moodle plugin
|
||||
window.parent.postMessage({
|
||||
type: 'videoSelected',
|
||||
embedUrl: embedUrl,
|
||||
videoId: mediaId
|
||||
}, '*');
|
||||
|
||||
console.log('Sent postMessage to parent:', { embedUrl, videoId: mediaId });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newSelectedMedia.delete(mediaId);
|
||||
// Normal mode: allow multiple selection
|
||||
newSelectedMedia.clear();
|
||||
prevState.selectedMedia.forEach((id) => newSelectedMedia.add(id));
|
||||
|
||||
if (isSelected) {
|
||||
newSelectedMedia.add(mediaId);
|
||||
} else {
|
||||
newSelectedMedia.delete(mediaId);
|
||||
}
|
||||
}
|
||||
|
||||
return { selectedMedia: newSelectedMedia };
|
||||
});
|
||||
}
|
||||
@@ -917,6 +949,7 @@ export class ProfileMediaPage extends Page {
|
||||
const authorData = ProfilePageStore.get('author-data');
|
||||
|
||||
const isMediaAuthor = authorData && authorData.username === MemberContext._currentValue.username;
|
||||
const isSelectMediaMode = inSelectMediaEmbedMode();
|
||||
|
||||
// Check if any filters are active (excluding default sort and tags)
|
||||
const hasActiveFilters =
|
||||
@@ -948,15 +981,16 @@ export class ProfileMediaPage extends Page {
|
||||
this.state.author ? (
|
||||
<ProfilePagesContent key="ProfilePagesContent">
|
||||
<MediaListWrapper
|
||||
title={this.state.title}
|
||||
title={inEmbeddedApp() ? undefined : this.state.title}
|
||||
className="items-list-ver"
|
||||
showBulkActions={isMediaAuthor}
|
||||
style={inEmbeddedApp() ? { marginTop: '24px' } : undefined}
|
||||
showBulkActions={!isSelectMediaMode && isMediaAuthor}
|
||||
selectedCount={this.state.selectedMedia.size}
|
||||
totalCount={this.state.availableMediaIds.length}
|
||||
onBulkAction={this.handleBulkAction}
|
||||
onSelectAll={this.handleSelectAll}
|
||||
onDeselectAll={this.handleDeselectAll}
|
||||
showAddMediaButton={isMediaAuthor}
|
||||
showAddMediaButton={!isSelectMediaMode && isMediaAuthor}
|
||||
>
|
||||
<ProfileMediaFilters
|
||||
hidden={this.state.hiddenFilters}
|
||||
@@ -979,7 +1013,7 @@ export class ProfileMediaPage extends Page {
|
||||
hideViews={!PageStore.get('config-media-item').displayViews}
|
||||
hideDate={!PageStore.get('config-media-item').displayPublishDate}
|
||||
canEdit={isMediaAuthor}
|
||||
showSelection={isMediaAuthor}
|
||||
showSelection={isMediaAuthor || isSelectMediaMode}
|
||||
hasAnySelection={this.state.selectedMedia.size > 0}
|
||||
selectedMedia={this.state.selectedMedia}
|
||||
onMediaSelection={this.handleMediaSelection}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ProfileMediaFilters } from '../components/search-filters/ProfileMediaFi
|
||||
import { ProfileMediaTags } from '../components/search-filters/ProfileMediaTags';
|
||||
import { ProfileMediaSorting } from '../components/search-filters/ProfileMediaSorting';
|
||||
import { BulkActionsModals } from '../components/BulkActionsModals';
|
||||
import { inEmbeddedApp, translateString } from '../utils/helpers';
|
||||
import { inEmbeddedApp, inSelectMediaEmbedMode } from '../utils/helpers';
|
||||
import { withBulkActions } from '../utils/hoc/withBulkActions';
|
||||
|
||||
import { Page } from './_Page';
|
||||
@@ -51,6 +51,7 @@ class ProfileSharedByMePage extends Page {
|
||||
availableTags: [],
|
||||
selectedTag: 'all',
|
||||
selectedSort: 'date_added_desc',
|
||||
selectedMedia: new Set(), // For select media mode
|
||||
};
|
||||
|
||||
this.authorDataLoad = this.authorDataLoad.bind(this);
|
||||
@@ -64,6 +65,7 @@ class ProfileSharedByMePage extends Page {
|
||||
this.onTagSelect = this.onTagSelect.bind(this);
|
||||
this.onSortSelect = this.onSortSelect.bind(this);
|
||||
this.onResponseDataLoaded = this.onResponseDataLoaded.bind(this);
|
||||
this.handleMediaSelection = this.handleMediaSelection.bind(this);
|
||||
|
||||
ProfilePageStore.on('load-author-data', this.authorDataLoad);
|
||||
}
|
||||
@@ -343,10 +345,55 @@ class ProfileSharedByMePage extends Page {
|
||||
}
|
||||
}
|
||||
|
||||
handleMediaSelection(mediaId, isSelected) {
|
||||
const isSelectMediaMode = inSelectMediaEmbedMode();
|
||||
|
||||
this.setState((prevState) => {
|
||||
const newSelectedMedia = new Set();
|
||||
|
||||
// In select media mode, only allow single selection
|
||||
if (isSelectMediaMode) {
|
||||
if (isSelected) {
|
||||
newSelectedMedia.add(mediaId);
|
||||
console.log('Selected media item:', mediaId);
|
||||
|
||||
// Send postMessage to parent window (Moodle TinyMCE plugin)
|
||||
if (window.parent !== window) {
|
||||
// Construct the embed URL
|
||||
const baseUrl = window.location.origin;
|
||||
const embedUrl = `${baseUrl}/embed?m=${mediaId}`;
|
||||
|
||||
// Send message in the format expected by the Moodle plugin
|
||||
window.parent.postMessage({
|
||||
type: 'videoSelected',
|
||||
embedUrl: embedUrl,
|
||||
videoId: mediaId
|
||||
}, '*');
|
||||
|
||||
console.log('Sent postMessage to parent:', { embedUrl, videoId: mediaId });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Normal mode: should not reach here as bulk actions handle this
|
||||
newSelectedMedia.clear();
|
||||
prevState.selectedMedia.forEach((id) => newSelectedMedia.add(id));
|
||||
|
||||
if (isSelected) {
|
||||
newSelectedMedia.add(mediaId);
|
||||
} else {
|
||||
newSelectedMedia.delete(mediaId);
|
||||
}
|
||||
}
|
||||
|
||||
return { selectedMedia: newSelectedMedia };
|
||||
});
|
||||
}
|
||||
|
||||
pageContent() {
|
||||
const authorData = ProfilePageStore.get('author-data');
|
||||
|
||||
const isMediaAuthor = authorData && authorData.username === MemberContext._currentValue.username;
|
||||
const isSelectMediaMode = inSelectMediaEmbedMode();
|
||||
|
||||
// Check if any filters are active
|
||||
const hasActiveFilters =
|
||||
@@ -375,11 +422,12 @@ class ProfileSharedByMePage extends Page {
|
||||
this.state.author ? (
|
||||
<ProfilePagesContent key="ProfilePagesContent">
|
||||
<MediaListWrapper
|
||||
title={this.state.title}
|
||||
title={inEmbeddedApp() ? undefined : this.state.title}
|
||||
className="items-list-ver"
|
||||
showBulkActions={isMediaAuthor}
|
||||
selectedCount={this.props.bulkActions.selectedMedia.size}
|
||||
totalCount={this.props.bulkActions.availableMediaIds.length}
|
||||
style={inEmbeddedApp() ? { marginTop: '24px' } : undefined}
|
||||
showBulkActions={!isSelectMediaMode && isMediaAuthor}
|
||||
selectedCount={isSelectMediaMode ? this.state.selectedMedia.size : this.props.bulkActions.selectedMedia.size}
|
||||
totalCount={isSelectMediaMode ? 0 : this.props.bulkActions.availableMediaIds.length}
|
||||
onBulkAction={this.props.bulkActions.handleBulkAction}
|
||||
onSelectAll={this.props.bulkActions.handleSelectAll}
|
||||
onDeselectAll={this.props.bulkActions.handleDeselectAll}
|
||||
@@ -396,19 +444,19 @@ class ProfileSharedByMePage extends Page {
|
||||
/>
|
||||
<ProfileMediaSorting hidden={this.state.hiddenSorting} onSortSelect={this.onSortSelect} />
|
||||
<LazyLoadItemListAsync
|
||||
key={`${this.state.requestUrl}-${this.props.bulkActions.listKey}`}
|
||||
key={isSelectMediaMode ? this.state.requestUrl : `${this.state.requestUrl}-${this.props.bulkActions.listKey}`}
|
||||
requestUrl={this.state.requestUrl}
|
||||
hideAuthor={true}
|
||||
itemsCountCallback={this.state.requestUrl ? this.getCountFunc : null}
|
||||
hideViews={!PageStore.get('config-media-item').displayViews}
|
||||
hideDate={!PageStore.get('config-media-item').displayPublishDate}
|
||||
canEdit={isMediaAuthor}
|
||||
canEdit={!isSelectMediaMode && isMediaAuthor}
|
||||
onResponseDataLoaded={this.onResponseDataLoaded}
|
||||
showSelection={isMediaAuthor}
|
||||
hasAnySelection={this.props.bulkActions.selectedMedia.size > 0}
|
||||
selectedMedia={this.props.bulkActions.selectedMedia}
|
||||
onMediaSelection={this.props.bulkActions.handleMediaSelection}
|
||||
onItemsUpdate={this.props.bulkActions.handleItemsUpdate}
|
||||
showSelection={isMediaAuthor || isSelectMediaMode}
|
||||
hasAnySelection={isSelectMediaMode ? this.state.selectedMedia.size > 0 : this.props.bulkActions.selectedMedia.size > 0}
|
||||
selectedMedia={isSelectMediaMode ? this.state.selectedMedia : this.props.bulkActions.selectedMedia}
|
||||
onMediaSelection={isSelectMediaMode ? this.handleMediaSelection : this.props.bulkActions.handleMediaSelection}
|
||||
onItemsUpdate={!isSelectMediaMode ? this.props.bulkActions.handleItemsUpdate : undefined}
|
||||
/>
|
||||
{isMediaAuthor && 0 === this.state.channelMediaCount && !this.state.query ? (
|
||||
<EmptySharedByMe name={this.state.author.name} />
|
||||
@@ -416,7 +464,7 @@ class ProfileSharedByMePage extends Page {
|
||||
</MediaListWrapper>
|
||||
</ProfilePagesContent>
|
||||
) : null,
|
||||
this.state.author && isMediaAuthor ? (
|
||||
this.state.author && isMediaAuthor && !isSelectMediaMode ? (
|
||||
<BulkActionsModals
|
||||
key="BulkActionsModals"
|
||||
{...this.props.bulkActions}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListA
|
||||
import { ProfileMediaFilters } from '../components/search-filters/ProfileMediaFilters';
|
||||
import { ProfileMediaTags } from '../components/search-filters/ProfileMediaTags';
|
||||
import { ProfileMediaSorting } from '../components/search-filters/ProfileMediaSorting';
|
||||
import { inEmbeddedApp, translateString } from '../utils/helpers';
|
||||
import { inEmbeddedApp, inSelectMediaEmbedMode } from '../utils/helpers';
|
||||
|
||||
import { Page } from './_Page';
|
||||
|
||||
@@ -49,6 +49,7 @@ export class ProfileSharedWithMePage extends Page {
|
||||
availableTags: [],
|
||||
selectedTag: 'all',
|
||||
selectedSort: 'date_added_desc',
|
||||
selectedMedia: new Set(), // For select media mode
|
||||
};
|
||||
|
||||
this.authorDataLoad = this.authorDataLoad.bind(this);
|
||||
@@ -62,6 +63,7 @@ export class ProfileSharedWithMePage extends Page {
|
||||
this.onTagSelect = this.onTagSelect.bind(this);
|
||||
this.onSortSelect = this.onSortSelect.bind(this);
|
||||
this.onResponseDataLoaded = this.onResponseDataLoaded.bind(this);
|
||||
this.handleMediaSelection = this.handleMediaSelection.bind(this);
|
||||
|
||||
ProfilePageStore.on('load-author-data', this.authorDataLoad);
|
||||
}
|
||||
@@ -341,10 +343,55 @@ export class ProfileSharedWithMePage extends Page {
|
||||
}
|
||||
}
|
||||
|
||||
handleMediaSelection(mediaId, isSelected) {
|
||||
const isSelectMediaMode = inSelectMediaEmbedMode();
|
||||
|
||||
this.setState((prevState) => {
|
||||
const newSelectedMedia = new Set();
|
||||
|
||||
// In select media mode, only allow single selection
|
||||
if (isSelectMediaMode) {
|
||||
if (isSelected) {
|
||||
newSelectedMedia.add(mediaId);
|
||||
console.log('Selected media item:', mediaId);
|
||||
|
||||
// Send postMessage to parent window (Moodle TinyMCE plugin)
|
||||
if (window.parent !== window) {
|
||||
// Construct the embed URL
|
||||
const baseUrl = window.location.origin;
|
||||
const embedUrl = `${baseUrl}/embed?m=${mediaId}`;
|
||||
|
||||
// Send message in the format expected by the Moodle plugin
|
||||
window.parent.postMessage({
|
||||
type: 'videoSelected',
|
||||
embedUrl: embedUrl,
|
||||
videoId: mediaId
|
||||
}, '*');
|
||||
|
||||
console.log('Sent postMessage to parent:', { embedUrl, videoId: mediaId });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Normal mode: no selection UI in this page normally
|
||||
newSelectedMedia.clear();
|
||||
prevState.selectedMedia.forEach((id) => newSelectedMedia.add(id));
|
||||
|
||||
if (isSelected) {
|
||||
newSelectedMedia.add(mediaId);
|
||||
} else {
|
||||
newSelectedMedia.delete(mediaId);
|
||||
}
|
||||
}
|
||||
|
||||
return { selectedMedia: newSelectedMedia };
|
||||
});
|
||||
}
|
||||
|
||||
pageContent() {
|
||||
const authorData = ProfilePageStore.get('author-data');
|
||||
|
||||
const isMediaAuthor = authorData && authorData.username === MemberContext._currentValue.username;
|
||||
const isSelectMediaMode = inSelectMediaEmbedMode();
|
||||
|
||||
// Check if any filters are active
|
||||
const hasActiveFilters =
|
||||
@@ -372,7 +419,11 @@ export class ProfileSharedWithMePage extends Page {
|
||||
) : null,
|
||||
this.state.author ? (
|
||||
<ProfilePagesContent key="ProfilePagesContent">
|
||||
<MediaListWrapper title={this.state.title} className="items-list-ver">
|
||||
<MediaListWrapper
|
||||
title={inEmbeddedApp() ? undefined : this.state.title}
|
||||
className="items-list-ver"
|
||||
style={inEmbeddedApp() ? { marginTop: '24px' } : undefined}
|
||||
>
|
||||
<ProfileMediaFilters
|
||||
hidden={this.state.hiddenFilters}
|
||||
tags={this.state.availableTags}
|
||||
@@ -393,6 +444,10 @@ export class ProfileSharedWithMePage extends Page {
|
||||
hideDate={!PageStore.get('config-media-item').displayPublishDate}
|
||||
canEdit={false}
|
||||
onResponseDataLoaded={this.onResponseDataLoaded}
|
||||
showSelection={isSelectMediaMode}
|
||||
hasAnySelection={this.state.selectedMedia.size > 0}
|
||||
selectedMedia={this.state.selectedMedia}
|
||||
onMediaSelection={this.handleMediaSelection}
|
||||
/>
|
||||
{isMediaAuthor && 0 === this.state.channelMediaCount && !this.state.query ? (
|
||||
<EmptySharedWithMe name={this.state.author.name} />
|
||||
|
||||
@@ -7,7 +7,7 @@ import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListA
|
||||
import { SearchMediaFiltersRow } from '../components/search-filters/SearchMediaFiltersRow';
|
||||
import { SearchResultsFilters } from '../components/search-filters/SearchResultsFilters';
|
||||
import { Page } from './_Page';
|
||||
import { translateString } from '../utils/helpers/';
|
||||
import { translateString, inEmbeddedApp } from '../utils/helpers/';
|
||||
|
||||
export class SearchPage extends Page {
|
||||
constructor(props) {
|
||||
@@ -115,7 +115,7 @@ export class SearchPage extends Page {
|
||||
} else {
|
||||
if (this.state.searchCategories) {
|
||||
title = null === this.state.resultsCount || 0 === this.state.resultsCount ? 'No' : this.state.resultsCount;
|
||||
title += ' ' + translateString('media in category') + ' "' + this.state.searchCategories + '"';
|
||||
title += ' ' + translateString(inEmbeddedApp() ? 'media in course' : 'media in category') + ' "' + this.state.searchCategories + '"';
|
||||
} else if (this.state.searchTags) {
|
||||
title = null === this.state.resultsCount || 0 === this.state.resultsCount ? 'No' : this.state.resultsCount;
|
||||
title += ' ' + translateString('media in tag') + ' "' + this.state.searchTags + '"';
|
||||
|
||||
@@ -3,18 +3,49 @@ export function inEmbeddedApp() {
|
||||
const params = new URL(globalThis.location.href).searchParams;
|
||||
const mode = params.get('mode');
|
||||
|
||||
if (mode === 'embed_mode') {
|
||||
sessionStorage.setItem('media_cms_embed_mode', 'true');
|
||||
if (mode === 'lms_embed_mode') {
|
||||
sessionStorage.setItem('lms_embed_mode', 'true');
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
if (mode === 'standard') {
|
||||
sessionStorage.removeItem('media_cms_embed_mode');
|
||||
sessionStorage.removeItem('lms_embed_mode');
|
||||
return false;
|
||||
}
|
||||
|
||||
return sessionStorage.getItem('media_cms_embed_mode') === 'true';
|
||||
return sessionStorage.getItem('lms_embed_mode') === 'true';
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isSelectMediaMode() {
|
||||
try {
|
||||
const params = new URL(globalThis.location.href).searchParams;
|
||||
const action = params.get('action');
|
||||
|
||||
return action === 'select_media';
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function inSelectMediaEmbedMode() {
|
||||
return inEmbeddedApp() && isSelectMediaMode();
|
||||
}
|
||||
|
||||
export function getLtiContextId(): string | null {
|
||||
try {
|
||||
const params = new URL(globalThis.location.href).searchParams;
|
||||
const contextId = params.get('lti_context_id');
|
||||
|
||||
if (contextId) {
|
||||
sessionStorage.setItem('lti_context_id', contextId);
|
||||
return contextId;
|
||||
}
|
||||
|
||||
return sessionStorage.getItem('lti_context_id');
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,4 +14,4 @@ export * from './quickSort';
|
||||
export * from './requests';
|
||||
export { translateString } from './translate';
|
||||
export { replaceString } from './replacementStrings';
|
||||
export * from './embeddedApp';
|
||||
export { inEmbeddedApp, inSelectMediaEmbedMode, isSelectMediaMode } from './embeddedApp';
|
||||
|
||||
@@ -195,13 +195,18 @@ class MediaPageStore extends EventEmitter {
|
||||
this.emit('loaded_media_data');
|
||||
}
|
||||
|
||||
this.loadPlaylists();
|
||||
if (MediaCMS.features.media.actions.comment_mention === true) {
|
||||
this.loadUsers();
|
||||
}
|
||||
// Skip loading playlists and comments when in embed mode (to reduce API calls)
|
||||
const isEmbedMode = window.location.pathname.startsWith('/embed');
|
||||
|
||||
if (this.mediacms_config.member.can.readComment) {
|
||||
this.loadComments();
|
||||
if (!isEmbedMode) {
|
||||
this.loadPlaylists();
|
||||
if (MediaCMS.features.media.actions.comment_mention === true) {
|
||||
this.loadUsers();
|
||||
}
|
||||
|
||||
if (this.mediacms_config.member.can.readComment) {
|
||||
this.loadComments();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
1745
frontend/yarn.lock
1745
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
302
install-rhel.sh
302
install-rhel.sh
@@ -1,302 +0,0 @@
|
||||
#!/bin/bash
|
||||
# should be run as root on a rhel8-like system
|
||||
|
||||
function update_permissions
|
||||
{
|
||||
# fix permissions of /srv/mediacms directory
|
||||
chown -R nginx:root $1
|
||||
}
|
||||
|
||||
echo "Welcome to the MediacMS installation!";
|
||||
|
||||
if [ `id -u` -ne 0 ]; then
|
||||
echo "Please run as root user"
|
||||
exit
|
||||
fi
|
||||
|
||||
|
||||
while true; do
|
||||
read -p "
|
||||
This script will attempt to perform a system update, install required dependencies, and configure PostgreSQL, NGINX, Redis and a few other utilities.
|
||||
It is expected to run on a new system **with no running instances of any these services**. Make sure you check the script before you continue. Then enter y or n
|
||||
" yn
|
||||
case $yn in
|
||||
[Yy]* ) echo "OK!"; break;;
|
||||
[Nn]* ) echo "Have a great day"; exit;;
|
||||
* ) echo "Please answer y or n.";;
|
||||
esac
|
||||
done
|
||||
|
||||
# update configuration files
|
||||
|
||||
sed -i 's/\/home\/mediacms\.io\/mediacms\/Bento4-SDK-1-6-0-637\.x86_64-unknown-linux\/bin\/mp4hls/\/srv\/mediacms\/bento4\/bin\/mp4hls/g' cms/settings.py
|
||||
sed -i 's/www-data/nginx/g;s/\/home\/mediacms\.io\/mediacms\/logs/\/var\/log\/mediacms/g;s/\/home\/mediacms\.io\/mediacms/\/srv\/mediacms/g;s/\/home\/mediacms\.io\/bin/\/srv\/mediacms\/virtualenv\/bin/g' deploy/local_install/celery_*.service
|
||||
sed -i 's/\/home\/mediacms\.io\/mediacms/\/srv\/mediacms/g' deploy/local_install/mediacms.io
|
||||
sed -i 's/\/home\/mediacms\.io\/bin/\/srv\/mediacms\/virtualenv\/bin/g;s/\/home\/mediacms\.io\/mediacms/\/srv\/mediacms/g' deploy/local_install/mediacms.service
|
||||
sed -i 's/\/home\/mediacms\.io\/mediacms/\/var\/log\/mediacms/g' deploy/local_install/mediacms_logrorate
|
||||
sed -i 's/www-data/nginx/g' deploy/local_install/nginx.conf
|
||||
sed -i 's/www-data/nginx/g;s/\/home\/mediacms\.io\/mediacms\/logs/\/var\/log\/mediacms/g;s/\/home\/mediacms\.io\/mediacms/\/srv\/mediacms/g;s/\/home\/mediacms\.io/\/srv\/mediacms\/virtualenv/g' deploy/local_install/uwsgi.ini
|
||||
|
||||
osVersion=
|
||||
|
||||
if [[ -f /etc/os-release ]]; then
|
||||
osVersion=$(grep ^ID /etc/os-release)
|
||||
fi
|
||||
|
||||
if [[ $osVersion == *"fedora"* ]] || [[ $osVersion == *"rhel"* ]] || [[ $osVersion == *"centos"* ]] || [[ *"rocky"* ]]; then
|
||||
dnf install -y epel-release https://mirrors.rpmfusion.org/free/el/rpmfusion-free-release-8.noarch.rpm yum-utils
|
||||
yum-config-manager --enable powertools
|
||||
dnf install -y python3-virtualenv python39-devel redis postgresql postgresql-server nginx git gcc vim unzip ImageMagick python3-certbot-nginx certbot wget xz ffmpeg policycoreutils-devel cmake gcc gcc-c++ wget git bsdtar
|
||||
else
|
||||
echo "unsupported or unknown os"
|
||||
exit -1
|
||||
fi
|
||||
|
||||
# fix permissions of /srv/mediacms directory
|
||||
update_permissions /srv/mediacms/
|
||||
|
||||
read -p "Enter portal URL, or press enter for localhost : " FRONTEND_HOST
|
||||
read -p "Enter portal name, or press enter for 'MediaCMS : " PORTAL_NAME
|
||||
|
||||
[ -z "$PORTAL_NAME" ] && PORTAL_NAME='MediaCMS'
|
||||
[ -z "$FRONTEND_HOST" ] && FRONTEND_HOST='localhost'
|
||||
|
||||
echo "Configuring postgres"
|
||||
if [ ! command -v postgresql-setup > /dev/null 2>&1 ]; then
|
||||
echo "Something went wrong, the command 'postgresql-setup' was not found in the system path."
|
||||
exit -1
|
||||
fi
|
||||
|
||||
postgresql-setup --initdb
|
||||
|
||||
# set authentication method for mediacms user to scram-sha-256
|
||||
sed -i 's/.*password_encryption.*/password_encryption = scram-sha-256/' /var/lib/pgsql/data/postgresql.conf
|
||||
sed -i '/# IPv4 local connections:/a host\tmediacms\tmediacms\t127.0.0.1/32\tscram-sha-256' /var/lib/pgsql/data/pg_hba.conf
|
||||
|
||||
systemctl enable postgresql.service --now
|
||||
|
||||
su -c "psql -c \"CREATE DATABASE mediacms\"" postgres
|
||||
su -c "psql -c \"CREATE USER mediacms WITH ENCRYPTED PASSWORD 'mediacms'\"" postgres
|
||||
su -c "psql -c \"GRANT ALL PRIVILEGES ON DATABASE mediacms TO mediacms\"" postgres
|
||||
|
||||
echo 'Creating python virtualenv on /srv/mediacms/virtualenv/'
|
||||
|
||||
mkdir /srv/mediacms/virtualenv/
|
||||
cd /srv/mediacms/virtualenv/
|
||||
virtualenv . --python=python3
|
||||
source /srv/mediacms/virtualenv/bin/activate
|
||||
cd /srv/mediacms/
|
||||
pip install -r requirements.txt
|
||||
|
||||
systemctl enable redis.service --now
|
||||
|
||||
SECRET_KEY=`python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'`
|
||||
|
||||
# remove http or https prefix
|
||||
FRONTEND_HOST=`echo "$FRONTEND_HOST" | sed -r 's/http:\/\///g'`
|
||||
FRONTEND_HOST=`echo "$FRONTEND_HOST" | sed -r 's/https:\/\///g'`
|
||||
|
||||
FRONTEND_HOST_HTTP_PREFIX='http://'$FRONTEND_HOST
|
||||
|
||||
echo 'FRONTEND_HOST='\'"$FRONTEND_HOST_HTTP_PREFIX"\' >> cms/local_settings.py
|
||||
echo 'PORTAL_NAME='\'"$PORTAL_NAME"\' >> cms/local_settings.py
|
||||
echo "SSL_FRONTEND_HOST = FRONTEND_HOST.replace('http', 'https')" >> cms/local_settings.py
|
||||
|
||||
echo 'SECRET_KEY='\'"$SECRET_KEY"\' >> cms/local_settings.py
|
||||
echo "LOCAL_INSTALL = True" >> cms/local_settings.py
|
||||
|
||||
mkdir /var/log/mediacms/
|
||||
mkdir pids
|
||||
|
||||
update_permissions /var/log/mediacms/
|
||||
|
||||
python manage.py migrate
|
||||
python manage.py loaddata fixtures/encoding_profiles.json
|
||||
python manage.py loaddata fixtures/categories.json
|
||||
python manage.py collectstatic --noinput
|
||||
|
||||
ADMIN_PASS=`python -c "import secrets;chars = 'abcdefghijklmnopqrstuvwxyz0123456789';print(''.join(secrets.choice(chars) for i in range(10)))"`
|
||||
echo "from users.models import User; User.objects.create_superuser('admin', 'admin@example.com', '$ADMIN_PASS')" | python manage.py shell
|
||||
|
||||
echo "from django.contrib.sites.models import Site; Site.objects.update(name='$FRONTEND_HOST', domain='$FRONTEND_HOST')" | python manage.py shell
|
||||
|
||||
update_permissions /srv/mediacms/
|
||||
|
||||
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
|
||||
cp deploy/local_install/mediacms.service /etc/systemd/system/mediacms.service
|
||||
|
||||
mkdir -p /etc/letsencrypt/live/$FRONTEND_HOST
|
||||
mkdir -p /etc/nginx/sites-enabled
|
||||
mkdir -p /etc/nginx/sites-available
|
||||
mkdir -p /etc/nginx/dhparams/
|
||||
rm -rf /etc/nginx/conf.d/default.conf
|
||||
rm -rf /etc/nginx/sites-enabled/default
|
||||
cp deploy/local_install/mediacms.io_fullchain.pem /etc/letsencrypt/live/$FRONTEND_HOST/fullchain.pem
|
||||
cp deploy/local_install/mediacms.io_privkey.pem /etc/letsencrypt/live/$FRONTEND_HOST/privkey.pem
|
||||
cp deploy/local_install/mediacms.io /etc/nginx/sites-available/mediacms.io
|
||||
ln -s /etc/nginx/sites-available/mediacms.io /etc/nginx/sites-enabled/mediacms.io
|
||||
cp deploy/local_install/uwsgi_params /etc/nginx/sites-enabled/uwsgi_params
|
||||
cp deploy/local_install/nginx.conf /etc/nginx/
|
||||
|
||||
# attempt to get a valid certificate for specified domain
|
||||
while true ; do
|
||||
echo "Would you like to run [c]ertbot, or [s]kip?"
|
||||
read -p " : " certbotConfig
|
||||
|
||||
case $certbotConfig in
|
||||
[cC*] )
|
||||
if [ "$FRONTEND_HOST" != "localhost" ]; then
|
||||
systemctl start
|
||||
echo 'attempt to get a valid certificate for specified url $FRONTEND_HOST'
|
||||
certbot --nginx -n --agree-tos --register-unsafely-without-email -d $FRONTEND_HOST
|
||||
certbot --nginx -n --agree-tos --register-unsafely-without-email -d $FRONTEND_HOST
|
||||
# unfortunately for some reason it needs to be run two times in order to create the entries
|
||||
# and directory structure!!!
|
||||
systemctl stop nginx
|
||||
|
||||
# Generate individual DH params
|
||||
openssl dhparam -out /etc/nginx/dhparams/dhparams.pem 4096
|
||||
fi
|
||||
|
||||
break
|
||||
;;
|
||||
[sS*] )
|
||||
echo "will not call certbot utility to update ssl certificate for url 'localhost', using default ssl certificate"
|
||||
cp deploy/local_install/dhparams.pem /etc/nginx/dhparams/dhparams.pem
|
||||
|
||||
break
|
||||
;;
|
||||
* )
|
||||
echo "Unknown option: $certbotConfig"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# configure bento4 utility installation, for HLS
|
||||
while true ; do
|
||||
echo "Configuring Bento4"
|
||||
echo "Would you like to [d]ownload a pre-compiled bento4 binary, or [b]uild it now?"
|
||||
read -p "b/d : " bentoConfig
|
||||
|
||||
case $bentoConfig in
|
||||
[bB*] )
|
||||
echo "Building bento4 from source"
|
||||
git clone -b v1.6.0-640 https://github.com/axiomatic-systems/Bento4 /srv/mediacms/bento4
|
||||
cd /srv/mediacms/bento4/
|
||||
mkdir bin
|
||||
cd /srv/mediacms/bento4/bin/
|
||||
cmake -DCMAKE_BUILD_TYPE=Release ..
|
||||
make -j$(nproc)
|
||||
|
||||
chmod +x ../Source/Python/utils/mp4-hls.py
|
||||
|
||||
echo -e '#!/bin/bash' >> mp4hls
|
||||
echo -e 'BASEDIR=$(pwd)' >> mp4hls
|
||||
echo -e 'exec python3 "$BASEDIR/../Source/Python/utils/mp4-hls.py"' >> mp4hls
|
||||
|
||||
chmod +x mp4hls
|
||||
|
||||
break
|
||||
;;
|
||||
[dD*] )
|
||||
cd /srv/mediacms/
|
||||
wget http://zebulon.bok.net/Bento4/binaries/Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip
|
||||
bsdtar -xf Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip -s '/Bento4-SDK-1-6-0-637.x86_64-unknown-linux/bento4/'
|
||||
|
||||
break
|
||||
;;
|
||||
* )
|
||||
echo "Unknown option: $bentoConfig"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
mkdir /srv/mediacms/media_files/hls
|
||||
|
||||
# update permissions
|
||||
|
||||
update_permissions /srv/mediacms/
|
||||
|
||||
# configure selinux
|
||||
|
||||
while true ; do
|
||||
echo "Configuring SELinux"
|
||||
echo "Would you like to [d]isable SELinux until next reboot, [c]onfigure our SELinux module, or [s]kip and not do any SELinux confgiguration?"
|
||||
read -p "d/c/s : " seConfig
|
||||
|
||||
case $seConfig in
|
||||
[Dd]* )
|
||||
echo "Disabling SELinux until next reboot"
|
||||
break
|
||||
;;
|
||||
[Cc]* )
|
||||
echo "Configuring custom mediacms selinux module"
|
||||
|
||||
semanage fcontext -a -t bin_t /srv/mediacms/virtualenv/bin/
|
||||
semanage fcontext -a -t httpd_sys_content_t "/srv/mediacms(/.*)?"
|
||||
restorecon -FRv /srv/mediacms/
|
||||
|
||||
sebools=(httpd_can_network_connect httpd_graceful_shutdown httpd_can_network_relay nis_enabled httpd_setrlimit domain_can_mmap_files)
|
||||
|
||||
for bool in "${sebools[@]}"
|
||||
do
|
||||
setsebool -P $bool 1
|
||||
done
|
||||
|
||||
cd /srv/mediacms/deploy/local_install/
|
||||
make -f /usr/share/selinux/devel/Makefile selinux-mediacms.pp
|
||||
semodule -i selinux-mediacms.pp
|
||||
|
||||
break
|
||||
;;
|
||||
[Ss]* )
|
||||
echo "Skipping SELinux configuration"
|
||||
break
|
||||
;;
|
||||
* )
|
||||
echo "Unknown option: $seConfig"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# configure firewall
|
||||
if command -v firewall-cmd > /dev/null 2>&1 ; then
|
||||
while true ; do
|
||||
echo "Configuring firewall"
|
||||
echo "Would you like to configure http, https, or skip and not do any firewall configuration?"
|
||||
read -p "http/https/skip : " fwConfig
|
||||
|
||||
case $fwConfig in
|
||||
http )
|
||||
echo "Opening port 80 until next reboot"
|
||||
firewall-cmd --add-port=80/tcp
|
||||
break
|
||||
;;
|
||||
https )
|
||||
echo "Opening port 443 permanently"
|
||||
firewall-cmd --add-port=443/tcp --permanent
|
||||
firewall-cmd --reload
|
||||
break
|
||||
;;
|
||||
skip )
|
||||
echo "Skipping firewall configuration"
|
||||
break
|
||||
;;
|
||||
* )
|
||||
echo "Unknown option: $fwConfig"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
fi
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl start celery_long.service
|
||||
systemctl start celery_short.service
|
||||
systemctl start celery_beat.service
|
||||
systemctl start mediacms.service
|
||||
systemctl start nginx.service
|
||||
|
||||
echo 'MediaCMS installation completed, open browser on http://'"$FRONTEND_HOST"' and login with user admin and password '"$ADMIN_PASS"''
|
||||
140
install.sh
140
install.sh
@@ -1,140 +0,0 @@
|
||||
#!/bin/bash
|
||||
# should be run as root and only on Ubuntu 20/22, Debian 10/11 (Buster/Bullseye) versions!
|
||||
echo "Welcome to the MediacMS installation!";
|
||||
|
||||
if [ `id -u` -ne 0 ]
|
||||
then echo "Please run as root"
|
||||
exit
|
||||
fi
|
||||
|
||||
|
||||
while true; do
|
||||
read -p "
|
||||
This script will attempt to perform a system update and install services including PostgreSQL, nginx and Django.
|
||||
It is expected to run on a new system **with no running instances of any these services**.
|
||||
This has been tested only in Ubuntu Linux 22 and 24. Make sure you check the script before you continue. Then enter yes or no
|
||||
" yn
|
||||
case $yn in
|
||||
[Yy]* ) echo "OK!"; break;;
|
||||
[Nn]* ) echo "Have a great day"; exit;;
|
||||
* ) echo "Please answer yes or no.";;
|
||||
esac
|
||||
done
|
||||
|
||||
apt-get update && apt-get -y upgrade && apt-get install pkg-config python3-venv python3-dev virtualenv redis-server postgresql nginx git gcc vim unzip imagemagick procps libxml2-dev libxmlsec1-dev libxmlsec1-openssl python3-certbot-nginx certbot wget xz-utils -y
|
||||
|
||||
# install ffmpeg
|
||||
echo "Downloading and installing ffmpeg"
|
||||
wget -q https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz
|
||||
mkdir -p tmp
|
||||
tar -xf ffmpeg-release-amd64-static.tar.xz --strip-components 1 -C tmp
|
||||
cp -v tmp/{ffmpeg,ffprobe,qt-faststart} /usr/local/bin
|
||||
rm -rf tmp ffmpeg-release-amd64-static.tar.xz
|
||||
echo "ffmpeg installed to /usr/local/bin"
|
||||
|
||||
read -p "Enter portal URL, or press enter for localhost : " FRONTEND_HOST
|
||||
read -p "Enter portal name, or press enter for 'MediaCMS : " PORTAL_NAME
|
||||
|
||||
[ -z "$PORTAL_NAME" ] && PORTAL_NAME='MediaCMS'
|
||||
[ -z "$FRONTEND_HOST" ] && FRONTEND_HOST='localhost'
|
||||
|
||||
echo 'Creating database to be used in MediaCMS'
|
||||
|
||||
su -c "psql -c \"CREATE DATABASE mediacms\"" postgres
|
||||
su -c "psql -c \"CREATE USER mediacms WITH ENCRYPTED PASSWORD 'mediacms'\"" postgres
|
||||
su -c "psql -c \"GRANT ALL PRIVILEGES ON DATABASE mediacms TO mediacms\"" postgres
|
||||
su -c "psql -d mediacms -c \"GRANT CREATE, USAGE ON SCHEMA public TO mediacms\"" postgres
|
||||
|
||||
echo 'Creating python virtualenv on /home/mediacms.io'
|
||||
|
||||
cd /home/mediacms.io
|
||||
virtualenv . --python=python3
|
||||
source /home/mediacms.io/bin/activate
|
||||
cd mediacms
|
||||
pip install --no-binary lxml,xmlsec -r requirements.txt
|
||||
|
||||
SECRET_KEY=`python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'`
|
||||
|
||||
# remove http or https prefix
|
||||
FRONTEND_HOST=`echo "$FRONTEND_HOST" | sed -r 's/http:\/\///g'`
|
||||
FRONTEND_HOST=`echo "$FRONTEND_HOST" | sed -r 's/https:\/\///g'`
|
||||
|
||||
sed -i s/localhost/$FRONTEND_HOST/g deploy/local_install/mediacms.io
|
||||
|
||||
FRONTEND_HOST_HTTP_PREFIX='http://'$FRONTEND_HOST
|
||||
|
||||
echo 'FRONTEND_HOST='\'"$FRONTEND_HOST_HTTP_PREFIX"\' >> cms/local_settings.py
|
||||
echo 'PORTAL_NAME='\'"$PORTAL_NAME"\' >> cms/local_settings.py
|
||||
echo "SSL_FRONTEND_HOST = FRONTEND_HOST.replace('http', 'https')" >> cms/local_settings.py
|
||||
|
||||
echo 'SECRET_KEY='\'"$SECRET_KEY"\' >> cms/local_settings.py
|
||||
echo "LOCAL_INSTALL = True" >> cms/local_settings.py
|
||||
|
||||
mkdir logs
|
||||
mkdir pids
|
||||
python manage.py migrate
|
||||
python manage.py loaddata fixtures/encoding_profiles.json
|
||||
python manage.py loaddata fixtures/categories.json
|
||||
python manage.py collectstatic --noinput
|
||||
|
||||
ADMIN_PASS=`python -c "import secrets;chars = 'abcdefghijklmnopqrstuvwxyz0123456789';print(''.join(secrets.choice(chars) for i in range(10)))"`
|
||||
echo "from users.models import User; User.objects.create_superuser('admin', 'admin@example.com', '$ADMIN_PASS')" | python manage.py shell
|
||||
|
||||
echo "from django.contrib.sites.models import Site; Site.objects.update(name='$FRONTEND_HOST', domain='$FRONTEND_HOST')" | python manage.py shell
|
||||
|
||||
chown -R www-data. /home/mediacms.io/
|
||||
cp deploy/local_install/celery_long.service /etc/systemd/system/celery_long.service && systemctl enable celery_long && systemctl start celery_long
|
||||
cp deploy/local_install/celery_short.service /etc/systemd/system/celery_short.service && systemctl enable celery_short && systemctl start celery_short
|
||||
cp deploy/local_install/celery_beat.service /etc/systemd/system/celery_beat.service && systemctl enable celery_beat &&systemctl start celery_beat
|
||||
cp deploy/local_install/mediacms.service /etc/systemd/system/mediacms.service && systemctl enable mediacms.service && systemctl start mediacms.service
|
||||
|
||||
mkdir -p /etc/letsencrypt/live/mediacms.io/
|
||||
mkdir -p /etc/letsencrypt/live/$FRONTEND_HOST
|
||||
mkdir -p /etc/nginx/sites-enabled
|
||||
mkdir -p /etc/nginx/sites-available
|
||||
mkdir -p /etc/nginx/dhparams/
|
||||
rm -rf /etc/nginx/conf.d/default.conf
|
||||
rm -rf /etc/nginx/sites-enabled/default
|
||||
cp deploy/local_install/mediacms.io_fullchain.pem /etc/letsencrypt/live/$FRONTEND_HOST/fullchain.pem
|
||||
cp deploy/local_install/mediacms.io_privkey.pem /etc/letsencrypt/live/$FRONTEND_HOST/privkey.pem
|
||||
cp deploy/local_install/dhparams.pem /etc/nginx/dhparams/dhparams.pem
|
||||
cp deploy/local_install/mediacms.io /etc/nginx/sites-available/mediacms.io
|
||||
ln -s /etc/nginx/sites-available/mediacms.io /etc/nginx/sites-enabled/mediacms.io
|
||||
cp deploy/local_install/uwsgi_params /etc/nginx/sites-enabled/uwsgi_params
|
||||
cp deploy/local_install/nginx.conf /etc/nginx/
|
||||
systemctl stop nginx
|
||||
systemctl start nginx
|
||||
|
||||
# attempt to get a valid certificate for specified domain
|
||||
|
||||
if [ "$FRONTEND_HOST" != "localhost" ]; then
|
||||
echo 'attempt to get a valid certificate for specified url $FRONTEND_HOST'
|
||||
certbot --nginx -n --agree-tos --register-unsafely-without-email -d $FRONTEND_HOST
|
||||
certbot --nginx -n --agree-tos --register-unsafely-without-email -d $FRONTEND_HOST
|
||||
# unfortunately for some reason it needs to be run two times in order to create the entries
|
||||
# and directory structure!!!
|
||||
systemctl restart nginx
|
||||
else
|
||||
echo "will not call certbot utility to update ssl certificate for url 'localhost', using default ssl certificate"
|
||||
fi
|
||||
|
||||
# Generate individual DH params
|
||||
if [ "$FRONTEND_HOST" != "localhost" ]; then
|
||||
# Only generate new DH params when using "real" certificates.
|
||||
openssl dhparam -out /etc/nginx/dhparams/dhparams.pem 4096
|
||||
systemctl restart nginx
|
||||
else
|
||||
echo "will not generate new DH params for url 'localhost', using default DH params"
|
||||
fi
|
||||
|
||||
# Bento4 utility installation, for HLS
|
||||
|
||||
cd /home/mediacms.io/mediacms
|
||||
wget http://zebulon.bok.net/Bento4/binaries/Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip
|
||||
unzip Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip
|
||||
mkdir /home/mediacms.io/mediacms/media_files/hls
|
||||
|
||||
# last, set default owner
|
||||
chown -R www-data. /home/mediacms.io/
|
||||
|
||||
echo 'MediaCMS installation completed, open browser on http://'"$FRONTEND_HOST"' and login with user admin and password '"$ADMIN_PASS"''
|
||||
49
lms-plugins/mediacms-moodle/Gruntfile.js
Normal file
49
lms-plugins/mediacms-moodle/Gruntfile.js
Normal file
@@ -0,0 +1,49 @@
|
||||
module.exports = function(grunt) {
|
||||
var path = require('path');
|
||||
|
||||
grunt.initConfig({
|
||||
babel: {
|
||||
options: {
|
||||
sourceMap: true,
|
||||
presets: ['@babel/preset-env'],
|
||||
plugins: ['@babel/plugin-transform-modules-amd'],
|
||||
moduleIds: true,
|
||||
getModuleId: function(moduleName) {
|
||||
// moduleName is the absolute or relative path to the file.
|
||||
// We need to convert 'tiny/mediacms/amd/src/filename' to 'tiny_mediacms/filename'
|
||||
var filename = path.basename(moduleName);
|
||||
return 'tiny_mediacms/' + filename;
|
||||
}
|
||||
},
|
||||
dist: {
|
||||
files: [{
|
||||
expand: true,
|
||||
cwd: 'tiny/mediacms/amd/src',
|
||||
src: ['*.js'],
|
||||
dest: 'tiny/mediacms/amd/build',
|
||||
ext: '.js'
|
||||
}]
|
||||
}
|
||||
},
|
||||
uglify: {
|
||||
options: {
|
||||
sourceMap: true,
|
||||
output: { comments: false }
|
||||
},
|
||||
dist: {
|
||||
files: [{
|
||||
expand: true,
|
||||
cwd: 'tiny/mediacms/amd/build',
|
||||
src: ['*.js', '!*.min.js'],
|
||||
dest: 'tiny/mediacms/amd/build',
|
||||
ext: '.min.js'
|
||||
}]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
grunt.loadNpmTasks('grunt-babel');
|
||||
grunt.loadNpmTasks('grunt-contrib-uglify');
|
||||
|
||||
grunt.registerTask('default', ['babel', 'uglify']);
|
||||
};
|
||||
72
lms-plugins/mediacms-moodle/INSTALL.txt
Normal file
72
lms-plugins/mediacms-moodle/INSTALL.txt
Normal file
@@ -0,0 +1,72 @@
|
||||
================================================================================
|
||||
MediaCMS Moodle Plugin Suite v1.0.0
|
||||
Installation Guide
|
||||
================================================================================
|
||||
|
||||
Requirements
|
||||
------------
|
||||
- Moodle 4.5 or later
|
||||
- MediaCMS instance
|
||||
- MediaCMS set as External Tool
|
||||
|
||||
|
||||
Installation
|
||||
------------------
|
||||
|
||||
1. Extract zip file to Moodle root public directory:
|
||||
|
||||
cd /var/www/moodle/public
|
||||
unzip mediacms-moodle-v1.0.0.zip
|
||||
|
||||
This will place files in:
|
||||
- filter/mediacms/
|
||||
- lib/editor/tiny/plugins/mediacms/
|
||||
|
||||
2. Set permissions
|
||||
chown -R www-data:www-data filter/mediacms
|
||||
chown -R www-data:www-data lib/editor/tiny/plugins/mediacms
|
||||
|
||||
3. Install through Moodle
|
||||
- Log in as Administrator
|
||||
- Go to: Site Administration → Notifications
|
||||
- Click "Upgrade Moodle database now"
|
||||
- Both plugins will be installed automatically
|
||||
- Set the MediaCMS tool under the LTI Tool
|
||||
|
||||
4. Make sure Filter is enabled
|
||||
- As Administrator, visit Plugins, 'Manage Filters', find MediaCMS filter and enable it.
|
||||
Then place it at the top of the filter. This is important, otherwise embeds won't load.
|
||||
|
||||
5. Enter 'My Media' on top navigation.
|
||||
- Log in as Administrator
|
||||
- Go to: Site Administration → Appearance → Advanced Theme settings -> Custom menu items:
|
||||
add: My Media|/filter/mediacms/my_media.php
|
||||
Notes: User needs to be enrolled on one course at least, otherwise visiting My Media won't show anything.
|
||||
The plugin needs to use the ID of an existing course to the 'My Media' page and thus it is going to use it
|
||||
and create a 'my media' page on the course, if it doesn't already exist. This is happening automatically the
|
||||
first time a user visits the global 'my media' link, if a 'my media' page activity does not exist in the course, the
|
||||
plugin will create it. If you don't want the 'my media' page to show up on the course, you can place it to a hidden part
|
||||
Create a part (eg 'Hidden Links'), select the Edit options (three vertical dots) and Hide it.
|
||||
Then place the 'My Media' activity there, select the three dots for the activity, Availability, and set "Make available but don't show on course page: Available to students if you provide a link."
|
||||
This option won't appear if the part is not hidden, so first you want to create the part, hide it, and then edit the setting for 'my media'
|
||||
|
||||
To summarize, for the 'my media' global link on top, a 'my media' is also required in each course, and you can either
|
||||
create it (as MediaCMS activity with any name) or let the first user that visits the global 'my media' have it created automatically.
|
||||
If you don't want this activity to be visible in the course, place it under a hidden part and set it's availability as "Make available but don't show on course page", which is only possible for items inside hidden parts.
|
||||
|
||||
|
||||
|
||||
What to expect
|
||||
-------
|
||||
|
||||
1. Create a test course
|
||||
2. Add a page or label
|
||||
3. Click MediaCMS button in TinyMCE editor
|
||||
4. Try inserting from video library or pasting a URL
|
||||
|
||||
SUPPORT
|
||||
-------
|
||||
Issues: https://github.com/mediacms-io/mediacms/issues
|
||||
Docs: https://docs.mediacms.io
|
||||
|
||||
================================================================================
|
||||
90
lms-plugins/mediacms-moodle/README.md
Normal file
90
lms-plugins/mediacms-moodle/README.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# MediaCMS for Moodle
|
||||
|
||||
**Version:** 1.0.0 | **Release Date:** 2026-02-12 | **Moodle:** 4.5+
|
||||
|
||||
This package provides complete MediaCMS integration for Moodle, consisting of two plugins that work together with **unified settings**:
|
||||
|
||||
1. **Filter Plugin (filter_mediacms):**
|
||||
* Handles LTI 1.3 authentication and secure video launches
|
||||
* Auto-converts MediaCMS URLs to embedded players
|
||||
* **Provides core settings** (MediaCMS URL, LTI Tool ID) used by both plugins
|
||||
* **Location:** `filter/mediacms`
|
||||
|
||||
2. **Editor Plugin (tiny_mediacms):**
|
||||
* Adds MediaCMS button to TinyMCE editor
|
||||
* Browse authenticated video library via LTI Deep Linking
|
||||
* Configure embed options (dimensions, display, start time)
|
||||
* **Reads core settings** from filter plugin
|
||||
* **Location:** `lib/editor/tiny/plugins/mediacms`
|
||||
|
||||
## Installation
|
||||
|
||||
This package is distributed as a single repository but contains two distinct Moodle plugins that must be installed in their respective directories.
|
||||
|
||||
### 1. Copy Files
|
||||
|
||||
Copy the directories into your Moodle installation as follows (example assuming Moodle is at `/var/www/moodle/public`):
|
||||
|
||||
* Copy `filter/mediacms` to `/var/www/moodle/public/filter/mediacms`.
|
||||
* Copy `tiny/mediacms` to `/var/www/moodle/public/lib/editor/tiny/plugins/mediacms`.
|
||||
|
||||
### 2. Set Permissions
|
||||
|
||||
Ensure the web server user (typically `www-data`) has ownership of the new directories:
|
||||
|
||||
```bash
|
||||
# Example for Ubuntu/Debian systems
|
||||
chown -R www-data:www-data /var/www/moodle/public/filter/mediacms
|
||||
chown -R www-data:www-data /var/www/moodle/public/lib/editor/tiny/plugins/mediacms
|
||||
chmod -R 755 /var/www/moodle/public/filter/mediacms
|
||||
chmod -R 755 /var/www/moodle/public/lib/editor/tiny/plugins/mediacms
|
||||
```
|
||||
|
||||
### 3. Install Plugins
|
||||
|
||||
1. Log in to Moodle as an Administrator.
|
||||
2. Go to **Site administration > Notifications**.
|
||||
3. Follow the prompts to upgrade the database and install the new plugins.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Step 1: Core Settings (Required) - Configure Once
|
||||
|
||||
Go to **Site administration > Plugins > Filters > MediaCMS** (Settings)
|
||||
|
||||
* **MediaCMS URL:** Enter your MediaCMS instance URL (e.g., `https://lti.mediacms.io`)
|
||||
* **LTI Tool:** Select the External Tool configuration for MediaCMS
|
||||
* *First create an LTI 1.3 tool at: Site administration > Plugins > Activity modules > External tool > Manage tools*
|
||||
|
||||
> **✨ Note:** These core settings are automatically used by **both** the filter and TinyMCE editor plugin.
|
||||
|
||||
### Step 2: Enable Filter
|
||||
|
||||
1. Go to **Site administration > Plugins > Filters > Manage filters**
|
||||
2. Set **MediaCMS** to "On"
|
||||
|
||||
### Step 3: Configure Auto-convert Defaults (Optional)
|
||||
|
||||
Go to **Site administration > Plugins > Text editors > TinyMCE editor > MediaCMS settings**
|
||||
|
||||
Configure default display options for auto-converted URLs:
|
||||
* Show video title
|
||||
* Link video title
|
||||
* Show related videos
|
||||
* Show user avatar
|
||||
|
||||
> **Note:** The core settings (URL, LTI Tool) are managed in the filter plugin settings.
|
||||
|
||||
## Usage
|
||||
|
||||
### For Teachers (Editor)
|
||||
|
||||
1. In any text editor (TinyMCE), click the **MediaCMS** icon (or "Insert MediaCMS Media" from the Insert menu).
|
||||
2. You can:
|
||||
* **Paste a URL:** Paste a View or Embed URL.
|
||||
* **Video Library:** Click the "Video Library" tab to browse and select videos (requires LTI Deep Linking configuration).
|
||||
3. The video will appear as a placeholder or iframe in the editor.
|
||||
|
||||
### For Students (Display)
|
||||
|
||||
When content is viewed, the Filter will ensure the video is loaded securely via LTI 1.3, authenticating the user with MediaCMS automatically.
|
||||
89
lms-plugins/mediacms-moodle/build.sh
Executable file
89
lms-plugins/mediacms-moodle/build.sh
Executable file
@@ -0,0 +1,89 @@
|
||||
#!/bin/bash
|
||||
|
||||
# MediaCMS Moodle Plugin Suite - Build Script
|
||||
# Creates distributable ZIP package
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${GREEN}======================================${NC}"
|
||||
echo -e "${GREEN}MediaCMS Moodle Plugin Suite Builder${NC}"
|
||||
echo -e "${GREEN}======================================${NC}"
|
||||
echo
|
||||
|
||||
# Configuration
|
||||
VERSION="1.0.0"
|
||||
BUILD_DATE=$(date +%Y%m%d)
|
||||
PACKAGE_NAME="mediacms-moodle-v${VERSION}"
|
||||
DIST_DIR="dist"
|
||||
BUILD_DIR="${DIST_DIR}/${PACKAGE_NAME}"
|
||||
|
||||
# Create clean dist directory
|
||||
echo -e "${YELLOW}→${NC} Cleaning dist directory..."
|
||||
rm -rf "${DIST_DIR}"
|
||||
mkdir -p "${BUILD_DIR}"
|
||||
|
||||
# Copy filter plugin
|
||||
echo -e "${YELLOW}→${NC} Copying filter plugin..."
|
||||
mkdir -p "${BUILD_DIR}/filter"
|
||||
cp -r filter/mediacms "${BUILD_DIR}/filter/"
|
||||
|
||||
# Copy TinyMCE plugin
|
||||
echo -e "${YELLOW}→${NC} Copying TinyMCE plugin..."
|
||||
mkdir -p "${BUILD_DIR}/lib/editor/tiny/plugins"
|
||||
cp -r tiny/mediacms "${BUILD_DIR}/lib/editor/tiny/plugins/"
|
||||
|
||||
# Copy documentation
|
||||
echo -e "${YELLOW}→${NC} Copying documentation..."
|
||||
cp README.md "${BUILD_DIR}/filter/mediacms/"
|
||||
cp INSTALL.txt "${BUILD_DIR}/filter/mediacms/"
|
||||
|
||||
# Clean up development files
|
||||
echo -e "${YELLOW}→${NC} Removing development files..."
|
||||
find "${BUILD_DIR}" -type d -name "node_modules" -exec rm -rf {} + 2>/dev/null || true
|
||||
find "${BUILD_DIR}" -type f -name ".DS_Store" -delete 2>/dev/null || true
|
||||
find "${BUILD_DIR}" -type f -name "*.log" -delete 2>/dev/null || true
|
||||
find "${BUILD_DIR}" -type d -name ".git" -exec rm -rf {} + 2>/dev/null || true
|
||||
find "${BUILD_DIR}" -type f -name ".gitignore" -delete 2>/dev/null || true
|
||||
|
||||
# Remove AMD source files (keep only built versions)
|
||||
echo -e "${YELLOW}→${NC} Cleaning AMD source files..."
|
||||
find "${BUILD_DIR}/lib/editor/tiny/plugins/mediacms/amd" -type f -name "*.js" ! -name "*-lazy.js" ! -path "*/build/*" -delete 2>/dev/null || true
|
||||
|
||||
# Create ZIP archive
|
||||
echo -e "${YELLOW}→${NC} Creating ZIP archive..."
|
||||
cd "${BUILD_DIR}"
|
||||
zip -r "../${PACKAGE_NAME}.zip" . -q
|
||||
cd ../..
|
||||
|
||||
# Create checksum
|
||||
echo -e "${YELLOW}→${NC} Generating checksum..."
|
||||
cd "${DIST_DIR}"
|
||||
sha256sum "${PACKAGE_NAME}.zip" > "${PACKAGE_NAME}.zip.sha256"
|
||||
cd ..
|
||||
|
||||
# Display results
|
||||
ZIP_SIZE=$(du -h "${DIST_DIR}/${PACKAGE_NAME}.zip" | cut -f1)
|
||||
echo
|
||||
echo -e "${GREEN}✓ Build complete!${NC}"
|
||||
echo
|
||||
echo "Package: ${DIST_DIR}/${PACKAGE_NAME}.zip"
|
||||
echo "Size: ${ZIP_SIZE}"
|
||||
echo "Checksum: ${DIST_DIR}/${PACKAGE_NAME}.zip.sha256"
|
||||
echo
|
||||
echo -e "${YELLOW}Contents:${NC}"
|
||||
echo " - filter/mediacms/ (includes docs)"
|
||||
echo " - lib/editor/tiny/plugins/mediacms/"
|
||||
echo
|
||||
echo -e "${GREEN}Ready for distribution!${NC}"
|
||||
echo
|
||||
|
||||
# Show checksum
|
||||
echo -e "${YELLOW}SHA256 Checksum:${NC}"
|
||||
cat "${DIST_DIR}/${PACKAGE_NAME}.zip.sha256"
|
||||
echo
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/**
|
||||
* Hook listener for filter_mediacms navigation hooks (Moodle 4.3+)
|
||||
*
|
||||
* @package filter_mediacms
|
||||
* @copyright 2026 MediaCMS
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace filter_mediacms;
|
||||
|
||||
/**
|
||||
* Extends the primary (top) navigation bar with a My Media link.
|
||||
*/
|
||||
class hook_listener {
|
||||
|
||||
/**
|
||||
* Called by the \core\hook\navigation\primary_extend hook.
|
||||
* Adds the My Media link to the primary nav bar when placement = 'top'.
|
||||
*/
|
||||
public static function extend_primary_navigation(
|
||||
\core\hook\navigation\primary_extend $hook
|
||||
): void {
|
||||
$placement = get_config('filter_mediacms', 'mymedia_placement');
|
||||
if ($placement !== 'top') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isloggedin() || isguestuser()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$url = new \moodle_url('/filter/mediacms/my_media.php');
|
||||
$node = \navigation_node::create(
|
||||
get_string('mymedia', 'filter_mediacms'),
|
||||
$url,
|
||||
\navigation_node::TYPE_CUSTOM,
|
||||
null,
|
||||
'mediacms_mymedia',
|
||||
new \pix_icon('i/media', '')
|
||||
);
|
||||
|
||||
$primarynav = $hook->get_primarynav();
|
||||
if ($primarynav === null) {
|
||||
return;
|
||||
}
|
||||
$primarynav->add_node($node);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace filter_mediacms\privacy;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
class provider implements \core_privacy\local\metadata\null_provider {
|
||||
public static function get_reason(): string {
|
||||
return 'privacy:metadata';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
namespace filter_mediacms;
|
||||
|
||||
use moodle_url;
|
||||
use html_writer;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* MediaCMS text filter.
|
||||
*
|
||||
* @package filter_mediacms
|
||||
* @copyright 2026 MediaCMS
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class text_filter extends \core_filters\text_filter {
|
||||
|
||||
/**
|
||||
* Filter method.
|
||||
*
|
||||
* @param string $text The text to filter.
|
||||
* @param array $options Filter options.
|
||||
* @return string The filtered text.
|
||||
*/
|
||||
public function filter($text, array $options = array()) {
|
||||
if (!is_string($text) or empty($text)) {
|
||||
return $text;
|
||||
}
|
||||
|
||||
$mediacmsurl = get_config('filter_mediacms', 'mediacmsurl');
|
||||
if (empty($mediacmsurl)) {
|
||||
return $text;
|
||||
}
|
||||
|
||||
$newtext = $text;
|
||||
|
||||
// 1. Handle [mediacms:TOKEN] tag
|
||||
$pattern_tag = '/\[mediacms:([a-zA-Z0-9]+)\]/';
|
||||
$newtext = preg_replace_callback($pattern_tag, [$this, 'callback_tag'], $newtext);
|
||||
|
||||
// 2. Auto-convert MediaCMS URLs to embedded players
|
||||
// First, protect text-only links from being converted
|
||||
// by temporarily replacing them with placeholders
|
||||
$textlink_placeholders = [];
|
||||
$textlink_pattern = '/<a\s+[^>]*data-mediacms-textlink=["\']true["\'][^>]*>.*?<\/a>/is';
|
||||
|
||||
$newtext = preg_replace_callback($textlink_pattern, function($matches) use (&$textlink_placeholders) {
|
||||
$placeholder = '###MEDIACMS_TEXTLINK_' . count($textlink_placeholders) . '###';
|
||||
$textlink_placeholders[$placeholder] = $matches[0];
|
||||
return $placeholder;
|
||||
}, $newtext);
|
||||
|
||||
// Regex for MediaCMS view URLs: https://domain/view?m=TOKEN
|
||||
// We need to be careful to match the configured domain
|
||||
$parsed_url = parse_url($mediacmsurl);
|
||||
$host = preg_quote($parsed_url['host'] ?? '', '/');
|
||||
$scheme = preg_quote($parsed_url['scheme'] ?? 'https', '/');
|
||||
|
||||
// Allow http or https, and optional path prefix
|
||||
$path_prefix = preg_quote(rtrim($parsed_url['path'] ?? '', '/'), '/');
|
||||
|
||||
// Pattern: https://HOST/PREFIX/view?m=TOKEN
|
||||
// Also handle /embed?m=TOKEN
|
||||
$pattern_url = '/(' . $scheme . ':\/\/' . $host . $path_prefix . '\/(view|embed)\?m=([a-zA-Z0-9]+)(?:&[^\s<]*)?)/';
|
||||
|
||||
$newtext = preg_replace_callback($pattern_url, [$this, 'callback_url'], $newtext);
|
||||
|
||||
// Restore protected text-only links as modal launchers
|
||||
foreach ($textlink_placeholders as $placeholder => $original) {
|
||||
$newtext = str_replace($placeholder, $this->transform_textlink($original), $newtext);
|
||||
}
|
||||
|
||||
return $newtext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for [mediacms:TOKEN]
|
||||
*/
|
||||
public function callback_tag($matches) {
|
||||
return $this->generate_iframe($matches[1], []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for URLs
|
||||
*/
|
||||
public function callback_url($matches) {
|
||||
// matches[0] is the full matched string
|
||||
// matches[1] is full URL, matches[3] is token
|
||||
|
||||
// Check if this URL is inside a text-only link
|
||||
// by looking at the context around the match
|
||||
$fullmatch = $matches[0];
|
||||
|
||||
// If this is already inside an <a> tag with data-mediacms-textlink="true",
|
||||
// return the original URL unchanged
|
||||
// We'll check this in the main filter method instead
|
||||
|
||||
$token = $matches[3];
|
||||
|
||||
// Extract additional embed parameters from the URL
|
||||
$embed_params = [];
|
||||
$full_url = $matches[1];
|
||||
|
||||
// Decode HTML entities (& -> &) before parsing
|
||||
$full_url = html_entity_decode($full_url, ENT_QUOTES | ENT_HTML5);
|
||||
|
||||
$parsed_url = parse_url($full_url);
|
||||
|
||||
if (isset($parsed_url['query'])) {
|
||||
parse_str($parsed_url['query'], $query_params);
|
||||
|
||||
// Extract embed-related parameters
|
||||
$supported_params = ['showTitle', 'showRelated', 'showUserAvatar', 'linkTitle', 't', 'width', 'height'];
|
||||
foreach ($supported_params as $param) {
|
||||
if (isset($query_params[$param])) {
|
||||
$embed_params[$param] = $query_params[$param];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->generate_iframe($token, $embed_params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the Iframe pointing to launch.php
|
||||
*/
|
||||
private function generate_iframe($token, $embed_params = []) {
|
||||
global $CFG, $COURSE;
|
||||
|
||||
// Use width/height from embed params if provided, no defaults
|
||||
$width = isset($embed_params['width']) ? $embed_params['width'] : null;
|
||||
$height = isset($embed_params['height']) ? $embed_params['height'] : null;
|
||||
$courseid = $COURSE->id ?? 0;
|
||||
|
||||
// Build launch URL parameters
|
||||
$launch_params = [
|
||||
'token' => $token,
|
||||
'courseid' => $courseid
|
||||
];
|
||||
|
||||
// Add width/height only if provided
|
||||
if ($width !== null) {
|
||||
$launch_params['width'] = $width;
|
||||
}
|
||||
if ($height !== null) {
|
||||
$launch_params['height'] = $height;
|
||||
}
|
||||
|
||||
// Add other embed parameters if provided (excluding width/height as they're already handled)
|
||||
foreach ($embed_params as $key => $value) {
|
||||
if ($key !== 'width' && $key !== 'height') {
|
||||
$launch_params[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$launchurl = new moodle_url('/filter/mediacms/launch.php', $launch_params);
|
||||
|
||||
// Build iframe attributes
|
||||
$iframe_attrs = [
|
||||
'src' => $launchurl->out(false),
|
||||
'frameborder' => 0,
|
||||
'allowfullscreen' => 'allowfullscreen',
|
||||
'class' => 'mediacms-embed',
|
||||
'title' => 'MediaCMS Video'
|
||||
];
|
||||
|
||||
// Add width/height attributes only if provided
|
||||
if ($width !== null) {
|
||||
$iframe_attrs['width'] = $width;
|
||||
}
|
||||
if ($height !== null) {
|
||||
$iframe_attrs['height'] = $height;
|
||||
}
|
||||
|
||||
$iframe = html_writer::tag('iframe', '', $iframe_attrs);
|
||||
|
||||
return $iframe;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a text-only link into a link that replaces itself with an inline iframe on click.
|
||||
*
|
||||
* @param string $anchor_html Original <a ...>...</a> HTML
|
||||
* @return string Transformed HTML (or original if token cannot be extracted)
|
||||
*/
|
||||
private function transform_textlink($anchor_html) {
|
||||
global $COURSE, $PAGE;
|
||||
|
||||
// Extract href.
|
||||
if (!preg_match('/href=["\']([^"\']+)["\']/', $anchor_html, $href_matches)) {
|
||||
return $anchor_html;
|
||||
}
|
||||
$href = html_entity_decode($href_matches[1], ENT_QUOTES | ENT_HTML5);
|
||||
|
||||
// Extract ?m=TOKEN.
|
||||
parse_str(parse_url($href, PHP_URL_QUERY) ?? '', $query_params);
|
||||
$token = $query_params['m'] ?? null;
|
||||
if (!$token || !preg_match('/^[a-zA-Z0-9]+$/', $token)) {
|
||||
return $anchor_html;
|
||||
}
|
||||
|
||||
// Extract inner link text.
|
||||
if (!preg_match('/<a[^>]*>(.*?)<\/a>/is', $anchor_html, $text_matches)) {
|
||||
return $anchor_html;
|
||||
}
|
||||
|
||||
$launch_url = new moodle_url('/filter/mediacms/launch.php', [
|
||||
'token' => $token,
|
||||
'courseid' => isset($COURSE->id) ? (int)$COURSE->id : 0,
|
||||
'show_media_page' => 'true',
|
||||
]);
|
||||
|
||||
if (!self::$textlink_js_added) {
|
||||
self::$textlink_js_added = true;
|
||||
$PAGE->requires->js_init_code(
|
||||
'document.addEventListener("click",function(e){'
|
||||
. 'var a=e.target.closest("a.mediacms-textlink-launch");if(!a)return;'
|
||||
. 'e.preventDefault();'
|
||||
. 'var f=document.createElement("iframe");'
|
||||
. 'f.src=a.dataset.launchUrl;'
|
||||
. 'f.style.cssText="width:100%;height:480px;border:none;display:block;";'
|
||||
. 'f.allowFullscreen=true;'
|
||||
. 'f.setAttribute("allow","autoplay *; fullscreen *; encrypted-media *;");'
|
||||
. 'a.parentNode.replaceChild(f,a);'
|
||||
. '});',
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
return html_writer::tag('a', $text_matches[1], [
|
||||
'href' => '#',
|
||||
'class' => 'mediacms-textlink-launch',
|
||||
'data-launch-url' => $launch_url->out(false),
|
||||
]);
|
||||
}
|
||||
|
||||
/** @var bool Whether the inline-iframe JS has already been added to the page. */
|
||||
private static $textlink_js_added = false;
|
||||
}
|
||||
13
lms-plugins/mediacms-moodle/filter/mediacms/db/hooks.php
Normal file
13
lms-plugins/mediacms-moodle/filter/mediacms/db/hooks.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
/**
|
||||
* Hook registrations for filter_mediacms.
|
||||
* Primary navigation is handled via extend_navigation() in lib.php instead.
|
||||
*
|
||||
* @package filter_mediacms
|
||||
* @copyright 2026 MediaCMS
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$callbacks = [];
|
||||
45
lms-plugins/mediacms-moodle/filter/mediacms/db/install.php
Normal file
45
lms-plugins/mediacms-moodle/filter/mediacms/db/install.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Post-installation hook.
|
||||
*/
|
||||
function xmldb_filter_mediacms_install() {
|
||||
global $CFG, $DB;
|
||||
require_once($CFG->libdir . '/filterlib.php');
|
||||
|
||||
// Enable the filter globally.
|
||||
filter_set_global_state('filter_mediacms', TEXTFILTER_ON);
|
||||
|
||||
// Move to top priority (lowest sortorder).
|
||||
$syscontextid = context_system::instance()->id;
|
||||
$filters = $DB->get_records('filter_active', ['contextid' => $syscontextid], 'sortorder ASC');
|
||||
|
||||
if (empty($filters)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Separate mediacms from other filters by inspecting the record property,
|
||||
// not the array key (get_records indexes by id, not by filter name).
|
||||
$mediacmsrecord = null;
|
||||
$otherrecords = [];
|
||||
foreach ($filters as $record) {
|
||||
if ($record->filter === 'filter_mediacms') {
|
||||
$mediacmsrecord = $record;
|
||||
} else {
|
||||
$otherrecords[] = $record;
|
||||
}
|
||||
}
|
||||
|
||||
// Reassign sortorders: mediacms first, then everyone else.
|
||||
$sortorder = 1;
|
||||
if ($mediacmsrecord) {
|
||||
$mediacmsrecord->sortorder = $sortorder++;
|
||||
$DB->update_record('filter_active', $mediacmsrecord);
|
||||
}
|
||||
foreach ($otherrecords as $record) {
|
||||
$record->sortorder = $sortorder++;
|
||||
$DB->update_record('filter_active', $record);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['filtername'] = 'MediaCMS';
|
||||
$string['pluginname'] = 'MediaCMS';
|
||||
$string['coresettings'] = 'Core MediaCMS Settings';
|
||||
$string['coresettings_desc'] = 'These settings are shared with the TinyMCE MediaCMS editor plugin.';
|
||||
$string['mediacmsurl'] = 'MediaCMS URL';
|
||||
$string['mediacmsurl_desc'] = 'The base URL of your MediaCMS instance (e.g., https://lti.mediacms.io). This setting is used by both the filter and the TinyMCE editor plugin.';
|
||||
$string['ltitoolid'] = 'LTI Tool';
|
||||
$string['ltitoolid_desc'] = 'Select the External Tool configuration for MediaCMS. This enables the video library in the TinyMCE editor and LTI authentication. To set up an LTI tool, go to Site Administration > Plugins > Activity modules > External tool > Manage tools.';
|
||||
$string['noltitoolsfound'] = 'No LTI tools found';
|
||||
$string['iframewidth'] = 'Default Width';
|
||||
$string['iframewidth_desc'] = 'Default width for embedded videos (pixels).';
|
||||
$string['iframeheight'] = 'Default Height';
|
||||
$string['iframeheight_desc'] = 'Default height for embedded videos (pixels).';
|
||||
$string['enableautoconvert'] = 'Auto-convert URLs';
|
||||
$string['enableautoconvert_desc'] = 'Automatically convert MediaCMS URLs (e.g., /view?m=xyz) in text to embedded players.';
|
||||
$string['privacy:metadata'] = 'The MediaCMS filter does not store any personal data.';
|
||||
|
||||
// My Media page.
|
||||
$string['mymedia'] = 'My Media';
|
||||
$string['mymedia_placement'] = 'My Media link placement';
|
||||
$string['mymedia_placement_desc'] = 'Where to display the My Media link in the Moodle interface.';
|
||||
$string['mymedia_placement_top'] = 'Top navigation';
|
||||
$string['mymedia_placement_user'] = 'User navigation';
|
||||
$string['notconfigured'] = 'MediaCMS is not fully configured. Please set the MediaCMS URL and LTI Tool in Site Administration → Plugins → Filters → MediaCMS.';
|
||||
$string['ltitoolnotfound'] = 'The configured LTI tool could not be found. Please check the MediaCMS filter settings.';
|
||||
$string['cannotcreatedummyactivity'] = 'Could not create the MediaCMS launcher activity. Please check course permissions.';
|
||||
173
lms-plugins/mediacms-moodle/filter/mediacms/launch.php
Normal file
173
lms-plugins/mediacms-moodle/filter/mediacms/launch.php
Normal file
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
/**
|
||||
* LTI Launch for MediaCMS Filter
|
||||
*
|
||||
* @package filter_mediacms
|
||||
* @copyright 2026 MediaCMS
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . '/../../config.php');
|
||||
require_once($CFG->dirroot . '/mod/lti/lib.php');
|
||||
require_once($CFG->dirroot . '/mod/lti/locallib.php');
|
||||
require_once($CFG->dirroot . '/course/modlib.php');
|
||||
require_once(__DIR__ . '/locallib.php');
|
||||
|
||||
global $SITE, $DB, $PAGE, $OUTPUT, $CFG;
|
||||
|
||||
require_login();
|
||||
|
||||
$mediatoken = required_param('token', PARAM_ALPHANUMEXT);
|
||||
$courseid = optional_param('courseid', 0, PARAM_INT);
|
||||
$height = optional_param('height', 0, PARAM_INT);
|
||||
$width = optional_param('width', 0, PARAM_INT);
|
||||
|
||||
// Extract embed parameters
|
||||
$showTitle = optional_param('showTitle', '', PARAM_TEXT);
|
||||
$showRelated = optional_param('showRelated', '', PARAM_TEXT);
|
||||
$showUserAvatar = optional_param('showUserAvatar', '', PARAM_TEXT);
|
||||
$linkTitle = optional_param('linkTitle', '', PARAM_TEXT);
|
||||
$startTime = optional_param('t', '', PARAM_TEXT);
|
||||
$show_media_page = optional_param('show_media_page', '', PARAM_TEXT);
|
||||
|
||||
// Get configuration
|
||||
$mediacmsurl = get_config('filter_mediacms', 'mediacmsurl');
|
||||
$ltitoolid = get_config('filter_mediacms', 'ltitoolid');
|
||||
|
||||
// No default dimensions - use what's provided or nothing
|
||||
|
||||
if (empty($mediacmsurl)) {
|
||||
die('MediaCMS URL not configured');
|
||||
}
|
||||
|
||||
// Tool Selection Logic
|
||||
$type = false;
|
||||
if (!empty($ltitoolid)) {
|
||||
$type = $DB->get_record('lti_types', ['id' => $ltitoolid]);
|
||||
}
|
||||
|
||||
if (!$type) {
|
||||
die('LTI tool not found or not configured.');
|
||||
}
|
||||
|
||||
// Set up context
|
||||
if ($courseid && $courseid != SITEID) {
|
||||
$context = context_course::instance($courseid);
|
||||
$course = get_course($courseid);
|
||||
} else {
|
||||
$context = context_system::instance();
|
||||
$course = $SITE;
|
||||
}
|
||||
|
||||
// Get or create the dummy activity (visible, non-stealth).
|
||||
try {
|
||||
$dummy_cmid = filter_mediacms_get_dummy_activity($courseid, $type->id);
|
||||
} catch (Exception $e) {
|
||||
if (!has_capability('moodle/course:manageactivities', $context)) {
|
||||
die('No MediaCMS activity found. Please ask a teacher to add one first.');
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$cm = get_coursemodule_from_id('lti', $dummy_cmid, 0, false, MUST_EXIST);
|
||||
$instance = $DB->get_record('lti', ['id' => $cm->instance], '*', MUST_EXIST);
|
||||
|
||||
$custom_params = ["media_friendly_token=" . $mediatoken];
|
||||
|
||||
// Add embed parameters if provided (check !== '' instead of !empty() because '0' is a valid value)
|
||||
if ($showTitle !== '') {
|
||||
$custom_params[] = "embed_show_title=" . $showTitle;
|
||||
}
|
||||
if ($showRelated !== '') {
|
||||
$custom_params[] = "embed_show_related=" . $showRelated;
|
||||
}
|
||||
if ($showUserAvatar !== '') {
|
||||
$custom_params[] = "embed_show_user_avatar=" . $showUserAvatar;
|
||||
}
|
||||
if ($linkTitle !== '') {
|
||||
$custom_params[] = "embed_link_title=" . $linkTitle;
|
||||
}
|
||||
if ($startTime !== '') {
|
||||
$custom_params[] = "embed_start_time=" . $startTime;
|
||||
}
|
||||
if ($show_media_page === 'true') {
|
||||
$custom_params[] = "show_media_page=true";
|
||||
}
|
||||
|
||||
$instance->instructorcustomparameters = implode("\n", $custom_params);
|
||||
$instance->name = 'MediaCMS Video: ' . $mediatoken;
|
||||
|
||||
// Set up page
|
||||
$page_params = [
|
||||
'token' => $mediatoken,
|
||||
'courseid' => $courseid,
|
||||
'width' => $width,
|
||||
'height' => $height
|
||||
];
|
||||
|
||||
// Add embed parameters to page URL if provided (check !== '' because '0' is valid)
|
||||
if ($showTitle !== '') {
|
||||
$page_params['showTitle'] = $showTitle;
|
||||
}
|
||||
if ($showRelated !== '') {
|
||||
$page_params['showRelated'] = $showRelated;
|
||||
}
|
||||
if ($showUserAvatar !== '') {
|
||||
$page_params['showUserAvatar'] = $showUserAvatar;
|
||||
}
|
||||
if ($linkTitle !== '') {
|
||||
$page_params['linkTitle'] = $linkTitle;
|
||||
}
|
||||
if ($startTime !== '') {
|
||||
$page_params['t'] = $startTime;
|
||||
}
|
||||
if ($show_media_page === 'true') {
|
||||
$page_params['show_media_page'] = 'true';
|
||||
}
|
||||
|
||||
$PAGE->set_url(new moodle_url('/filter/mediacms/launch.php', $page_params));
|
||||
$PAGE->set_context($context);
|
||||
$PAGE->set_pagelayout('embedded');
|
||||
$PAGE->set_title('MediaCMS');
|
||||
|
||||
// Get type config
|
||||
$typeconfig = lti_get_type_type_config($type->id);
|
||||
|
||||
// Initiate LTI Login with proper cmid (for permissions) and custom token
|
||||
$content = lti_initiate_login($course->id, $dummy_cmid, $instance, $typeconfig, null, $instance->name);
|
||||
|
||||
// CRITICAL: Inject media_token and embed parameters as hidden fields in OIDC form
|
||||
// MediaCMS will encode them in state and inject into custom claims (fallback mechanism)
|
||||
$hidden_fields = '<input type="hidden" name="media_token" value="' . htmlspecialchars($mediatoken, ENT_QUOTES) . '" />';
|
||||
|
||||
// Add embed parameters as hidden fields
|
||||
if ($showTitle !== '') {
|
||||
$hidden_fields .= '<input type="hidden" name="embed_show_title" value="' . htmlspecialchars($showTitle, ENT_QUOTES) . '" />';
|
||||
}
|
||||
if ($showRelated !== '') {
|
||||
$hidden_fields .= '<input type="hidden" name="embed_show_related" value="' . htmlspecialchars($showRelated, ENT_QUOTES) . '" />';
|
||||
}
|
||||
if ($showUserAvatar !== '') {
|
||||
$hidden_fields .= '<input type="hidden" name="embed_show_user_avatar" value="' . htmlspecialchars($showUserAvatar, ENT_QUOTES) . '" />';
|
||||
}
|
||||
if ($linkTitle !== '') {
|
||||
$hidden_fields .= '<input type="hidden" name="embed_link_title" value="' . htmlspecialchars($linkTitle, ENT_QUOTES) . '" />';
|
||||
}
|
||||
if ($startTime !== '') {
|
||||
$hidden_fields .= '<input type="hidden" name="embed_start_time" value="' . htmlspecialchars($startTime, ENT_QUOTES) . '" />';
|
||||
}
|
||||
if ($show_media_page === 'true') {
|
||||
$hidden_fields .= '<input type="hidden" name="show_media_page" value="true" />';
|
||||
}
|
||||
if ($width) {
|
||||
$hidden_fields .= '<input type="hidden" name="embed_width" value="' . htmlspecialchars($width, ENT_QUOTES) . '" />';
|
||||
}
|
||||
if ($height) {
|
||||
$hidden_fields .= '<input type="hidden" name="embed_height" value="' . htmlspecialchars($height, ENT_QUOTES) . '" />';
|
||||
}
|
||||
|
||||
$content = str_replace('</form>', $hidden_fields . '</form>', $content);
|
||||
|
||||
echo $OUTPUT->header();
|
||||
echo $content;
|
||||
echo $OUTPUT->footer();
|
||||
72
lms-plugins/mediacms-moodle/filter/mediacms/lib.php
Normal file
72
lms-plugins/mediacms-moodle/filter/mediacms/lib.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/**
|
||||
* Navigation callbacks for filter_mediacms
|
||||
*
|
||||
* @package filter_mediacms
|
||||
* @copyright 2026 MediaCMS
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Add My Media to the global / flat navigation (nav drawer).
|
||||
* Fires on every page when placement is set to 'top'.
|
||||
* In Moodle 4.x Boost the nav drawer is opened via the hamburger icon
|
||||
* and the node appears as a top-level item alongside Home / My courses.
|
||||
*/
|
||||
function filter_mediacms_extend_navigation(global_navigation $navigation): void {
|
||||
$placement = get_config('filter_mediacms', 'mymedia_placement');
|
||||
if ($placement !== 'top') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isloggedin() || isguestuser()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$url = new moodle_url('/filter/mediacms/my_media.php');
|
||||
$node = navigation_node::create(
|
||||
get_string('mymedia', 'filter_mediacms'),
|
||||
$url,
|
||||
navigation_node::TYPE_CUSTOM,
|
||||
null,
|
||||
'mediacms_mymedia',
|
||||
new pix_icon('i/media', '')
|
||||
);
|
||||
// showinflatnavigation = true makes it visible in the Boost nav drawer.
|
||||
$node->showinflatnavigation = true;
|
||||
$navigation->add_node($node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add My Media to the user account / settings navigation.
|
||||
* Fires when placement is set to 'user'.
|
||||
* In Moodle 4.x Boost this section is reachable via avatar → Preferences.
|
||||
*/
|
||||
function filter_mediacms_extend_navigation_user_settings(
|
||||
navigation_node $navigation,
|
||||
stdClass $user,
|
||||
context_user $usercontext,
|
||||
stdClass $course,
|
||||
context_course $coursecontext
|
||||
): void {
|
||||
$placement = get_config('filter_mediacms', 'mymedia_placement');
|
||||
if ($placement !== 'user') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isloggedin() || isguestuser()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$url = new moodle_url('/filter/mediacms/my_media.php');
|
||||
$navigation->add(
|
||||
get_string('mymedia', 'filter_mediacms'),
|
||||
$url,
|
||||
navigation_node::TYPE_SETTING,
|
||||
null,
|
||||
'mediacms_mymedia',
|
||||
new pix_icon('i/media', '')
|
||||
);
|
||||
}
|
||||
64
lms-plugins/mediacms-moodle/filter/mediacms/locallib.php
Normal file
64
lms-plugins/mediacms-moodle/filter/mediacms/locallib.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
/**
|
||||
* Local helper functions for filter_mediacms
|
||||
*
|
||||
* @package filter_mediacms
|
||||
* @copyright 2026 MediaCMS
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Find the first LTI activity for the MediaCMS tool in a course, or create a
|
||||
* visible dummy one if none exists. Repairs any existing stealth/hidden activity.
|
||||
*
|
||||
* @param int $courseid
|
||||
* @param int $typeid LTI tool type ID
|
||||
* @return int course-module ID
|
||||
*/
|
||||
function filter_mediacms_get_dummy_activity($courseid, $typeid) {
|
||||
global $DB;
|
||||
|
||||
$sql = "SELECT cm.id
|
||||
FROM {course_modules} cm
|
||||
JOIN {modules} m ON m.id = cm.module
|
||||
JOIN {lti} lti ON lti.id = cm.instance
|
||||
WHERE cm.course = :courseid
|
||||
AND m.name = 'lti'
|
||||
AND lti.typeid = :typeid
|
||||
AND cm.deletioninprogress = 0
|
||||
ORDER BY cm.visible DESC, cm.visibleoncoursepage DESC
|
||||
LIMIT 1";
|
||||
|
||||
$existing = $DB->get_record_sql($sql, ['courseid' => $courseid, 'typeid' => $typeid]);
|
||||
if ($existing) {
|
||||
return $existing->id;
|
||||
}
|
||||
|
||||
// Create the dummy activity then immediately force visibleoncoursepage=0,
|
||||
// because add_moduleinfo() always defaults it to 1.
|
||||
$moduleinfo = new stdClass();
|
||||
$moduleinfo->course = $courseid;
|
||||
$moduleinfo->module = $DB->get_field('modules', 'id', ['name' => 'lti']);
|
||||
$moduleinfo->modulename = 'lti';
|
||||
$moduleinfo->section = 0;
|
||||
$moduleinfo->visible = 1;
|
||||
$moduleinfo->visibleoncoursepage = 1;
|
||||
$moduleinfo->availability = null;
|
||||
$moduleinfo->showdescription = 0;
|
||||
$moduleinfo->name = 'My Media';
|
||||
$moduleinfo->intro = '';
|
||||
$moduleinfo->introformat = FORMAT_HTML;
|
||||
$moduleinfo->typeid = $typeid;
|
||||
$moduleinfo->instructorchoiceacceptgrades = 0;
|
||||
$moduleinfo->grade = 0;
|
||||
$moduleinfo->instructorchoicesendname = 1;
|
||||
$moduleinfo->instructorchoicesendemailaddr = 1;
|
||||
$moduleinfo->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS;
|
||||
$moduleinfo->instructorcustomparameters = '';
|
||||
|
||||
$result = add_moduleinfo($moduleinfo, get_course($courseid));
|
||||
|
||||
return $result->coursemodule;
|
||||
}
|
||||
97
lms-plugins/mediacms-moodle/filter/mediacms/lti_launch.php
Normal file
97
lms-plugins/mediacms-moodle/filter/mediacms/lti_launch.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
/**
|
||||
* My Media LTI launch page — runs inside the iframe from my_media.php.
|
||||
*
|
||||
* Builds custom_publishdata (enrolled courses + roles) and initiates
|
||||
* the LTI 1.3 OIDC login flow, outputting the auto-submit form directly.
|
||||
*
|
||||
* @package filter_mediacms
|
||||
* @copyright 2026 MediaCMS
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . '/../../config.php');
|
||||
require_once($CFG->dirroot . '/mod/lti/lib.php');
|
||||
require_once($CFG->dirroot . '/mod/lti/locallib.php');
|
||||
require_once($CFG->dirroot . '/course/modlib.php');
|
||||
require_once(__DIR__ . '/locallib.php');
|
||||
|
||||
global $SITE, $DB, $CFG, $USER;
|
||||
|
||||
require_login();
|
||||
|
||||
$mediacmsurl = get_config('filter_mediacms', 'mediacmsurl');
|
||||
$ltitoolid = get_config('filter_mediacms', 'ltitoolid');
|
||||
|
||||
if (empty($mediacmsurl) || empty($ltitoolid)) {
|
||||
throw new moodle_exception('notconfigured', 'filter_mediacms');
|
||||
}
|
||||
|
||||
$type = $DB->get_record('lti_types', ['id' => $ltitoolid]);
|
||||
if (!$type) {
|
||||
throw new moodle_exception('ltitoolnotfound', 'filter_mediacms');
|
||||
}
|
||||
|
||||
// Build custom_publishdata: all courses the user is enrolled in + role.
|
||||
$enrolled_courses = enrol_get_users_courses($USER->id, true, ['id', 'shortname', 'fullname']);
|
||||
|
||||
$publish_data = [];
|
||||
foreach ($enrolled_courses as $enrolled_course) {
|
||||
if ((int)$enrolled_course->id === SITEID) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$course_context = context_course::instance($enrolled_course->id);
|
||||
$roles = get_user_roles($course_context, $USER->id, false);
|
||||
|
||||
$role_shortname = 'student';
|
||||
if (!empty($roles)) {
|
||||
$role = reset($roles);
|
||||
$role_shortname = $role->shortname;
|
||||
}
|
||||
|
||||
$publish_data[] = [
|
||||
'id' => (int)$enrolled_course->id,
|
||||
'shortname' => $enrolled_course->shortname,
|
||||
'fullname' => $enrolled_course->fullname,
|
||||
'role' => $role_shortname,
|
||||
];
|
||||
}
|
||||
|
||||
$publishdata_b64 = base64_encode(json_encode($publish_data));
|
||||
|
||||
// Use a course the user is actually enrolled in so they have mod/lti:view during
|
||||
// the OIDC flow. Fall back to SITEID only for admins with no course enrolments.
|
||||
$launch_courseid = SITEID;
|
||||
$launch_course = $SITE;
|
||||
foreach ($enrolled_courses as $ec) {
|
||||
if ((int)$ec->id !== SITEID) {
|
||||
$launch_courseid = (int)$ec->id;
|
||||
$launch_course = get_course($launch_courseid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Get or create the dummy activity (visible, non-stealth).
|
||||
try {
|
||||
$dummy_cmid = filter_mediacms_get_dummy_activity($launch_courseid, $type->id);
|
||||
} catch (Exception $e) {
|
||||
throw new moodle_exception('cannotcreatedummyactivity', 'filter_mediacms');
|
||||
}
|
||||
|
||||
$cm = get_coursemodule_from_id('lti', $dummy_cmid, 0, false, MUST_EXIST);
|
||||
$instance = $DB->get_record('lti', ['id' => $cm->instance], '*', MUST_EXIST);
|
||||
|
||||
// DEBUG: log enrolled courses retrieved.
|
||||
error_log('MediaCMS My Media publishdata courses (' . count($publish_data) . '): ' . json_encode($publish_data));
|
||||
|
||||
// Write publishdata to DB — Moodle's auth.php re-reads the instance from DB
|
||||
// when building the LTI launch JWT, so in-memory changes are ignored.
|
||||
$DB->set_field('lti', 'instructorcustomparameters', 'publishdata=' . $publishdata_b64, ['id' => $cm->instance]);
|
||||
$instance->instructorcustomparameters = 'publishdata=' . $publishdata_b64;
|
||||
$instance->name = 'MediaCMS My Media';
|
||||
|
||||
$typeconfig = lti_get_type_type_config($type->id);
|
||||
$content = lti_initiate_login($launch_course->id, $dummy_cmid, $instance, $typeconfig, null, $instance->name);
|
||||
|
||||
echo $content;
|
||||
36
lms-plugins/mediacms-moodle/filter/mediacms/my_media.php
Normal file
36
lms-plugins/mediacms-moodle/filter/mediacms/my_media.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
/**
|
||||
* My Media page — renders the Moodle shell with an LTI iframe.
|
||||
*
|
||||
* @package filter_mediacms
|
||||
* @copyright 2026 MediaCMS
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . '/../../config.php');
|
||||
|
||||
global $SITE, $PAGE, $OUTPUT, $USER;
|
||||
|
||||
require_login();
|
||||
|
||||
$context = context_system::instance();
|
||||
|
||||
$PAGE->set_context($context);
|
||||
$PAGE->set_url(new moodle_url('/filter/mediacms/my_media.php'));
|
||||
$PAGE->set_course($SITE);
|
||||
$PAGE->set_pagelayout('mydashboard');
|
||||
$PAGE->set_title(get_string('mymedia', 'filter_mediacms'));
|
||||
$PAGE->set_heading(get_string('mymedia', 'filter_mediacms'));
|
||||
|
||||
echo $OUTPUT->header();
|
||||
|
||||
$attr = [
|
||||
'id' => 'contentframe',
|
||||
'src' => (new moodle_url('/filter/mediacms/lti_launch.php'))->out(false),
|
||||
'allowfullscreen' => 'true',
|
||||
'allow' => 'autoplay *; fullscreen *; encrypted-media *; camera *; microphone *;',
|
||||
'style' => 'border:none;display:block;width:100%;height:calc(100vh - 120px);',
|
||||
];
|
||||
echo html_writer::tag('iframe', '', $attr);
|
||||
|
||||
echo $OUTPUT->footer();
|
||||
54
lms-plugins/mediacms-moodle/filter/mediacms/settings.php
Normal file
54
lms-plugins/mediacms-moodle/filter/mediacms/settings.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
defined('MOODLE_INTERNAL') || die;
|
||||
|
||||
if ($ADMIN->fulltree) {
|
||||
// Info heading
|
||||
$settings->add(new admin_setting_heading(
|
||||
'filter_mediacms/coresettings',
|
||||
get_string('coresettings', 'filter_mediacms'),
|
||||
get_string('coresettings_desc', 'filter_mediacms')
|
||||
));
|
||||
|
||||
// MediaCMS URL
|
||||
$settings->add(new admin_setting_configtext(
|
||||
'filter_mediacms/mediacmsurl',
|
||||
get_string('mediacmsurl', 'filter_mediacms'),
|
||||
get_string('mediacmsurl_desc', 'filter_mediacms'),
|
||||
'https://lti.mediacms.io',
|
||||
PARAM_URL
|
||||
));
|
||||
|
||||
// LTI Tool Selector
|
||||
$ltioptions = [0 => get_string('noltitoolsfound', 'filter_mediacms')];
|
||||
try {
|
||||
$tools = $DB->get_records('lti_types', null, 'name ASC', 'id, name, baseurl');
|
||||
if (!empty($tools)) {
|
||||
$ltioptions = [0 => get_string('choose')];
|
||||
foreach ($tools as $tool) {
|
||||
$ltioptions[$tool->id] = $tool->name . ' (' . $tool->baseurl . ')';
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// Database might not be ready during install
|
||||
}
|
||||
|
||||
$settings->add(new admin_setting_configselect(
|
||||
'filter_mediacms/ltitoolid',
|
||||
get_string('ltitoolid', 'filter_mediacms'),
|
||||
get_string('ltitoolid_desc', 'filter_mediacms'),
|
||||
0,
|
||||
$ltioptions
|
||||
));
|
||||
|
||||
// My Media link placement.
|
||||
$settings->add(new admin_setting_configselect(
|
||||
'filter_mediacms/mymedia_placement',
|
||||
get_string('mymedia_placement', 'filter_mediacms'),
|
||||
get_string('mymedia_placement_desc', 'filter_mediacms'),
|
||||
'top',
|
||||
[
|
||||
'top' => get_string('mymedia_placement_top', 'filter_mediacms'),
|
||||
'user' => get_string('mymedia_placement_user', 'filter_mediacms'),
|
||||
]
|
||||
));
|
||||
}
|
||||
8
lms-plugins/mediacms-moodle/filter/mediacms/version.php
Normal file
8
lms-plugins/mediacms-moodle/filter/mediacms/version.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->version = 2026021200; // 2026-02-12
|
||||
$plugin->requires = 2024100700; // Requires Moodle 4.5+
|
||||
$plugin->component = 'filter_mediacms';
|
||||
$plugin->maturity = MATURITY_STABLE;
|
||||
$plugin->release = 'v1.0.0';
|
||||
2457
lms-plugins/mediacms-moodle/package-lock.json
generated
Normal file
2457
lms-plugins/mediacms-moodle/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
89
lms-plugins/mediacms-moodle/package.json
Normal file
89
lms-plugins/mediacms-moodle/package.json
Normal file
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"name": "mediacms-moodle",
|
||||
"version": "1.0.0",
|
||||
"description": "This package provides the integration between MediaCMS and Moodle (versions 4.x and 5.x). It consists of two components that work together to provide a seamless video experience:",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.28.6",
|
||||
"@babel/core": "^7.29.0",
|
||||
"@babel/plugin-transform-modules-amd": "^7.27.1",
|
||||
"@babel/preset-env": "^7.29.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"anymatch": "^3.1.3",
|
||||
"babel-plugin-polyfill-corejs2": "^0.4.15",
|
||||
"babel-plugin-polyfill-corejs3": "^0.14.0",
|
||||
"babel-plugin-polyfill-regenerator": "^0.6.6",
|
||||
"balanced-match": "^1.0.2",
|
||||
"baseline-browser-mapping": "^2.9.19",
|
||||
"binary-extensions": "^2.3.0",
|
||||
"brace-expansion": "^1.1.12",
|
||||
"braces": "^3.0.3",
|
||||
"browserslist": "^4.28.1",
|
||||
"caniuse-lite": "^1.0.30001766",
|
||||
"chokidar": "^3.6.0",
|
||||
"commander": "^6.2.1",
|
||||
"concat-map": "^0.0.1",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"core-js-compat": "^3.48.0",
|
||||
"debug": "^4.4.3",
|
||||
"electron-to-chromium": "^1.5.283",
|
||||
"escalade": "^3.2.0",
|
||||
"esutils": "^2.0.3",
|
||||
"fill-range": "^7.1.1",
|
||||
"fs-readdir-recursive": "^1.1.0",
|
||||
"fs.realpath": "^1.0.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
"glob": "^7.2.3",
|
||||
"glob-parent": "^5.1.2",
|
||||
"hasown": "^2.0.2",
|
||||
"inflight": "^1.0.6",
|
||||
"inherits": "^2.0.4",
|
||||
"is-binary-path": "^2.1.0",
|
||||
"is-core-module": "^2.16.1",
|
||||
"is-extglob": "^2.1.1",
|
||||
"is-glob": "^4.0.3",
|
||||
"is-number": "^7.0.0",
|
||||
"js-tokens": "^4.0.0",
|
||||
"jsesc": "^3.1.0",
|
||||
"json5": "^2.2.3",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lru-cache": "^5.1.1",
|
||||
"make-dir": "^2.1.0",
|
||||
"minimatch": "^3.1.2",
|
||||
"ms": "^2.1.3",
|
||||
"node-releases": "^2.0.27",
|
||||
"normalize-path": "^3.0.0",
|
||||
"once": "^1.4.0",
|
||||
"path-is-absolute": "^1.0.1",
|
||||
"path-parse": "^1.0.7",
|
||||
"picocolors": "^1.1.1",
|
||||
"picomatch": "^2.3.1",
|
||||
"pify": "^4.0.1",
|
||||
"readdirp": "^3.6.0",
|
||||
"regenerate": "^1.4.2",
|
||||
"regenerate-unicode-properties": "^10.2.2",
|
||||
"regexpu-core": "^6.4.0",
|
||||
"regjsgen": "^0.8.0",
|
||||
"regjsparser": "^0.13.0",
|
||||
"resolve": "^1.22.11",
|
||||
"semver": "^6.3.1",
|
||||
"slash": "^2.0.0",
|
||||
"supports-preserve-symlinks-flag": "^1.0.0",
|
||||
"to-regex-range": "^5.0.1",
|
||||
"unicode-canonical-property-names-ecmascript": "^2.0.1",
|
||||
"unicode-match-property-ecmascript": "^2.0.0",
|
||||
"unicode-match-property-value-ecmascript": "^2.2.1",
|
||||
"unicode-property-aliases-ecmascript": "^2.2.0",
|
||||
"update-browserslist-db": "^1.2.3",
|
||||
"wrappy": "^1.0.2",
|
||||
"yallist": "^3.1.1"
|
||||
}
|
||||
}
|
||||
206
lms-plugins/mediacms-moodle/tiny/mediacms/AUTOCONVERT.md
Executable file
206
lms-plugins/mediacms-moodle/tiny/mediacms/AUTOCONVERT.md
Executable file
@@ -0,0 +1,206 @@
|
||||
# MediaCMS URL Auto-Convert Feature
|
||||
|
||||
This feature automatically converts pasted MediaCMS video URLs into embedded video players within the TinyMCE editor.
|
||||
|
||||
## Overview
|
||||
|
||||
When a user pastes a MediaCMS video URL like:
|
||||
```
|
||||
https://deic.mediacms.io/view?m=JpBd1Zvdl
|
||||
```
|
||||
|
||||
It is automatically converted to an embedded video player:
|
||||
```html
|
||||
<div class="tiny-iframe-responsive" contenteditable="false">
|
||||
<iframe
|
||||
style="width: 100%; max-width: calc(100vh * 16 / 9); aspect-ratio: 16 / 9; display: block; margin: auto; border: 0;"
|
||||
src="https://deic.mediacms.io/embed?m=JpBd1Zvdl&showTitle=1&showRelated=1&showUserAvatar=1&linkTitle=1"
|
||||
allowfullscreen="allowfullscreen">
|
||||
</iframe>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Supported URL Formats
|
||||
|
||||
The auto-convert feature recognizes MediaCMS view URLs in this format:
|
||||
- `https://[domain]/view?m=[VIDEO_ID]`
|
||||
|
||||
Examples:
|
||||
- `https://deic.mediacms.io/view?m=JpBd1Zvdl`
|
||||
- `https://your-mediacms-instance.com/view?m=abc123`
|
||||
|
||||
## Configuration
|
||||
|
||||
### Accessing Settings
|
||||
|
||||
1. Log in to Moodle as an administrator
|
||||
2. Navigate to: **Site administration** → **Plugins** → **Text editors** → **TinyMCE editor** → **MediaCMS**
|
||||
3. Scroll to the **Auto-convert MediaCMS URLs** section
|
||||
|
||||
### Available Settings
|
||||
|
||||
| Setting | Description | Default |
|
||||
|---------|-------------|---------|
|
||||
| **Enable auto-convert** | Turn the auto-convert feature on or off | Enabled |
|
||||
| **MediaCMS base URL** | Restrict auto-conversion to a specific MediaCMS domain | Empty (allow all) |
|
||||
| **Show video title** | Display the video title in the embedded player | Enabled |
|
||||
| **Link video title** | Make the video title clickable, linking to the original video page | Enabled |
|
||||
| **Show related videos** | Display related videos after the current video ends | Enabled |
|
||||
| **Show user avatar** | Display the uploader's avatar in the embedded player | Enabled |
|
||||
|
||||
### Settings Location in Moodle
|
||||
|
||||
The settings are stored in the Moodle database under the `tiny_mediacms` plugin configuration:
|
||||
|
||||
- `tiny_mediacms/autoconvertenabled` - Enable/disable auto-convert
|
||||
- `tiny_mediacms/autoconvert_baseurl` - MediaCMS base URL (e.g., https://deic.mediacms.io)
|
||||
- `tiny_mediacms/autoconvert_showtitle` - Show title option
|
||||
- `tiny_mediacms/autoconvert_linktitle` - Link title option
|
||||
- `tiny_mediacms/autoconvert_showrelated` - Show related option
|
||||
- `tiny_mediacms/autoconvert_showuseravatar` - Show user avatar option
|
||||
|
||||
### Base URL Configuration
|
||||
|
||||
The **MediaCMS base URL** setting controls which MediaCMS instances are recognized for auto-conversion:
|
||||
|
||||
- **Empty (default)**: Any MediaCMS URL will be auto-converted (e.g., URLs from any `*/view?m=*` pattern)
|
||||
- **Specific URL**: Only URLs from the specified domain will be auto-converted
|
||||
|
||||
Example configurations:
|
||||
- `https://deic.mediacms.io` - Only convert URLs from deic.mediacms.io
|
||||
- `https://media.myuniversity.edu` - Only convert URLs from your institution's MediaCMS
|
||||
|
||||
## Technical Details
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
amd/src/
|
||||
├── autoconvert.js # Main auto-convert module
|
||||
├── plugin.js # Plugin initialization (imports autoconvert)
|
||||
└── options.js # Configuration options definition
|
||||
|
||||
classes/
|
||||
└── plugininfo.php # Passes PHP settings to JavaScript
|
||||
|
||||
settings.php # Admin settings page definition
|
||||
|
||||
lang/en/
|
||||
└── tiny_mediacms.php # Language strings for settings
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Paste Detection**: The `autoconvert.js` module listens for `paste` events on the TinyMCE editor
|
||||
2. **URL Validation**: When text is pasted, it checks if it matches the MediaCMS URL pattern
|
||||
3. **HTML Generation**: If valid, it generates the responsive iframe HTML with configured options
|
||||
4. **Content Insertion**: The original URL is replaced with the embedded video
|
||||
|
||||
### JavaScript Configuration
|
||||
|
||||
The settings are passed from PHP to JavaScript via the `plugininfo.php` class:
|
||||
|
||||
```php
|
||||
protected static function get_autoconvert_configuration(): array {
|
||||
$baseurl = get_config('tiny_mediacms', 'autoconvert_baseurl');
|
||||
|
||||
return [
|
||||
'data' => [
|
||||
'autoConvertEnabled' => (bool) get_config('tiny_mediacms', 'autoconvertenabled'),
|
||||
'autoConvertBaseUrl' => !empty($baseurl) ? $baseurl : '',
|
||||
'autoConvertOptions' => [
|
||||
'showTitle' => (bool) get_config('tiny_mediacms', 'autoconvert_showtitle'),
|
||||
'linkTitle' => (bool) get_config('tiny_mediacms', 'autoconvert_linktitle'),
|
||||
'showRelated' => (bool) get_config('tiny_mediacms', 'autoconvert_showrelated'),
|
||||
'showUserAvatar' => (bool) get_config('tiny_mediacms', 'autoconvert_showuseravatar'),
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### Default Values (in options.js)
|
||||
|
||||
If PHP settings are not configured, the JavaScript uses these defaults:
|
||||
|
||||
```javascript
|
||||
registerOption(dataName, {
|
||||
processor: 'object',
|
||||
"default": {
|
||||
autoConvertEnabled: true,
|
||||
autoConvertBaseUrl: '', // Empty = allow all MediaCMS domains
|
||||
autoConvertOptions: {
|
||||
showTitle: true,
|
||||
linkTitle: true,
|
||||
showRelated: true,
|
||||
showUserAvatar: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
### Disabling Auto-Convert
|
||||
|
||||
To disable the feature entirely:
|
||||
1. Go to the plugin settings (see "Accessing Settings" above)
|
||||
2. Uncheck **Enable auto-convert**
|
||||
3. Save changes
|
||||
|
||||
### Programmatic Configuration
|
||||
|
||||
You can also set these values directly in the database using Moodle's `set_config()` function:
|
||||
|
||||
```php
|
||||
// Disable auto-convert
|
||||
set_config('autoconvertenabled', 0, 'tiny_mediacms');
|
||||
|
||||
// Set the MediaCMS base URL (restrict to specific domain)
|
||||
set_config('autoconvert_baseurl', 'https://deic.mediacms.io', 'tiny_mediacms');
|
||||
|
||||
// Customize embed options
|
||||
set_config('autoconvert_showtitle', 1, 'tiny_mediacms');
|
||||
set_config('autoconvert_linktitle', 0, 'tiny_mediacms');
|
||||
set_config('autoconvert_showrelated', 0, 'tiny_mediacms');
|
||||
set_config('autoconvert_showuseravatar', 1, 'tiny_mediacms');
|
||||
```
|
||||
|
||||
### CLI Configuration
|
||||
|
||||
Using Moodle CLI:
|
||||
|
||||
```bash
|
||||
# Enable auto-convert
|
||||
php admin/cli/cfg.php --component=tiny_mediacms --name=autoconvertenabled --set=1
|
||||
|
||||
# Set the MediaCMS base URL
|
||||
php admin/cli/cfg.php --component=tiny_mediacms --name=autoconvert_baseurl --set=https://deic.mediacms.io
|
||||
|
||||
# Disable showing related videos
|
||||
php admin/cli/cfg.php --component=tiny_mediacms --name=autoconvert_showrelated --set=0
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Auto-convert not working
|
||||
|
||||
1. **Check if enabled**: Verify the setting is enabled in plugin settings
|
||||
2. **Clear caches**: Purge all caches (Site administration → Development → Purge all caches)
|
||||
3. **Check URL format**: Ensure the URL matches the pattern `https://[domain]/view?m=[VIDEO_ID]`
|
||||
4. **Browser console**: Check for JavaScript errors in the browser developer console
|
||||
|
||||
### Rebuilding JavaScript
|
||||
|
||||
If you modify the source files, rebuild using:
|
||||
|
||||
```bash
|
||||
cd /path/to/moodle
|
||||
npx grunt amd --root=public/lib/editor/tiny/plugins/mediacms
|
||||
```
|
||||
|
||||
Note: Requires Node.js 22.x or compatible version as specified in Moodle's requirements.
|
||||
|
||||
## Version History
|
||||
|
||||
- **1.0.0** - Initial implementation of auto-convert feature
|
||||
20
lms-plugins/mediacms-moodle/tiny/mediacms/README.md
Normal file
20
lms-plugins/mediacms-moodle/tiny/mediacms/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# TinyMCE MediaCMS Plugin for Moodle
|
||||
|
||||
A TinyMCE editor plugin for Moodle that provides media embedding capabilities with MediaCMS/LTI integration.
|
||||
|
||||
## Build Information
|
||||
|
||||
### Preparation
|
||||
1. Get and extract Moodle 5.1
|
||||
2. cp -r lms-plugins/mediacms-moodle/tiny/mediacms/ moodle/public/lib/editor/tiny/plugins/
|
||||
3. nvm use 22 && cd moodle/public && npm install
|
||||
|
||||
### Actual build
|
||||
4. cd lib/editor/tiny/plugins/mediacms && npx grunt amd
|
||||
|
||||
### Test the output
|
||||
5. To test the output:
|
||||
cp * ../../../../../../../lms-plugins/mediacms-moodle/tiny/mediacms/ -r
|
||||
|
||||
6. Then copy to Moodle server and purge caches
|
||||
|
||||
15
lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/autoconvert.min.js
vendored
Executable file
15
lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/autoconvert.min.js
vendored
Executable file
@@ -0,0 +1,15 @@
|
||||
define("tiny_mediacms/autoconvert",["exports","./options"],(function(_exports,_options){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setupAutoConvert=_exports.isMediaCMSUrl=_exports.convertToEmbed=void 0;
|
||||
/**
|
||||
* Tiny MediaCMS Auto-convert module.
|
||||
*
|
||||
* This module automatically converts pasted MediaCMS URLs into embedded videos.
|
||||
* When a user pastes a MediaCMS video URL (e.g., https://deic.mediacms.io/view?m=JpBd1Zvdl),
|
||||
* it will be automatically converted to an iframe embed.
|
||||
*
|
||||
* @module tiny_mediacms/autoconvert
|
||||
* @copyright 2024
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
const MEDIACMS_VIEW_URL_PATTERN=/^(https?:\/\/[^\/]+)\/view\?m=([a-zA-Z0-9_-]+)$/,parseMediaCMSUrl=text=>{if(!text||"string"!=typeof text)return null;const trimmed=text.trim(),match=trimmed.match(MEDIACMS_VIEW_URL_PATTERN);return match?{baseUrl:match[1],videoId:match[2],originalUrl:trimmed}:null},isDomainAllowed=(parsed,config)=>{const configuredBaseUrl=config.autoConvertBaseUrl||config.mediacmsBaseUrl;if(!configuredBaseUrl)return!0;try{const configuredUrl=new URL(configuredBaseUrl),pastedUrl=new URL(parsed.baseUrl);return configuredUrl.host===pastedUrl.host}catch(e){return!0}},generateEmbedHtml=function(parsed){let options=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const embedUrl=new URL("".concat(parsed.baseUrl,"/embed"));embedUrl.searchParams.set("m",parsed.videoId),embedUrl.searchParams.set("showTitle",!1!==options.showTitle?"1":"0"),embedUrl.searchParams.set("showRelated",!1!==options.showRelated?"1":"0"),embedUrl.searchParams.set("showUserAvatar",!1!==options.showUserAvatar?"1":"0"),embedUrl.searchParams.set("linkTitle",!1!==options.linkTitle?"1":"0");const html='<iframe src="'.concat(embedUrl.toString(),'" ')+'style="width: 100%; aspect-ratio: 16 / 9; display: block; border: 0;" allowfullscreen="allowfullscreen"></iframe>';return html};_exports.setupAutoConvert=editor=>{const config=(0,_options.getData)(editor)||{};!1!==config.autoConvertEnabled&&(editor.on("paste",(e=>{handlePasteEvent(editor,e,config)})),editor.on("input",(e=>{handleInputEvent(editor,e,config)})))};const handlePasteEvent=(editor,e,config)=>{const clipboardData=e.clipboardData||window.clipboardData;if(!clipboardData)return;const text=clipboardData.getData("text/plain")||clipboardData.getData("text");if(!text)return;const parsed=parseMediaCMSUrl(text);if(!parsed)return;if(!isDomainAllowed(parsed,config))return;e.preventDefault(),e.stopPropagation();const embedHtml=generateEmbedHtml(parsed,config.autoConvertOptions||{});setTimeout((()=>{editor.insertContent(embedHtml),editor.selection.collapse(!1)}),0)},handleInputEvent=(editor,e,config)=>{if("insertFromPaste"!==e.inputType&&"insertText"!==e.inputType)return;const node=editor.selection.getNode();if(!node||"P"!==node.nodeName)return;const text=node.textContent||"",parsed=parseMediaCMSUrl(text);if(!parsed||!isDomainAllowed(parsed,config))return;const trimmedHtml=node.innerHTML.trim();if(trimmedHtml!==text.trim()&&!trimmedHtml.startsWith(text.trim()))return;const embedHtml=generateEmbedHtml(parsed,config.autoConvertOptions||{});setTimeout((()=>{const currentText=node.textContent||"",currentParsed=parseMediaCMSUrl(currentText);currentParsed&¤tParsed.originalUrl===parsed.originalUrl&&(editor.selection.select(node),editor.insertContent(embedHtml))}),100)};_exports.isMediaCMSUrl=text=>null!==parseMediaCMSUrl(text);_exports.convertToEmbed=function(url){let options=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const parsed=parseMediaCMSUrl(url);return parsed?generateEmbedHtml(parsed,options):null}}));
|
||||
|
||||
//# sourceMappingURL=autoconvert.min.js.map
|
||||
File diff suppressed because one or more lines are too long
10
lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/commands.min.js
vendored
Executable file
10
lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/commands.min.js
vendored
Executable file
@@ -0,0 +1,10 @@
|
||||
define("tiny_mediacms/commands",["exports","core/str","./common","./iframeembed","editor_tiny/utils"],(function(_exports,_str,_common,_iframeembed,_utils){var obj;
|
||||
/**
|
||||
* Tiny Media commands.
|
||||
*
|
||||
* @module tiny_mediacms/commands
|
||||
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getSetup=void 0,_iframeembed=(obj=_iframeembed)&&obj.__esModule?obj:{default:obj};const isIframe=node=>"iframe"===node.nodeName.toLowerCase()||node.classList&&node.classList.contains("tiny-iframe-responsive")||node.classList&&node.classList.contains("tiny-mediacms-iframe-wrapper")||"a"===node.nodeName.toLowerCase()&&"true"===node.getAttribute("data-mediacms-textlink"),setupIframeOverlays=(editor,handleIframeAction)=>{const processIframes=()=>{const editorBody=editor.getBody();if(!editorBody)return;editorBody.querySelectorAll("iframe").forEach((iframe=>{var _iframe$parentElement;if(null!==(_iframe$parentElement=iframe.parentElement)&&void 0!==_iframe$parentElement&&_iframe$parentElement.classList.contains("tiny-mediacms-iframe-wrapper"))return;if(iframe.hasAttribute("data-mce-object")||iframe.hasAttribute("data-mce-placeholder"))return;const wrapper=editor.getDoc().createElement("div");wrapper.className="tiny-mediacms-iframe-wrapper",wrapper.setAttribute("contenteditable","false");const editBtn=editor.getDoc().createElement("button");editBtn.className="tiny-mediacms-edit-btn",editBtn.setAttribute("type","button"),editBtn.setAttribute("title","Edit media embed options"),editBtn.textContent="EDIT",iframe.parentNode.insertBefore(wrapper,iframe),wrapper.appendChild(iframe),wrapper.appendChild(editBtn)}))},handleOverlayClick=e=>{const editBtn=e.target.closest(".tiny-mediacms-edit-btn");if(!editBtn)return;e.preventDefault(),e.stopPropagation();const wrapper=editBtn.closest(".tiny-mediacms-iframe-wrapper");if(!wrapper)return;wrapper.querySelector("iframe")&&(editor.selection.select(wrapper),handleIframeAction())};editor.on("init",(()=>{(()=>{const editorDoc=editor.getDoc();if(!editorDoc)return;if(editorDoc.getElementById("tiny-mediacms-overlay-styles"))return;const style=editorDoc.createElement("style");style.id="tiny-mediacms-overlay-styles",style.textContent="\n .tiny-mediacms-iframe-wrapper {\n display: inline-block;\n position: relative;\n line-height: 0;\n vertical-align: top;\n margin-top: 40px;\n }\n .tiny-mediacms-iframe-wrapper iframe {\n display: block;\n }\n .tiny-mediacms-edit-btn {\n position: absolute;\n top: -15px;\n left: 50%;\n transform: translateX(-50%);\n background: rgba(0, 0, 0, 0.7);\n color: #ffffff;\n border: none;\n border-radius: 3px;\n cursor: pointer;\n z-index: 10;\n padding: 4px 12px;\n margin: 0;\n font-size: 12px;\n font-weight: bold;\n text-decoration: none;\n box-shadow: 0 2px 4px rgba(0,0,0,0.3);\n transition: background 0.15s, box-shadow 0.15s;\n display: inline-block;\n box-sizing: border-box;\n }\n .tiny-mediacms-edit-btn:hover {\n background: rgba(0, 0, 0, 0.85);\n box-shadow: 0 3px 6px rgba(0,0,0,0.4);\n }\n ",editorDoc.head.appendChild(style)})(),processIframes(),editor.getBody().addEventListener("click",handleOverlayClick)})),editor.on("SetContent",(()=>{processIframes()})),editor.on("PastePostProcess",(()=>{setTimeout(processIframes,100)})),editor.on("Undo Redo",(()=>{processIframes()})),editor.on("Change",(()=>{setTimeout(processIframes,50)})),editor.on("NodeChange",(()=>{processIframes()}))};_exports.getSetup=async()=>{const[iframeButtonText]=await(0,_str.getStrings)(["iframebuttontitle"].map((key=>({key:key,component:_common.component})))),[iframeButtonImage]=await Promise.all([(0,_utils.getButtonImage)("icon",_common.component)]);return editor=>{((editor,iframeButtonText,iframeButtonImage)=>{const handleIframeAction=()=>{new _iframeembed.default(editor).displayDialogue()};editor.ui.registry.addIcon(_common.iframeIcon,iframeButtonImage.html),editor.ui.registry.addToggleButton(_common.iframeButtonName,{icon:_common.iframeIcon,tooltip:iframeButtonText,onAction:handleIframeAction,onSetup:api=>{const selector=["iframe:not([data-mce-object]):not([data-mce-placeholder])",".tiny-iframe-responsive",".tiny-mediacms-iframe-wrapper",'a[data-mediacms-textlink="true"]'].join(",");return editor.selection.selectorChangedWithUnbind(selector,api.setActive).unbind}}),editor.ui.registry.addMenuItem(_common.iframeMenuItemName,{icon:_common.iframeIcon,text:iframeButtonText,onAction:handleIframeAction}),editor.ui.registry.addContextToolbar(_common.iframeButtonName,{predicate:isIframe,items:_common.iframeButtonName,position:"node",scope:"node"}),editor.ui.registry.addContextMenu(_common.iframeButtonName,{update:isIframe}),setupIframeOverlays(editor,handleIframeAction)})(editor,iframeButtonText,iframeButtonImage)}}}));
|
||||
|
||||
//# sourceMappingURL=commands.min.js.map
|
||||
1
lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/commands.min.js.map
Executable file
1
lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/commands.min.js.map
Executable file
File diff suppressed because one or more lines are too long
3
lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/common.min.js
vendored
Executable file
3
lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/common.min.js
vendored
Executable file
@@ -0,0 +1,3 @@
|
||||
define("tiny_mediacms/common",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={pluginName:"tiny_mediacms/plugin",component:"tiny_mediacms",iframeButtonName:"tiny_mediacms_iframe",iframeMenuItemName:"tiny_mediacms_iframe",iframeIcon:"tiny_mediacms_iframe"},_exports.default}));
|
||||
|
||||
//# sourceMappingURL=common.min.js.map
|
||||
1
lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/common.min.js.map
Executable file
1
lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/common.min.js.map
Executable file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"common.min.js","sources":["../src/common.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny Media common values.\n *\n * @module tiny_mediacms/common\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport default {\n pluginName: 'tiny_mediacms/plugin',\n component: 'tiny_mediacms',\n iframeButtonName: 'tiny_mediacms_iframe',\n iframeMenuItemName: 'tiny_mediacms_iframe',\n iframeIcon: 'tiny_mediacms_iframe',\n};\n"],"names":["pluginName","component","iframeButtonName","iframeMenuItemName","iframeIcon"],"mappings":"sKAuBe,CACXA,WAAY,uBACZC,UAAW,gBACXC,iBAAkB,uBAClBC,mBAAoB,uBACpBC,WAAY"}
|
||||
3
lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/configuration.min.js
vendored
Executable file
3
lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/configuration.min.js
vendored
Executable file
@@ -0,0 +1,3 @@
|
||||
define("tiny_mediacms/configuration",["exports","./common","editor_tiny/utils"],(function(_exports,_common,_utils){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.configure=void 0;_exports.configure=instanceConfig=>{return{contextmenu:(0,_utils.addContextmenuItem)(instanceConfig.contextmenu,_common.iframeButtonName),menu:(menu=instanceConfig.menu,menu.insert.items="".concat(_common.iframeMenuItemName," ").concat(menu.insert.items),menu),toolbar:(toolbar=instanceConfig.toolbar,toolbar.map((section=>("content"===section.name&§ion.items.unshift(_common.iframeButtonName),section))))};var toolbar,menu}}));
|
||||
|
||||
//# sourceMappingURL=configuration.min.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"configuration.min.js","sources":["../src/configuration.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny Media configuration.\n *\n * @module tiny_mediacms/configuration\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {\n iframeButtonName,\n iframeMenuItemName,\n} from './common';\nimport {\n addContextmenuItem,\n} from 'editor_tiny/utils';\n\nconst configureMenu = (menu) => {\n // Add the Iframe Embed to the insert menu.\n menu.insert.items = `${iframeMenuItemName} ${menu.insert.items}`;\n\n return menu;\n};\n\nconst configureToolbar = (toolbar) => {\n // The toolbar contains an array of named sections.\n // The Moodle integration ensures that there is a section called 'content'.\n\n return toolbar.map((section) => {\n if (section.name === 'content') {\n // Insert the iframe button at the start of it.\n section.items.unshift(iframeButtonName);\n }\n\n return section;\n });\n};\n\nexport const configure = (instanceConfig) => {\n // Update the instance configuration to add the Iframe Embed menu option to the menus and toolbars.\n return {\n contextmenu: addContextmenuItem(instanceConfig.contextmenu, iframeButtonName),\n menu: configureMenu(instanceConfig.menu),\n toolbar: configureToolbar(instanceConfig.toolbar),\n };\n};\n"],"names":["instanceConfig","contextmenu","iframeButtonName","menu","insert","items","iframeMenuItemName","toolbar","map","section","name","unshift"],"mappings":"wNAoD0BA,uBAEf,CACHC,aAAa,6BAAmBD,eAAeC,YAAaC,0BAC5DC,MAzBeA,KAyBKH,eAAeG,KAvBvCA,KAAKC,OAAOC,gBAAWC,uCAAsBH,KAAKC,OAAOC,OAElDF,MAsBHI,SAnBkBA,QAmBQP,eAAeO,QAftCA,QAAQC,KAAKC,UACK,YAAjBA,QAAQC,MAERD,QAAQJ,MAAMM,QAAQT,0BAGnBO,aAVWF,IAAAA,QAPHJ"}
|
||||
3
lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/embed.min.js
vendored
Executable file
3
lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/embed.min.js
vendored
Executable file
File diff suppressed because one or more lines are too long
1
lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/embed.min.js.map
Executable file
1
lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/embed.min.js.map
Executable file
File diff suppressed because one or more lines are too long
3
lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/embedmodal.min.js
vendored
Executable file
3
lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/embedmodal.min.js
vendored
Executable file
@@ -0,0 +1,3 @@
|
||||
define("tiny_mediacms/embedmodal",["exports","core/modal","./common"],(function(_exports,_modal,_common){var obj;function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal=(obj=_modal)&&obj.__esModule?obj:{default:obj};class EmbedModal extends _modal.default{registerEventListeners(){super.registerEventListeners(),this.registerCloseOnSave(),this.registerCloseOnCancel()}configure(modalConfig){modalConfig.large=!0,modalConfig.removeOnClose=!0,modalConfig.show=!0,super.configure(modalConfig)}}return _exports.default=EmbedModal,_defineProperty(EmbedModal,"TYPE","".concat(_common.component,"/modal")),_defineProperty(EmbedModal,"TEMPLATE","".concat(_common.component,"/embed_media_modal")),_exports.default}));
|
||||
|
||||
//# sourceMappingURL=embedmodal.min.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"embedmodal.min.js","sources":["../src/embedmodal.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Embedded Media Management Modal for Tiny.\n *\n * @module tiny_mediacms/embedmodal\n * @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Modal from 'core/modal';\nimport {component} from './common';\n\nexport default class EmbedModal extends Modal {\n static TYPE = `${component}/modal`;\n static TEMPLATE = `${component}/embed_media_modal`;\n\n registerEventListeners() {\n // Call the parent registration.\n super.registerEventListeners();\n\n // Register to close on save/cancel.\n this.registerCloseOnSave();\n this.registerCloseOnCancel();\n }\n\n configure(modalConfig) {\n modalConfig.large = true;\n modalConfig.removeOnClose = true;\n modalConfig.show = true;\n\n super.configure(modalConfig);\n }\n}\n"],"names":["EmbedModal","Modal","registerEventListeners","registerCloseOnSave","registerCloseOnCancel","configure","modalConfig","large","removeOnClose","show","component"],"mappings":"iaA0BqBA,mBAAmBC,eAIpCC,+BAEUA,8BAGDC,2BACAC,wBAGTC,UAAUC,aACNA,YAAYC,OAAQ,EACpBD,YAAYE,eAAgB,EAC5BF,YAAYG,MAAO,QAEbJ,UAAUC,iEAlBHN,4BACAU,6CADAV,gCAEIU"}
|
||||
3
lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/iframeembed.min.js
vendored
Executable file
3
lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/iframeembed.min.js
vendored
Executable file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
3
lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/iframemodal.min.js
vendored
Executable file
3
lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/iframemodal.min.js
vendored
Executable file
@@ -0,0 +1,3 @@
|
||||
define("tiny_mediacms/iframemodal",["exports","core/modal","./common"],(function(_exports,_modal,_common){var obj;function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal=(obj=_modal)&&obj.__esModule?obj:{default:obj};class IframeModal extends _modal.default{registerEventListeners(){super.registerEventListeners(),this.registerCloseOnSave(),this.registerCloseOnCancel()}configure(modalConfig){modalConfig.large=!0,modalConfig.removeOnClose=!0,modalConfig.show=!0,super.configure(modalConfig)}}return _exports.default=IframeModal,_defineProperty(IframeModal,"TYPE","".concat(_common.component,"/iframemodal")),_defineProperty(IframeModal,"TEMPLATE","".concat(_common.component,"/iframe_embed_modal")),_exports.default}));
|
||||
|
||||
//# sourceMappingURL=iframemodal.min.js.map
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user