mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-11-20 13:36:05 -05:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3997bfb1c | ||
|
|
4b0718c43f | ||
|
|
91d8179fa0 | ||
|
|
6532b19849 | ||
|
|
6ea8fd12a3 | ||
|
|
d971bb955f | ||
|
|
b52b008f89 | ||
|
|
30cf5d7176 | ||
|
|
6fd9a7d37f | ||
|
|
9c6d13559b | ||
|
|
8ec97a8219 | ||
|
|
de8f9ca718 | ||
|
|
a4bedca4db | ||
|
|
da565b3bfc | ||
|
|
239ff6cb60 | ||
|
|
da840b156d | ||
|
|
b08d493823 | ||
|
|
25eaa35758 | ||
|
|
cba2ed75ed |
6
.github/workflows/lint_test.yml
vendored
6
.github/workflows/lint_test.yml
vendored
@@ -8,8 +8,8 @@ jobs:
|
||||
pre-commit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: pre-commit/action@v2.0.0
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v3
|
||||
- uses: pre-commit/action@v3.0.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
17
.github/workflows/pre-commit.yml
vendored
Normal file
17
.github/workflows/pre-commit.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: pre-commit
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
pre-commit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v3
|
||||
- uses: pre-commit/action@v3.0.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -1,15 +1,15 @@
|
||||
repos:
|
||||
- repo: https://gitlab.com/pycqa/flake8
|
||||
rev: 3.7.9
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: 6.0.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.5.4
|
||||
rev: 5.12.0
|
||||
hooks:
|
||||
- id: isort
|
||||
args: ["--profile", "black"]
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.3.0
|
||||
rev: 23.1.0
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
# MediaCMS
|
||||
|
||||
[](https://lgtm.com/projects/g/mediacms-io/mediacms/context:python)
|
||||
[](https://lgtm.com/projects/g/mediacms-io/mediacms/context:javascript)
|
||||
<br/>
|
||||
[](https://raw.githubusercontent.com/mediacms-io/mediacms/main/LICENSE.txt)
|
||||
[](https://github.com/mediacms-io/mediacms/releases/)
|
||||
[](https://hub.docker.com/repository/docker/mediacms/mediacms/)
|
||||
[](https://hub.docker.com/r/mediacms/mediacms)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
@@ -5,7 +5,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
|
||||
@@ -6,7 +6,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
|
||||
@@ -18,7 +18,6 @@ class FastPaginationWithoutCount(PageNumberPagination):
|
||||
django_paginator_class = FasterDjangoPaginator
|
||||
|
||||
def get_paginated_response(self, data):
|
||||
|
||||
return Response(
|
||||
OrderedDict(
|
||||
[
|
||||
|
||||
@@ -7,6 +7,7 @@ DEBUG = False
|
||||
# PORTAL NAME, this is the portal title and
|
||||
# is also shown on several places as emails
|
||||
PORTAL_NAME = "MediaCMS"
|
||||
PORTAL_DESCRIPTION = ""
|
||||
LANGUAGE_CODE = "en-us"
|
||||
TIME_ZONE = "Europe/London"
|
||||
|
||||
@@ -86,6 +87,7 @@ MAX_MEDIA_PER_PLAYLIST = 70
|
||||
UPLOAD_MAX_SIZE = 800 * 1024 * 1000 * 5
|
||||
|
||||
MAX_CHARS_FOR_COMMENT = 10000 # so that it doesn't end up huge
|
||||
TIMESTAMP_IN_TIMEBAR = False # shows timestamped comments in the timebar for videos
|
||||
ALLOW_MENTION_IN_COMMENTS = False # allowing to mention other users with @ in the comments
|
||||
|
||||
# valid options: content, author
|
||||
@@ -479,4 +481,5 @@ if GLOBAL_LOGIN_REQUIRED:
|
||||
r'/accounts/login/$',
|
||||
r'/accounts/logout/$',
|
||||
r'/accounts/signup/$',
|
||||
r'/api/v[0-9]+/',
|
||||
]
|
||||
|
||||
@@ -16,6 +16,10 @@ server {
|
||||
|
||||
location /media {
|
||||
alias /home/mediacms.io/mediacms/media_files ;
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
|
||||
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
|
||||
}
|
||||
|
||||
location / {
|
||||
|
||||
@@ -18,6 +18,10 @@ services:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile-dev
|
||||
image: mediacms/mediacms-dev:latest
|
||||
environment:
|
||||
ADMIN_USER: 'admin'
|
||||
ADMIN_PASSWORD: 'admin'
|
||||
ADMIN_EMAIL: 'admin@localhost'
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
- [13. How To Add A Static Page To The Sidebar](#13-how-to-add-a-static-page-to-the-sidebar)
|
||||
- [14. Add Google Analytics](#14-add-google-analytics)
|
||||
- [15. Debugging email issues](#15-debugging-email-issues)
|
||||
- [16. Frequently Asked Questions](#16-frequently-asked-questions)
|
||||
|
||||
|
||||
## 1. Welcome
|
||||
@@ -459,7 +460,9 @@ to be written
|
||||
Through the admin section - http://your_installation/admin/
|
||||
|
||||
## 12. Video transcoding
|
||||
Add / remove resolutions and profiles through http://your_installation/admin/encodeprofile
|
||||
Add / remove resolutions and profiles by modifying the database table of `Encode profiles` through https://your_installation/admin/files/encodeprofile/
|
||||
|
||||
For example, the `Active` state of any profile can be toggled to enable or disable it.
|
||||
|
||||
## 13. How To Add A Static Page To The Sidebar
|
||||
|
||||
@@ -687,3 +690,41 @@ For example, while specifying wrong password for my Gmail account I get
|
||||
```
|
||||
SMTPAuthenticationError: (535, b'5.7.8 Username and Password not accepted. Learn more at\n5.7.8 https://support.google.com/mail/?p=BadCredentials d4sm12687785wrc.34 - gsmtp')
|
||||
```
|
||||
|
||||
## 16. Frequently Asked Questions
|
||||
Video is playing but preview thumbnails are not showing for large video files
|
||||
|
||||
Chances are that the sprites file was not created correctly.
|
||||
The output of files.tasks.produce_sprite_from_video() function in this case is something like this
|
||||
|
||||
```
|
||||
convert-im6.q16: width or height exceeds limit `/tmp/img001.jpg' @ error/cache.c/OpenPixelCache/3912.
|
||||
```
|
||||
|
||||
Solution: edit file `/etc/ImageMagick-6/policy.xml` and set bigger values for the lines that contain width and height. For example
|
||||
|
||||
```
|
||||
<policy domain="resource" name="height" value="16000KP"/>
|
||||
<policy domain="resource" name="width" value="16000KP"/>
|
||||
```
|
||||
|
||||
Newly added video files now will be able to produce the sprites file needed for thumbnail previews. To re-run that task on existing videos, enter the Django shell
|
||||
|
||||
|
||||
```
|
||||
root@8433f923ccf5:/home/mediacms.io/mediacms# source /home/mediacms.io/bin/activate
|
||||
root@8433f923ccf5:/home/mediacms.io/mediacms# python manage.py shell
|
||||
Python 3.8.14 (default, Sep 13 2022, 02:23:58)
|
||||
```
|
||||
|
||||
and run
|
||||
|
||||
```
|
||||
In [1]: from files.models import Media
|
||||
In [2]: from files.tasks import produce_sprite_from_video
|
||||
|
||||
In [3]: for media in Media.objects.filter(media_type='video', sprites=''):
|
||||
...: produce_sprite_from_video(media.friendly_token)
|
||||
```
|
||||
|
||||
this will re-create the sprites for videos that the task failed.
|
||||
|
||||
@@ -54,6 +54,13 @@ docker-compose -f docker-compose-dev.yaml build
|
||||
docker-compose -f docker-compose-dev.yaml up
|
||||
```
|
||||
|
||||
An `admin` user is created during the installation process. Its attributes are defined in `docker-compose-dev.yaml`:
|
||||
```
|
||||
ADMIN_USER: 'admin'
|
||||
ADMIN_PASSWORD: 'admin'
|
||||
ADMIN_EMAIL: 'admin@localhost'
|
||||
```
|
||||
|
||||
### Frontend application changes
|
||||
Eg change `frontend/src/static/js/pages/HomePage.tsx` , dev application refreshes in a number of seconds (hot reloading) and I see the changes, once I'm happy I can run
|
||||
|
||||
|
||||
BIN
docs/images/TimebarComments_Hit.png
Normal file
BIN
docs/images/TimebarComments_Hit.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 750 KiB |
BIN
docs/images/TimebarComments_Hover.png
Normal file
BIN
docs/images/TimebarComments_Hover.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
@@ -7,6 +7,7 @@
|
||||
- [Search media](#search-media)
|
||||
- [Using Timestamps for sharing](#using-timestamps-for-sharing)
|
||||
- [Mentionning users in comments](#Mentionning-users-in-comments)
|
||||
- [Show comments in the Timebar](#Show-comments-in-the-Timebar)
|
||||
- [Share media](#share-media)
|
||||
- [Embed media](#embed-media)
|
||||
- [Customize my profile options](#customize-my-profile-options)
|
||||
@@ -234,6 +235,17 @@ Comments send with mentions will contain a link to the user page, and can be set
|
||||
<img src="./images/Mention4.png"/>
|
||||
</p>
|
||||
|
||||
## Show comments in the Timebar
|
||||
|
||||
When enabled, comments including a timestamp will also be displayed in the current video Timebar as a little colorful dot. The comment can be previewed by hovering the dot (left image) and it will be displayed on top of the video when reaching the correct time (right image).
|
||||
|
||||
Only comments with correct timestamps formats (HH:MM:SS or MM:SS) will be picked up and appear in the Timebar.
|
||||
|
||||
<p align="left">
|
||||
<img src="./images/TimebarComments_Hover.png" height="180" alt="Comment preview on hover"/>
|
||||
<img src="./images/TimebarComments_Hit.png" height="180" alt="Comment shown when the timestamp is reached "/>
|
||||
</p>
|
||||
|
||||
## Search media
|
||||
How search can be used
|
||||
|
||||
|
||||
@@ -6,13 +6,15 @@ from .methods import is_mediacms_editor, is_mediacms_manager
|
||||
def stuff(request):
|
||||
"""Pass settings to the frontend"""
|
||||
ret = {}
|
||||
ret["FRONTEND_HOST"] = request.build_absolute_uri('/')
|
||||
ret["FRONTEND_HOST"] = request.build_absolute_uri('/').rstrip('/')
|
||||
ret["DEFAULT_THEME"] = settings.DEFAULT_THEME
|
||||
ret["PORTAL_NAME"] = settings.PORTAL_NAME
|
||||
ret["PORTAL_DESCRIPTION"] = settings.PORTAL_DESCRIPTION
|
||||
ret["LOAD_FROM_CDN"] = settings.LOAD_FROM_CDN
|
||||
ret["CAN_LOGIN"] = settings.LOGIN_ALLOWED
|
||||
ret["CAN_REGISTER"] = settings.REGISTER_ALLOWED
|
||||
ret["CAN_UPLOAD_MEDIA"] = settings.UPLOAD_MEDIA_ALLOWED
|
||||
ret["TIMESTAMP_IN_TIMEBAR"] = settings.TIMESTAMP_IN_TIMEBAR
|
||||
ret["CAN_MENTION_IN_COMMENTS"] = settings.ALLOW_MENTION_IN_COMMENTS
|
||||
ret["CAN_LIKE_MEDIA"] = settings.CAN_LIKE_MEDIA
|
||||
ret["CAN_DISLIKE_MEDIA"] = settings.CAN_DISLIKE_MEDIA
|
||||
|
||||
@@ -102,7 +102,7 @@ class SearchRSSFeed(Feed):
|
||||
description = "Latest Media RSS feed"
|
||||
|
||||
def link(self, obj):
|
||||
return f"/rss/search"
|
||||
return "/rss/search"
|
||||
|
||||
def get_object(self, request):
|
||||
category = request.GET.get("c", "")
|
||||
|
||||
@@ -305,7 +305,6 @@ def show_related_media_author(media, request, limit):
|
||||
|
||||
|
||||
def show_related_media_calculated(media, request, limit):
|
||||
|
||||
"""Return a list of related media based on ML recommendations
|
||||
A big todo!
|
||||
"""
|
||||
|
||||
@@ -10,7 +10,6 @@ import files.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
@@ -8,7 +8,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('files', '0002_auto_20201201_0712'),
|
||||
]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import glob
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -313,7 +314,6 @@ class Media(models.Model):
|
||||
self.__original_uploaded_poster = self.uploaded_poster
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
if not self.title:
|
||||
self.title = self.media_file.path.split("/")[-1]
|
||||
|
||||
@@ -371,7 +371,6 @@ class Media(models.Model):
|
||||
# will run only when a poster is uploaded for the first time
|
||||
if self.uploaded_poster and self.uploaded_poster != self.__original_uploaded_poster:
|
||||
with open(self.uploaded_poster.path, "rb") as f:
|
||||
|
||||
# set this otherwise gets to infinite loop
|
||||
self.__original_uploaded_poster = self.uploaded_poster
|
||||
|
||||
@@ -579,9 +578,7 @@ class Media(models.Model):
|
||||
|
||||
# attempt to break media file in chunks
|
||||
if self.duration > settings.CHUNKIZE_VIDEO_DURATION and chunkize:
|
||||
|
||||
for profile in profiles:
|
||||
|
||||
if profile.extension == "gif":
|
||||
profiles.remove(profile)
|
||||
encoding = Encoding(media=self, profile=profile)
|
||||
@@ -1405,6 +1402,13 @@ def media_file_delete(sender, instance, **kwargs):
|
||||
helpers.rm_dir(p)
|
||||
instance.user.update_user_media()
|
||||
|
||||
# remove extra zombie thumbnails
|
||||
if instance.thumbnail:
|
||||
thumbnails_path = os.path.dirname(instance.thumbnail.path)
|
||||
thumbnails = glob.glob(f'{thumbnails_path}/{instance.uid.hex}.*')
|
||||
for thumbnail in thumbnails:
|
||||
helpers.rm_file(thumbnail)
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Media.category.through)
|
||||
def media_m2m(sender, instance, **kwargs):
|
||||
|
||||
@@ -268,7 +268,6 @@ def encode_media(
|
||||
# return False
|
||||
|
||||
with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as temp_dir:
|
||||
|
||||
tf = create_temp_file(suffix=".{0}".format(profile.extension), dir=temp_dir)
|
||||
tfpass = create_temp_file(suffix=".{0}".format(profile.extension), dir=temp_dir)
|
||||
ffmpeg_commands = produce_ffmpeg_commands(
|
||||
|
||||
8
frontend/package-lock.json
generated
8
frontend/package-lock.json
generated
@@ -5955,7 +5955,13 @@
|
||||
}
|
||||
},
|
||||
"mediacms-vjs-plugin": {
|
||||
"version": "file:packages/vjs-plugin"
|
||||
"version": "file:packages/vjs-plugin",
|
||||
"requires": {
|
||||
"mediacms-vjs-plugin-font-icons": "file:packages/vjs-plugin-font-icons"
|
||||
}
|
||||
},
|
||||
"mediacms-vjs-plugin-font-icons": {
|
||||
"version": "file:packages/vjs-plugin-font-icons"
|
||||
},
|
||||
"memory-fs": {
|
||||
"version": "0.4.1",
|
||||
|
||||
@@ -8,6 +8,10 @@ import { PageActions, MediaPageActions } from '../../utils/actions/';
|
||||
import { LinksContext, MemberContext, SiteContext } from '../../utils/contexts/';
|
||||
import { PopupMain, UserThumbnail } from '../_shared';
|
||||
|
||||
import './videojs-markers.js';
|
||||
import './videojs.markers.css';
|
||||
import {enableMarkers, addMarker} from './videojs-markers_config.js'
|
||||
|
||||
import './Comments.scss';
|
||||
|
||||
const commentsText = {
|
||||
@@ -426,6 +430,12 @@ export default function CommentsList(props) {
|
||||
|
||||
function onCommentsLoad() {
|
||||
const retrievedComments = [...MediaPageStore.get('media-comments')];
|
||||
const video = videojs('vjs_video_3');
|
||||
|
||||
if (MediaCMS.features.media.actions.timestampTimebar)
|
||||
{
|
||||
enableMarkers(video);
|
||||
}
|
||||
|
||||
if (MediaCMS.features.media.actions.comment_mention === true)
|
||||
{
|
||||
@@ -433,14 +443,18 @@ export default function CommentsList(props) {
|
||||
comment.text = setMentions(comment.text);
|
||||
});
|
||||
}
|
||||
retrievedComments.forEach(comment => {
|
||||
comment.text = setTimestampAnchors(comment.text);
|
||||
});
|
||||
|
||||
displayCommentsRelatedAlert();
|
||||
setComments(retrievedComments);
|
||||
}
|
||||
|
||||
video.one('loadedmetadata', () => {
|
||||
retrievedComments.forEach(comment => {
|
||||
comment.text = setTimestampAnchorsAndMarkers(comment.text, video);
|
||||
});
|
||||
|
||||
displayCommentsRelatedAlert();
|
||||
setComments([...retrievedComments]);
|
||||
});
|
||||
setComments([...retrievedComments]);
|
||||
}
|
||||
|
||||
function setMentions(text)
|
||||
{
|
||||
let sanitizedComment = text.split('@(_').join("<a href=\"/user/");
|
||||
@@ -448,7 +462,7 @@ export default function CommentsList(props) {
|
||||
return sanitizedComment.split('_]').join("</a>");
|
||||
}
|
||||
|
||||
function setTimestampAnchors(text)
|
||||
function setTimestampAnchorsAndMarkers(text, videoPlayer)
|
||||
{
|
||||
function wrapTimestampWithAnchor(match, string)
|
||||
{
|
||||
@@ -460,6 +474,10 @@ export default function CommentsList(props) {
|
||||
s += m * parseInt(split.pop(), 10);
|
||||
m *= 60;
|
||||
}
|
||||
if (MediaCMS.features.media.actions.timestampTimebar)
|
||||
{
|
||||
addMarker(videoPlayer, s, text);
|
||||
}
|
||||
|
||||
searchParameters.set('t', s)
|
||||
const wrapped = "<a href=\"" + MediaPageStore.get('media-url').split('?')[0] + "?" + searchParameters + "\">" + match + "</a>";
|
||||
|
||||
525
frontend/src/static/js/components/comments/videojs-markers.js
Normal file
525
frontend/src/static/js/components/comments/videojs-markers.js
Normal file
@@ -0,0 +1,525 @@
|
||||
// based on https://github.com/spchuang/videojs-markers
|
||||
|
||||
(function (global, factory) {
|
||||
if (typeof define === "function" && define.amd) {
|
||||
define(['mediacms-player'], factory);
|
||||
} else if (typeof exports !== "undefined") {
|
||||
factory(require('mediacms-player'));
|
||||
} else {
|
||||
var mod = {
|
||||
exports: {}
|
||||
};
|
||||
global = window;
|
||||
factory(global.videojs);
|
||||
global.videojsMarkers = mod.exports;
|
||||
}
|
||||
})(this, function (_video) {
|
||||
/*! videojs-markers - v1.0.1 - 2018-02-03
|
||||
* Copyright (c) 2018 ; Licensed */
|
||||
'use strict';
|
||||
|
||||
var _video2 = _interopRequireDefault(_video);
|
||||
|
||||
function _interopRequireDefault(obj) {
|
||||
return obj && obj.__esModule ? obj : {
|
||||
default: obj
|
||||
};
|
||||
}
|
||||
|
||||
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
|
||||
return typeof obj;
|
||||
} : function (obj) {
|
||||
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
|
||||
};
|
||||
|
||||
// default setting
|
||||
var defaultSetting = {
|
||||
markerStyle: {
|
||||
'width': '7px',
|
||||
'border-radius': '30%',
|
||||
'background-color': 'red'
|
||||
},
|
||||
markerTip: {
|
||||
display: true,
|
||||
text: function text(marker) {
|
||||
return "Break: " + marker.text;
|
||||
},
|
||||
time: function time(marker) {
|
||||
return marker.time;
|
||||
}
|
||||
},
|
||||
breakOverlay: {
|
||||
display: true,
|
||||
displayTime: 3,
|
||||
text: function text(marker) {
|
||||
return "Break overlay: " + marker.overlayText;
|
||||
},
|
||||
style: {
|
||||
'width': '100%',
|
||||
'height': '20%',
|
||||
'background-color': 'rgba(0,0,0,0.7)',
|
||||
'color': 'white',
|
||||
'font-size': '17px'
|
||||
}
|
||||
},
|
||||
onMarkerClick: function onMarkerClick(marker) {},
|
||||
onMarkerReached: function onMarkerReached(marker, index) {},
|
||||
markers: []
|
||||
};
|
||||
|
||||
// create a non-colliding random number
|
||||
function generateUUID() {
|
||||
var d = new Date().getTime();
|
||||
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
var r = (d + Math.random() * 16) % 16 | 0;
|
||||
d = Math.floor(d / 16);
|
||||
return (c == 'x' ? r : r & 0x3 | 0x8).toString(16);
|
||||
});
|
||||
return uuid;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the size of an element and its position
|
||||
* a default Object with 0 on each of its properties
|
||||
* its return in case there's an error
|
||||
* @param {Element} element el to get the size and position
|
||||
* @return {DOMRect|Object} size and position of an element
|
||||
*/
|
||||
function getElementBounding(element) {
|
||||
var elementBounding;
|
||||
var defaultBoundingRect = {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
right: 0
|
||||
};
|
||||
|
||||
try {
|
||||
elementBounding = element.getBoundingClientRect();
|
||||
} catch (e) {
|
||||
elementBounding = defaultBoundingRect;
|
||||
}
|
||||
|
||||
return elementBounding;
|
||||
}
|
||||
|
||||
var NULL_INDEX = -1;
|
||||
|
||||
function registerVideoJsMarkersPlugin(options) {
|
||||
// copied from video.js/src/js/utils/merge-options.js since
|
||||
// videojs 4 doens't support it by defualt.
|
||||
if (!_video2.default.mergeOptions) {
|
||||
var isPlain = function isPlain(value) {
|
||||
return !!value && (typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object' && toString.call(value) === '[object Object]' && value.constructor === Object;
|
||||
};
|
||||
|
||||
var mergeOptions = function mergeOptions(source1, source2) {
|
||||
|
||||
var result = {};
|
||||
var sources = [source1, source2];
|
||||
sources.forEach(function (source) {
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
Object.keys(source).forEach(function (key) {
|
||||
var value = source[key];
|
||||
if (!isPlain(value)) {
|
||||
result[key] = value;
|
||||
return;
|
||||
}
|
||||
if (!isPlain(result[key])) {
|
||||
result[key] = {};
|
||||
}
|
||||
result[key] = mergeOptions(result[key], value);
|
||||
});
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
_video2.default.mergeOptions = mergeOptions;
|
||||
}
|
||||
|
||||
if (!_video2.default.createEl) {
|
||||
_video2.default.createEl = function (tagName, props, attrs) {
|
||||
var el = _video2.default.Player.prototype.createEl(tagName, props);
|
||||
if (!!attrs) {
|
||||
Object.keys(attrs).forEach(function (key) {
|
||||
el.setAttribute(key, attrs[key]);
|
||||
});
|
||||
}
|
||||
return el;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* register the markers plugin (dependent on jquery)
|
||||
*/
|
||||
var setting = _video2.default.mergeOptions(defaultSetting, options),
|
||||
markersMap = {},
|
||||
markersList = [],
|
||||
// list of markers sorted by time
|
||||
currentMarkerIndex = NULL_INDEX,
|
||||
player = this,
|
||||
markerTip = null,
|
||||
breakOverlay = null,
|
||||
overlayIndex = NULL_INDEX;
|
||||
|
||||
function sortMarkersList() {
|
||||
// sort the list by time in asc order
|
||||
markersList.sort(function (a, b) {
|
||||
return setting.markerTip.time(a) - setting.markerTip.time(b);
|
||||
});
|
||||
}
|
||||
|
||||
function addMarkers(newMarkers) {
|
||||
newMarkers.forEach(function (marker) {
|
||||
marker.key = generateUUID();
|
||||
|
||||
player.el().querySelector('.vjs-progress-holder').appendChild(createMarkerDiv(marker));
|
||||
|
||||
// store marker in an internal hash map
|
||||
markersMap[marker.key] = marker;
|
||||
markersList.push(marker);
|
||||
});
|
||||
|
||||
sortMarkersList();
|
||||
}
|
||||
|
||||
function getPosition(marker) {
|
||||
return setting.markerTip.time(marker) / player.duration() * 100;
|
||||
}
|
||||
|
||||
function setMarkderDivStyle(marker, markerDiv) {
|
||||
markerDiv.className = 'vjs-marker ' + (marker.class || "");
|
||||
|
||||
Object.keys(setting.markerStyle).forEach(function (key) {
|
||||
markerDiv.style[key] = setting.markerStyle[key];
|
||||
});
|
||||
|
||||
// hide out-of-bound markers
|
||||
var ratio = marker.time / player.duration();
|
||||
if (ratio < 0 || ratio > 1) {
|
||||
markerDiv.style.display = 'none';
|
||||
}
|
||||
|
||||
// set position
|
||||
markerDiv.style.left = getPosition(marker) + '%';
|
||||
if (marker.duration) {
|
||||
markerDiv.style.width = marker.duration / player.duration() * 100 + '%';
|
||||
markerDiv.style.marginLeft = '0px';
|
||||
} else {
|
||||
var markerDivBounding = getElementBounding(markerDiv);
|
||||
markerDiv.style.marginLeft = markerDivBounding.width / 2 + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
function createMarkerDiv(marker) {
|
||||
|
||||
var markerDiv = _video2.default.createEl('div', {}, {
|
||||
'data-marker-key': marker.key,
|
||||
'data-marker-time': setting.markerTip.time(marker)
|
||||
});
|
||||
|
||||
setMarkderDivStyle(marker, markerDiv);
|
||||
|
||||
// bind click event to seek to marker time
|
||||
markerDiv.addEventListener('click', function (e) {
|
||||
var preventDefault = false;
|
||||
if (typeof setting.onMarkerClick === "function") {
|
||||
// if return false, prevent default behavior
|
||||
preventDefault = setting.onMarkerClick(marker) === false;
|
||||
}
|
||||
|
||||
if (!preventDefault) {
|
||||
var key = this.getAttribute('data-marker-key');
|
||||
player.currentTime(setting.markerTip.time(markersMap[key]));
|
||||
}
|
||||
});
|
||||
|
||||
if (setting.markerTip.display) {
|
||||
registerMarkerTipHandler(markerDiv);
|
||||
}
|
||||
|
||||
return markerDiv;
|
||||
}
|
||||
|
||||
function updateMarkers(force) {
|
||||
// update UI for markers whose time changed
|
||||
markersList.forEach(function (marker) {
|
||||
var markerDiv = player.el().querySelector(".vjs-marker[data-marker-key='" + marker.key + "']");
|
||||
var markerTime = setting.markerTip.time(marker);
|
||||
|
||||
if (force || markerDiv.getAttribute('data-marker-time') !== markerTime) {
|
||||
setMarkderDivStyle(marker, markerDiv);
|
||||
markerDiv.setAttribute('data-marker-time', markerTime);
|
||||
}
|
||||
});
|
||||
sortMarkersList();
|
||||
}
|
||||
|
||||
function removeMarkers(indexArray) {
|
||||
// reset overlay
|
||||
if (!!breakOverlay) {
|
||||
overlayIndex = NULL_INDEX;
|
||||
breakOverlay.style.visibility = "hidden";
|
||||
}
|
||||
currentMarkerIndex = NULL_INDEX;
|
||||
|
||||
var deleteIndexList = [];
|
||||
indexArray.forEach(function (index) {
|
||||
var marker = markersList[index];
|
||||
if (marker) {
|
||||
// delete from memory
|
||||
delete markersMap[marker.key];
|
||||
deleteIndexList.push(index);
|
||||
|
||||
// delete from dom
|
||||
var el = player.el().querySelector(".vjs-marker[data-marker-key='" + marker.key + "']");
|
||||
el && el.parentNode.removeChild(el);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
// clean up markers array
|
||||
deleteIndexList.reverse();
|
||||
deleteIndexList.forEach(function (deleteIndex) {
|
||||
markersList.splice(deleteIndex, 1);
|
||||
});
|
||||
} catch (error) {
|
||||
//Splice is the most likely culprit
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
// sort again
|
||||
sortMarkersList();
|
||||
}
|
||||
|
||||
// attach hover event handler
|
||||
function registerMarkerTipHandler(markerDiv) {
|
||||
markerDiv.addEventListener('mouseover', function () {
|
||||
var marker = markersMap[markerDiv.getAttribute('data-marker-key')];
|
||||
if (!!markerTip) {
|
||||
markerTip.querySelector('.vjs-tip-inner').innerText = setting.markerTip.text(marker);
|
||||
// margin-left needs to minus the padding length to align correctly with the marker
|
||||
markerTip.style.left = getPosition(marker) + '%';
|
||||
var markerTipBounding = getElementBounding(markerTip);
|
||||
var markerDivBounding = getElementBounding(markerDiv);
|
||||
markerTip.style.marginLeft = -parseFloat(markerTipBounding.width / 2) + parseFloat(markerDivBounding.width / 4) + 'px';
|
||||
markerTip.style.visibility = 'visible';
|
||||
}
|
||||
});
|
||||
|
||||
markerDiv.addEventListener('mouseout', function () {
|
||||
if (!!markerTip) {
|
||||
markerTip.style.visibility = "hidden";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initializeMarkerTip() {
|
||||
markerTip = _video2.default.createEl('div', {
|
||||
className: 'vjs-tip',
|
||||
innerHTML: "<div class='vjs-tip-arrow'></div><div class='vjs-tip-inner'></div>"
|
||||
});
|
||||
player.el().querySelector('.vjs-progress-holder').appendChild(markerTip);
|
||||
}
|
||||
|
||||
// show or hide break overlays
|
||||
function updateBreakOverlay() {
|
||||
if (!setting.breakOverlay.display || currentMarkerIndex < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var currentTime = player.currentTime();
|
||||
var marker = markersList[currentMarkerIndex];
|
||||
var markerTime = setting.markerTip.time(marker);
|
||||
|
||||
if (currentTime >= markerTime && currentTime <= markerTime + setting.breakOverlay.displayTime) {
|
||||
if (overlayIndex !== currentMarkerIndex) {
|
||||
overlayIndex = currentMarkerIndex;
|
||||
if (breakOverlay) {
|
||||
breakOverlay.querySelector('.vjs-break-overlay-text').innerHTML = setting.breakOverlay.text(marker);
|
||||
}
|
||||
}
|
||||
|
||||
if (breakOverlay) {
|
||||
breakOverlay.style.visibility = "visible";
|
||||
}
|
||||
} else {
|
||||
overlayIndex = NULL_INDEX;
|
||||
if (breakOverlay) {
|
||||
breakOverlay.style.visibility = "hidden";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// problem when the next marker is within the overlay display time from the previous marker
|
||||
function initializeOverlay() {
|
||||
breakOverlay = _video2.default.createEl('div', {
|
||||
className: 'vjs-break-overlay',
|
||||
innerHTML: "<div class='vjs-break-overlay-text'></div>"
|
||||
});
|
||||
Object.keys(setting.breakOverlay.style).forEach(function (key) {
|
||||
if (breakOverlay) {
|
||||
breakOverlay.style[key] = setting.breakOverlay.style[key];
|
||||
}
|
||||
});
|
||||
player.el().appendChild(breakOverlay);
|
||||
overlayIndex = NULL_INDEX;
|
||||
}
|
||||
|
||||
function onTimeUpdate() {
|
||||
onUpdateMarker();
|
||||
updateBreakOverlay();
|
||||
options.onTimeUpdateAfterMarkerUpdate && options.onTimeUpdateAfterMarkerUpdate();
|
||||
}
|
||||
|
||||
function onUpdateMarker() {
|
||||
/*
|
||||
check marker reached in between markers
|
||||
the logic here is that it triggers a new marker reached event only if the player
|
||||
enters a new marker range (e.g. from marker 1 to marker 2). Thus, if player is on marker 1 and user clicked on marker 1 again, no new reached event is triggered)
|
||||
*/
|
||||
if (!markersList.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var getNextMarkerTime = function getNextMarkerTime(index) {
|
||||
if (index < markersList.length - 1) {
|
||||
return setting.markerTip.time(markersList[index + 1]);
|
||||
}
|
||||
// next marker time of last marker would be end of video time
|
||||
return player.duration();
|
||||
};
|
||||
var currentTime = player.currentTime();
|
||||
var newMarkerIndex = NULL_INDEX;
|
||||
|
||||
if (currentMarkerIndex !== NULL_INDEX) {
|
||||
// check if staying at same marker
|
||||
var nextMarkerTime = getNextMarkerTime(currentMarkerIndex);
|
||||
if (currentTime >= setting.markerTip.time(markersList[currentMarkerIndex]) && currentTime < nextMarkerTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check for ending (at the end current time equals player duration)
|
||||
if (currentMarkerIndex === markersList.length - 1 && currentTime === player.duration()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// check first marker, no marker is selected
|
||||
if (currentTime < setting.markerTip.time(markersList[0])) {
|
||||
newMarkerIndex = NULL_INDEX;
|
||||
} else {
|
||||
// look for new index
|
||||
for (var i = 0; i < markersList.length; i++) {
|
||||
nextMarkerTime = getNextMarkerTime(i);
|
||||
if (currentTime >= setting.markerTip.time(markersList[i]) && currentTime < nextMarkerTime) {
|
||||
newMarkerIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set new marker index
|
||||
if (newMarkerIndex !== currentMarkerIndex) {
|
||||
// trigger event if index is not null
|
||||
if (newMarkerIndex !== NULL_INDEX && options.onMarkerReached) {
|
||||
options.onMarkerReached(markersList[newMarkerIndex], newMarkerIndex);
|
||||
}
|
||||
currentMarkerIndex = newMarkerIndex;
|
||||
}
|
||||
}
|
||||
|
||||
// setup the whole thing
|
||||
function initialize() {
|
||||
if (setting.markerTip.display) {
|
||||
initializeMarkerTip();
|
||||
}
|
||||
|
||||
// remove existing markers if already initialized
|
||||
player.markers.removeAll();
|
||||
addMarkers(setting.markers);
|
||||
|
||||
if (setting.breakOverlay.display) {
|
||||
initializeOverlay();
|
||||
}
|
||||
onTimeUpdate();
|
||||
player.on("timeupdate", onTimeUpdate);
|
||||
player.off("loadedmetadata");
|
||||
}
|
||||
|
||||
// setup the plugin after we loaded video's meta data
|
||||
player.on("loadedmetadata", function () {
|
||||
initialize();
|
||||
});
|
||||
|
||||
// exposed plugin API
|
||||
player.markers = {
|
||||
getMarkers: function getMarkers() {
|
||||
return markersList;
|
||||
},
|
||||
next: function next() {
|
||||
// go to the next marker from current timestamp
|
||||
var currentTime = player.currentTime();
|
||||
for (var i = 0; i < markersList.length; i++) {
|
||||
var markerTime = setting.markerTip.time(markersList[i]);
|
||||
if (markerTime > currentTime) {
|
||||
player.currentTime(markerTime);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
prev: function prev() {
|
||||
// go to previous marker
|
||||
var currentTime = player.currentTime();
|
||||
for (var i = markersList.length - 1; i >= 0; i--) {
|
||||
var markerTime = setting.markerTip.time(markersList[i]);
|
||||
// add a threshold
|
||||
if (markerTime + 0.5 < currentTime) {
|
||||
player.currentTime(markerTime);
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
add: function add(newMarkers) {
|
||||
// add new markers given an array of index
|
||||
addMarkers(newMarkers);
|
||||
},
|
||||
remove: function remove(indexArray) {
|
||||
// remove markers given an array of index
|
||||
removeMarkers(indexArray);
|
||||
},
|
||||
removeAll: function removeAll() {
|
||||
var indexArray = [];
|
||||
for (var i = 0; i < markersList.length; i++) {
|
||||
indexArray.push(i);
|
||||
}
|
||||
removeMarkers(indexArray);
|
||||
},
|
||||
// force - force all markers to be updated, regardless of if they have changed or not.
|
||||
updateTime: function updateTime(force) {
|
||||
// notify the plugin to update the UI for changes in marker times
|
||||
updateMarkers(force);
|
||||
},
|
||||
reset: function reset(newMarkers) {
|
||||
// remove all the existing markers and add new ones
|
||||
player.markers.removeAll();
|
||||
addMarkers(newMarkers);
|
||||
},
|
||||
destroy: function destroy() {
|
||||
// unregister the plugins and clean up even handlers
|
||||
player.markers.removeAll();
|
||||
breakOverlay && breakOverlay.remove();
|
||||
markerTip && markerTip.remove();
|
||||
player.off("timeupdate", updateBreakOverlay);
|
||||
delete player.markers;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
_video2.default.plugin('markers', registerVideoJsMarkersPlugin);
|
||||
});
|
||||
//# sourceMappingURL=videojs-markers.js.map
|
||||
@@ -0,0 +1,44 @@
|
||||
//markers on the timebar
|
||||
const markerStyle = {
|
||||
width: "15px",
|
||||
"background-color": "#DD7373"
|
||||
};
|
||||
|
||||
//the comment overlay
|
||||
const overlayStyle = {
|
||||
width: "100%",
|
||||
height: "auto",
|
||||
};
|
||||
|
||||
function enableMarkers(video) {
|
||||
if (!(typeof video.markers === 'object')) {
|
||||
video.markers({
|
||||
markerStyle: markerStyle,
|
||||
markerTip: {
|
||||
display: true,
|
||||
text: function (marker) {
|
||||
return marker.text;
|
||||
}
|
||||
},
|
||||
breakOverlay: {
|
||||
display: true,
|
||||
displayTime: 20,
|
||||
text: function (marker) {
|
||||
return marker.text;
|
||||
},
|
||||
style : overlayStyle
|
||||
},
|
||||
markers: []
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function addMarker(videoPlayer, time, text)
|
||||
{
|
||||
videoPlayer.markers.add([{
|
||||
time: time,
|
||||
text: text
|
||||
}]);
|
||||
}
|
||||
|
||||
export {enableMarkers, addMarker};
|
||||
@@ -0,0 +1,59 @@
|
||||
.vjs-marker {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0em;
|
||||
opacity: 1;
|
||||
height: 100%;
|
||||
transition: opacity .2s ease;
|
||||
-webkit-transition: opacity .2s ease;
|
||||
-moz-transition: opacity .2s ease;
|
||||
z-index: 100;
|
||||
}
|
||||
.vjs-marker:hover {
|
||||
cursor: pointer;
|
||||
-webkit-transform: scale(1.3, 1.3);
|
||||
-moz-transform: scale(1.3, 1.3);
|
||||
-o-transform: scale(1.3, 1.3);
|
||||
-ms-transform: scale(1.3, 1.3);
|
||||
transform: scale(1.3, 1.3);
|
||||
}
|
||||
.vjs-tip {
|
||||
visibility: hidden;
|
||||
display: block;
|
||||
opacity: 0.8;
|
||||
padding: 5px;
|
||||
font-size: 10px;
|
||||
position: absolute;
|
||||
bottom: 14px;
|
||||
z-index: 100000;
|
||||
}
|
||||
.vjs-tip .vjs-tip-arrow {
|
||||
background: url(data:image/gif;base64,R0lGODlhCQAJAIABAAAAAAAAACH5BAEAAAEALAAAAAAJAAkAAAIRjAOnwIrcDJxvwkplPtchVQAAOw==) no-repeat top left;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
margin-left: -4px;
|
||||
background-position: bottom left;
|
||||
position: absolute;
|
||||
width: 9px;
|
||||
height: 5px;
|
||||
}
|
||||
.vjs-tip .vjs-tip-inner {
|
||||
border-radius: 3px;
|
||||
-moz-border-radius: 3px;
|
||||
-webkit-border-radius: 3px;
|
||||
padding: 5px 8px 4px 8px;
|
||||
background-color: black;
|
||||
color: white;
|
||||
max-width: 200px;
|
||||
text-align: center;
|
||||
}
|
||||
.vjs-break-overlay {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
z-index: 100000;
|
||||
top: 0;
|
||||
}
|
||||
.vjs-break-overlay .vjs-break-overlay-text {
|
||||
padding: 9px;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -192,7 +192,7 @@ export function VideoPlayer(props) {
|
||||
document.addEventListener('visibilitychange', initPlayer);
|
||||
}
|
||||
|
||||
player.player.one('loadedmetadata', () => {
|
||||
player && player.player.one('loadedmetadata', () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const paramT = Number(urlParams.get('t'));
|
||||
const timestamp = !isNaN(paramT) ? paramT : 0;
|
||||
|
||||
@@ -1,33 +1,21 @@
|
||||
Django==3.1.12
|
||||
djangorestframework==3.12.2
|
||||
django-allauth==0.44.0
|
||||
|
||||
psycopg2-binary==2.8.6
|
||||
|
||||
uwsgi==2.0.19.1
|
||||
|
||||
django-redis==4.12.1
|
||||
celery==4.4.7
|
||||
|
||||
drf-yasg==1.20.0
|
||||
|
||||
Pillow==8.2.0
|
||||
django-imagekit
|
||||
|
||||
markdown
|
||||
django-filter
|
||||
|
||||
filetype
|
||||
django-mptt
|
||||
|
||||
django-crispy-forms
|
||||
|
||||
django-imagekit==4.1.0
|
||||
markdown==3.3.6
|
||||
django-filter==21.1
|
||||
filetype==1.0.10
|
||||
django-mptt==0.13.4
|
||||
django-crispy-forms==1.13.0
|
||||
requests==2.25.0
|
||||
|
||||
django-celery-email
|
||||
m3u8
|
||||
|
||||
django-ckeditor
|
||||
django-celery-email==3.0.0
|
||||
m3u8==1.0.0
|
||||
django-ckeditor==6.2.0
|
||||
django-debug-toolbar==3.2.4
|
||||
|
||||
django-login-required-middleware==0.6.1
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,6 +5,8 @@
|
||||
|
||||
<link rel="canonical" href="{{FRONTEND_HOST}}{{media_object.get_absolute_url}}">
|
||||
|
||||
<meta name="description" content="{{PORTAL_DESCRIPTION}}">
|
||||
|
||||
<meta property="og:title" content="{{PORTAL_NAME}}">
|
||||
<meta property="og:url" content="{{FRONTEND_HOST}}">
|
||||
<meta property="og:type" content="website">
|
||||
|
||||
@@ -22,6 +22,7 @@ MediaCMS.features = {
|
||||
dislike: {% if CAN_DISLIKE_MEDIA %}true{% else %}false{% endif %},
|
||||
download: true,
|
||||
comment: true,
|
||||
timestampTimebar: {% if TIMESTAMP_IN_TIMEBAR %}true{% else %}false{% endif %},
|
||||
comment_mention: {% if CAN_MENTION_IN_COMMENTS %}true{% else %}false{% endif %},
|
||||
save: true,
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@ MediaCMS.site = {
|
||||
id: 'mediacms',
|
||||
title: "{{PORTAL_NAME}}",
|
||||
url: '{{FRONTEND_HOST}}',
|
||||
api: '{{FRONTEND_HOST}}api/v1',
|
||||
api: '{{FRONTEND_HOST}}/api/v1',
|
||||
theme: {
|
||||
mode: '{{DEFAULT_THEME}}',
|
||||
switch: {
|
||||
|
||||
@@ -10,7 +10,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
|
||||
@@ -176,7 +176,6 @@ Sender email: %s\n
|
||||
|
||||
|
||||
class UserList(APIView):
|
||||
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
||||
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.6
|
||||
2.0
|
||||
|
||||
Reference in New Issue
Block a user