mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-03-10 23:17:21 -04:00
Compare commits
242 Commits
frontend-u
...
ltimoodle1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c8a464845 | ||
|
|
e0974433a4 | ||
|
|
e130c7403c | ||
|
|
182fb9599a | ||
|
|
eadaffb27c | ||
|
|
27bba37a55 | ||
|
|
90b480b4f8 | ||
|
|
583a8c4512 | ||
|
|
b806ffd7f5 | ||
|
|
a6f7ce71c6 | ||
|
|
ab5f86ee4d | ||
|
|
216365fdb6 | ||
|
|
66b19b99f2 | ||
|
|
692b6b5f09 | ||
|
|
814a7d3cee | ||
|
|
31571fc4cb | ||
|
|
edd40d45b4 | ||
|
|
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 | ||
|
|
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,11 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [7.6.0](https://github.com/mediacms-io/mediacms/compare/v7.5.0...v7.6.0) (2026-02-07)
|
||||
|
||||
### Features
|
||||
|
||||
* Create SECURITY.md ([#1485](https://github.com/mediacms-io/mediacms/issues/1485)) ([11449c2](https://github.com/mediacms-io/mediacms/commit/11449c2187d0f450b86915d88f92595a1825e4cf))
|
||||
|
||||
## [7.5.0](https://github.com/mediacms-io/mediacms/compare/v7.4.0...v7.5.0) (2026-02-06)
|
||||
|
||||
### Features
|
||||
|
||||
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
|
||||
|
||||
54
SECURITY.md
54
SECURITY.md
@@ -1,54 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
Thank you for helping improve the security of MediaCMS.
|
||||
We take security vulnerabilities seriously and appreciate responsible disclosure.
|
||||
|
||||
---
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability in MediaCMS, **please do not open a public GitHub issue**.
|
||||
|
||||
Instead, report it using one of the following methods:
|
||||
|
||||
- **GitHub Security Advisories (preferred)**
|
||||
Use the "Report a vulnerability" feature in this repository.
|
||||
|
||||
- **Contact Form**
|
||||
Submit details via the official contact page:
|
||||
https://mediacms.io/contact/
|
||||
|
||||
Please include as much of the following information as possible:
|
||||
- Affected version(s)
|
||||
- Detailed description of the issue
|
||||
- Steps to reproduce (PoC if available)
|
||||
- Impact assessment (e.g. RCE, XSS, privilege escalation)
|
||||
- Any potential mitigations you are aware of
|
||||
|
||||
---
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Security updates are provided for the **latest stable release** of MediaCMS.
|
||||
Older versions may not receive security patches.
|
||||
|
||||
---
|
||||
|
||||
## Disclosure Policy
|
||||
|
||||
- We aim to acknowledge reports within **7 days**
|
||||
- We aim to provide a fix or mitigation within **90 days**, depending on severity
|
||||
- Please allow us time to investigate before any public disclosure
|
||||
|
||||
We follow responsible disclosure practices and will coordinate disclosure timelines when appropriate.
|
||||
|
||||
---
|
||||
|
||||
## Recognition
|
||||
|
||||
At this time, MediaCMS does not operate a formal bug bounty program.
|
||||
However, we are happy to acknowledge valid security reports in release notes or advisories (with your permission).
|
||||
|
||||
---
|
||||
|
||||
Thank you for helping keep MediaCMS secure.
|
||||
@@ -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.9"
|
||||
VERSION = "8.12"
|
||||
|
||||
@@ -73,3 +73,8 @@ Can be set through the SAML Configurations tab:
|
||||
3. **Group Role Mapping**: Maps the role returned by SAML (as set in the SAML Configuration tab) with the role in groups that user will be added
|
||||
4. **Group mapping**: This creates groups associated with this IDP. Group ids as they come from SAML, associated with MediaCMS groups
|
||||
5. **Category Mapping**: This maps a group id (from SAML response) with a category in MediaCMS
|
||||
|
||||
|
||||
## More options
|
||||
USER_SEARCH_FIELD = "name_username_email"
|
||||
ALLOW_MEDIA_REPLACEMENT = True
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -48,6 +48,7 @@ translation_strings = {
|
||||
"DELETE MEDIA": "حذف الوسائط",
|
||||
"DOWNLOAD": "تحميل",
|
||||
"DURATION": "المدة",
|
||||
"Delete Comments": "",
|
||||
"Delete Media": "حذف الوسائط",
|
||||
"Delete media": "حذف الوسائط",
|
||||
"Disable Comments": "تعطيل التعليقات",
|
||||
@@ -70,6 +71,7 @@ translation_strings = {
|
||||
"Failed to change owner. Please try again.": "فشل تغيير المالك. يرجى المحاولة مرة أخرى.",
|
||||
"Failed to copy media.": "فشل نسخ الوسائط.",
|
||||
"Failed to create playlist": "فشل إنشاء قائمة التشغيل",
|
||||
"Failed to delete comments.": "",
|
||||
"Failed to delete media. Please try again.": "فشل حذف الوسائط. يرجى المحاولة مرة أخرى.",
|
||||
"Failed to disable comments.": "فشل تعطيل التعليقات.",
|
||||
"Failed to disable download.": "فشل تعطيل التنزيل.",
|
||||
@@ -178,6 +180,10 @@ translation_strings = {
|
||||
"Select all media": "تحديد جميع الوسائط",
|
||||
"Select publish state:": "اختر حالة النشر:",
|
||||
"Selected": "محدد",
|
||||
"Share with Co-Editors": "",
|
||||
"Share with Co-Owners": "",
|
||||
"Share with Co-Viewers": "",
|
||||
"Share with Course Members": "",
|
||||
"Shared by me": "مشاركة مني",
|
||||
"Shared with me": "مشاركة معي",
|
||||
"Sign in": "تسجيل الدخول",
|
||||
@@ -196,6 +202,7 @@ translation_strings = {
|
||||
"Successfully Enabled comments": "تم تفعيل التعليقات بنجاح",
|
||||
"Successfully changed owner": "تم تغيير المالك بنجاح",
|
||||
"Successfully deleted": "تم الحذف بنجاح",
|
||||
"Successfully deleted comments": "",
|
||||
"Successfully updated": "تم التحديث بنجاح",
|
||||
"Successfully updated categories": "تم تحديث الفئات بنجاح",
|
||||
"Successfully updated playlist membership": "تم تحديث عضوية قائمة التشغيل بنجاح",
|
||||
@@ -240,6 +247,7 @@ translation_strings = {
|
||||
"Welcome": "مرحباً",
|
||||
"You are going to copy": "سوف تقوم بالنسخ",
|
||||
"You are going to delete": "سوف تقوم بالحذف",
|
||||
"You are going to delete all comments from": "",
|
||||
"You are going to disable comments to": "سوف تقوم بتعطيل التعليقات لـ",
|
||||
"You are going to disable download for": "سوف تقوم بتعطيل التنزيل لـ",
|
||||
"You are going to enable comments to": "سوف تقوم بتفعيل التعليقات لـ",
|
||||
|
||||
@@ -48,6 +48,7 @@ translation_strings = {
|
||||
"DELETE MEDIA": "মিডিয়া মুছুন",
|
||||
"DOWNLOAD": "ডাউনলোড",
|
||||
"DURATION": "সময়কাল",
|
||||
"Delete Comments": "",
|
||||
"Delete Media": "",
|
||||
"Delete media": "মিডিয়া মুছুন",
|
||||
"Disable Comments": "",
|
||||
@@ -70,6 +71,7 @@ translation_strings = {
|
||||
"Failed to change owner. Please try again.": "",
|
||||
"Failed to copy media.": "মিডিয়া কপি করতে ব্যর্থ হয়েছে।",
|
||||
"Failed to create playlist": "",
|
||||
"Failed to delete comments.": "",
|
||||
"Failed to delete media. Please try again.": "মিডিয়া মুছতে ব্যর্থ হয়েছে। দয়া করে আবার চেষ্টা করুন।",
|
||||
"Failed to disable comments.": "মন্তব্য নিষ্ক্রিয় করতে ব্যর্থ হয়েছে।",
|
||||
"Failed to disable download.": "ডাউনলোড নিষ্ক্রিয় করতে ব্যর্থ হয়েছে।",
|
||||
@@ -178,6 +180,10 @@ translation_strings = {
|
||||
"Select all media": "",
|
||||
"Select publish state:": "",
|
||||
"Selected": "",
|
||||
"Share with Co-Editors": "",
|
||||
"Share with Co-Owners": "",
|
||||
"Share with Co-Viewers": "",
|
||||
"Share with Course Members": "",
|
||||
"Shared by me": "আমার দ্বারা শেয়ার করা",
|
||||
"Shared with me": "আমার সাথে শেয়ার করা",
|
||||
"Sign in": "সাইন ইন করুন",
|
||||
@@ -196,6 +202,7 @@ translation_strings = {
|
||||
"Successfully Enabled comments": "মন্তব্য সফলভাবে সক্রিয় হয়েছে",
|
||||
"Successfully changed owner": "",
|
||||
"Successfully deleted": "সফলভাবে মুছে ফেলা হয়েছে",
|
||||
"Successfully deleted comments": "",
|
||||
"Successfully updated": "",
|
||||
"Successfully updated categories": "",
|
||||
"Successfully updated playlist membership": "",
|
||||
@@ -240,6 +247,7 @@ translation_strings = {
|
||||
"Welcome": "স্বাগতম",
|
||||
"You are going to copy": "আপনি কপি করতে চলেছেন",
|
||||
"You are going to delete": "আপনি মুছে ফেলতে চলেছেন",
|
||||
"You are going to delete all comments from": "",
|
||||
"You are going to disable comments to": "আপনি মন্তব্য নিষ্ক্রিয় করতে চলেছেন",
|
||||
"You are going to disable download for": "আপনি ডাউনলোড নিষ্ক্রিয় করতে চলেছেন",
|
||||
"You are going to enable comments to": "আপনি মন্তব্য সক্রিয় করতে চলেছেন",
|
||||
|
||||
@@ -48,6 +48,7 @@ translation_strings = {
|
||||
"DELETE MEDIA": "SLET MEDIE",
|
||||
"DOWNLOAD": "HENT",
|
||||
"DURATION": "VARIGHED",
|
||||
"Delete Comments": "",
|
||||
"Delete Media": "Slet Medie",
|
||||
"Delete media": "Slet medie",
|
||||
"Disable Comments": "Deaktiver Kommentarer",
|
||||
@@ -70,6 +71,7 @@ translation_strings = {
|
||||
"Failed to change owner. Please try again.": "Ændring af ejer mislykkedes. Prøv venligst igen.",
|
||||
"Failed to copy media.": "Kopiering af medie mislykkedes.",
|
||||
"Failed to create playlist": "Oprettelse af playliste mislykkedes",
|
||||
"Failed to delete comments.": "",
|
||||
"Failed to delete media. Please try again.": "Sletning af medie mislykkedes. Prøv venligst igen.",
|
||||
"Failed to disable comments.": "Deaktivering af kommentarer mislykkedes.",
|
||||
"Failed to disable download.": "Deaktivering af download mislykkedes.",
|
||||
@@ -178,6 +180,10 @@ translation_strings = {
|
||||
"Select all media": "Vælg alle medier",
|
||||
"Select publish state:": "Vælg publiceringsstatus:",
|
||||
"Selected": "Valgt",
|
||||
"Share with Co-Editors": "",
|
||||
"Share with Co-Owners": "",
|
||||
"Share with Co-Viewers": "",
|
||||
"Share with Course Members": "",
|
||||
"Shared by me": "Delt af mig",
|
||||
"Shared with me": "Delt med mig",
|
||||
"Sign in": "Log ind",
|
||||
@@ -196,6 +202,7 @@ translation_strings = {
|
||||
"Successfully Enabled comments": "Kommentarer aktiveret med succes",
|
||||
"Successfully changed owner": "Ejer ændret med succes",
|
||||
"Successfully deleted": "Slettet med succes",
|
||||
"Successfully deleted comments": "",
|
||||
"Successfully updated": "Opdateret med succes",
|
||||
"Successfully updated categories": "Kategorier opdateret med succes",
|
||||
"Successfully updated playlist membership": "Playlistemedlemskab opdateret med succes",
|
||||
@@ -240,6 +247,7 @@ translation_strings = {
|
||||
"Welcome": "Velkommen",
|
||||
"You are going to copy": "Du er ved at kopiere",
|
||||
"You are going to delete": "Du er ved at slette",
|
||||
"You are going to delete all comments from": "",
|
||||
"You are going to disable comments to": "Du er ved at deaktivere kommentarer til",
|
||||
"You are going to disable download for": "Du er ved at deaktivere download for",
|
||||
"You are going to enable comments to": "Du er ved at aktivere kommentarer til",
|
||||
|
||||
@@ -48,6 +48,7 @@ translation_strings = {
|
||||
"DELETE MEDIA": "MEDIEN LÖSCHEN",
|
||||
"DOWNLOAD": "HERUNTERLADEN",
|
||||
"DURATION": "DAUER",
|
||||
"Delete Comments": "",
|
||||
"Delete Media": "Medien löschen",
|
||||
"Delete media": "Medien löschen",
|
||||
"Disable Comments": "Kommentare deaktivieren",
|
||||
@@ -70,6 +71,7 @@ translation_strings = {
|
||||
"Failed to change owner. Please try again.": "Fehler beim Ändern des Eigentümers. Bitte versuchen Sie es erneut.",
|
||||
"Failed to copy media.": "Fehler beim Kopieren der Medien.",
|
||||
"Failed to create playlist": "Fehler beim Erstellen der Playlist",
|
||||
"Failed to delete comments.": "",
|
||||
"Failed to delete media. Please try again.": "Fehler beim Löschen der Medien. Bitte versuchen Sie es erneut.",
|
||||
"Failed to disable comments.": "Fehler beim Deaktivieren der Kommentare.",
|
||||
"Failed to disable download.": "Fehler beim Deaktivieren des Downloads.",
|
||||
@@ -178,6 +180,10 @@ translation_strings = {
|
||||
"Select all media": "Alle Medien auswählen",
|
||||
"Select publish state:": "Veröffentlichungsstatus auswählen:",
|
||||
"Selected": "Ausgewählt",
|
||||
"Share with Co-Editors": "",
|
||||
"Share with Co-Owners": "",
|
||||
"Share with Co-Viewers": "",
|
||||
"Share with Course Members": "",
|
||||
"Shared by me": "Von mir geteilt",
|
||||
"Shared with me": "Mit mir geteilt",
|
||||
"Sign in": "Anmelden",
|
||||
@@ -196,6 +202,7 @@ translation_strings = {
|
||||
"Successfully Enabled comments": "Kommentare erfolgreich aktiviert",
|
||||
"Successfully changed owner": "Eigentümer erfolgreich geändert",
|
||||
"Successfully deleted": "Erfolgreich gelöscht",
|
||||
"Successfully deleted comments": "",
|
||||
"Successfully updated": "Erfolgreich aktualisiert",
|
||||
"Successfully updated categories": "Kategorien erfolgreich aktualisiert",
|
||||
"Successfully updated playlist membership": "Playlist-Mitgliedschaft erfolgreich aktualisiert",
|
||||
@@ -240,6 +247,7 @@ translation_strings = {
|
||||
"Welcome": "Willkommen",
|
||||
"You are going to copy": "Sie werden kopieren",
|
||||
"You are going to delete": "Sie werden löschen",
|
||||
"You are going to delete all comments from": "",
|
||||
"You are going to disable comments to": "Sie werden Kommentare deaktivieren für",
|
||||
"You are going to disable download for": "Sie werden Download deaktivieren für",
|
||||
"You are going to enable comments to": "Sie werden Kommentare aktivieren für",
|
||||
|
||||
@@ -48,6 +48,7 @@ translation_strings = {
|
||||
"DELETE MEDIA": "ΔΙΑΓΡΑΦΗ ΑΡΧΕΙΟΥ",
|
||||
"DOWNLOAD": "ΚΑΤΕΒΑΣΜΑ",
|
||||
"DURATION": "ΔΙΑΡΚΕΙΑ",
|
||||
"Delete Comments": "",
|
||||
"Delete Media": "Διαγραφή Αρχείου",
|
||||
"Delete media": "Διαγραφή αρχείου",
|
||||
"Disable Comments": "Απενεργοποίηση Σχολίων",
|
||||
@@ -70,6 +71,7 @@ translation_strings = {
|
||||
"Failed to change owner. Please try again.": "Αποτυχία αλλαγής ιδιοκτήτη. Παρακαλώ δοκιμάστε ξανά.",
|
||||
"Failed to copy media.": "Αποτυχία αντιγραφής αρχείου.",
|
||||
"Failed to create playlist": "Αποτυχία δημιουργίας λίστας",
|
||||
"Failed to delete comments.": "",
|
||||
"Failed to delete media. Please try again.": "Αποτυχία διαγραφής αρχείου. Παρακαλώ δοκιμάστε ξανά.",
|
||||
"Failed to disable comments.": "Αποτυχία απενεργοποίησης σχολίων.",
|
||||
"Failed to disable download.": "Αποτυχία απενεργοποίησης λήψης.",
|
||||
@@ -178,6 +180,10 @@ translation_strings = {
|
||||
"Select all media": "Επιλογή όλων των αρχείων",
|
||||
"Select publish state:": "Επιλέξτε κατάσταση δημοσίευσης:",
|
||||
"Selected": "Επιλεγμένα",
|
||||
"Share with Co-Editors": "",
|
||||
"Share with Co-Owners": "",
|
||||
"Share with Co-Viewers": "",
|
||||
"Share with Course Members": "",
|
||||
"Shared by me": "Κοινοποιήθηκαν από εμένα",
|
||||
"Shared with me": "Κοινοποιήθηκαν σε εμένα",
|
||||
"Sign in": "Σύνδεση",
|
||||
@@ -196,6 +202,7 @@ translation_strings = {
|
||||
"Successfully Enabled comments": "Τα σχόλια ενεργοποιήθηκαν με επιτυχία",
|
||||
"Successfully changed owner": "Ο ιδιοκτήτης άλλαξε με επιτυχία",
|
||||
"Successfully deleted": "Διαγράφηκε με επιτυχία",
|
||||
"Successfully deleted comments": "",
|
||||
"Successfully updated": "Ενημερώθηκε με επιτυχία",
|
||||
"Successfully updated categories": "Οι κατηγορίες ενημερώθηκαν με επιτυχία",
|
||||
"Successfully updated playlist membership": "Η συμμετοχή στη λίστα ενημερώθηκε με επιτυχία",
|
||||
@@ -240,6 +247,7 @@ translation_strings = {
|
||||
"Welcome": "Καλώς ήρθατε",
|
||||
"You are going to copy": "Πρόκειται να αντιγράψετε",
|
||||
"You are going to delete": "Πρόκειται να διαγράψετε",
|
||||
"You are going to delete all comments from": "",
|
||||
"You are going to disable comments to": "Πρόκειται να απενεργοποιήσετε τα σχόλια για",
|
||||
"You are going to disable download for": "Πρόκειται να απενεργοποιήσετε τη λήψη για",
|
||||
"You are going to enable comments to": "Πρόκειται να ενεργοποιήσετε τα σχόλια για",
|
||||
|
||||
@@ -48,6 +48,7 @@ translation_strings = {
|
||||
"DELETE": "",
|
||||
"DELETE MEDIA": "",
|
||||
"Delete media": "",
|
||||
"Delete Comments": "",
|
||||
"Delete Media": "",
|
||||
"Disable Comments": "",
|
||||
"Disable Download": "",
|
||||
@@ -71,6 +72,7 @@ translation_strings = {
|
||||
"Failed to change owner. Please try again.": "",
|
||||
"Failed to copy media.": "",
|
||||
"Failed to create playlist": "",
|
||||
"Failed to delete comments.": "",
|
||||
"Failed to delete media. Please try again.": "",
|
||||
"Failed to disable comments.": "",
|
||||
"Failed to disable download.": "",
|
||||
@@ -184,6 +186,10 @@ translation_strings = {
|
||||
"Selected": "",
|
||||
"selected": "",
|
||||
"SHARE": "",
|
||||
"Share with Co-Editors": "",
|
||||
"Share with Co-Owners": "",
|
||||
"Share with Co-Viewers": "",
|
||||
"Share with Course Members": "",
|
||||
"Shared by me": "",
|
||||
"Shared with me": "",
|
||||
"SHOW MORE": "",
|
||||
@@ -201,6 +207,7 @@ translation_strings = {
|
||||
"Successfully changed owner": "",
|
||||
"Successfully Copied": "",
|
||||
"Successfully deleted": "",
|
||||
"Successfully deleted comments": "",
|
||||
"Successfully Disabled comments": "",
|
||||
"Successfully Disabled Download": "",
|
||||
"Successfully Enabled comments": "",
|
||||
@@ -252,6 +259,7 @@ translation_strings = {
|
||||
"yet": "",
|
||||
"You are going to copy": "",
|
||||
"You are going to delete": "",
|
||||
"You are going to delete all comments from": "",
|
||||
"You are going to disable comments to": "",
|
||||
"You are going to disable download for": "",
|
||||
"You are going to enable comments to": "",
|
||||
|
||||
@@ -48,6 +48,7 @@ translation_strings = {
|
||||
"DELETE MEDIA": "ELIMINAR MEDIOS",
|
||||
"DOWNLOAD": "DESCARGAR",
|
||||
"DURATION": "DURACIÓN",
|
||||
"Delete Comments": "",
|
||||
"Delete Media": "Eliminar Medio",
|
||||
"Delete media": "Eliminar medios",
|
||||
"Disable Comments": "Deshabilitar Comentarios",
|
||||
@@ -70,6 +71,7 @@ translation_strings = {
|
||||
"Failed to change owner. Please try again.": "Error al cambiar propietario. Por favor, inténtelo de nuevo.",
|
||||
"Failed to copy media.": "Error al copiar medios.",
|
||||
"Failed to create playlist": "Error al crear lista de reproducción",
|
||||
"Failed to delete comments.": "",
|
||||
"Failed to delete media. Please try again.": "Error al eliminar medios. Por favor, inténtelo de nuevo.",
|
||||
"Failed to disable comments.": "Error al deshabilitar comentarios.",
|
||||
"Failed to disable download.": "Error al deshabilitar descarga.",
|
||||
@@ -178,6 +180,10 @@ translation_strings = {
|
||||
"Select all media": "Seleccionar todos los medios",
|
||||
"Select publish state:": "Seleccionar estado de publicación:",
|
||||
"Selected": "Seleccionado",
|
||||
"Share with Co-Editors": "",
|
||||
"Share with Co-Owners": "",
|
||||
"Share with Co-Viewers": "",
|
||||
"Share with Course Members": "",
|
||||
"Shared by me": "Compartido por mí",
|
||||
"Shared with me": "Compartido conmigo",
|
||||
"Sign in": "Iniciar sesión",
|
||||
@@ -196,6 +202,7 @@ translation_strings = {
|
||||
"Successfully Enabled comments": "Comentarios habilitados exitosamente",
|
||||
"Successfully changed owner": "Propietario cambiado exitosamente",
|
||||
"Successfully deleted": "Eliminado exitosamente",
|
||||
"Successfully deleted comments": "",
|
||||
"Successfully updated": "Actualizado exitosamente",
|
||||
"Successfully updated categories": "Categorías actualizadas exitosamente",
|
||||
"Successfully updated playlist membership": "Membresía de lista de reproducción actualizada exitosamente",
|
||||
@@ -240,6 +247,7 @@ translation_strings = {
|
||||
"Welcome": "Bienvenido",
|
||||
"You are going to copy": "Vas a copiar",
|
||||
"You are going to delete": "Vas a eliminar",
|
||||
"You are going to delete all comments from": "",
|
||||
"You are going to disable comments to": "Vas a deshabilitar comentarios de",
|
||||
"You are going to disable download for": "Vas a deshabilitar descarga de",
|
||||
"You are going to enable comments to": "Vas a habilitar comentarios de",
|
||||
|
||||
@@ -49,6 +49,7 @@ translation_strings = {
|
||||
"DELETE MEDIA": "SUPPRIMER LE MÉDIA",
|
||||
"DOWNLOAD": "TÉLÉCHARGER",
|
||||
"DURATION": "DURÉE",
|
||||
"Delete Comments": "",
|
||||
"Delete Media": "Supprimer le média",
|
||||
"Delete media": "Supprimer le média",
|
||||
"Disable Comments": "Désactiver les commentaires",
|
||||
@@ -71,6 +72,7 @@ translation_strings = {
|
||||
"Failed to change owner. Please try again.": "Échec du changement de propriétaire. Veuillez réessayer.",
|
||||
"Failed to copy media.": "Échec de la copie du média.",
|
||||
"Failed to create playlist": "Échec de la création de la playlist",
|
||||
"Failed to delete comments.": "",
|
||||
"Failed to delete media. Please try again.": "Échec de la suppression du média. Veuillez réessayer.",
|
||||
"Failed to disable comments.": "Échec de la désactivation des commentaires.",
|
||||
"Failed to disable download.": "Échec de la désactivation du téléchargement.",
|
||||
@@ -179,6 +181,10 @@ translation_strings = {
|
||||
"Select all media": "Sélectionner tous les médias",
|
||||
"Select publish state:": "Sélectionner l'état de publication:",
|
||||
"Selected": "Sélectionné",
|
||||
"Share with Co-Editors": "",
|
||||
"Share with Co-Owners": "",
|
||||
"Share with Co-Viewers": "",
|
||||
"Share with Course Members": "",
|
||||
"Shared by me": "Partagé par moi",
|
||||
"Shared with me": "Partagé avec moi",
|
||||
"Sign in": "Se connecter",
|
||||
@@ -197,6 +203,7 @@ translation_strings = {
|
||||
"Successfully Enabled comments": "Commentaires activés avec succès",
|
||||
"Successfully changed owner": "Propriétaire changé avec succès",
|
||||
"Successfully deleted": "Supprimé avec succès",
|
||||
"Successfully deleted comments": "",
|
||||
"Successfully updated": "Mis à jour avec succès",
|
||||
"Successfully updated categories": "Catégories mises à jour avec succès",
|
||||
"Successfully updated playlist membership": "Adhésion à la playlist mise à jour avec succès",
|
||||
@@ -241,6 +248,7 @@ translation_strings = {
|
||||
"Welcome": "Bienvenue",
|
||||
"You are going to copy": "Vous allez copier",
|
||||
"You are going to delete": "Vous allez supprimer",
|
||||
"You are going to delete all comments from": "",
|
||||
"You are going to disable comments to": "Vous allez désactiver les commentaires de",
|
||||
"You are going to disable download for": "Vous allez désactiver le téléchargement de",
|
||||
"You are going to enable comments to": "Vous allez activer les commentaires de",
|
||||
|
||||
@@ -48,6 +48,7 @@ translation_strings = {
|
||||
"DELETE MEDIA": "מחק מדיה",
|
||||
"DOWNLOAD": "הורד",
|
||||
"DURATION": "משך",
|
||||
"Delete Comments": "",
|
||||
"Delete Media": "",
|
||||
"Delete media": "מחק מדיה",
|
||||
"Disable Comments": "",
|
||||
@@ -70,6 +71,7 @@ translation_strings = {
|
||||
"Failed to change owner. Please try again.": "",
|
||||
"Failed to copy media.": "העתקת המדיה נכשלה.",
|
||||
"Failed to create playlist": "",
|
||||
"Failed to delete comments.": "",
|
||||
"Failed to delete media. Please try again.": "מחיקת המדיה נכשלה. אנא נסה שוב.",
|
||||
"Failed to disable comments.": "ביטול התגובות נכשל.",
|
||||
"Failed to disable download.": "ביטול ההורדה נכשל.",
|
||||
@@ -178,6 +180,10 @@ translation_strings = {
|
||||
"Select all media": "",
|
||||
"Select publish state:": "",
|
||||
"Selected": "",
|
||||
"Share with Co-Editors": "",
|
||||
"Share with Co-Owners": "",
|
||||
"Share with Co-Viewers": "",
|
||||
"Share with Course Members": "",
|
||||
"Shared by me": "שותף על ידי",
|
||||
"Shared with me": "שותף איתי",
|
||||
"Sign in": "התחבר",
|
||||
@@ -196,6 +202,7 @@ translation_strings = {
|
||||
"Successfully Enabled comments": "התגובות הופעלו בהצלחה",
|
||||
"Successfully changed owner": "",
|
||||
"Successfully deleted": "נמחק בהצלחה",
|
||||
"Successfully deleted comments": "",
|
||||
"Successfully updated": "",
|
||||
"Successfully updated categories": "",
|
||||
"Successfully updated playlist membership": "",
|
||||
@@ -240,6 +247,7 @@ translation_strings = {
|
||||
"Welcome": "ברוך הבא",
|
||||
"You are going to copy": "אתה עומד להעתיק",
|
||||
"You are going to delete": "אתה עומד למחוק",
|
||||
"You are going to delete all comments from": "",
|
||||
"You are going to disable comments to": "אתה עומד לבטל תגובות ל",
|
||||
"You are going to disable download for": "אתה עומד לבטל הורדה עבור",
|
||||
"You are going to enable comments to": "אתה עומד להפעיל תגובות ל",
|
||||
|
||||
@@ -48,6 +48,7 @@ translation_strings = {
|
||||
"DELETE MEDIA": "मीडिया हटाएं",
|
||||
"DOWNLOAD": "डाउनलोड करें",
|
||||
"DURATION": "अवधि",
|
||||
"Delete Comments": "",
|
||||
"Delete Media": "मीडिया हटाएं",
|
||||
"Delete media": "मीडिया हटाएं",
|
||||
"Disable Comments": "टिप्पणियां अक्षम करें",
|
||||
@@ -70,6 +71,7 @@ translation_strings = {
|
||||
"Failed to change owner. Please try again.": "स्वामी बदलने में विफल। कृपया पुनः प्रयास करें।",
|
||||
"Failed to copy media.": "मीडिया कॉपी करने में विफल।",
|
||||
"Failed to create playlist": "प्लेलिस्ट बनाने में विफल",
|
||||
"Failed to delete comments.": "",
|
||||
"Failed to delete media. Please try again.": "मीडिया हटाने में विफल। कृपया पुनः प्रयास करें।",
|
||||
"Failed to disable comments.": "टिप्पणियों को अक्षम करने में विफल।",
|
||||
"Failed to disable download.": "डाउनलोड अक्षम करने में विफल।",
|
||||
@@ -178,6 +180,10 @@ translation_strings = {
|
||||
"Select all media": "सभी मीडिया चुनें",
|
||||
"Select publish state:": "प्रकाशन स्थिति चुनें:",
|
||||
"Selected": "चयनित",
|
||||
"Share with Co-Editors": "",
|
||||
"Share with Co-Owners": "",
|
||||
"Share with Co-Viewers": "",
|
||||
"Share with Course Members": "",
|
||||
"Shared by me": "मेरे द्वारा साझा किया गया",
|
||||
"Shared with me": "मेरे साथ साझा किया गया",
|
||||
"Sign in": "साइन इन करें",
|
||||
@@ -196,6 +202,7 @@ translation_strings = {
|
||||
"Successfully Enabled comments": "टिप्पणियां सफलतापूर्वक सक्षम की गईं",
|
||||
"Successfully changed owner": "स्वामी सफलतापूर्वक बदला गया",
|
||||
"Successfully deleted": "सफलतापूर्वक हटाया गया",
|
||||
"Successfully deleted comments": "",
|
||||
"Successfully updated": "सफलतापूर्वक अपडेट किया गया",
|
||||
"Successfully updated categories": "श्रेणियां सफलतापूर्वक अपडेट की गईं",
|
||||
"Successfully updated playlist membership": "प्लेलिस्ट सदस्यता सफलतापूर्वक अपडेट की गई",
|
||||
@@ -240,6 +247,7 @@ translation_strings = {
|
||||
"Welcome": "स्वागत है",
|
||||
"You are going to copy": "आप कॉपी करने जा रहे हैं",
|
||||
"You are going to delete": "आप हटाने जा रहे हैं",
|
||||
"You are going to delete all comments from": "",
|
||||
"You are going to disable comments to": "आप टिप्पणियों को अक्षम करने जा रहे हैं",
|
||||
"You are going to disable download for": "आप डाउनलोड को अक्षम करने जा रहे हैं",
|
||||
"You are going to enable comments to": "आप टिप्पणियों को सक्षम करने जा रहे हैं",
|
||||
|
||||
@@ -48,6 +48,7 @@ translation_strings = {
|
||||
"DELETE MEDIA": "HAPUS MEDIA",
|
||||
"DOWNLOAD": "UNDUH",
|
||||
"DURATION": "DURASI",
|
||||
"Delete Comments": "",
|
||||
"Delete Media": "Hapus Media",
|
||||
"Delete media": "Hapus media",
|
||||
"Disable Comments": "Nonaktifkan Komentar",
|
||||
@@ -70,6 +71,7 @@ translation_strings = {
|
||||
"Failed to change owner. Please try again.": "Gagal mengganti pemilik. Silakan coba lagi.",
|
||||
"Failed to copy media.": "Gagal menyalin media.",
|
||||
"Failed to create playlist": "Gagal membuat daftar putar",
|
||||
"Failed to delete comments.": "",
|
||||
"Failed to delete media. Please try again.": "Gagal menghapus media. Silakan coba lagi.",
|
||||
"Failed to disable comments.": "Gagal menonaktifkan komentar.",
|
||||
"Failed to disable download.": "Gagal menonaktifkan unduhan.",
|
||||
@@ -178,6 +180,10 @@ translation_strings = {
|
||||
"Select all media": "Pilih semua media",
|
||||
"Select publish state:": "Pilih status publikasi:",
|
||||
"Selected": "Dipilih",
|
||||
"Share with Co-Editors": "",
|
||||
"Share with Co-Owners": "",
|
||||
"Share with Co-Viewers": "",
|
||||
"Share with Course Members": "",
|
||||
"Shared by me": "Dibagikan oleh saya",
|
||||
"Shared with me": "Dibagikan dengan saya",
|
||||
"Sign in": "Masuk",
|
||||
@@ -196,6 +202,7 @@ translation_strings = {
|
||||
"Successfully Enabled comments": "Komentar berhasil diaktifkan",
|
||||
"Successfully changed owner": "Berhasil mengganti pemilik",
|
||||
"Successfully deleted": "Berhasil dihapus",
|
||||
"Successfully deleted comments": "",
|
||||
"Successfully updated": "Berhasil diperbarui",
|
||||
"Successfully updated categories": "Kategori berhasil diperbarui",
|
||||
"Successfully updated playlist membership": "Keanggotaan daftar putar berhasil diperbarui",
|
||||
@@ -240,6 +247,7 @@ translation_strings = {
|
||||
"Welcome": "Selamat datang",
|
||||
"You are going to copy": "Anda akan menyalin",
|
||||
"You are going to delete": "Anda akan menghapus",
|
||||
"You are going to delete all comments from": "",
|
||||
"You are going to disable comments to": "Anda akan menonaktifkan komentar untuk",
|
||||
"You are going to disable download for": "Anda akan menonaktifkan unduhan untuk",
|
||||
"You are going to enable comments to": "Anda akan mengaktifkan komentar untuk",
|
||||
|
||||
@@ -49,6 +49,7 @@ translation_strings = {
|
||||
"DELETE MEDIA": "ELIMINA MEDIA",
|
||||
"DOWNLOAD": "SCARICA",
|
||||
"DURATION": "DURATA",
|
||||
"Delete Comments": "",
|
||||
"Delete Media": "Elimina Media",
|
||||
"Delete media": "Elimina media",
|
||||
"Disable Comments": "Disabilita Commenti",
|
||||
@@ -71,6 +72,7 @@ translation_strings = {
|
||||
"Failed to change owner. Please try again.": "Impossibile cambiare proprietario. Riprova.",
|
||||
"Failed to copy media.": "Impossibile copiare il media.",
|
||||
"Failed to create playlist": "Impossibile creare playlist",
|
||||
"Failed to delete comments.": "",
|
||||
"Failed to delete media. Please try again.": "Impossibile eliminare il media. Riprova.",
|
||||
"Failed to disable comments.": "Impossibile disabilitare i commenti.",
|
||||
"Failed to disable download.": "Impossibile disabilitare il download.",
|
||||
@@ -179,6 +181,10 @@ translation_strings = {
|
||||
"Select all media": "Seleziona tutti i media",
|
||||
"Select publish state:": "Seleziona stato di pubblicazione:",
|
||||
"Selected": "Selezionato",
|
||||
"Share with Co-Editors": "",
|
||||
"Share with Co-Owners": "",
|
||||
"Share with Co-Viewers": "",
|
||||
"Share with Course Members": "",
|
||||
"Shared by me": "Condiviso da me",
|
||||
"Shared with me": "Condiviso con me",
|
||||
"Sign in": "Login",
|
||||
@@ -197,6 +203,7 @@ translation_strings = {
|
||||
"Successfully Enabled comments": "Commenti abilitati con successo",
|
||||
"Successfully changed owner": "Proprietario cambiato con successo",
|
||||
"Successfully deleted": "Eliminato con successo",
|
||||
"Successfully deleted comments": "",
|
||||
"Successfully updated": "Aggiornato con successo",
|
||||
"Successfully updated categories": "Categorie aggiornate con successo",
|
||||
"Successfully updated playlist membership": "Appartenenza alla playlist aggiornata con successo",
|
||||
@@ -241,6 +248,7 @@ translation_strings = {
|
||||
"Welcome": "Benvenuto",
|
||||
"You are going to copy": "Stai per copiare",
|
||||
"You are going to delete": "Stai per eliminare",
|
||||
"You are going to delete all comments from": "",
|
||||
"You are going to disable comments to": "Stai per disabilitare i commenti di",
|
||||
"You are going to disable download for": "Stai per disabilitare il download di",
|
||||
"You are going to enable comments to": "Stai per abilitare i commenti di",
|
||||
|
||||
@@ -48,6 +48,7 @@ translation_strings = {
|
||||
"DELETE MEDIA": "メディアを削除",
|
||||
"DOWNLOAD": "ダウンロード",
|
||||
"DURATION": "期間",
|
||||
"Delete Comments": "",
|
||||
"Delete Media": "メディアを削除",
|
||||
"Delete media": "メディアを削除",
|
||||
"Disable Comments": "コメントを無効化",
|
||||
@@ -70,6 +71,7 @@ translation_strings = {
|
||||
"Failed to change owner. Please try again.": "所有者の変更に失敗しました。もう一度お試しください。",
|
||||
"Failed to copy media.": "メディアのコピーに失敗しました。",
|
||||
"Failed to create playlist": "プレイリストの作成に失敗しました",
|
||||
"Failed to delete comments.": "",
|
||||
"Failed to delete media. Please try again.": "メディアの削除に失敗しました。もう一度お試しください。",
|
||||
"Failed to disable comments.": "コメントの無効化に失敗しました。",
|
||||
"Failed to disable download.": "ダウンロードの無効化に失敗しました。",
|
||||
@@ -178,6 +180,10 @@ translation_strings = {
|
||||
"Select all media": "すべてのメディアを選択",
|
||||
"Select publish state:": "公開状態を選択:",
|
||||
"Selected": "選択済み",
|
||||
"Share with Co-Editors": "",
|
||||
"Share with Co-Owners": "",
|
||||
"Share with Co-Viewers": "",
|
||||
"Share with Course Members": "",
|
||||
"Shared by me": "自分が共有",
|
||||
"Shared with me": "共有されたもの",
|
||||
"Sign in": "サインイン",
|
||||
@@ -196,6 +202,7 @@ translation_strings = {
|
||||
"Successfully Enabled comments": "コメントが正常に有効化されました",
|
||||
"Successfully changed owner": "所有者が正常に変更されました",
|
||||
"Successfully deleted": "正常に削除されました",
|
||||
"Successfully deleted comments": "",
|
||||
"Successfully updated": "正常に更新されました",
|
||||
"Successfully updated categories": "カテゴリーが正常に更新されました",
|
||||
"Successfully updated playlist membership": "プレイリストメンバーシップが正常に更新されました",
|
||||
@@ -240,6 +247,7 @@ translation_strings = {
|
||||
"Welcome": "ようこそ",
|
||||
"You are going to copy": "コピーします",
|
||||
"You are going to delete": "削除します",
|
||||
"You are going to delete all comments from": "",
|
||||
"You are going to disable comments to": "コメントを無効化します",
|
||||
"You are going to disable download for": "ダウンロードを無効化します",
|
||||
"You are going to enable comments to": "コメントを有効化します",
|
||||
|
||||
@@ -48,6 +48,7 @@ translation_strings = {
|
||||
"DELETE MEDIA": "미디어 삭제",
|
||||
"DOWNLOAD": "다운로드",
|
||||
"DURATION": "재생 시간",
|
||||
"Delete Comments": "",
|
||||
"Delete Media": "미디어 삭제",
|
||||
"Delete media": "미디어 삭제",
|
||||
"Disable Comments": "댓글 비활성화",
|
||||
@@ -70,6 +71,7 @@ translation_strings = {
|
||||
"Failed to change owner. Please try again.": "소유자 변경에 실패했습니다. 다시 시도해주세요.",
|
||||
"Failed to copy media.": "미디어 복사에 실패했습니다.",
|
||||
"Failed to create playlist": "재생 목록 만들기 실패",
|
||||
"Failed to delete comments.": "",
|
||||
"Failed to delete media. Please try again.": "미디어 삭제에 실패했습니다. 다시 시도해주세요.",
|
||||
"Failed to disable comments.": "댓글 비활성화에 실패했습니다.",
|
||||
"Failed to disable download.": "다운로드 비활성화에 실패했습니다.",
|
||||
@@ -178,6 +180,10 @@ translation_strings = {
|
||||
"Select all media": "모든 미디어 선택",
|
||||
"Select publish state:": "게시 상태 선택:",
|
||||
"Selected": "선택됨",
|
||||
"Share with Co-Editors": "",
|
||||
"Share with Co-Owners": "",
|
||||
"Share with Co-Viewers": "",
|
||||
"Share with Course Members": "",
|
||||
"Shared by me": "내가 공유함",
|
||||
"Shared with me": "나와 공유됨",
|
||||
"Sign in": "로그인",
|
||||
@@ -196,6 +202,7 @@ translation_strings = {
|
||||
"Successfully Enabled comments": "댓글이 활성화되었습니다",
|
||||
"Successfully changed owner": "소유자가 변경되었습니다",
|
||||
"Successfully deleted": "삭제 성공",
|
||||
"Successfully deleted comments": "",
|
||||
"Successfully updated": "업데이트 성공",
|
||||
"Successfully updated categories": "카테고리가 업데이트되었습니다",
|
||||
"Successfully updated playlist membership": "재생 목록 멤버십이 업데이트되었습니다",
|
||||
@@ -240,6 +247,7 @@ translation_strings = {
|
||||
"Welcome": "환영합니다",
|
||||
"You are going to copy": "복사하려고 합니다",
|
||||
"You are going to delete": "삭제하려고 합니다",
|
||||
"You are going to delete all comments from": "",
|
||||
"You are going to disable comments to": "댓글을 비활성화하려고 합니다",
|
||||
"You are going to disable download for": "다운로드를 비활성화하려고 합니다",
|
||||
"You are going to enable comments to": "댓글을 활성화하려고 합니다",
|
||||
|
||||
@@ -48,6 +48,7 @@ translation_strings = {
|
||||
"DELETE MEDIA": "MEDIA VERWIJDEREN",
|
||||
"DOWNLOAD": "DOWNLOADEN",
|
||||
"DURATION": "DUUR",
|
||||
"Delete Comments": "",
|
||||
"Delete Media": "Media verwijderen",
|
||||
"Delete media": "Media verwijderen",
|
||||
"Disable Comments": "Reacties uitschakelen",
|
||||
@@ -70,6 +71,7 @@ translation_strings = {
|
||||
"Failed to change owner. Please try again.": "Eigenaar wijzigen mislukt. Probeer het opnieuw.",
|
||||
"Failed to copy media.": "Media kopiëren mislukt.",
|
||||
"Failed to create playlist": "Afspeellijst maken mislukt",
|
||||
"Failed to delete comments.": "",
|
||||
"Failed to delete media. Please try again.": "Media verwijderen mislukt. Probeer het opnieuw.",
|
||||
"Failed to disable comments.": "Reacties uitschakelen mislukt.",
|
||||
"Failed to disable download.": "Download uitschakelen mislukt.",
|
||||
@@ -178,6 +180,10 @@ translation_strings = {
|
||||
"Select all media": "Alle media selecteren",
|
||||
"Select publish state:": "Selecteer publicatiestatus:",
|
||||
"Selected": "Geselecteerd",
|
||||
"Share with Co-Editors": "",
|
||||
"Share with Co-Owners": "",
|
||||
"Share with Co-Viewers": "",
|
||||
"Share with Course Members": "",
|
||||
"Shared by me": "Gedeeld door mij",
|
||||
"Shared with me": "Gedeeld met mij",
|
||||
"Sign in": "Inloggen",
|
||||
@@ -196,6 +202,7 @@ translation_strings = {
|
||||
"Successfully Enabled comments": "Reacties succesvol ingeschakeld",
|
||||
"Successfully changed owner": "Eigenaar succesvol gewijzigd",
|
||||
"Successfully deleted": "Succesvol verwijderd",
|
||||
"Successfully deleted comments": "",
|
||||
"Successfully updated": "Succesvol bijgewerkt",
|
||||
"Successfully updated categories": "Categorieën succesvol bijgewerkt",
|
||||
"Successfully updated playlist membership": "Afspeellijstlidmaatschap succesvol bijgewerkt",
|
||||
@@ -240,6 +247,7 @@ translation_strings = {
|
||||
"Welcome": "Welkom",
|
||||
"You are going to copy": "Je gaat kopiëren",
|
||||
"You are going to delete": "Je gaat verwijderen",
|
||||
"You are going to delete all comments from": "",
|
||||
"You are going to disable comments to": "Je gaat reacties uitschakelen voor",
|
||||
"You are going to disable download for": "Je gaat download uitschakelen voor",
|
||||
"You are going to enable comments to": "Je gaat reacties inschakelen voor",
|
||||
|
||||
@@ -48,6 +48,7 @@ translation_strings = {
|
||||
"DELETE MEDIA": "EXCLUIR MÍDIA",
|
||||
"DOWNLOAD": "BAIXAR",
|
||||
"DURATION": "DURAÇÃO",
|
||||
"Delete Comments": "",
|
||||
"Delete Media": "Excluir mídia",
|
||||
"Delete media": "Excluir mídia",
|
||||
"Disable Comments": "Desativar comentários",
|
||||
@@ -70,6 +71,7 @@ translation_strings = {
|
||||
"Failed to change owner. Please try again.": "Falha ao mudar proprietário. Por favor, tente novamente.",
|
||||
"Failed to copy media.": "Falha ao copiar mídia.",
|
||||
"Failed to create playlist": "Falha ao criar playlist",
|
||||
"Failed to delete comments.": "",
|
||||
"Failed to delete media. Please try again.": "Falha ao excluir mídia. Por favor, tente novamente.",
|
||||
"Failed to disable comments.": "Falha ao desativar comentários.",
|
||||
"Failed to disable download.": "Falha ao desativar download.",
|
||||
@@ -178,6 +180,10 @@ translation_strings = {
|
||||
"Select all media": "Selecionar todas as mídias",
|
||||
"Select publish state:": "Selecionar estado de publicação:",
|
||||
"Selected": "Selecionado",
|
||||
"Share with Co-Editors": "",
|
||||
"Share with Co-Owners": "",
|
||||
"Share with Co-Viewers": "",
|
||||
"Share with Course Members": "",
|
||||
"Shared by me": "Compartilhado por mim",
|
||||
"Shared with me": "Compartilhado comigo",
|
||||
"Sign in": "Entrar",
|
||||
@@ -196,6 +202,7 @@ translation_strings = {
|
||||
"Successfully Enabled comments": "Comentários ativados com sucesso",
|
||||
"Successfully changed owner": "Proprietário alterado com sucesso",
|
||||
"Successfully deleted": "Excluído com sucesso",
|
||||
"Successfully deleted comments": "",
|
||||
"Successfully updated": "Atualizado com sucesso",
|
||||
"Successfully updated categories": "Categorias atualizadas com sucesso",
|
||||
"Successfully updated playlist membership": "Associação da playlist atualizada com sucesso",
|
||||
@@ -240,6 +247,7 @@ translation_strings = {
|
||||
"Welcome": "Bem-vindo",
|
||||
"You are going to copy": "Você vai copiar",
|
||||
"You are going to delete": "Você vai excluir",
|
||||
"You are going to delete all comments from": "",
|
||||
"You are going to disable comments to": "Você vai desativar comentários de",
|
||||
"You are going to disable download for": "Você vai desativar download de",
|
||||
"You are going to enable comments to": "Você vai ativar comentários de",
|
||||
|
||||
@@ -48,6 +48,7 @@ translation_strings = {
|
||||
"DELETE MEDIA": "УДАЛИТЬ МЕДИА",
|
||||
"DOWNLOAD": "СКАЧАТЬ",
|
||||
"DURATION": "ДЛИТЕЛЬНОСТЬ",
|
||||
"Delete Comments": "",
|
||||
"Delete Media": "Удалить медиа",
|
||||
"Delete media": "Удалить медиа",
|
||||
"Disable Comments": "Отключить комментарии",
|
||||
@@ -70,6 +71,7 @@ translation_strings = {
|
||||
"Failed to change owner. Please try again.": "Не удалось изменить владельца. Пожалуйста, попробуйте снова.",
|
||||
"Failed to copy media.": "Не удалось скопировать медиа.",
|
||||
"Failed to create playlist": "Не удалось создать плейлист",
|
||||
"Failed to delete comments.": "",
|
||||
"Failed to delete media. Please try again.": "Не удалось удалить медиа. Пожалуйста, попробуйте снова.",
|
||||
"Failed to disable comments.": "Не удалось отключить комментарии.",
|
||||
"Failed to disable download.": "Не удалось отключить загрузку.",
|
||||
@@ -178,6 +180,10 @@ translation_strings = {
|
||||
"Select all media": "Выбрать все медиа",
|
||||
"Select publish state:": "Выберите состояние публикации:",
|
||||
"Selected": "Выбрано",
|
||||
"Share with Co-Editors": "",
|
||||
"Share with Co-Owners": "",
|
||||
"Share with Co-Viewers": "",
|
||||
"Share with Course Members": "",
|
||||
"Shared by me": "Мной поделено",
|
||||
"Shared with me": "Поделено со мной",
|
||||
"Sign in": "Войти",
|
||||
@@ -196,6 +202,7 @@ translation_strings = {
|
||||
"Successfully Enabled comments": "Комментарии успешно включены",
|
||||
"Successfully changed owner": "Владелец успешно изменен",
|
||||
"Successfully deleted": "Успешно удалено",
|
||||
"Successfully deleted comments": "",
|
||||
"Successfully updated": "Успешно обновлено",
|
||||
"Successfully updated categories": "Категории успешно обновлены",
|
||||
"Successfully updated playlist membership": "Членство в плейлисте успешно обновлено",
|
||||
@@ -240,6 +247,7 @@ translation_strings = {
|
||||
"Welcome": "Добро пожаловать",
|
||||
"You are going to copy": "Вы собираетесь скопировать",
|
||||
"You are going to delete": "Вы собираетесь удалить",
|
||||
"You are going to delete all comments from": "",
|
||||
"You are going to disable comments to": "Вы собираетесь отключить комментарии для",
|
||||
"You are going to disable download for": "Вы собираетесь отключить загрузку для",
|
||||
"You are going to enable comments to": "Вы собираетесь включить комментарии для",
|
||||
|
||||
@@ -48,6 +48,7 @@ translation_strings = {
|
||||
"DELETE MEDIA": "IZBRIŠI MEDIJ",
|
||||
"DOWNLOAD": "PRENESI",
|
||||
"DURATION": "TRAJANJE",
|
||||
"Delete Comments": "",
|
||||
"Delete Media": "Izbriši Medij",
|
||||
"Delete media": "Izbriši medij",
|
||||
"Disable Comments": "Onemogoči Komentarje",
|
||||
@@ -70,6 +71,7 @@ translation_strings = {
|
||||
"Failed to change owner. Please try again.": "Spreminjanje lastnika ni uspelo. Prosim poskusite ponovno.",
|
||||
"Failed to copy media.": "Kopiranje medija ni uspelo.",
|
||||
"Failed to create playlist": "Ustvarjanje seznama predvajanja ni uspelo",
|
||||
"Failed to delete comments.": "",
|
||||
"Failed to delete media. Please try again.": "Brisanje medija ni uspelo. Prosim poskusite ponovno.",
|
||||
"Failed to disable comments.": "Onemogočanje komentarjev ni uspelo.",
|
||||
"Failed to disable download.": "Onemogočanje prenosa ni uspelo.",
|
||||
@@ -178,6 +180,10 @@ translation_strings = {
|
||||
"Select all media": "Izberi vse medije",
|
||||
"Select publish state:": "Izberi stanje objave:",
|
||||
"Selected": "Izbrano",
|
||||
"Share with Co-Editors": "",
|
||||
"Share with Co-Owners": "",
|
||||
"Share with Co-Viewers": "",
|
||||
"Share with Course Members": "",
|
||||
"Shared by me": "Deljeno z moje strani",
|
||||
"Shared with me": "Deljeno z mano",
|
||||
"Sign in": "Prijava",
|
||||
@@ -196,6 +202,7 @@ translation_strings = {
|
||||
"Successfully Enabled comments": "Komentarji uspešno omogočeni",
|
||||
"Successfully changed owner": "Lastnik uspešno spremenjen",
|
||||
"Successfully deleted": "Uspešno izbrisano",
|
||||
"Successfully deleted comments": "",
|
||||
"Successfully updated": "Uspešno posodobljeno",
|
||||
"Successfully updated categories": "Kategorije uspešno posodobljene",
|
||||
"Successfully updated playlist membership": "Članstvo seznama predvajanja uspešno posodobljeno",
|
||||
@@ -240,6 +247,7 @@ translation_strings = {
|
||||
"Welcome": "Dobrodošli",
|
||||
"You are going to copy": "Kopirate",
|
||||
"You are going to delete": "Brišete",
|
||||
"You are going to delete all comments from": "",
|
||||
"You are going to disable comments to": "Onemogočate komentarje za",
|
||||
"You are going to disable download for": "Onemogočate prenos za",
|
||||
"You are going to enable comments to": "Omogočate komentarje za",
|
||||
|
||||
@@ -48,6 +48,7 @@ translation_strings = {
|
||||
"DELETE MEDIA": "MEDYAYI SİL",
|
||||
"DOWNLOAD": "İNDİR",
|
||||
"DURATION": "SÜRE",
|
||||
"Delete Comments": "",
|
||||
"Delete Media": "Medyayı Sil",
|
||||
"Delete media": "Medyayı sil",
|
||||
"Disable Comments": "Yorumları Devre Dışı Bırak",
|
||||
@@ -70,6 +71,7 @@ translation_strings = {
|
||||
"Failed to change owner. Please try again.": "Sahip değiştirilemedi. Lütfen tekrar deneyin.",
|
||||
"Failed to copy media.": "Medya kopyalanamadı.",
|
||||
"Failed to create playlist": "Çalma listesi oluşturulamadı",
|
||||
"Failed to delete comments.": "",
|
||||
"Failed to delete media. Please try again.": "Medya silinemedi. Lütfen tekrar deneyin.",
|
||||
"Failed to disable comments.": "Yorumlar devre dışı bırakılamadı.",
|
||||
"Failed to disable download.": "İndirme devre dışı bırakılamadı.",
|
||||
@@ -178,6 +180,10 @@ translation_strings = {
|
||||
"Select all media": "Tüm medyayı seç",
|
||||
"Select publish state:": "Yayınlanma durumunu seç:",
|
||||
"Selected": "Seçildi",
|
||||
"Share with Co-Editors": "",
|
||||
"Share with Co-Owners": "",
|
||||
"Share with Co-Viewers": "",
|
||||
"Share with Course Members": "",
|
||||
"Shared by me": "Paylaştıklarım",
|
||||
"Shared with me": "Benimle paylaşılanlar",
|
||||
"Sign in": "Giriş Yap",
|
||||
@@ -196,6 +202,7 @@ translation_strings = {
|
||||
"Successfully Enabled comments": "Yorumlar başarıyla etkinleştirildi",
|
||||
"Successfully changed owner": "Sahip başarıyla değiştirildi",
|
||||
"Successfully deleted": "Başarıyla silindi",
|
||||
"Successfully deleted comments": "",
|
||||
"Successfully updated": "Başarıyla güncellendi",
|
||||
"Successfully updated categories": "Kategoriler başarıyla güncellendi",
|
||||
"Successfully updated playlist membership": "Çalma listesi üyeliği başarıyla güncellendi",
|
||||
@@ -240,6 +247,7 @@ translation_strings = {
|
||||
"Welcome": "Hoş geldiniz",
|
||||
"You are going to copy": "Kopyalayacaksınız",
|
||||
"You are going to delete": "Sileceksiniz",
|
||||
"You are going to delete all comments from": "",
|
||||
"You are going to disable comments to": "Yorumları devre dışı bırakacaksınız",
|
||||
"You are going to disable download for": "İndirmeyi devre dışı bırakacaksınız",
|
||||
"You are going to enable comments to": "Yorumları etkinleştireceksiniz",
|
||||
|
||||
@@ -48,6 +48,7 @@ translation_strings = {
|
||||
"DELETE MEDIA": "میڈیا حذف کریں",
|
||||
"DOWNLOAD": "ڈاؤن لوڈ",
|
||||
"DURATION": "دورانیہ",
|
||||
"Delete Comments": "",
|
||||
"Delete Media": "میڈیا حذف کریں",
|
||||
"Delete media": "میڈیا حذف کریں",
|
||||
"Disable Comments": "تبصرے غیر فعال کریں",
|
||||
@@ -70,6 +71,7 @@ translation_strings = {
|
||||
"Failed to change owner. Please try again.": "مالک تبدیل کرنے میں ناکام۔ براہ کرم دوبارہ کوشش کریں۔",
|
||||
"Failed to copy media.": "میڈیا کاپی کرنے میں ناکام۔",
|
||||
"Failed to create playlist": "پلے لسٹ بنانے میں ناکام",
|
||||
"Failed to delete comments.": "",
|
||||
"Failed to delete media. Please try again.": "میڈیا حذف کرنے میں ناکام۔ براہ کرم دوبارہ کوشش کریں۔",
|
||||
"Failed to disable comments.": "تبصرے غیر فعال کرنے میں ناکام۔",
|
||||
"Failed to disable download.": "ڈاؤن لوڈ غیر فعال کرنے میں ناکام۔",
|
||||
@@ -178,6 +180,10 @@ translation_strings = {
|
||||
"Select all media": "تمام میڈیا منتخب کریں",
|
||||
"Select publish state:": "اشاعت کی حالت منتخب کریں:",
|
||||
"Selected": "منتخب شدہ",
|
||||
"Share with Co-Editors": "",
|
||||
"Share with Co-Owners": "",
|
||||
"Share with Co-Viewers": "",
|
||||
"Share with Course Members": "",
|
||||
"Shared by me": "میری طرف سے شیئر کیا گیا",
|
||||
"Shared with me": "میرے ساتھ شیئر کیا گیا",
|
||||
"Sign in": "سائن ان کریں",
|
||||
@@ -196,6 +202,7 @@ translation_strings = {
|
||||
"Successfully Enabled comments": "تبصرے کامیابی سے فعال ہو گئے",
|
||||
"Successfully changed owner": "مالک کامیابی سے تبدیل ہو گیا",
|
||||
"Successfully deleted": "کامیابی سے حذف ہو گیا",
|
||||
"Successfully deleted comments": "",
|
||||
"Successfully updated": "کامیابی سے اپ ڈیٹ ہو گیا",
|
||||
"Successfully updated categories": "اقسام کامیابی سے اپ ڈیٹ ہو گئیں",
|
||||
"Successfully updated playlist membership": "پلے لسٹ ممبرشپ کامیابی سے اپ ڈیٹ ہو گئی",
|
||||
@@ -240,6 +247,7 @@ translation_strings = {
|
||||
"Welcome": "خوش آمدید",
|
||||
"You are going to copy": "آپ کاپی کرنے جا رہے ہیں",
|
||||
"You are going to delete": "آپ حذف کرنے جا رہے ہیں",
|
||||
"You are going to delete all comments from": "",
|
||||
"You are going to disable comments to": "آپ تبصرے غیر فعال کرنے جا رہے ہیں",
|
||||
"You are going to disable download for": "آپ ڈاؤن لوڈ غیر فعال کرنے جا رہے ہیں",
|
||||
"You are going to enable comments to": "آپ تبصرے فعال کرنے جا رہے ہیں",
|
||||
|
||||
@@ -48,6 +48,7 @@ translation_strings = {
|
||||
"DELETE MEDIA": "删除媒体",
|
||||
"DOWNLOAD": "下载",
|
||||
"DURATION": "时长",
|
||||
"Delete Comments": "",
|
||||
"Delete Media": "",
|
||||
"Delete media": "删除媒体",
|
||||
"Disable Comments": "",
|
||||
@@ -70,6 +71,7 @@ translation_strings = {
|
||||
"Failed to change owner. Please try again.": "",
|
||||
"Failed to copy media.": "复制媒体失败。",
|
||||
"Failed to create playlist": "",
|
||||
"Failed to delete comments.": "",
|
||||
"Failed to delete media. Please try again.": "删除媒体失败。请重试。",
|
||||
"Failed to disable comments.": "禁用评论失败。",
|
||||
"Failed to disable download.": "禁用下载失败。",
|
||||
@@ -178,6 +180,10 @@ translation_strings = {
|
||||
"Select all media": "",
|
||||
"Select publish state:": "",
|
||||
"Selected": "",
|
||||
"Share with Co-Editors": "",
|
||||
"Share with Co-Owners": "",
|
||||
"Share with Co-Viewers": "",
|
||||
"Share with Course Members": "",
|
||||
"Shared by me": "我分享的",
|
||||
"Shared with me": "分享给我的",
|
||||
"Sign in": "登录",
|
||||
@@ -196,6 +202,7 @@ translation_strings = {
|
||||
"Successfully Enabled comments": "评论已成功启用",
|
||||
"Successfully changed owner": "",
|
||||
"Successfully deleted": "删除成功",
|
||||
"Successfully deleted comments": "",
|
||||
"Successfully updated": "",
|
||||
"Successfully updated categories": "",
|
||||
"Successfully updated playlist membership": "",
|
||||
@@ -240,6 +247,7 @@ translation_strings = {
|
||||
"Welcome": "欢迎",
|
||||
"You are going to copy": "您将复制",
|
||||
"You are going to delete": "您将删除",
|
||||
"You are going to delete all comments from": "",
|
||||
"You are going to disable comments to": "您将禁用评论",
|
||||
"You are going to disable download for": "您将禁用下载",
|
||||
"You are going to enable comments to": "您将启用评论",
|
||||
|
||||
@@ -48,6 +48,7 @@ translation_strings = {
|
||||
"DELETE MEDIA": "刪除影片",
|
||||
"DOWNLOAD": "下載",
|
||||
"DURATION": "時長",
|
||||
"Delete Comments": "",
|
||||
"Delete Media": "",
|
||||
"Delete media": "刪除媒體",
|
||||
"Disable Comments": "",
|
||||
@@ -70,6 +71,7 @@ translation_strings = {
|
||||
"Failed to change owner. Please try again.": "",
|
||||
"Failed to copy media.": "複製媒體失敗。",
|
||||
"Failed to create playlist": "",
|
||||
"Failed to delete comments.": "",
|
||||
"Failed to delete media. Please try again.": "刪除媒體失敗。請再試一次。",
|
||||
"Failed to disable comments.": "停用留言失敗。",
|
||||
"Failed to disable download.": "停用下載失敗。",
|
||||
@@ -178,6 +180,10 @@ translation_strings = {
|
||||
"Select all media": "",
|
||||
"Select publish state:": "",
|
||||
"Selected": "",
|
||||
"Share with Co-Editors": "",
|
||||
"Share with Co-Owners": "",
|
||||
"Share with Co-Viewers": "",
|
||||
"Share with Course Members": "",
|
||||
"Shared by me": "我分享的",
|
||||
"Shared with me": "與我分享",
|
||||
"Sign in": "登入",
|
||||
@@ -196,6 +202,7 @@ translation_strings = {
|
||||
"Successfully Enabled comments": "成功啟用留言",
|
||||
"Successfully changed owner": "",
|
||||
"Successfully deleted": "成功刪除",
|
||||
"Successfully deleted comments": "",
|
||||
"Successfully updated": "",
|
||||
"Successfully updated categories": "",
|
||||
"Successfully updated playlist membership": "",
|
||||
@@ -240,6 +247,7 @@ translation_strings = {
|
||||
"Welcome": "歡迎",
|
||||
"You are going to copy": "您即將複製",
|
||||
"You are going to delete": "您即將刪除",
|
||||
"You are going to delete all comments from": "",
|
||||
"You are going to disable comments to": "您即將停用留言",
|
||||
"You are going to disable download for": "您即將停用下載",
|
||||
"You are going to enable comments to": "您即將啟用留言",
|
||||
|
||||
@@ -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"""
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ from ..methods import (
|
||||
)
|
||||
from ..models import (
|
||||
Category,
|
||||
Comment,
|
||||
EncodeProfile,
|
||||
Media,
|
||||
MediaPermission,
|
||||
@@ -164,7 +165,14 @@ class MediaList(APIView):
|
||||
if not self.request.user.is_authenticated:
|
||||
media = Media.objects.none()
|
||||
else:
|
||||
media = Media.objects.filter(permissions__owner_user=self.request.user).prefetch_related("user", "tags").distinct()
|
||||
base_queryset = Media.objects.prefetch_related("user", "tags")
|
||||
conditions = Q(permissions__owner_user=self.request.user)
|
||||
|
||||
if getattr(settings, 'USE_RBAC', False):
|
||||
rbac_categories = request.user.get_rbac_categories_as_contributor()
|
||||
conditions |= Q(category__in=rbac_categories, user=self.request.user)
|
||||
|
||||
media = base_queryset.filter(conditions).distinct()
|
||||
elif show_param == "shared_with_me":
|
||||
if not self.request.user.is_authenticated:
|
||||
media = Media.objects.none()
|
||||
@@ -178,7 +186,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)
|
||||
@@ -295,6 +303,7 @@ class MediaBulkUserActions(APIView):
|
||||
enum=[
|
||||
"enable_comments",
|
||||
"disable_comments",
|
||||
"delete_comments",
|
||||
"delete_media",
|
||||
"enable_download",
|
||||
"disable_download",
|
||||
@@ -379,6 +388,10 @@ class MediaBulkUserActions(APIView):
|
||||
media.update(enable_comments=False)
|
||||
return Response({"detail": f"Comments disabled for {media.count()} media items"})
|
||||
|
||||
elif action == "delete_comments":
|
||||
deleted_count, _ = Comment.objects.filter(media__in=media).delete()
|
||||
return Response({"detail": f"{deleted_count} comments deleted"})
|
||||
|
||||
elif action == "delete_media":
|
||||
count = media.count()
|
||||
media.delete()
|
||||
@@ -492,8 +505,9 @@ class MediaBulkUserActions(APIView):
|
||||
|
||||
users = (
|
||||
MediaPermission.objects.filter(media__in=media, permission=ownership_type)
|
||||
.exclude(user=request.user)
|
||||
.values('user__name', 'user__username')
|
||||
.annotate(media_count=Count('media', distinct=True))
|
||||
.annotate(media_count=Count('media'))
|
||||
.filter(media_count=media_count)
|
||||
)
|
||||
|
||||
@@ -574,12 +588,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 +730,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)
|
||||
|
||||
3
frontend/.vscode/settings.json
vendored
3
frontend/.vscode/settings.json
vendored
@@ -1,4 +1,3 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"prettier.configPath": "../.prettierrc"
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
@@ -5,5 +5,5 @@ module.exports = {
|
||||
'^.+\\.tsx?$': 'ts-jest',
|
||||
'^.+\\.jsx?$': 'babel-jest',
|
||||
},
|
||||
collectCoverageFrom: ['src/**', '!src/static/lib/**'],
|
||||
collectCoverageFrom: ['src/**'],
|
||||
};
|
||||
|
||||
@@ -21,9 +21,6 @@
|
||||
"@babel/core": "^7.26.9",
|
||||
"@babel/preset-env": "^7.26.9",
|
||||
"@babel/preset-react": "^7.26.3",
|
||||
"@testing-library/dom": "^8.20.1",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@types/flux": "^3.1.15",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/minimatch": "^5.1.2",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -47,7 +47,7 @@ export const BulkActionChangeOwnerModal: React.FC<BulkActionChangeOwnerModalProp
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/v1/users?name=${encodeURIComponent(name)}`);
|
||||
const response = await fetch(`/api/v1/users?name=${encodeURIComponent(name)}&exclude_self=True`);
|
||||
if (!response.ok) {
|
||||
throw new Error(translateString('Failed to search users'));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -8,14 +9,15 @@ interface BulkActionsDropdownProps {
|
||||
}
|
||||
|
||||
const BULK_ACTIONS = [
|
||||
{ value: 'add-remove-coviewers', label: translateString('Add / Remove Co-Viewers'), enabled: true },
|
||||
{ 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-coviewers', label: inEmbeddedApp() ? translateString('Share with Co-Viewers') : translateString('Add / Remove Co-Viewers'), enabled: true },
|
||||
{ value: 'add-remove-coeditors', label: inEmbeddedApp() ? translateString('Share with Co-Editors') : translateString('Add / Remove Co-Editors'), enabled: true },
|
||||
{ value: 'add-remove-coowners', label: inEmbeddedApp() ? translateString('Share with Co-Owners') : 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 Members') : 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 },
|
||||
{ value: 'delete-comments', label: translateString('Delete Comments'), enabled: true },
|
||||
{ value: 'enable-download', label: translateString('Enable Download'), enabled: true },
|
||||
{ value: 'disable-download', label: translateString('Disable Download'), enabled: true },
|
||||
{ value: 'publish-state', label: translateString('Publish State'), 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) => (
|
||||
|
||||
@@ -8,7 +8,7 @@ import { PageStore, ProfilePageStore } from '../utils/stores/';
|
||||
import ProfilePagesHeader from '../components/profile-page/ProfilePagesHeader';
|
||||
import ProfilePagesContent from '../components/profile-page/ProfilePagesContent';
|
||||
import { MediaListRow } from '../components/MediaListRow';
|
||||
import { ProfileMediaPage } from './ProfileMediaPage';
|
||||
import { ProfileMediaPageBase } from './ProfileMediaPage';
|
||||
|
||||
class ChannelContactForm extends React.PureComponent {
|
||||
constructor(props) {
|
||||
@@ -149,7 +149,7 @@ class ChannelContactForm extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
export class ProfileAboutPage extends ProfileMediaPage {
|
||||
export class ProfileAboutPage extends ProfileMediaPageBase {
|
||||
constructor(props) {
|
||||
super(props, 'author-about');
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ import { MediaListWrapper } from '../components/MediaListWrapper';
|
||||
import ProfilePagesHeader from '../components/profile-page/ProfilePagesHeader';
|
||||
import ProfilePagesContent from '../components/profile-page/ProfilePagesContent';
|
||||
import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListAsync.jsx';
|
||||
import { ProfileMediaPage } from './ProfileMediaPage';
|
||||
import { ProfileMediaPageBase } from './ProfileMediaPage';
|
||||
|
||||
export class ProfileHistoryPage extends ProfileMediaPage {
|
||||
export class ProfileHistoryPage extends ProfileMediaPageBase {
|
||||
constructor(props) {
|
||||
super(props, 'author-history');
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ import { MediaListWrapper } from '../components/MediaListWrapper';
|
||||
import ProfilePagesHeader from '../components/profile-page/ProfilePagesHeader';
|
||||
import ProfilePagesContent from '../components/profile-page/ProfilePagesContent';
|
||||
import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListAsync';
|
||||
import { ProfileMediaPage } from './ProfileMediaPage';
|
||||
import { ProfileMediaPageBase } from './ProfileMediaPage';
|
||||
|
||||
export class ProfileLikedPage extends ProfileMediaPage {
|
||||
export class ProfileLikedPage extends ProfileMediaPageBase {
|
||||
constructor(props) {
|
||||
super(props, 'author-liked');
|
||||
|
||||
|
||||
@@ -3,27 +3,22 @@ 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';
|
||||
import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListAsync';
|
||||
import { BulkActionConfirmModal } from '../components/BulkActionConfirmModal';
|
||||
import { BulkActionPermissionModal } from '../components/BulkActionPermissionModal';
|
||||
import { BulkActionPlaylistModal } from '../components/BulkActionPlaylistModal';
|
||||
import { BulkActionChangeOwnerModal } from '../components/BulkActionChangeOwnerModal';
|
||||
import { BulkActionPublishStateModal } from '../components/BulkActionPublishStateModal';
|
||||
import { BulkActionCategoryModal } from '../components/BulkActionCategoryModal';
|
||||
import { BulkActionTagModal } from '../components/BulkActionTagModal';
|
||||
import { BulkActionsModals } from '../components/BulkActionsModals';
|
||||
import { ProfileMediaFilters } from '../components/search-filters/ProfileMediaFilters';
|
||||
import { ProfileMediaTags } from '../components/search-filters/ProfileMediaTags';
|
||||
import { ProfileMediaSorting } from '../components/search-filters/ProfileMediaSorting';
|
||||
import { withBulkActions } from '../utils/hoc/withBulkActions';
|
||||
|
||||
import { Page } from './_Page';
|
||||
|
||||
import '../components/profile-page/ProfilePage.scss';
|
||||
|
||||
export class ProfileMediaPage extends Page {
|
||||
class ProfileMediaPage extends Page {
|
||||
constructor(props, pageSlug) {
|
||||
super(props, 'string' === typeof pageSlug ? pageSlug : 'author-home');
|
||||
|
||||
@@ -36,15 +31,7 @@ export class ProfileMediaPage extends Page {
|
||||
title: this.props.title,
|
||||
query: ProfilePageStore.get('author-query'),
|
||||
requestUrl: null,
|
||||
selectedMedia: new Set(),
|
||||
availableMediaIds: [],
|
||||
showConfirmModal: false,
|
||||
pendingAction: null,
|
||||
confirmMessage: '',
|
||||
listKey: 0,
|
||||
notificationMessage: '',
|
||||
showNotification: false,
|
||||
notificationType: 'success',
|
||||
selectedMedia: new Set(), // For select media embed mode only
|
||||
hiddenFilters: true,
|
||||
hiddenTags: true,
|
||||
hiddenSorting: true,
|
||||
@@ -52,13 +39,6 @@ export class ProfileMediaPage extends Page {
|
||||
availableTags: [],
|
||||
selectedTag: 'all',
|
||||
selectedSort: 'date_added_desc',
|
||||
showPermissionModal: false,
|
||||
permissionType: null,
|
||||
showPlaylistModal: false,
|
||||
showChangeOwnerModal: false,
|
||||
showPublishStateModal: false,
|
||||
showCategoryModal: false,
|
||||
showTagModal: false,
|
||||
};
|
||||
|
||||
this.authorDataLoad = this.authorDataLoad.bind(this);
|
||||
@@ -66,20 +46,6 @@ export class ProfileMediaPage extends Page {
|
||||
this.getCountFunc = this.getCountFunc.bind(this);
|
||||
this.changeRequestQuery = this.changeRequestQuery.bind(this);
|
||||
this.handleMediaSelection = this.handleMediaSelection.bind(this);
|
||||
this.handleBulkAction = this.handleBulkAction.bind(this);
|
||||
this.handleConfirmCancel = this.handleConfirmCancel.bind(this);
|
||||
this.handleConfirmProceed = this.handleConfirmProceed.bind(this);
|
||||
this.clearSelectionAndRefresh = this.clearSelectionAndRefresh.bind(this);
|
||||
this.clearSelection = this.clearSelection.bind(this);
|
||||
this.executeEnableComments = this.executeEnableComments.bind(this);
|
||||
this.executeDisableComments = this.executeDisableComments.bind(this);
|
||||
this.executeEnableDownload = this.executeEnableDownload.bind(this);
|
||||
this.executeDisableDownload = this.executeDisableDownload.bind(this);
|
||||
this.executeCopyMedia = this.executeCopyMedia.bind(this);
|
||||
this.showNotification = this.showNotification.bind(this);
|
||||
this.handleSelectAll = this.handleSelectAll.bind(this);
|
||||
this.handleDeselectAll = this.handleDeselectAll.bind(this);
|
||||
this.handleItemsUpdate = this.handleItemsUpdate.bind(this);
|
||||
this.onToggleFiltersClick = this.onToggleFiltersClick.bind(this);
|
||||
this.onToggleTagsClick = this.onToggleTagsClick.bind(this);
|
||||
this.onToggleSortingClick = this.onToggleSortingClick.bind(this);
|
||||
@@ -87,24 +53,6 @@ export class ProfileMediaPage extends Page {
|
||||
this.onTagSelect = this.onTagSelect.bind(this);
|
||||
this.onSortSelect = this.onSortSelect.bind(this);
|
||||
this.onResponseDataLoaded = this.onResponseDataLoaded.bind(this);
|
||||
this.handlePermissionModalCancel = this.handlePermissionModalCancel.bind(this);
|
||||
this.handlePermissionModalSuccess = this.handlePermissionModalSuccess.bind(this);
|
||||
this.handlePermissionModalError = this.handlePermissionModalError.bind(this);
|
||||
this.handlePlaylistModalCancel = this.handlePlaylistModalCancel.bind(this);
|
||||
this.handlePlaylistModalSuccess = this.handlePlaylistModalSuccess.bind(this);
|
||||
this.handlePlaylistModalError = this.handlePlaylistModalError.bind(this);
|
||||
this.handleChangeOwnerModalCancel = this.handleChangeOwnerModalCancel.bind(this);
|
||||
this.handleChangeOwnerModalSuccess = this.handleChangeOwnerModalSuccess.bind(this);
|
||||
this.handleChangeOwnerModalError = this.handleChangeOwnerModalError.bind(this);
|
||||
this.handlePublishStateModalCancel = this.handlePublishStateModalCancel.bind(this);
|
||||
this.handlePublishStateModalSuccess = this.handlePublishStateModalSuccess.bind(this);
|
||||
this.handlePublishStateModalError = this.handlePublishStateModalError.bind(this);
|
||||
this.handleCategoryModalCancel = this.handleCategoryModalCancel.bind(this);
|
||||
this.handleCategoryModalSuccess = this.handleCategoryModalSuccess.bind(this);
|
||||
this.handleCategoryModalError = this.handleCategoryModalError.bind(this);
|
||||
this.handleTagModalCancel = this.handleTagModalCancel.bind(this);
|
||||
this.handleTagModalSuccess = this.handleTagModalSuccess.bind(this);
|
||||
this.handleTagModalError = this.handleTagModalError.bind(this);
|
||||
|
||||
ProfilePageStore.on('load-author-data', this.authorDataLoad);
|
||||
}
|
||||
@@ -202,397 +150,29 @@ export class ProfileMediaPage extends Page {
|
||||
}
|
||||
|
||||
handleMediaSelection(mediaId, isSelected) {
|
||||
// Only used in select media embed mode; normal mode is handled by bulkActions
|
||||
this.setState((prevState) => {
|
||||
const newSelectedMedia = new Set(prevState.selectedMedia);
|
||||
const newSelectedMedia = new Set();
|
||||
|
||||
if (isSelected) {
|
||||
newSelectedMedia.add(mediaId);
|
||||
} else {
|
||||
newSelectedMedia.delete(mediaId);
|
||||
|
||||
if (window.parent !== window) {
|
||||
const baseUrl = window.location.origin;
|
||||
const embedUrl = `${baseUrl}/embed?m=${mediaId}`;
|
||||
|
||||
window.parent.postMessage({
|
||||
type: 'videoSelected',
|
||||
embedUrl: embedUrl,
|
||||
videoId: mediaId
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
|
||||
return { selectedMedia: newSelectedMedia };
|
||||
});
|
||||
}
|
||||
|
||||
handleBulkAction(action) {
|
||||
const selectedCount = this.state.selectedMedia.size;
|
||||
|
||||
if (selectedCount === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'delete-media') {
|
||||
this.setState({
|
||||
showConfirmModal: true,
|
||||
pendingAction: action,
|
||||
confirmMessage:
|
||||
translateString('You are going to delete') +
|
||||
` ${selectedCount} ` +
|
||||
translateString('media, are you sure?'),
|
||||
});
|
||||
} else if (action === 'enable-comments') {
|
||||
this.setState({
|
||||
showConfirmModal: true,
|
||||
pendingAction: action,
|
||||
confirmMessage:
|
||||
translateString('You are going to enable comments to') +
|
||||
` ${selectedCount} ` +
|
||||
translateString('media, are you sure?'),
|
||||
});
|
||||
} else if (action === 'disable-comments') {
|
||||
this.setState({
|
||||
showConfirmModal: true,
|
||||
pendingAction: action,
|
||||
confirmMessage:
|
||||
translateString('You are going to disable comments to') +
|
||||
` ${selectedCount} ` +
|
||||
translateString('media, are you sure?'),
|
||||
});
|
||||
} else if (action === 'enable-download') {
|
||||
this.setState({
|
||||
showConfirmModal: true,
|
||||
pendingAction: action,
|
||||
confirmMessage:
|
||||
translateString('You are going to enable download for') +
|
||||
` ${selectedCount} ` +
|
||||
translateString('media, are you sure?'),
|
||||
});
|
||||
} else if (action === 'disable-download') {
|
||||
this.setState({
|
||||
showConfirmModal: true,
|
||||
pendingAction: action,
|
||||
confirmMessage:
|
||||
translateString('You are going to disable download for') +
|
||||
` ${selectedCount} ` +
|
||||
translateString('media, are you sure?'),
|
||||
});
|
||||
} else if (action === 'copy-media') {
|
||||
this.setState({
|
||||
showConfirmModal: true,
|
||||
pendingAction: action,
|
||||
confirmMessage:
|
||||
translateString('You are going to copy') +
|
||||
` ${selectedCount} ` +
|
||||
translateString('media, are you sure?'),
|
||||
});
|
||||
} else if (action === 'add-remove-coviewers') {
|
||||
this.setState({
|
||||
showPermissionModal: true,
|
||||
permissionType: 'viewer',
|
||||
});
|
||||
} else if (action === 'add-remove-coeditors') {
|
||||
this.setState({
|
||||
showPermissionModal: true,
|
||||
permissionType: 'editor',
|
||||
});
|
||||
} else if (action === 'add-remove-coowners') {
|
||||
this.setState({
|
||||
showPermissionModal: true,
|
||||
permissionType: 'owner',
|
||||
});
|
||||
} else if (action === 'add-remove-playlist') {
|
||||
this.setState({
|
||||
showPlaylistModal: true,
|
||||
});
|
||||
} else if (action === 'change-owner') {
|
||||
this.setState({
|
||||
showChangeOwnerModal: true,
|
||||
});
|
||||
} else if (action === 'publish-state') {
|
||||
this.setState({
|
||||
showPublishStateModal: true,
|
||||
});
|
||||
} else if (action === 'add-remove-category') {
|
||||
this.setState({
|
||||
showCategoryModal: true,
|
||||
});
|
||||
} else if (action === 'add-remove-tags') {
|
||||
this.setState({
|
||||
showTagModal: true,
|
||||
});
|
||||
} else {
|
||||
// Other actions can be implemented later
|
||||
}
|
||||
}
|
||||
|
||||
handleConfirmCancel() {
|
||||
this.setState({
|
||||
showConfirmModal: false,
|
||||
pendingAction: null,
|
||||
confirmMessage: '',
|
||||
});
|
||||
}
|
||||
|
||||
handleConfirmProceed() {
|
||||
const action = this.state.pendingAction;
|
||||
this.setState({
|
||||
showConfirmModal: false,
|
||||
pendingAction: null,
|
||||
confirmMessage: '',
|
||||
});
|
||||
|
||||
if (action === 'delete-media') {
|
||||
this.executeDeleteMedia();
|
||||
} else if (action === 'enable-comments') {
|
||||
this.executeEnableComments();
|
||||
} else if (action === 'disable-comments') {
|
||||
this.executeDisableComments();
|
||||
} else if (action === 'enable-download') {
|
||||
this.executeEnableDownload();
|
||||
} else if (action === 'disable-download') {
|
||||
this.executeDisableDownload();
|
||||
} else if (action === 'copy-media') {
|
||||
this.executeCopyMedia();
|
||||
}
|
||||
}
|
||||
|
||||
executeDeleteMedia() {
|
||||
const selectedIds = Array.from(this.state.selectedMedia);
|
||||
const selectedCount = selectedIds.length;
|
||||
|
||||
fetch('/api/v1/media/user/bulk_actions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': this.getCsrfToken(),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'delete_media',
|
||||
media_ids: selectedIds,
|
||||
}),
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete media');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
const message =
|
||||
selectedCount === 1
|
||||
? translateString('The media was deleted successfully.')
|
||||
: translateString('Successfully deleted') + ` ${selectedCount} ` + translateString('media.');
|
||||
this.showNotification(message);
|
||||
this.clearSelectionAndRefresh();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.showNotification(translateString('Failed to delete media. Please try again.'), 'error');
|
||||
this.clearSelectionAndRefresh();
|
||||
});
|
||||
}
|
||||
|
||||
getCsrfToken() {
|
||||
const name = 'csrftoken';
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
if (cookie.substring(0, name.length + 1) === name + '=') {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
clearSelectionAndRefresh() {
|
||||
// Clear selected media and increment listKey to force re-render
|
||||
this.setState((prevState) => ({
|
||||
selectedMedia: new Set(),
|
||||
listKey: prevState.listKey + 1,
|
||||
}));
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
// Clear selected media without refreshing
|
||||
this.setState({
|
||||
selectedMedia: new Set(),
|
||||
});
|
||||
}
|
||||
|
||||
executeEnableComments() {
|
||||
const selectedIds = Array.from(this.state.selectedMedia);
|
||||
|
||||
fetch('/api/v1/media/user/bulk_actions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': this.getCsrfToken(),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'enable_comments',
|
||||
media_ids: selectedIds,
|
||||
}),
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to enable comments');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
this.showNotification(translateString('Successfully Enabled comments'));
|
||||
this.clearSelection();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.showNotification(translateString('Failed to enable comments.'), 'error');
|
||||
this.clearSelection();
|
||||
});
|
||||
}
|
||||
|
||||
executeDisableComments() {
|
||||
const selectedIds = Array.from(this.state.selectedMedia);
|
||||
|
||||
fetch('/api/v1/media/user/bulk_actions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': this.getCsrfToken(),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'disable_comments',
|
||||
media_ids: selectedIds,
|
||||
}),
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to disable comments');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
this.showNotification(translateString('Successfully Disabled comments'));
|
||||
this.clearSelection();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.showNotification(translateString('Failed to disable comments.'), 'error');
|
||||
this.clearSelection();
|
||||
});
|
||||
}
|
||||
|
||||
executeEnableDownload() {
|
||||
const selectedIds = Array.from(this.state.selectedMedia);
|
||||
|
||||
fetch('/api/v1/media/user/bulk_actions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': this.getCsrfToken(),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'enable_download',
|
||||
media_ids: selectedIds,
|
||||
}),
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to enable download');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
this.showNotification(translateString('Successfully Enabled Download'));
|
||||
this.clearSelection();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.showNotification(translateString('Failed to enable download.'), 'error');
|
||||
this.clearSelection();
|
||||
});
|
||||
}
|
||||
|
||||
executeDisableDownload() {
|
||||
const selectedIds = Array.from(this.state.selectedMedia);
|
||||
|
||||
fetch('/api/v1/media/user/bulk_actions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': this.getCsrfToken(),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'disable_download',
|
||||
media_ids: selectedIds,
|
||||
}),
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to disable download');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
this.showNotification(translateString('Successfully Disabled Download'));
|
||||
this.clearSelection();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.showNotification(translateString('Failed to disable download.'), 'error');
|
||||
this.clearSelection();
|
||||
});
|
||||
}
|
||||
|
||||
executeCopyMedia() {
|
||||
const selectedIds = Array.from(this.state.selectedMedia);
|
||||
|
||||
fetch('/api/v1/media/user/bulk_actions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': this.getCsrfToken(),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'copy_media',
|
||||
media_ids: selectedIds,
|
||||
}),
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to copy media');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
this.showNotification(translateString('Successfully Copied'));
|
||||
this.clearSelectionAndRefresh();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.showNotification(translateString('Failed to copy media.'), 'error');
|
||||
this.clearSelection();
|
||||
});
|
||||
}
|
||||
|
||||
showNotification(message, type = 'success') {
|
||||
this.setState({
|
||||
notificationMessage: message,
|
||||
showNotification: true,
|
||||
notificationType: type,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.setState({ showNotification: false });
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
handleItemsUpdate(items) {
|
||||
// Extract media IDs from loaded items
|
||||
const mediaIds = items.map((item) => item.friendly_token || item.uid || item.id);
|
||||
this.setState({ availableMediaIds: mediaIds });
|
||||
}
|
||||
|
||||
handleSelectAll() {
|
||||
// Select all available media
|
||||
this.setState({
|
||||
selectedMedia: new Set(this.state.availableMediaIds),
|
||||
});
|
||||
}
|
||||
|
||||
handleDeselectAll() {
|
||||
// Clear all selections
|
||||
this.setState({
|
||||
selectedMedia: new Set(),
|
||||
});
|
||||
}
|
||||
|
||||
onToggleFiltersClick() {
|
||||
this.setState({
|
||||
hiddenFilters: !this.state.hiddenFilters,
|
||||
@@ -619,7 +199,6 @@ export class ProfileMediaPage extends Page {
|
||||
|
||||
onTagSelect(tag) {
|
||||
this.setState({ selectedTag: tag }, () => {
|
||||
// Apply tag filter
|
||||
this.onFiltersUpdate({
|
||||
media_type: this.state.filterArgs.includes('media_type')
|
||||
? this.state.filterArgs.match(/media_type=([^&]*)/)?.[1]
|
||||
@@ -641,7 +220,6 @@ export class ProfileMediaPage extends Page {
|
||||
|
||||
onSortSelect(sortOption) {
|
||||
this.setState({ selectedSort: sortOption }, () => {
|
||||
// Apply sort filter
|
||||
this.onFiltersUpdate({
|
||||
media_type: this.state.filterArgs.includes('media_type')
|
||||
? this.state.filterArgs.match(/media_type=([^&]*)/)?.[1]
|
||||
@@ -690,19 +268,16 @@ export class ProfileMediaPage extends Page {
|
||||
break;
|
||||
}
|
||||
|
||||
// Handle duration filter
|
||||
if (updatedArgs.duration && updatedArgs.duration !== 'all') {
|
||||
args.duration = updatedArgs.duration;
|
||||
}
|
||||
|
||||
// Handle publish state filter
|
||||
if (updatedArgs.publish_state && updatedArgs.publish_state !== 'all') {
|
||||
args.publish_state = updatedArgs.publish_state;
|
||||
}
|
||||
|
||||
switch (updatedArgs.sort_by) {
|
||||
case 'date_added_desc':
|
||||
// Default sorting, no need to add parameters
|
||||
break;
|
||||
case 'date_added_asc':
|
||||
args.ordering = 'asc';
|
||||
@@ -727,7 +302,6 @@ export class ProfileMediaPage extends Page {
|
||||
break;
|
||||
}
|
||||
|
||||
// Handle tag filter
|
||||
if (updatedArgs.tag && updatedArgs.tag !== 'all') {
|
||||
args.t = updatedArgs.tag;
|
||||
}
|
||||
@@ -743,10 +317,8 @@ export class ProfileMediaPage extends Page {
|
||||
this.setState(
|
||||
{
|
||||
filterArgs: newArgs.length ? '&' + newArgs.join('&') : '',
|
||||
selectedMedia: new Set(), // Clear selected items when filter changes
|
||||
},
|
||||
function () {
|
||||
// Update the request URL with new filter args
|
||||
if (!this.state.author) {
|
||||
return;
|
||||
}
|
||||
@@ -773,137 +345,7 @@ export class ProfileMediaPage extends Page {
|
||||
);
|
||||
}
|
||||
|
||||
handlePermissionModalCancel() {
|
||||
this.setState({
|
||||
showPermissionModal: false,
|
||||
permissionType: null,
|
||||
});
|
||||
}
|
||||
|
||||
handlePermissionModalSuccess(message) {
|
||||
this.showNotification(message);
|
||||
this.clearSelection();
|
||||
this.setState({
|
||||
showPermissionModal: false,
|
||||
permissionType: null,
|
||||
});
|
||||
}
|
||||
|
||||
handlePermissionModalError(message) {
|
||||
this.showNotification(message, 'error');
|
||||
this.setState({
|
||||
showPermissionModal: false,
|
||||
permissionType: null,
|
||||
});
|
||||
}
|
||||
|
||||
handlePlaylistModalCancel() {
|
||||
this.setState({
|
||||
showPlaylistModal: false,
|
||||
});
|
||||
}
|
||||
|
||||
handlePlaylistModalSuccess(message) {
|
||||
this.showNotification(message);
|
||||
this.clearSelection();
|
||||
this.setState({
|
||||
showPlaylistModal: false,
|
||||
});
|
||||
}
|
||||
|
||||
handlePlaylistModalError(message) {
|
||||
this.showNotification(message, 'error');
|
||||
this.setState({
|
||||
showPlaylistModal: false,
|
||||
});
|
||||
}
|
||||
|
||||
handleChangeOwnerModalCancel() {
|
||||
this.setState({
|
||||
showChangeOwnerModal: false,
|
||||
});
|
||||
}
|
||||
|
||||
handleChangeOwnerModalSuccess(message) {
|
||||
this.showNotification(message);
|
||||
this.clearSelectionAndRefresh();
|
||||
this.setState({
|
||||
showChangeOwnerModal: false,
|
||||
});
|
||||
}
|
||||
|
||||
handleChangeOwnerModalError(message) {
|
||||
this.showNotification(message, 'error');
|
||||
this.setState({
|
||||
showChangeOwnerModal: false,
|
||||
});
|
||||
}
|
||||
|
||||
handlePublishStateModalCancel() {
|
||||
this.setState({
|
||||
showPublishStateModal: false,
|
||||
});
|
||||
}
|
||||
|
||||
handlePublishStateModalSuccess(message) {
|
||||
this.showNotification(message);
|
||||
this.clearSelectionAndRefresh();
|
||||
this.setState({
|
||||
showPublishStateModal: false,
|
||||
});
|
||||
}
|
||||
|
||||
handlePublishStateModalError(message) {
|
||||
this.showNotification(message, 'error');
|
||||
this.setState({
|
||||
showPublishStateModal: false,
|
||||
});
|
||||
}
|
||||
|
||||
handleCategoryModalCancel() {
|
||||
this.setState({
|
||||
showCategoryModal: false,
|
||||
});
|
||||
}
|
||||
|
||||
handleCategoryModalSuccess(message) {
|
||||
this.showNotification(message);
|
||||
this.clearSelection();
|
||||
this.setState({
|
||||
showCategoryModal: false,
|
||||
});
|
||||
}
|
||||
|
||||
handleCategoryModalError(message) {
|
||||
this.showNotification(message, 'error');
|
||||
this.setState({
|
||||
showCategoryModal: false,
|
||||
});
|
||||
}
|
||||
|
||||
handleTagModalCancel() {
|
||||
this.setState({
|
||||
showTagModal: false,
|
||||
});
|
||||
}
|
||||
|
||||
handleTagModalSuccess(message) {
|
||||
this.showNotification(message);
|
||||
this.clearSelection();
|
||||
this.setState({
|
||||
showTagModal: false,
|
||||
});
|
||||
}
|
||||
|
||||
handleTagModalError(message) {
|
||||
this.showNotification(message, 'error');
|
||||
this.setState({
|
||||
showTagModal: false,
|
||||
});
|
||||
}
|
||||
|
||||
onResponseDataLoaded(responseData) {
|
||||
// Extract tags from response
|
||||
if (responseData && responseData.tags) {
|
||||
const tags = responseData.tags
|
||||
.split(',')
|
||||
@@ -917,8 +359,8 @@ 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 =
|
||||
this.state.filterArgs &&
|
||||
(this.state.filterArgs.includes('media_type=') ||
|
||||
@@ -948,15 +390,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}
|
||||
selectedCount={this.state.selectedMedia.size}
|
||||
totalCount={this.state.availableMediaIds.length}
|
||||
onBulkAction={this.handleBulkAction}
|
||||
onSelectAll={this.handleSelectAll}
|
||||
onDeselectAll={this.handleDeselectAll}
|
||||
showAddMediaButton={isMediaAuthor}
|
||||
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}
|
||||
showAddMediaButton={!isSelectMediaMode && isMediaAuthor}
|
||||
>
|
||||
<ProfileMediaFilters
|
||||
hidden={this.state.hiddenFilters}
|
||||
@@ -972,105 +415,51 @@ export class ProfileMediaPage extends Page {
|
||||
/>
|
||||
<ProfileMediaSorting hidden={this.state.hiddenSorting} onSortSelect={this.onSortSelect} />
|
||||
<LazyLoadItemListAsync
|
||||
key={`${this.state.requestUrl}-${this.state.listKey}`}
|
||||
key={`${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}
|
||||
showSelection={isMediaAuthor}
|
||||
hasAnySelection={this.state.selectedMedia.size > 0}
|
||||
selectedMedia={this.state.selectedMedia}
|
||||
onMediaSelection={this.handleMediaSelection}
|
||||
onItemsUpdate={this.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}
|
||||
onResponseDataLoaded={this.onResponseDataLoaded}
|
||||
/>
|
||||
</MediaListWrapper>
|
||||
</ProfilePagesContent>
|
||||
) : null,
|
||||
<BulkActionConfirmModal
|
||||
key="BulkActionConfirmModal"
|
||||
isOpen={this.state.showConfirmModal}
|
||||
message={this.state.confirmMessage}
|
||||
onCancel={this.handleConfirmCancel}
|
||||
onProceed={this.handleConfirmProceed}
|
||||
/>,
|
||||
<BulkActionPermissionModal
|
||||
key="BulkActionPermissionModal"
|
||||
isOpen={this.state.showPermissionModal}
|
||||
permissionType={this.state.permissionType}
|
||||
selectedMediaIds={Array.from(this.state.selectedMedia)}
|
||||
onCancel={this.handlePermissionModalCancel}
|
||||
onSuccess={this.handlePermissionModalSuccess}
|
||||
onError={this.handlePermissionModalError}
|
||||
csrfToken={this.getCsrfToken()}
|
||||
/>,
|
||||
<BulkActionPlaylistModal
|
||||
key="BulkActionPlaylistModal"
|
||||
isOpen={this.state.showPlaylistModal}
|
||||
selectedMediaIds={Array.from(this.state.selectedMedia)}
|
||||
onCancel={this.handlePlaylistModalCancel}
|
||||
onSuccess={this.handlePlaylistModalSuccess}
|
||||
onError={this.handlePlaylistModalError}
|
||||
csrfToken={this.getCsrfToken()}
|
||||
username={this.state.author ? this.state.author.username : ''}
|
||||
/>,
|
||||
<BulkActionChangeOwnerModal
|
||||
key="BulkActionChangeOwnerModal"
|
||||
isOpen={this.state.showChangeOwnerModal}
|
||||
selectedMediaIds={Array.from(this.state.selectedMedia)}
|
||||
onCancel={this.handleChangeOwnerModalCancel}
|
||||
onSuccess={this.handleChangeOwnerModalSuccess}
|
||||
onError={this.handleChangeOwnerModalError}
|
||||
csrfToken={this.getCsrfToken()}
|
||||
/>,
|
||||
<BulkActionPublishStateModal
|
||||
key="BulkActionPublishStateModal"
|
||||
isOpen={this.state.showPublishStateModal}
|
||||
selectedMediaIds={Array.from(this.state.selectedMedia)}
|
||||
onCancel={this.handlePublishStateModalCancel}
|
||||
onSuccess={this.handlePublishStateModalSuccess}
|
||||
onError={this.handlePublishStateModalError}
|
||||
csrfToken={this.getCsrfToken()}
|
||||
/>,
|
||||
<BulkActionCategoryModal
|
||||
key="BulkActionCategoryModal"
|
||||
isOpen={this.state.showCategoryModal}
|
||||
selectedMediaIds={Array.from(this.state.selectedMedia)}
|
||||
onCancel={this.handleCategoryModalCancel}
|
||||
onSuccess={this.handleCategoryModalSuccess}
|
||||
onError={this.handleCategoryModalError}
|
||||
csrfToken={this.getCsrfToken()}
|
||||
/>,
|
||||
<BulkActionTagModal
|
||||
key="BulkActionTagModal"
|
||||
isOpen={this.state.showTagModal}
|
||||
selectedMediaIds={Array.from(this.state.selectedMedia)}
|
||||
onCancel={this.handleTagModalCancel}
|
||||
onSuccess={this.handleTagModalSuccess}
|
||||
onError={this.handleTagModalError}
|
||||
csrfToken={this.getCsrfToken()}
|
||||
/>,
|
||||
this.state.showNotification ? (
|
||||
<div
|
||||
key="SimpleNotification"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
bottom: '20px',
|
||||
left: '260px',
|
||||
backgroundColor: this.state.notificationType === 'error' ? '#f44336' : '#4CAF50',
|
||||
color: 'white',
|
||||
padding: '16px 24px',
|
||||
borderRadius: '4px',
|
||||
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
|
||||
zIndex: 1000,
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
}}
|
||||
>
|
||||
{this.state.notificationMessage}
|
||||
</div>
|
||||
this.state.author && isMediaAuthor && !isSelectMediaMode ? (
|
||||
<BulkActionsModals
|
||||
key="BulkActionsModals"
|
||||
{...this.props.bulkActions}
|
||||
selectedMediaIds={Array.from(this.props.bulkActions.selectedMedia)}
|
||||
csrfToken={this.props.bulkActions.getCsrfToken()}
|
||||
username={this.state.author.username}
|
||||
onConfirmCancel={this.props.bulkActions.handleConfirmCancel}
|
||||
onConfirmProceed={this.props.bulkActions.handleConfirmProceed}
|
||||
onPermissionModalCancel={this.props.bulkActions.handlePermissionModalCancel}
|
||||
onPermissionModalSuccess={this.props.bulkActions.handlePermissionModalSuccess}
|
||||
onPermissionModalError={this.props.bulkActions.handlePermissionModalError}
|
||||
onPlaylistModalCancel={this.props.bulkActions.handlePlaylistModalCancel}
|
||||
onPlaylistModalSuccess={this.props.bulkActions.handlePlaylistModalSuccess}
|
||||
onPlaylistModalError={this.props.bulkActions.handlePlaylistModalError}
|
||||
onChangeOwnerModalCancel={this.props.bulkActions.handleChangeOwnerModalCancel}
|
||||
onChangeOwnerModalSuccess={this.props.bulkActions.handleChangeOwnerModalSuccess}
|
||||
onChangeOwnerModalError={this.props.bulkActions.handleChangeOwnerModalError}
|
||||
onPublishStateModalCancel={this.props.bulkActions.handlePublishStateModalCancel}
|
||||
onPublishStateModalSuccess={this.props.bulkActions.handlePublishStateModalSuccess}
|
||||
onPublishStateModalError={this.props.bulkActions.handlePublishStateModalError}
|
||||
onCategoryModalCancel={this.props.bulkActions.handleCategoryModalCancel}
|
||||
onCategoryModalSuccess={this.props.bulkActions.handleCategoryModalSuccess}
|
||||
onCategoryModalError={this.props.bulkActions.handleCategoryModalError}
|
||||
onTagModalCancel={this.props.bulkActions.handleTagModalCancel}
|
||||
onTagModalSuccess={this.props.bulkActions.handleTagModalSuccess}
|
||||
onTagModalError={this.props.bulkActions.handleTagModalError}
|
||||
/>
|
||||
) : null,
|
||||
];
|
||||
}
|
||||
@@ -1078,8 +467,17 @@ export class ProfileMediaPage extends Page {
|
||||
|
||||
ProfileMediaPage.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
bulkActions: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
ProfileMediaPage.defaultProps = {
|
||||
title: 'Uploads',
|
||||
};
|
||||
|
||||
// Export the raw class for subclasses to extend
|
||||
export { ProfileMediaPage as ProfileMediaPageBase };
|
||||
|
||||
// Export the HOC-wrapped version as the renderable component
|
||||
const WrappedProfileMediaPage = withBulkActions(ProfileMediaPage);
|
||||
export { WrappedProfileMediaPage as ProfileMediaPage };
|
||||
export default WrappedProfileMediaPage;
|
||||
|
||||
@@ -6,9 +6,9 @@ import { MediaListWrapper } from '../components/MediaListWrapper';
|
||||
import ProfilePagesHeader from '../components/profile-page/ProfilePagesHeader';
|
||||
import ProfilePagesContent from '../components/profile-page/ProfilePagesContent';
|
||||
import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListAsync.jsx';
|
||||
import { ProfileMediaPage } from './ProfileMediaPage';
|
||||
import { ProfileMediaPageBase } from './ProfileMediaPage';
|
||||
|
||||
export class ProfilePlaylistsPage extends ProfileMediaPage {
|
||||
export class ProfilePlaylistsPage extends ProfileMediaPageBase {
|
||||
constructor(props) {
|
||||
super(props, 'author-playlists');
|
||||
|
||||
|
||||
@@ -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 + '"';
|
||||
|
||||
@@ -45,12 +45,14 @@ export const LayoutProvider = ({ children }) => {
|
||||
const site = useContext(SiteContext);
|
||||
const cache = new BrowserCache('MediaCMS[' + site.id + '][layout]', 86400);
|
||||
|
||||
const isMediaPage = useMemo(() => PageStore.get('current-page') === 'media', []);
|
||||
const isMediaPage = useMemo(() => PageStore.get('current-page') === 'media' || window.MediaCMS?.mediaId !== undefined, []);
|
||||
const isEmbeddedApp = useMemo(() => inEmbeddedApp(), []);
|
||||
|
||||
const enabledSidebar = Boolean(document.getElementById('app-sidebar') || document.querySelector('.page-sidebar'));
|
||||
|
||||
const [visibleSidebar, setVisibleSidebar] = useState(cache.get('visible-sidebar'));
|
||||
const [visibleSidebar, setVisibleSidebar] = useState(
|
||||
isMediaPage || isEmbeddedApp ? false : cache.get('visible-sidebar')
|
||||
);
|
||||
const [visibleMobileSearch, setVisibleMobileSearch] = useState(false);
|
||||
|
||||
const toggleMobileSearch = () => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -111,6 +111,10 @@ export function useBulkActions() {
|
||||
setShowConfirmModal(true);
|
||||
setPendingAction(action);
|
||||
setConfirmMessage(translateString('You are going to disable comments to') + ` ${selectedCount} ` + translateString('media, are you sure?'));
|
||||
} else if (action === 'delete-comments') {
|
||||
setShowConfirmModal(true);
|
||||
setPendingAction(action);
|
||||
setConfirmMessage(translateString('You are going to delete all comments from') + ` ${selectedCount} ` + translateString('media, are you sure?'));
|
||||
} else if (action === 'enable-download') {
|
||||
setShowConfirmModal(true);
|
||||
setPendingAction(action);
|
||||
@@ -165,6 +169,8 @@ export function useBulkActions() {
|
||||
executeEnableComments();
|
||||
} else if (action === 'disable-comments') {
|
||||
executeDisableComments();
|
||||
} else if (action === 'delete-comments') {
|
||||
executeDeleteComments();
|
||||
} else if (action === 'enable-download') {
|
||||
executeEnableDownload();
|
||||
} else if (action === 'disable-download') {
|
||||
@@ -271,6 +277,37 @@ export function useBulkActions() {
|
||||
});
|
||||
};
|
||||
|
||||
// Execute delete comments
|
||||
const executeDeleteComments = () => {
|
||||
const selectedIds = Array.from(selectedMedia);
|
||||
|
||||
fetch('/api/v1/media/user/bulk_actions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken(),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'delete_comments',
|
||||
media_ids: selectedIds,
|
||||
}),
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete comments');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
showNotificationMessage(translateString('Successfully deleted comments'));
|
||||
clearSelection();
|
||||
})
|
||||
.catch((error) => {
|
||||
showNotificationMessage(translateString('Failed to delete comments.'), 'error');
|
||||
clearSelection();
|
||||
});
|
||||
};
|
||||
|
||||
// Execute enable download
|
||||
const executeEnableDownload = () => {
|
||||
const selectedIds = Array.from(selectedMedia);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,89 +9,88 @@ let browserCache;
|
||||
const _StoreData = {};
|
||||
|
||||
class VideoPlayerStore extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.mediacms_config = mediacmsConfig(window.MediaCMS);
|
||||
this.mediacms_config = mediacmsConfig(window.MediaCMS);
|
||||
|
||||
browserCache = new BrowserCache(this.mediacms_config.site.id, 86400); // Keep cache data "fresh" for one day.
|
||||
browserCache = new BrowserCache(this.mediacms_config.site.id, 86400); // Keep cache data "fresh" for one day.
|
||||
|
||||
_StoreData.inTheaterMode = browserCache.get('in-theater-mode');
|
||||
_StoreData.inTheaterMode = null !== _StoreData.inTheaterMode ? _StoreData.inTheaterMode : !1;
|
||||
_StoreData.inTheaterMode = browserCache.get('in-theater-mode');
|
||||
_StoreData.inTheaterMode = null !== _StoreData.inTheaterMode ? _StoreData.inTheaterMode : !1;
|
||||
|
||||
_StoreData.playerVolume = browserCache.get('player-volume');
|
||||
_StoreData.playerVolume =
|
||||
null === _StoreData.playerVolume ? 1 : Math.max(Math.min(Number(_StoreData.playerVolume), 1), 0);
|
||||
_StoreData.playerVolume = browserCache.get('player-volume');
|
||||
_StoreData.playerVolume =
|
||||
null === _StoreData.playerVolume ? 1 : Math.max(Math.min(Number(_StoreData.playerVolume), 1), 0);
|
||||
|
||||
_StoreData.playerSoundMuted = browserCache.get('player-sound-muted');
|
||||
_StoreData.playerSoundMuted = null !== _StoreData.playerSoundMuted ? _StoreData.playerSoundMuted : !1;
|
||||
_StoreData.playerSoundMuted = browserCache.get('player-sound-muted');
|
||||
_StoreData.playerSoundMuted = null !== _StoreData.playerSoundMuted ? _StoreData.playerSoundMuted : !1;
|
||||
|
||||
_StoreData.videoQuality = browserCache.get('video-quality');
|
||||
_StoreData.videoQuality = null !== _StoreData.videoQuality ? _StoreData.videoQuality : 'Auto';
|
||||
_StoreData.videoQuality = browserCache.get('video-quality');
|
||||
_StoreData.videoQuality = null !== _StoreData.videoQuality ? _StoreData.videoQuality : 'Auto';
|
||||
|
||||
_StoreData.videoPlaybackSpeed = browserCache.get('video-playback-speed');
|
||||
_StoreData.videoPlaybackSpeed = null !== _StoreData.videoPlaybackSpeed ? _StoreData.videoPlaybackSpeed : !1;
|
||||
_StoreData.videoPlaybackSpeed = browserCache.get('video-playback-speed');
|
||||
_StoreData.videoPlaybackSpeed = null !== _StoreData.videoPlaybackSpeed ? _StoreData.videoPlaybackSpeed : !1;
|
||||
}
|
||||
|
||||
get(type) {
|
||||
let r = null;
|
||||
switch (type) {
|
||||
case 'player-volume':
|
||||
r = _StoreData.playerVolume;
|
||||
break;
|
||||
case 'player-sound-muted':
|
||||
r = _StoreData.playerSoundMuted;
|
||||
break;
|
||||
case 'in-theater-mode':
|
||||
r = _StoreData.inTheaterMode;
|
||||
break;
|
||||
case 'video-data':
|
||||
r = _StoreData.videoData;
|
||||
break;
|
||||
case 'video-quality':
|
||||
r = _StoreData.videoQuality;
|
||||
break;
|
||||
case 'video-playback-speed':
|
||||
r = _StoreData.videoPlaybackSpeed;
|
||||
break;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
get(type) {
|
||||
let r = null;
|
||||
switch (type) {
|
||||
case 'player-volume':
|
||||
r = _StoreData.playerVolume;
|
||||
break;
|
||||
case 'player-sound-muted':
|
||||
r = _StoreData.playerSoundMuted;
|
||||
break;
|
||||
case 'in-theater-mode':
|
||||
r = _StoreData.inTheaterMode;
|
||||
break;
|
||||
case 'video-data':
|
||||
r = _StoreData.videoData;
|
||||
break;
|
||||
case 'video-quality':
|
||||
r = _StoreData.videoQuality;
|
||||
break;
|
||||
case 'video-playback-speed':
|
||||
r = _StoreData.videoPlaybackSpeed;
|
||||
break;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
actions_handler(action) {
|
||||
switch (action.type) {
|
||||
case 'TOGGLE_VIEWER_MODE':
|
||||
_StoreData.inTheaterMode = !_StoreData.inTheaterMode;
|
||||
browserCache.set('in-theater-mode', _StoreData.inTheaterMode);
|
||||
this.emit('changed_viewer_mode');
|
||||
break;
|
||||
case 'SET_VIEWER_MODE':
|
||||
_StoreData.inTheaterMode = action.inTheaterMode;
|
||||
browserCache.set('in-theater-mode', _StoreData.inTheaterMode);
|
||||
this.emit('changed_viewer_mode');
|
||||
break;
|
||||
case 'SET_PLAYER_VOLUME':
|
||||
_StoreData.playerVolume = action.playerVolume;
|
||||
browserCache.set('player-volume', action.playerVolume);
|
||||
this.emit('changed_player_volume');
|
||||
break;
|
||||
case 'SET_PLAYER_SOUND_MUTED':
|
||||
_StoreData.playerSoundMuted = action.playerSoundMuted;
|
||||
browserCache.set('player-sound-muted', action.playerSoundMuted);
|
||||
this.emit('changed_player_sound_muted');
|
||||
break;
|
||||
case 'SET_VIDEO_QUALITY':
|
||||
_StoreData.videoQuality = action.quality;
|
||||
browserCache.set('video-quality', action.quality);
|
||||
this.emit('changed_video_quality');
|
||||
break;
|
||||
case 'SET_VIDEO_PLAYBACK_SPEED':
|
||||
_StoreData.videoPlaybackSpeed = action.playbackSpeed;
|
||||
browserCache.set('video-playback-speed', action.playbackSpeed);
|
||||
this.emit('changed_video_playback_speed');
|
||||
break;
|
||||
}
|
||||
actions_handler(action) {
|
||||
switch (action.type) {
|
||||
case 'TOGGLE_VIEWER_MODE':
|
||||
_StoreData.inTheaterMode = !_StoreData.inTheaterMode;
|
||||
this.emit('changed_viewer_mode');
|
||||
break;
|
||||
case 'SET_VIEWER_MODE':
|
||||
_StoreData.inTheaterMode = action.inTheaterMode;
|
||||
browserCache.set('in-theater-mode', _StoreData.inTheaterMode);
|
||||
this.emit('changed_viewer_mode');
|
||||
break;
|
||||
case 'SET_PLAYER_VOLUME':
|
||||
_StoreData.playerVolume = action.playerVolume;
|
||||
browserCache.set('player-volume', action.playerVolume);
|
||||
this.emit('changed_player_volume');
|
||||
break;
|
||||
case 'SET_PLAYER_SOUND_MUTED':
|
||||
_StoreData.playerSoundMuted = action.playerSoundMuted;
|
||||
browserCache.set('player-sound-muted', action.playerSoundMuted);
|
||||
this.emit('changed_player_sound_muted');
|
||||
break;
|
||||
case 'SET_VIDEO_QUALITY':
|
||||
_StoreData.videoQuality = action.quality;
|
||||
browserCache.set('video-quality', action.quality);
|
||||
this.emit('changed_video_quality');
|
||||
break;
|
||||
case 'SET_VIDEO_PLAYBACK_SPEED':
|
||||
_StoreData.videoPlaybackSpeed = action.playbackSpeed;
|
||||
browserCache.set('video-playback-speed', action.playbackSpeed);
|
||||
this.emit('changed_video_playback_speed');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default exportStore(new VideoPlayerStore(), 'actions_handler');
|
||||
|
||||
@@ -1,385 +0,0 @@
|
||||
export const sampleGlobalMediaCMS = {
|
||||
profileId: 'john',
|
||||
site: {
|
||||
id: 'my-site',
|
||||
url: 'https://example.com/',
|
||||
api: 'https://example.com/api/',
|
||||
title: 'Example',
|
||||
theme: { mode: 'dark', switch: { enabled: true, position: 'sidebar' } },
|
||||
logo: {
|
||||
lightMode: { img: '/img/light.png', svg: '/img/light.svg' },
|
||||
darkMode: { img: '/img/dark.png', svg: '/img/dark.svg' },
|
||||
},
|
||||
devEnv: false,
|
||||
useRoundedCorners: true,
|
||||
version: '1.0.0',
|
||||
taxonomies: {
|
||||
tags: { enabled: true, title: 'Topic Tags' },
|
||||
categories: { enabled: false, title: 'Kinds' },
|
||||
},
|
||||
pages: {
|
||||
featured: { enabled: true, title: 'Featured picks' },
|
||||
latest: { enabled: true, title: 'Recent uploads' },
|
||||
members: { enabled: true, title: 'People' },
|
||||
recommended: { enabled: false, title: 'You may like' },
|
||||
},
|
||||
userPages: {
|
||||
liked: { enabled: true, title: 'Favorites' },
|
||||
history: { enabled: true, title: 'Watched' },
|
||||
},
|
||||
},
|
||||
url: {
|
||||
home: '/',
|
||||
admin: '/admin',
|
||||
error404: '/404',
|
||||
latestMedia: '/latest',
|
||||
featuredMedia: '/featured',
|
||||
recommendedMedia: '/recommended',
|
||||
signin: '/signin',
|
||||
signout: '/signout',
|
||||
register: '/register',
|
||||
changePassword: '/password',
|
||||
members: '/members',
|
||||
search: '/search',
|
||||
likedMedia: '/liked',
|
||||
history: '/history',
|
||||
addMedia: '/add',
|
||||
editChannel: '/edit/channel',
|
||||
editProfile: '/edit/profile',
|
||||
tags: '/tags',
|
||||
categories: '/categories',
|
||||
manageMedia: '/manage/media',
|
||||
manageUsers: '/manage/users',
|
||||
manageComments: '/manage/comments',
|
||||
},
|
||||
api: {
|
||||
media: 'v1/media/',
|
||||
playlists: 'v1/playlists',
|
||||
members: 'v1/users',
|
||||
liked: 'v1/user/liked',
|
||||
history: 'v1/user/history',
|
||||
tags: 'v1/tags',
|
||||
categories: 'v1/categories',
|
||||
manage_media: 'v1/manage/media',
|
||||
manage_users: 'v1/manage/users',
|
||||
manage_comments: 'v1/manage/comments',
|
||||
search: 'v1/search',
|
||||
actions: 'v1/actions',
|
||||
comments: 'v1/comments',
|
||||
},
|
||||
contents: {
|
||||
header: {
|
||||
right: '',
|
||||
onLogoRight: '',
|
||||
},
|
||||
notifications: {
|
||||
messages: { addToLiked: 'Yay', removeFromLiked: 'Oops', addToDisliked: 'nay', removeFromDisliked: 'ok' },
|
||||
},
|
||||
sidebar: {
|
||||
belowNavMenu: '__belowNavMenu__',
|
||||
belowThemeSwitcher: '__belowThemeSwitcher__',
|
||||
footer: '__footer__',
|
||||
mainMenuExtraItems: [
|
||||
{ text: '__text_1__', link: '__link_1__', icon: '__icon_1__', className: '__className_1__' },
|
||||
],
|
||||
navMenuItems: [
|
||||
{ text: '__text_2__', link: '__link_2__', icon: '__icon_2__', className: '__className_2__' },
|
||||
],
|
||||
},
|
||||
uploader: {
|
||||
belowUploadArea: '__belowUploadArea__',
|
||||
postUploadMessage: '__postUploadMessage__',
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
home: {
|
||||
sections: {
|
||||
latest: { title: 'Latest T' },
|
||||
featured: { title: 'Featured T' },
|
||||
recommended: { title: 'Recommended T' },
|
||||
},
|
||||
},
|
||||
media: { categoriesWithTitle: true, htmlInDescription: true, hideViews: true, related: { initialSize: 5 } },
|
||||
profile: { htmlInDescription: true, includeHistory: true, includeLikedMedia: true },
|
||||
search: { advancedFilters: true },
|
||||
},
|
||||
features: {
|
||||
mediaItem: { hideAuthor: true, hideViews: false, hideDate: true },
|
||||
media: {
|
||||
actions: {
|
||||
like: true,
|
||||
dislike: true,
|
||||
report: true,
|
||||
comment: true,
|
||||
comment_mention: true,
|
||||
download: true,
|
||||
save: true,
|
||||
share: true,
|
||||
},
|
||||
shareOptions: ['embed', 'email'],
|
||||
},
|
||||
playlists: { mediaTypes: ['audio'] },
|
||||
sideBar: { hideHomeLink: false, hideTagsLink: true, hideCategoriesLink: false },
|
||||
embeddedVideo: { initialDimensions: { width: 640, height: 360 } },
|
||||
headerBar: { hideLogin: false, hideRegister: true },
|
||||
},
|
||||
user: {
|
||||
is: { anonymous: false, admin: true },
|
||||
name: ' John ',
|
||||
username: ' john ',
|
||||
thumbnail: ' /img/j.png ',
|
||||
can: {
|
||||
changePassword: true,
|
||||
deleteProfile: true,
|
||||
addComment: true,
|
||||
mentionComment: true,
|
||||
deleteComment: true,
|
||||
editMedia: true,
|
||||
deleteMedia: true,
|
||||
editSubtitle: true,
|
||||
manageMedia: true,
|
||||
manageUsers: true,
|
||||
manageComments: true,
|
||||
contactUser: true,
|
||||
canSeeMembersPage: true,
|
||||
usersNeedsToBeApproved: false,
|
||||
addMedia: true,
|
||||
editProfile: true,
|
||||
readComment: true,
|
||||
},
|
||||
pages: { about: '/u/john/about ', media: '/u/john ', playlists: '/u/john/playlists ' },
|
||||
},
|
||||
};
|
||||
|
||||
export const sampleMediaCMSConfig = {
|
||||
api: {
|
||||
archive: {
|
||||
tags: '',
|
||||
categories: '',
|
||||
},
|
||||
featured: '',
|
||||
manage: {
|
||||
media: '',
|
||||
users: '',
|
||||
comments: '',
|
||||
},
|
||||
media: '',
|
||||
playlists: '/v1/playlists',
|
||||
recommended: '',
|
||||
search: {
|
||||
query: '',
|
||||
titles: './search.html?titles=',
|
||||
tag: '',
|
||||
category: '',
|
||||
},
|
||||
user: {
|
||||
liked: '',
|
||||
history: '',
|
||||
playlists: '/playlists/?author=',
|
||||
},
|
||||
users: '/users',
|
||||
},
|
||||
contents: {
|
||||
header: {
|
||||
right: '',
|
||||
onLogoRight: '',
|
||||
},
|
||||
uploader: {
|
||||
belowUploadArea: '',
|
||||
postUploadMessage: '',
|
||||
},
|
||||
sidebar: {
|
||||
belowNavMenu: '__belowNavMenu__',
|
||||
belowThemeSwitcher: '__belowThemeSwitcher__',
|
||||
footer: '__footer__',
|
||||
mainMenuExtra: {
|
||||
items: [{ text: '__text_1__', link: '__link_1__', icon: '__icon_1__', className: '__className_1__' }],
|
||||
},
|
||||
navMenu: {
|
||||
items: [{ text: '__text_2__', link: '__link_2__', icon: '__icon_2__', className: '__className_2__' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
enabled: {
|
||||
taxonomies: sampleGlobalMediaCMS.site.taxonomies,
|
||||
pages: {
|
||||
featured: { enabled: true, title: 'Featured picks' },
|
||||
latest: { enabled: true, title: 'Recent uploads' },
|
||||
members: { enabled: true, title: 'People' },
|
||||
recommended: { enabled: true, title: 'You may like' },
|
||||
liked: { enabled: true, title: 'Favorites' },
|
||||
history: { enabled: true, title: 'Watched' },
|
||||
},
|
||||
},
|
||||
member: {
|
||||
name: null,
|
||||
username: 'john',
|
||||
thumbnail: null,
|
||||
is: {
|
||||
admin: false,
|
||||
anonymous: false,
|
||||
},
|
||||
can: {
|
||||
addComment: false,
|
||||
addMedia: false,
|
||||
canSeeMembersPage: false,
|
||||
changePassword: false,
|
||||
contactUser: false,
|
||||
deleteComment: false,
|
||||
deleteMedia: false,
|
||||
deleteProfile: false,
|
||||
dislikeMedia: false,
|
||||
downloadMedia: false,
|
||||
editMedia: false,
|
||||
editProfile: false,
|
||||
editSubtitle: false,
|
||||
likeMedia: false,
|
||||
login: false,
|
||||
manageComments: false,
|
||||
manageMedia: false,
|
||||
manageUsers: false,
|
||||
mentionComment: false,
|
||||
readComment: true,
|
||||
register: false,
|
||||
reportMedia: false,
|
||||
saveMedia: true,
|
||||
shareMedia: false,
|
||||
usersNeedsToBeApproved: false,
|
||||
},
|
||||
pages: {
|
||||
home: null,
|
||||
about: null,
|
||||
media: null,
|
||||
playlists: null,
|
||||
},
|
||||
},
|
||||
media: {
|
||||
item: {
|
||||
displayAuthor: false,
|
||||
displayViews: false,
|
||||
displayPublishDate: false,
|
||||
},
|
||||
share: {
|
||||
options: [],
|
||||
},
|
||||
},
|
||||
notifications: {
|
||||
messages: {
|
||||
addToLiked: '',
|
||||
removeFromLiked: '',
|
||||
addToDisliked: '',
|
||||
removeFromDisliked: '',
|
||||
},
|
||||
},
|
||||
options: {
|
||||
pages: {
|
||||
home: {
|
||||
sections: {
|
||||
latest: {
|
||||
title: '',
|
||||
},
|
||||
featured: {
|
||||
title: '',
|
||||
},
|
||||
recommended: {
|
||||
title: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
search: {
|
||||
advancedFilters: false,
|
||||
},
|
||||
media: {
|
||||
categoriesWithTitle: true,
|
||||
htmlInDescription: true,
|
||||
related: { initialSize: 5 },
|
||||
displayViews: true,
|
||||
},
|
||||
profile: {
|
||||
htmlInDescription: false,
|
||||
includeHistory: false,
|
||||
includeLikedMedia: false,
|
||||
},
|
||||
},
|
||||
embedded: {
|
||||
video: {
|
||||
dimensions: {
|
||||
width: 0,
|
||||
widthUnit: 'px',
|
||||
height: 0,
|
||||
heightUnit: 'px',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
playlists: {
|
||||
mediaTypes: [],
|
||||
},
|
||||
sidebar: {
|
||||
hideHomeLink: false,
|
||||
hideTagsLink: false,
|
||||
hideCategoriesLink: false,
|
||||
},
|
||||
site: {
|
||||
api: '',
|
||||
id: '',
|
||||
title: '',
|
||||
url: '',
|
||||
useRoundedCorners: false,
|
||||
version: '',
|
||||
},
|
||||
theme: {
|
||||
logo: {
|
||||
lightMode: { img: '/img/light.png', svg: '/img/light.svg' },
|
||||
darkMode: { img: '/img/dark.png', svg: '/img/dark.svg' },
|
||||
},
|
||||
mode: 'dark',
|
||||
switch: {
|
||||
enabled: true,
|
||||
position: 'sidebar',
|
||||
},
|
||||
},
|
||||
url: {
|
||||
admin: '',
|
||||
archive: {
|
||||
categories: '',
|
||||
tags: '',
|
||||
},
|
||||
changePassword: '',
|
||||
embed: '',
|
||||
error404: '',
|
||||
featured: '',
|
||||
home: '',
|
||||
latest: '',
|
||||
manage: {
|
||||
comments: '',
|
||||
media: '',
|
||||
users: '',
|
||||
},
|
||||
members: '',
|
||||
profile: {
|
||||
about: '',
|
||||
media: '',
|
||||
playlists: '',
|
||||
shared_by_me: '',
|
||||
shared_with_me: '',
|
||||
},
|
||||
recommended: '',
|
||||
register: '',
|
||||
search: {
|
||||
base: '',
|
||||
category: '',
|
||||
query: '',
|
||||
tag: '',
|
||||
},
|
||||
signin: '',
|
||||
signout: '',
|
||||
user: {
|
||||
addMedia: '',
|
||||
editChannel: '',
|
||||
editProfile: '',
|
||||
history: '',
|
||||
liked: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,145 +0,0 @@
|
||||
import * as MediaPageActions from '../../../src/static/js/utils/actions/MediaPageActions';
|
||||
import dispatcher from '../../../src/static/js/utils/dispatcher';
|
||||
|
||||
// Mock the dispatcher module used by MediaPageActions
|
||||
jest.mock('../../../src/static/js/utils/dispatcher', () => ({ dispatch: jest.fn() }));
|
||||
|
||||
describe('utils/actions', () => {
|
||||
describe('MediaPageActions', () => {
|
||||
const dispatch = dispatcher.dispatch;
|
||||
|
||||
beforeEach(() => {
|
||||
(dispatcher.dispatch as jest.Mock).mockClear();
|
||||
});
|
||||
|
||||
describe('loadMediaData', () => {
|
||||
it('Should dispatch LOAD_MEDIA_DATA action', () => {
|
||||
MediaPageActions.loadMediaData();
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'LOAD_MEDIA_DATA' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('likeMedia / dislikeMedia', () => {
|
||||
it('Should dispatch LIKE_MEDIA action', () => {
|
||||
MediaPageActions.likeMedia();
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'LIKE_MEDIA' });
|
||||
});
|
||||
|
||||
it('Should dispatch DISLIKE_MEDIA action', () => {
|
||||
MediaPageActions.dislikeMedia();
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'DISLIKE_MEDIA' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('reportMedia', () => {
|
||||
it('Should dispatch REPORT_MEDIA with empty string when description is undefined', () => {
|
||||
MediaPageActions.reportMedia();
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'REPORT_MEDIA', reportDescription: '' });
|
||||
});
|
||||
|
||||
// @todo: Revisit this behavior
|
||||
it('Should dispatch REPORT_MEDIA with stripped description when provided', () => {
|
||||
MediaPageActions.reportMedia(' some text ');
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'REPORT_MEDIA', reportDescription: 'sometext' });
|
||||
});
|
||||
|
||||
// @todo: Revisit this behavior
|
||||
it('Should remove all whitespace characters including newlines and tabs', () => {
|
||||
MediaPageActions.reportMedia('\n\t spaced\ntext \t');
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'REPORT_MEDIA', reportDescription: 'spacedtext' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('copyShareLink / copyEmbedMediaCode', () => {
|
||||
it('Should dispatch COPY_SHARE_LINK carrying the provided input element', () => {
|
||||
const inputElem = document.createElement('input');
|
||||
MediaPageActions.copyShareLink(inputElem);
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'COPY_SHARE_LINK', inputElement: inputElem });
|
||||
});
|
||||
|
||||
it('Should dispatch COPY_EMBED_MEDIA_CODE carrying the provided textarea element', () => {
|
||||
const textarea = document.createElement('textarea');
|
||||
MediaPageActions.copyEmbedMediaCode(textarea);
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'COPY_EMBED_MEDIA_CODE', inputElement: textarea });
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeMedia', () => {
|
||||
it('Should dispatch REMOVE_MEDIA action', () => {
|
||||
MediaPageActions.removeMedia();
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'REMOVE_MEDIA' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('comments', () => {
|
||||
it('Should dispatch SUBMIT_COMMENT with provided text', () => {
|
||||
const commentText = 'Nice one';
|
||||
MediaPageActions.submitComment(commentText);
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'SUBMIT_COMMENT', commentText });
|
||||
});
|
||||
|
||||
it('Should dispatch DELETE_COMMENT with provided comment id', () => {
|
||||
const commentId = 'c-123';
|
||||
MediaPageActions.deleteComment(commentId);
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'DELETE_COMMENT', commentId });
|
||||
});
|
||||
|
||||
// @todo: Revisit this behavior
|
||||
it('Should dispatch DELETE_COMMENT with numeric comment id', () => {
|
||||
const commentId = 42;
|
||||
MediaPageActions.deleteComment(commentId);
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'DELETE_COMMENT', commentId });
|
||||
});
|
||||
});
|
||||
|
||||
describe('playlists', () => {
|
||||
it('Should dispatch CREATE_PLAYLIST with provided data', () => {
|
||||
const payload = { title: 'My list', description: 'Desc' };
|
||||
MediaPageActions.createPlaylist(payload);
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'CREATE_PLAYLIST', playlist_data: payload });
|
||||
});
|
||||
|
||||
it('Should dispatch ADD_MEDIA_TO_PLAYLIST with ids', () => {
|
||||
const playlist_id = 'pl-1';
|
||||
const media_id = 'm-1';
|
||||
MediaPageActions.addMediaToPlaylist(playlist_id, media_id);
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'ADD_MEDIA_TO_PLAYLIST', playlist_id, media_id });
|
||||
});
|
||||
|
||||
it('Should dispatch REMOVE_MEDIA_FROM_PLAYLIST with ids', () => {
|
||||
const playlist_id = 'pl-1';
|
||||
const media_id = 'm-1';
|
||||
MediaPageActions.removeMediaFromPlaylist(playlist_id, media_id);
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'REMOVE_MEDIA_FROM_PLAYLIST', playlist_id, media_id });
|
||||
});
|
||||
|
||||
it('Should dispatch APPEND_NEW_PLAYLIST with provided playlist data', () => {
|
||||
const playlist_data = {
|
||||
playlist_id: 'pl-2',
|
||||
add_date: new Date('2020-01-01T00:00:00Z'),
|
||||
description: 'Cool',
|
||||
title: 'T',
|
||||
media_list: ['a', 'b'],
|
||||
};
|
||||
MediaPageActions.addNewPlaylist(playlist_data);
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'APPEND_NEW_PLAYLIST', playlist_data });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,55 +0,0 @@
|
||||
import * as PageActions from '../../../src/static/js/utils/actions/PageActions';
|
||||
import dispatcher from '../../../src/static/js/utils/dispatcher';
|
||||
|
||||
// Mock the dispatcher module used by PageActions
|
||||
jest.mock('../../../src/static/js/utils/dispatcher', () => ({ dispatch: jest.fn() }));
|
||||
|
||||
describe('utils/actions', () => {
|
||||
describe('PageActions', () => {
|
||||
const dispatch = dispatcher.dispatch;
|
||||
|
||||
beforeEach(() => {
|
||||
(dispatcher.dispatch as jest.Mock).mockClear();
|
||||
});
|
||||
|
||||
describe('initPage', () => {
|
||||
it('Should dispatch INIT_PAGE with provided page string', () => {
|
||||
PageActions.initPage('home');
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'INIT_PAGE', page: 'home' });
|
||||
});
|
||||
|
||||
// @todo: Revisit this behavior
|
||||
it('Should dispatch INIT_PAGE with empty string', () => {
|
||||
PageActions.initPage('');
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'INIT_PAGE', page: '' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleMediaAutoPlay', () => {
|
||||
it('Should dispatch TOGGLE_AUTO_PLAY action', () => {
|
||||
PageActions.toggleMediaAutoPlay();
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'TOGGLE_AUTO_PLAY' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('addNotification', () => {
|
||||
it('Should dispatch ADD_NOTIFICATION with message and id', () => {
|
||||
const notification = 'Saved!';
|
||||
const notificationId = 'notif-1';
|
||||
PageActions.addNotification(notification, notificationId);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'ADD_NOTIFICATION', notification, notificationId });
|
||||
});
|
||||
|
||||
// @todo: Revisit this behavior
|
||||
it('Should dispatch ADD_NOTIFICATION with empty notification message', () => {
|
||||
const notification = '';
|
||||
const notificationId = 'id-empty';
|
||||
PageActions.addNotification(notification, notificationId);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'ADD_NOTIFICATION', notification, notificationId });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,96 +0,0 @@
|
||||
import { PlaylistPageActions } from '../../../src/static/js/utils/actions';
|
||||
import dispatcher from '../../../src/static/js/utils/dispatcher';
|
||||
|
||||
// Mock the dispatcher module used by PlaylistPageActions
|
||||
jest.mock('../../../src/static/js/utils/dispatcher', () => ({ dispatch: jest.fn() }));
|
||||
|
||||
describe('utils/actions', () => {
|
||||
describe('PlaylistPageActions', () => {
|
||||
const dispatch = dispatcher.dispatch;
|
||||
|
||||
beforeEach(() => {
|
||||
(dispatcher.dispatch as jest.Mock).mockClear();
|
||||
});
|
||||
|
||||
describe('loadPlaylistData', () => {
|
||||
it('Should dispatch LOAD_PLAYLIST_DATA action', () => {
|
||||
PlaylistPageActions.loadPlaylistData();
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'LOAD_PLAYLIST_DATA' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleSave', () => {
|
||||
it('Should dispatch TOGGLE_SAVE action', () => {
|
||||
PlaylistPageActions.toggleSave();
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'TOGGLE_SAVE' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatePlaylist', () => {
|
||||
it('Should dispatch UPDATE_PLAYLIST with provided title and description', () => {
|
||||
const payload = { title: 'My Playlist', description: 'A description' };
|
||||
PlaylistPageActions.updatePlaylist(payload);
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'UPDATE_PLAYLIST', playlist_data: payload });
|
||||
});
|
||||
|
||||
// @todo: Revisit this behavior
|
||||
it('Should dispatch UPDATE_PLAYLIST with empty strings for title and description', () => {
|
||||
const payload = { title: '', description: '' };
|
||||
PlaylistPageActions.updatePlaylist(payload);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'UPDATE_PLAYLIST', playlist_data: payload });
|
||||
});
|
||||
});
|
||||
|
||||
describe('removePlaylist', () => {
|
||||
it('Should dispatch REMOVE_PLAYLIST action', () => {
|
||||
PlaylistPageActions.removePlaylist();
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'REMOVE_PLAYLIST' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('removedMediaFromPlaylist', () => {
|
||||
it('Should dispatch MEDIA_REMOVED_FROM_PLAYLIST with media and playlist ids', () => {
|
||||
PlaylistPageActions.removedMediaFromPlaylist('m1', 'p1');
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: 'MEDIA_REMOVED_FROM_PLAYLIST',
|
||||
media_id: 'm1',
|
||||
playlist_id: 'p1',
|
||||
});
|
||||
});
|
||||
|
||||
// @todo: Revisit this behavior
|
||||
it('Should dispatch MEDIA_REMOVED_FROM_PLAYLIST with empty ids as strings', () => {
|
||||
PlaylistPageActions.removedMediaFromPlaylist('', '');
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: 'MEDIA_REMOVED_FROM_PLAYLIST',
|
||||
media_id: '',
|
||||
playlist_id: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reorderedMediaInPlaylist', () => {
|
||||
it('Should dispatch PLAYLIST_MEDIA_REORDERED with provided array', () => {
|
||||
const items = [
|
||||
{ id: '1', url: '/1', thumbnail_url: '/t1' },
|
||||
{ id: '2', url: '/2', thumbnail_url: '/t2' },
|
||||
];
|
||||
PlaylistPageActions.reorderedMediaInPlaylist(items);
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'PLAYLIST_MEDIA_REORDERED', playlist_media: items });
|
||||
});
|
||||
|
||||
// @todo: Revisit this behavior
|
||||
it('Should dispatch PLAYLIST_MEDIA_REORDERED with empty array for playlist media', () => {
|
||||
const items: any[] = [];
|
||||
PlaylistPageActions.reorderedMediaInPlaylist(items);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'PLAYLIST_MEDIA_REORDERED', playlist_media: items });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user