Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ce8dba163 | ||
|
|
48838ac406 | ||
|
|
062e4be6c2 | ||
|
|
fb00f94bfa | ||
|
|
755df50c41 | ||
|
|
ba94989e6a | ||
|
|
86cc0442d8 | ||
|
|
2dde4257f7 | ||
|
|
3afff52ebf | ||
|
|
c27e3caff6 | ||
|
|
16e2c32d17 | ||
|
|
f4f6fa5962 | ||
|
|
b9e35c66a3 | ||
|
|
360a647eb5 | ||
|
|
7237040777 |
4
.github/workflows/python.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
- name: Run Django Tests
|
||||
run: docker-compose -f docker-compose-dev.yaml exec -T web pytest
|
||||
run: docker-compose -f docker-compose-dev.yaml exec --env TESTING=True -T web pytest
|
||||
|
||||
- name: Tear down the Stack
|
||||
run: docker-compose -f docker-compose-dev.yaml down
|
||||
run: docker-compose -f docker-compose-dev.yaml down
|
||||
|
||||
11
Dockerfile
@@ -28,9 +28,6 @@ FROM python:3.8-slim-buster as runtime-image
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV ADMIN_USER='admin'
|
||||
ENV ADMIN_PASSWORD='mediacms'
|
||||
ENV ADMIN_EMAIL='admin@localhost'
|
||||
|
||||
# See: https://github.com/celery/celery/issues/6285#issuecomment-715316219
|
||||
ENV CELERY_APP='cms'
|
||||
@@ -50,11 +47,17 @@ ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
COPY --chown=www-data:www-data --from=compile-image /home/mediacms.io /home/mediacms.io
|
||||
|
||||
RUN apt-get update -y && apt-get -y upgrade && apt-get install --no-install-recommends \
|
||||
supervisor nginx ffmpeg imagemagick procps -y && \
|
||||
supervisor nginx imagemagick procps wget xz-utils -y && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
apt-get purge --auto-remove && \
|
||||
apt-get clean
|
||||
|
||||
RUN wget -q https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz && \
|
||||
mkdir -p ffmpeg-tmp && \
|
||||
tar -xf ffmpeg-release-amd64-static.tar.xz --strip-components 1 -C ffmpeg-tmp && \
|
||||
cp -v ffmpeg-tmp/ffmpeg ffmpeg-tmp/ffprobe ffmpeg-tmp/qt-faststart /usr/local/bin && \
|
||||
rm -rf ffmpeg-tmp ffmpeg-release-amd64-static.tar.xz
|
||||
|
||||
WORKDIR /home/mediacms.io/mediacms
|
||||
|
||||
EXPOSE 9000 80
|
||||
|
||||
27
README.md
@@ -4,7 +4,7 @@
|
||||
[](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://github.com/mediacms-io/mediacms/releases/)
|
||||
[](https://hub.docker.com/repository/docker/mediacms/mediacms/)
|
||||
|
||||
|
||||
@@ -83,16 +83,27 @@ For a small to medium installation, with a few hours of video uploaded daily, an
|
||||
In terms of disk space, think of what the needs will be. A general rule is to multiply by three the size of the expected uploaded videos (since the system keeps original versions, encoded versions plus HLS), so if you receive 1G of videos daily and maintain all of them, you should consider a 1T disk across a year (1G * 3 * 365).
|
||||
|
||||
|
||||
## Releases
|
||||
|
||||
Visit [Releases Page](https://github.com/mediacms-io/mediacms/releases) for detailed Changelog
|
||||
|
||||
|
||||
## Installation / Maintanance
|
||||
|
||||
There are two ways to run MediaCMS, through Docker Compose and through installing it on a server via an automation script that installs and configures all needed services. Find the related pages:
|
||||
|
||||
* [Single Server](docs/Single_Server.md) page
|
||||
* [Docker Compose](docs/Docker_Compose.md) page
|
||||
* [Single Server](docs/admins_docs.md#2-server-installation) page
|
||||
* [Docker Compose](docs/admins_docs.md#3-docker-installation) page
|
||||
|
||||
## Configuration
|
||||
|
||||
Several options are available on cms/settings.py, most of the things that are allowed or should be disallowed are described there. It is advisable to override any of them by adding it to cms/local_settings.py. All configuration options will be documented gradually on the [Configuration](docs/Configuration.md) page.
|
||||
Visit [Configuration](docs/admins_docs.md#5-configuration) page.
|
||||
|
||||
|
||||
## Documentation
|
||||
* [Users documentation](docs/user_docs.md) page
|
||||
* [Administrators documentation](docs/admins_docs.md) page
|
||||
* [Developers documentation](docs/developers_docs.md) page
|
||||
|
||||
|
||||
## Technology
|
||||
@@ -117,12 +128,6 @@ If you like the project, here's a few things you can do
|
||||
- Star the project
|
||||
- Add functionality, work on a PR, fix an issue!
|
||||
|
||||
## Developers info
|
||||
|
||||
- API documentation through Swagger is available under /swagger URL of your installation -example https://demo.mediacms.io/swagger/
|
||||
- We're working on proper documentation for users, managers and developers, until then checkout what's available on the docs/ folder of this repository
|
||||
- Before you send a PR, make sure your code is properly formatted. For that, use `pre-commit install` to install a pre-commit hook and run `pre-commit run --all` and fix everything before you commit. This pre-commit will check for your code lint everytime you commit a code.
|
||||
- Checkout the [Code of conduct page](CODE_OF_CONDUCT.md) if you want to contribute to this repository
|
||||
|
||||
## Contact
|
||||
info@mediacms.io
|
||||
info@mediacms.io
|
||||
|
||||
@@ -146,10 +146,13 @@ STATIC_ROOT = BASE_DIR + "/static/"
|
||||
# where uploaded + encoded media are stored
|
||||
MEDIA_ROOT = BASE_DIR + "/media_files/"
|
||||
|
||||
MEDIA_UPLOAD_DIR = os.path.join(MEDIA_ROOT, "original/")
|
||||
MEDIA_ENCODING_DIR = os.path.join(MEDIA_ROOT, "encoded/")
|
||||
THUMBNAIL_UPLOAD_DIR = os.path.join(MEDIA_UPLOAD_DIR, "thumbnails/")
|
||||
SUBTITLES_UPLOAD_DIR = os.path.join(MEDIA_UPLOAD_DIR, "subtitles/")
|
||||
# these used to be os.path.join(MEDIA_ROOT, "folder/") but update to
|
||||
# Django 3.1.9 requires not absolute paths to be utilized...
|
||||
|
||||
MEDIA_UPLOAD_DIR = "original/"
|
||||
MEDIA_ENCODING_DIR = "encoded/"
|
||||
THUMBNAIL_UPLOAD_DIR = f"{MEDIA_UPLOAD_DIR}/thumbnails/"
|
||||
SUBTITLES_UPLOAD_DIR = f"{MEDIA_UPLOAD_DIR}/subtitles/"
|
||||
HLS_DIR = os.path.join(MEDIA_ROOT, "hls/")
|
||||
|
||||
FFMPEG_COMMAND = "ffmpeg" # this is the path
|
||||
@@ -441,6 +444,13 @@ LOCAL_INSTALL = False
|
||||
# it is placed here so it can be overrided on local_settings.py
|
||||
GLOBAL_LOGIN_REQUIRED = False
|
||||
|
||||
# TODO: separate settings on production/development more properly, for now
|
||||
# this should be ok
|
||||
CELERY_TASK_ALWAYS_EAGER = False
|
||||
if os.environ.get("TESTING"):
|
||||
CELERY_TASK_ALWAYS_EAGER = True
|
||||
|
||||
|
||||
try:
|
||||
# keep a local_settings.py file for local overrides
|
||||
from .local_settings import * # noqa
|
||||
|
||||
14
cms/urls.py
@@ -1,7 +1,7 @@
|
||||
import debug_toolbar
|
||||
from django.conf.urls import include, url
|
||||
from django.conf.urls import include, re_path
|
||||
from django.contrib import admin
|
||||
from django.urls import path, re_path
|
||||
from django.urls import path
|
||||
from django.views.generic.base import TemplateView
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.views import get_schema_view
|
||||
@@ -15,15 +15,15 @@ schema_view = get_schema_view(
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^__debug__/", include(debug_toolbar.urls)),
|
||||
re_path(r"^__debug__/", include(debug_toolbar.urls)),
|
||||
path(
|
||||
"robots.txt",
|
||||
TemplateView.as_view(template_name="robots.txt", content_type="text/plain"),
|
||||
),
|
||||
url(r"^", include("files.urls")),
|
||||
url(r"^", include("users.urls")),
|
||||
url(r"^accounts/", include("allauth.urls")),
|
||||
url(r"^api-auth/", include("rest_framework.urls")),
|
||||
re_path(r"^", include("files.urls")),
|
||||
re_path(r"^", include("users.urls")),
|
||||
re_path(r"^accounts/", include("allauth.urls")),
|
||||
re_path(r"^api-auth/", include("rest_framework.urls")),
|
||||
path("admin/", admin.site.urls),
|
||||
re_path(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
|
||||
re_path(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
|
||||
|
||||
@@ -4,20 +4,29 @@ RANDOM_ADMIN_PASS=`python -c "import secrets;chars = 'abcdefghijklmnopqrstuvwxyz
|
||||
ADMIN_PASSWORD=${ADMIN_PASSWORD:-$RANDOM_ADMIN_PASS}
|
||||
|
||||
if [ X"$ENABLE_MIGRATIONS" = X"yes" ]; then
|
||||
echo "Running migrations service"
|
||||
python manage.py migrate
|
||||
python manage.py loaddata fixtures/encoding_profiles.json
|
||||
python manage.py loaddata fixtures/categories.json
|
||||
EXISTING_INSTALLATION=`echo "from users.models import User; print(User.objects.exists())" |python manage.py shell`
|
||||
if [ "$EXISTING_INSTALLATION" = "True" ]; then
|
||||
echo "Loaddata has already run"
|
||||
else
|
||||
echo "Running loaddata and creating admin user"
|
||||
python manage.py loaddata fixtures/encoding_profiles.json
|
||||
python manage.py loaddata fixtures/categories.json
|
||||
|
||||
# post_save, needs redis to succeed (ie. migrate depends on redis)
|
||||
DJANGO_SUPERUSER_PASSWORD=$ADMIN_PASSWORD python manage.py createsuperuser \
|
||||
--no-input \
|
||||
--username=$ADMIN_USER \
|
||||
--email=$ADMIN_EMAIL \
|
||||
--database=default || true
|
||||
echo "Created admin user with password: $ADMIN_PASSWORD"
|
||||
|
||||
fi
|
||||
echo "RUNNING COLLECTSTATIC"
|
||||
|
||||
python manage.py collectstatic --noinput
|
||||
|
||||
echo "Admin Password: $ADMIN_PASSWORD"
|
||||
|
||||
# post_save, needs redis to succeed (ie. migrate depends on redis)
|
||||
DJANGO_SUPERUSER_PASSWORD=$ADMIN_PASSWORD python manage.py createsuperuser \
|
||||
--no-input \
|
||||
--username=$ADMIN_USER \
|
||||
--email=$ADMIN_EMAIL \
|
||||
--database=default || true
|
||||
|
||||
# echo "Updating hostname ..."
|
||||
# TODO: Get the FRONTEND_HOST from cms/local_settings.py
|
||||
# echo "from django.contrib.sites.models import Site; Site.objects.update(name='$FRONTEND_HOST', domain='$FRONTEND_HOST')" | python manage.py shell
|
||||
|
||||
@@ -18,6 +18,11 @@ services:
|
||||
ENABLE_CELERY_SHORT: 'no'
|
||||
ENABLE_CELERY_LONG: 'no'
|
||||
ENABLE_CELERY_BEAT: 'no'
|
||||
ADMIN_USER: 'admin'
|
||||
ADMIN_EMAIL: 'admin@localhost'
|
||||
#ADMIN_PASSWORD: 'uncomment_and_set_password_here'
|
||||
command: "./deploy/docker/prestart.sh"
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -20,6 +20,8 @@ services:
|
||||
ENABLE_CELERY_SHORT: 'no'
|
||||
ENABLE_CELERY_LONG: 'no'
|
||||
ENABLE_CELERY_BEAT: 'no'
|
||||
command: "./deploy/docker/prestart.sh"
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
@@ -36,6 +38,9 @@ services:
|
||||
ENABLE_CELERY_SHORT: 'no'
|
||||
ENABLE_CELERY_LONG: 'no'
|
||||
ENABLE_MIGRATIONS: 'no'
|
||||
ADMIN_USER: 'admin'
|
||||
ADMIN_EMAIL: 'admin@localhost'
|
||||
#ADMIN_PASSWORD: 'uncomment_and_set_password_here'
|
||||
VIRTUAL_HOST: localhost
|
||||
depends_on:
|
||||
- migrations
|
||||
|
||||
@@ -38,6 +38,11 @@ services:
|
||||
ENABLE_CELERY_SHORT: 'no'
|
||||
ENABLE_CELERY_LONG: 'no'
|
||||
ENABLE_CELERY_BEAT: 'no'
|
||||
ADMIN_USER: 'admin'
|
||||
ADMIN_EMAIL: 'admin@localhost'
|
||||
#ADMIN_PASSWORD: 'uncomment_and_set_password_here'
|
||||
command: "./deploy/docker/prestart.sh"
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -11,6 +11,11 @@ services:
|
||||
ENABLE_CELERY_SHORT: 'no'
|
||||
ENABLE_CELERY_LONG: 'no'
|
||||
ENABLE_CELERY_BEAT: 'no'
|
||||
ADMIN_USER: 'admin'
|
||||
ADMIN_EMAIL: 'admin@localhost'
|
||||
#ADMIN_PASSWORD: 'uncomment_and_set_password_here'
|
||||
command: "./deploy/docker/prestart.sh"
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -11,6 +11,11 @@ services:
|
||||
ENABLE_CELERY_SHORT: 'no'
|
||||
ENABLE_CELERY_LONG: 'no'
|
||||
ENABLE_CELERY_BEAT: 'no'
|
||||
ADMIN_USER: 'admin'
|
||||
ADMIN_EMAIL: 'admin@localhost'
|
||||
#ADMIN_PASSWORD: 'uncomment_and_set_password_here'
|
||||
command: "./deploy/docker/prestart.sh"
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
## How To Add A Static Page To The Sidebar
|
||||
|
||||
### 1. Create your html page in templates/cms/
|
||||
e.g. duplicate and rename about.html
|
||||
```
|
||||
sudo cp templates/cms/about.html templates/cms/volunteer.html
|
||||
```
|
||||
|
||||
### 2. Create your css file in static/css/
|
||||
```
|
||||
touch static/css/volunteer.css
|
||||
```
|
||||
|
||||
### 3. In your html file, update block headermeta to reflect your new page
|
||||
```
|
||||
{% block headermeta %}
|
||||
<meta property="og:title" content="Volunteer - {{PORTAL_NAME}}">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:description" content="">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": [{
|
||||
"@type": "ListItem",
|
||||
"position": 1,
|
||||
"name": "{{PORTAL_NAME}}",
|
||||
"item": {
|
||||
"@type": "WebPage",
|
||||
"@id": "{{FRONTEND_HOST}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 2,
|
||||
"name": "Volunteer",
|
||||
"item": {
|
||||
"@type": "VolunteerPage",
|
||||
"@id": "{{FRONTEND_HOST}}/volunteer"
|
||||
}
|
||||
}]
|
||||
}
|
||||
</script>
|
||||
<link href="{% static "css/volunteer.css" %}" rel="stylesheet"/>
|
||||
{% endblock headermeta %}
|
||||
```
|
||||
|
||||
### 4. In your html file, update block innercontent to reflect your actual content
|
||||
Write whatever you like.
|
||||
|
||||
### 5. In your css file, write matching styles for you html file.
|
||||
Write whatever you like.
|
||||
|
||||
### 6. Add your view to files/views.py
|
||||
```
|
||||
def volunteer(request):
|
||||
"""Volunteer view"""
|
||||
context = {}
|
||||
return render(request, "cms/volunteer.html", context)
|
||||
```
|
||||
|
||||
### 7. Add your url pattern to files/urls.py
|
||||
```
|
||||
urlpatterns = [
|
||||
url(r"^$", views.index),
|
||||
url(r"^about", views.about, name="about"),
|
||||
url(r"^volunteer", views.volunteer, name="volunteer"),
|
||||
```
|
||||
|
||||
### 8. Add your page to the left sidebar
|
||||
To add a link to your page as a menu item in the left sidebar,
|
||||
add the following code after the last line in _commons.js
|
||||
```
|
||||
/* Checks that a given selector has loaded. */
|
||||
const checkElement = async selector => {
|
||||
while ( document.querySelector(selector) === null) {
|
||||
await new Promise( resolve => requestAnimationFrame(resolve) )
|
||||
}
|
||||
return document.querySelector(selector);
|
||||
};
|
||||
|
||||
/* Checks that sidebar nav menu has loaded, then adds menu item. */
|
||||
checkElement('.nav-menu')
|
||||
.then((element) => {
|
||||
(function(){
|
||||
var a = document.createElement('a');
|
||||
a.href = "/volunteer";
|
||||
a.title = "Volunteer";
|
||||
|
||||
var s = document.createElement('span');
|
||||
s.className = "menu-item-icon";
|
||||
|
||||
var icon = document.createElement('i');
|
||||
icon.className = "material-icons";
|
||||
icon.setAttribute("data-icon", "people");
|
||||
|
||||
s.appendChild(icon);
|
||||
a.appendChild(s);
|
||||
|
||||
var linkText = document.createTextNode("Volunteer");
|
||||
var t = document.createElement('span');
|
||||
|
||||
t.appendChild(linkText);
|
||||
a.appendChild(t);
|
||||
|
||||
var listItem = document.createElement('li');
|
||||
listItem.className = "link-item";
|
||||
listItem.appendChild(a);
|
||||
|
||||
//if signed out use 3rd nav-menu
|
||||
var elem = document.querySelector(".nav-menu:nth-child(3) nav ul");
|
||||
var loc = elem.innerText;
|
||||
if (loc.includes("About")){
|
||||
elem.insertBefore(listItem, elem.children[2]);
|
||||
} else { //if signed in use 4th nav-menu
|
||||
elem = document.querySelector(".nav-menu:nth-child(4) nav ul");
|
||||
elem.insertBefore(listItem, elem.children[2]);
|
||||
}
|
||||
})();
|
||||
});
|
||||
```
|
||||
|
||||
### 9. Restart the mediacms web server
|
||||
On docker:
|
||||
```
|
||||
sudo docker stop mediacms_web_1 && sudo docker start mediacms_web_1
|
||||
```
|
||||
|
||||
Otherwise
|
||||
```
|
||||
sudo systemctl restart mediacms
|
||||
```
|
||||
@@ -1,270 +0,0 @@
|
||||
## Configuration
|
||||
|
||||
A number of options are available on `cms/settings.py`.
|
||||
|
||||
It is advisable to override any of them by adding it to `local_settings.py` .
|
||||
|
||||
In case of a the single server installation, add to `cms/local_settings.py` .
|
||||
|
||||
In case of a docker compose installation, add to `deploy/docker/local_settings.py` . This will automatically overwrite `cms/local_settings.py` .
|
||||
|
||||
Any change needs restart of MediaCMS in order to take effect.
|
||||
|
||||
Single server installation: edit `cms/local_settings.py`, make a change and restart MediaCMS
|
||||
|
||||
```bash
|
||||
#systemctl restart mediacms
|
||||
```
|
||||
|
||||
Docker Compose installation: edit `deploy/docker/local_settings.py`, make a change and restart MediaCMS containers
|
||||
|
||||
```bash
|
||||
#docker-compose restart web celery_worker celery_beat
|
||||
```
|
||||
|
||||
### change portal logo
|
||||
|
||||
Set a new svg file for the white theme (`static/images/logo_dark.svg`) or the dark theme (`static/images/logo_light.svg`)
|
||||
|
||||
### set global portal title
|
||||
|
||||
set `PORTAL_NAME`, eg
|
||||
|
||||
```
|
||||
PORTAL_NAME = 'my awesome portal'
|
||||
```
|
||||
|
||||
### who can add media
|
||||
|
||||
By default `CAN_ADD_MEDIA = "all"` means that all registered users can add media. Other valid options are:
|
||||
|
||||
- **email_verified**, a user not only has to register an account but also verify the email (by clicking the link sent upon registration). Apparently email configuration need to work, otherise users won't receive emails.
|
||||
|
||||
- **advancedUser**, only users that are marked as advanced users can add media. Admins or MediaCMS managers can make users advanced users by editing their profile and selecting advancedUser.
|
||||
|
||||
### what is the portal workflow
|
||||
|
||||
The `PORTAL_WORKFLOW` variable specifies what happens to newly uploaded media, whether they appear on listings (as the index page, or search)
|
||||
|
||||
- **public** is the default option and means that a media can appear on listings. If media type is video, it will appear once at least a task that produces an encoded version of the file has finished succesfully. For other type of files, as image/audio they appear instantly
|
||||
|
||||
- **private** means that newly uploaded content is private - only users can see it or MediaCMS editors, managers and admins. Those can also set the status to public or unlisted
|
||||
|
||||
- **unlisted** means that items are unlisted. However if a user visits the url of an unlisted media, it will be shown (as opposed to private)
|
||||
|
||||
|
||||
### show/hide the Sign in button
|
||||
|
||||
to show button:
|
||||
```
|
||||
LOGIN_ALLOWED = True
|
||||
```
|
||||
|
||||
to hide button:
|
||||
|
||||
```
|
||||
LOGIN_ALLOWED = False
|
||||
```
|
||||
|
||||
### show/hide the Register button
|
||||
|
||||
to show button:
|
||||
```
|
||||
REGISTER_ALLOWED = True
|
||||
```
|
||||
|
||||
to hide button:
|
||||
|
||||
```
|
||||
REGISTER_ALLOWED = False
|
||||
```
|
||||
|
||||
|
||||
### show/hide the upload media button
|
||||
|
||||
To show:
|
||||
|
||||
```
|
||||
UPLOAD_MEDIA_ALLOWED = True
|
||||
```
|
||||
|
||||
To hide:
|
||||
|
||||
```
|
||||
UPLOAD_MEDIA_ALLOWED = False
|
||||
```
|
||||
|
||||
### show/hide the actions buttons (like/dislike/report)
|
||||
|
||||
Make changes (True/False) to any of the following:
|
||||
|
||||
```
|
||||
- CAN_LIKE_MEDIA = True # whether the like media appears
|
||||
- CAN_DISLIKE_MEDIA = True # whether the dislike media appears
|
||||
- CAN_REPORT_MEDIA = True # whether the report media appears
|
||||
- CAN_SHARE_MEDIA = True # whether the share media appears
|
||||
```
|
||||
|
||||
### show/hide the download option on a media
|
||||
|
||||
Edit `templates/config/installation/features.html` and set
|
||||
|
||||
```
|
||||
download: false
|
||||
```
|
||||
|
||||
### automatically hide media upon being reported
|
||||
|
||||
set a low number for variable `REPORTED_TIMES_THRESHOLD`
|
||||
eg
|
||||
|
||||
```
|
||||
REPORTED_TIMES_THRESHOLD = 2
|
||||
```
|
||||
|
||||
once the limit is reached, media goes to private state and an email is sent to admins
|
||||
|
||||
### set a custom message on the media upload page
|
||||
|
||||
this message will appear below the media drag and drop form
|
||||
|
||||
```
|
||||
PRE_UPLOAD_MEDIA_MESSAGE = 'custom message'
|
||||
```
|
||||
|
||||
### set email settings
|
||||
|
||||
Set correct settings per provider
|
||||
|
||||
```
|
||||
DEFAULT_FROM_EMAIL = 'info@mediacms.io'
|
||||
EMAIL_HOST_PASSWORD = 'xyz'
|
||||
EMAIL_HOST_USER = 'info@mediacms.io'
|
||||
EMAIL_USE_TLS = True
|
||||
SERVER_EMAIL = DEFAULT_FROM_EMAIL
|
||||
EMAIL_HOST = 'mediacms.io'
|
||||
EMAIL_PORT = 587
|
||||
ADMIN_EMAIL_LIST = ['info@mediacms.io']
|
||||
```
|
||||
|
||||
### disallow user registrations from specific domains
|
||||
|
||||
set domains that are not valid for registration via this variable:
|
||||
|
||||
```
|
||||
RESTRICTED_DOMAINS_FOR_USER_REGISTRATION = [
|
||||
'xxx.com', 'emaildomainwhatever.com']
|
||||
```
|
||||
|
||||
### require a review by MediaCMS editors/managers/admins
|
||||
|
||||
set value
|
||||
|
||||
```
|
||||
MEDIA_IS_REVIEWED = False
|
||||
```
|
||||
|
||||
any uploaded media now needs to be reviewed before it can appear to the listings.
|
||||
MediaCMS editors/managers/admins can visit the media page and edit it, where they can see the option to mark media as reviewed. By default this is set to True, so all media don't require to be reviewed
|
||||
|
||||
### specify maximum number of media for a playlist
|
||||
|
||||
set a different threshold on variable `MAX_MEDIA_PER_PLAYLIST`
|
||||
|
||||
eg
|
||||
|
||||
```
|
||||
MAX_MEDIA_PER_PLAYLIST = 14
|
||||
```
|
||||
|
||||
### specify maximum size of a media that can be uploaded
|
||||
|
||||
change `UPLOAD_MAX_SIZE`.
|
||||
|
||||
default is 4GB
|
||||
|
||||
```
|
||||
UPLOAD_MAX_SIZE = 800 * 1024 * 1000 * 5
|
||||
```
|
||||
|
||||
### specify maximum size of comments
|
||||
|
||||
change `MAX_CHARS_FOR_COMMENT`
|
||||
|
||||
default:
|
||||
|
||||
```
|
||||
MAX_CHARS_FOR_COMMENT = 10000
|
||||
```
|
||||
|
||||
### how many files to upload in parallel
|
||||
|
||||
set a different threshold for `UPLOAD_MAX_FILES_NUMBER`
|
||||
default:
|
||||
|
||||
```
|
||||
UPLOAD_MAX_FILES_NUMBER = 100
|
||||
```
|
||||
|
||||
### force users confirm their email upon registrations
|
||||
|
||||
default option for email confirmation is optional. Set this to mandatory in order to force users confirm their email before they can login
|
||||
|
||||
```
|
||||
ACCOUNT_EMAIL_VERIFICATION = 'optional'
|
||||
```
|
||||
|
||||
### rate limit account login attempts
|
||||
|
||||
after this number is reached
|
||||
|
||||
```
|
||||
ACCOUNT_LOGIN_ATTEMPTS_LIMIT = 20
|
||||
```
|
||||
|
||||
sets a timeout (in seconds)
|
||||
|
||||
```
|
||||
ACCOUNT_LOGIN_ATTEMPTS_TIMEOUT = 5
|
||||
```
|
||||
|
||||
### disallow user registration
|
||||
|
||||
set the following variable to False
|
||||
|
||||
```
|
||||
USERS_CAN_SELF_REGISTER = True
|
||||
```
|
||||
|
||||
### configure notifications
|
||||
|
||||
Global notifications that are implemented are controlled by the following options:
|
||||
|
||||
```
|
||||
USERS_NOTIFICATIONS = {
|
||||
'MEDIA_ADDED': True,
|
||||
}
|
||||
```
|
||||
|
||||
If you want to disable notification for new media, set to False
|
||||
|
||||
Admins also receive notifications on different events, set any of the following to False to disable
|
||||
|
||||
```
|
||||
ADMINS_NOTIFICATIONS = {
|
||||
'NEW_USER': True,
|
||||
'MEDIA_ADDED': True,
|
||||
'MEDIA_REPORTED': True,
|
||||
}
|
||||
```
|
||||
|
||||
- NEW_USER: a new user is added
|
||||
- MEDIA_ADDED: a media is added
|
||||
- MEDIA_REPORTED: the report for a media was hit
|
||||
|
||||
|
||||
|
||||
### Google Analytics
|
||||
|
||||
Checkout the instructions by alberto98fx on [Google Analytics](/docs/robots_and_analytics.md) page.
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
# Docker Compose
|
||||
|
||||
## Installation
|
||||
Install a recent version of [Docker](https://docs.docker.com/get-docker/), and [Docker Compose](https://docs.docker.com/compose/install/).
|
||||
|
||||
For Ubuntu 18/20 systems this is:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||
sudo sh get-docker.sh
|
||||
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
```
|
||||
|
||||
Then run as root
|
||||
|
||||
```bash
|
||||
git clone https://github.com/mediacms-io/mediacms
|
||||
cd mediacms
|
||||
```
|
||||
|
||||
The default option is to serve MediaCMS on all ips available of the server (including localhost).
|
||||
If you want to explore more options (including setup of https with letsencrypt certificate) checkout the docs on the [Docker deployment](/docs/Docker_deployment.md) page for different docker-compose setups to use.
|
||||
|
||||
Run
|
||||
|
||||
```bash
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
This will download all MediaCMS related Docker images and start all containers. Once it finishes, MediaCMS will be installed and available on http://localhost or http://ip
|
||||
|
||||
|
||||
## Update
|
||||
|
||||
Get latest MediaCMS image and stop/start containers
|
||||
|
||||
```bash
|
||||
cd /path/to/mediacms/installation
|
||||
docker pull mediacms/mediacms
|
||||
docker-compose down
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
## Configuration
|
||||
Checkout the configuration docs on [Configuration](/docs/Configuration.md) page.
|
||||
|
||||
|
||||
## Maintenance
|
||||
Database is stored on ../postgres_data/ and media_files on media_files/
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
# MediaCMS on Docker
|
||||
|
||||
The mediacms image is built to use supervisord as the main process, which manages one or more services required to run mediacms. We can toggle which services are run in a given container by setting the environment variables below to `yes` or `no`:
|
||||
|
||||
* ENABLE_UWSGI
|
||||
* ENABLE_NGINX
|
||||
* ENABLE_CELERY_BEAT
|
||||
* ENABLE_CELERY_SHORT
|
||||
* ENABLE_CELERY_LONG
|
||||
* ENABLE_MIGRATIONS
|
||||
|
||||
By default, all these services are enabled, but in order to create a scaleable deployment, some of them can be disabled, splitting the service up into smaller services.
|
||||
|
||||
Also see the `Dockerfile` for other environment variables which you may wish to override. Application settings, eg. `FRONTEND_HOST` can also be overridden by updating the `deploy/docker/local_settings.py` file.
|
||||
|
||||
See example deployments in the sections below. These example deployments have been tested on `docker-compose version 1.27.4` running on `Docker version 19.03.13`
|
||||
|
||||
To run, update the configs above if necessary, build the image by running `docker-compose build`, then run `docker-compose run`
|
||||
|
||||
# Simple Deployment, accessed as http://localhost
|
||||
|
||||
The main container runs migrations, mediacms_web, celery_beat, celery_workers (celery_short and celery_long services), exposed on port 80 supported by redis and postgres database.
|
||||
|
||||
The FRONTEND_HOST in `deploy/docker/local_settings.py` is configured as http://localhost, on the docker host machine.
|
||||
|
||||
# Server with ssl certificate through letsencrypt service, accessed as https://my_domain.com
|
||||
Before trying this out make sure the ip points to my_domain.com.
|
||||
|
||||
With this method [this deployment](../docker-compose-letsencrypt.yaml) is used.
|
||||
|
||||
Edit this file and set `VIRTUAL_HOST` as my_domain.com, `LETSENCRYPT_HOST` as my_domain.com, and your email on `LETSENCRYPT_EMAIL`
|
||||
|
||||
Edit `deploy/docker/local_settings.py` and set https://my_domain.com as `FRONTEND_HOST`
|
||||
|
||||
Now run docker-compose -f docker-compose-letsencrypt.yaml up, when installation finishes you will be able to access https://my_domain.com using a valid Letsencrypt certificate!
|
||||
|
||||
# Advanced Deployment, accessed as http://localhost:8000
|
||||
|
||||
Here we can run 1 mediacms_web instance, with the FRONTEND_HOST in `deploy/docker/local_settings.py` configured as http://localhost:8000. This is bootstrapped by a single migrations instance and supported by a single celery_beat instance and 1 or more celery_worker instances. Redis and postgres containers are also used for persistence. Clients can access the service on http://localhost:8000, on the docker host machine. This is similar to [this deployment](../docker-compose.yaml), with a `port` defined in FRONTEND_HOST.
|
||||
|
||||
# Advanced Deployment, with reverse proxy, accessed as http://mediacms.io
|
||||
|
||||
Here we can use `jwilder/nginx-proxy` to reverse proxy to 1 or more instances of mediacms_web supported by other services as mentioned in the previous deployment. The FRONTEND_HOST in `deploy/docker/local_settings.py` is configured as http://mediacms.io, nginx-proxy has port 80 exposed. Clients can access the service on http://mediacms.io (Assuming DNS or the hosts file is setup correctly to point to the IP of the nginx-proxy instance). This is similar to [this deployment](../docker-compose-http-proxy.yaml).
|
||||
|
||||
# Advanced Deployment, with reverse proxy, accessed as https://localhost
|
||||
|
||||
The reverse proxy (`jwilder/nginx-proxy`) can be configured to provide SSL termination using self-signed certificates, letsencrypt or CA signed certificates (see: https://hub.docker.com/r/jwilder/nginx-proxy or [LetsEncrypt Example](https://www.singularaspect.com/use-nginx-proxy-and-letsencrypt-companion-to-host-multiple-websites/) ). In this case the FRONTEND_HOST should be set to https://mediacms.io. This is similar to [this deployment](../docker-compose-http-proxy.yaml).
|
||||
|
||||
# A Scaleable Deployment Architecture (Docker, Swarm, Kubernetes)
|
||||
|
||||
The architecture below generalises all the deployment scenarios above, and provides a conceptual design for other deployments based on kubernetes and docker swarm. It allows for horizontal scaleability through the use of multiple mediacms_web instances and celery_workers. For large deployments, managed postgres, redis and storage may be adopted.
|
||||
|
||||

|
||||
@@ -1,38 +0,0 @@
|
||||
# Single Server
|
||||
|
||||
## Installation
|
||||
|
||||
The core dependencies are Python3, Django3, Celery, PostgreSQL, Redis, ffmpeg. Any system that can have these dependencies installed, can run MediaCMS. But we strongly suggest installing on Linux Ubuntu 18 or 20 versions.
|
||||
|
||||
Installation on a Ubuntu 18 or 20 system with git utility installed should be completed in a few minutes with the following steps.
|
||||
Make sure you run it as user root, on a clear system, since the automatic script will install and configure the following services: Celery/PostgreSQL/Redis/Nginx and will override any existing settings.
|
||||
|
||||
Automated script - tested on Ubuntu 18, Ubuntu 20, and Debian Buster
|
||||
|
||||
```bash
|
||||
mkdir /home/mediacms.io && cd /home/mediacms.io/
|
||||
git clone https://github.com/mediacms-io/mediacms
|
||||
cd /home/mediacms.io/mediacms/ && bash ./install.sh
|
||||
```
|
||||
|
||||
The script will ask if you have a URL where you want to deploy MediaCMS, otherwise it will use localhost. If you provide a URL, it will use Let's Encrypt service to install a valid ssl certificate.
|
||||
|
||||
|
||||
## Update
|
||||
|
||||
If you've used the above way to install MediaCMS, update with the following:
|
||||
|
||||
```bash
|
||||
cd /home/mediacms.io/mediacms # enter mediacms directory
|
||||
source /home/mediacms.io/bin/activate # use virtualenv
|
||||
git pull # update code
|
||||
python manage.py migrate # run Django migrations
|
||||
sudo systemctl restart mediacms celery_long celery_short # restart services
|
||||
```
|
||||
|
||||
## Configuration
|
||||
Checkout the configuration docs on [Configuration](/docs/Configuration.md) page.
|
||||
|
||||
|
||||
## Maintenance
|
||||
Database can be backed up with pg_dump and media_files on /home/mediacms.io/mediacms/media_files include original files and encoded/transcoded versions
|
||||
@@ -1,20 +0,0 @@
|
||||
## User scenarios to test
|
||||
|
||||
|
||||
## test video media + image
|
||||
try uploading a video + image, make sure they get encoded well and check they appear on index/search/category/author page
|
||||
try editing/setting metadata, confirm action is performed, also that are searchable
|
||||
try adding custom poster, confirm it loads well on video page/listings
|
||||
try specifying different thumbnail time, confirm an automatic screenshot is taken
|
||||
|
||||
|
||||
## portal workflow
|
||||
change workflow to unlisted, check they don't appear on index/search/category/author page
|
||||
|
||||
## users management
|
||||
create an admin, a MediaCMS editor and MediaCMS manager. All should see edit/delete on a media and also comments, and action should work.
|
||||
For users edit and delete, only MediaCMS manager and admin should see edit/delete and these actions should work.
|
||||
|
||||
## test subtitle
|
||||
add language and test subtitling
|
||||
|
||||
650
docs/admins_docs.md
Normal file
@@ -0,0 +1,650 @@
|
||||
# Administrators documentation
|
||||
|
||||
## Table of contents
|
||||
- [1. Welcome](#1-welcome)
|
||||
- [2. Server Installaton](#2-server-installation)
|
||||
- [3. Docker Installation](#3-docker-installation)
|
||||
- [4. Docker Deployement options](#4-docker-deployment-options)
|
||||
- [5. Configuration](#5-configuration)
|
||||
- [6. Manage pages](#6-manage-pages)
|
||||
- [7. Django admin dashboard](#7-django-admin-dashboard)
|
||||
- [8. On portal workflow](#8-on-portal-workflow)
|
||||
- [9. On user roles](#9-on-user-roles)
|
||||
- [10. Adding languages for Captions and subtitles](#10-adding-languages-for-captions-and-subtitles)
|
||||
- [11. Add/delete categories and tags](#11-adddelete-categories-and-tags)
|
||||
- [12. Video transcoding](#12-video-transcoding)
|
||||
- [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)
|
||||
|
||||
|
||||
## 1. Welcome
|
||||
This page is created for MediaCMS administrators that are responsible for setting up the software, maintaining it and making modifications.
|
||||
|
||||
## 2. Server Installation
|
||||
|
||||
The core dependencies are Python3, Django3, Celery, PostgreSQL, Redis, ffmpeg. Any system that can have these dependencies installed, can run MediaCMS. But we strongly suggest installing on Linux Ubuntu 18 or 20 versions.
|
||||
|
||||
Installation on a Ubuntu 18 or 20 system with git utility installed should be completed in a few minutes with the following steps.
|
||||
Make sure you run it as user root, on a clear system, since the automatic script will install and configure the following services: Celery/PostgreSQL/Redis/Nginx and will override any existing settings.
|
||||
|
||||
Automated script - tested on Ubuntu 18, Ubuntu 20, and Debian Buster
|
||||
|
||||
```bash
|
||||
mkdir /home/mediacms.io && cd /home/mediacms.io/
|
||||
git clone https://github.com/mediacms-io/mediacms
|
||||
cd /home/mediacms.io/mediacms/ && bash ./install.sh
|
||||
```
|
||||
|
||||
The script will ask if you have a URL where you want to deploy MediaCMS, otherwise it will use localhost. If you provide a URL, it will use Let's Encrypt service to install a valid ssl certificate.
|
||||
|
||||
|
||||
### Update
|
||||
|
||||
If you've used the above way to install MediaCMS, update with the following:
|
||||
|
||||
```bash
|
||||
cd /home/mediacms.io/mediacms # enter mediacms directory
|
||||
source /home/mediacms.io/bin/activate # use virtualenv
|
||||
git pull # update code
|
||||
python manage.py migrate # run Django migrations
|
||||
sudo systemctl restart mediacms celery_long celery_short # restart services
|
||||
```
|
||||
|
||||
### Configuration
|
||||
Checkout the configuration section here.
|
||||
|
||||
|
||||
### Maintenance
|
||||
Database can be backed up with pg_dump and media_files on /home/mediacms.io/mediacms/media_files include original files and encoded/transcoded versions
|
||||
|
||||
|
||||
## 3. Docker Installation
|
||||
|
||||
## Installation
|
||||
Install a recent version of [Docker](https://docs.docker.com/get-docker/), and [Docker Compose](https://docs.docker.com/compose/install/).
|
||||
|
||||
For Ubuntu 18/20 systems this is:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||
sudo sh get-docker.sh
|
||||
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
```
|
||||
|
||||
Then run as root
|
||||
|
||||
```bash
|
||||
git clone https://github.com/mediacms-io/mediacms
|
||||
cd mediacms
|
||||
```
|
||||
|
||||
The default option is to serve MediaCMS on all ips available of the server (including localhost).
|
||||
If you want to explore more options (including setup of https with letsencrypt certificate) checkout [Docker deployment](/docs/admins_docs.md#4-docker-deployment-options) section for different docker-compose setups to use.
|
||||
|
||||
Run
|
||||
|
||||
```bash
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
This will download all MediaCMS related Docker images and start all containers. Once it finishes, MediaCMS will be installed and available on http://localhost or http://ip
|
||||
|
||||
A user admin has been created with random password, you should be able to see it at the end of migrations container, eg
|
||||
|
||||
```
|
||||
migrations_1 | Created admin user with password: gwg1clfkwf
|
||||
```
|
||||
|
||||
or if you have set the ADMIN_PASSWORD variable on docker-compose file you have used (example `docker-compose.yaml`), that variable will be set as the admin user's password
|
||||
|
||||
### Update
|
||||
|
||||
Get latest MediaCMS image and stop/start containers
|
||||
|
||||
```bash
|
||||
cd /path/to/mediacms/installation
|
||||
docker pull mediacms/mediacms
|
||||
docker-compose down
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
## Configuration
|
||||
Checkout the configuration docs here.
|
||||
|
||||
|
||||
### Maintenance
|
||||
Database is stored on ../postgres_data/ and media_files on media_files/
|
||||
|
||||
|
||||
## 4. Docker Deployment options
|
||||
|
||||
The mediacms image is built to use supervisord as the main process, which manages one or more services required to run mediacms. We can toggle which services are run in a given container by setting the environment variables below to `yes` or `no`:
|
||||
|
||||
* ENABLE_UWSGI
|
||||
* ENABLE_NGINX
|
||||
* ENABLE_CELERY_BEAT
|
||||
* ENABLE_CELERY_SHORT
|
||||
* ENABLE_CELERY_LONG
|
||||
* ENABLE_MIGRATIONS
|
||||
|
||||
By default, all these services are enabled, but in order to create a scaleable deployment, some of them can be disabled, splitting the service up into smaller services.
|
||||
|
||||
Also see the `Dockerfile` for other environment variables which you may wish to override. Application settings, eg. `FRONTEND_HOST` can also be overridden by updating the `deploy/docker/local_settings.py` file.
|
||||
|
||||
See example deployments in the sections below. These example deployments have been tested on `docker-compose version 1.27.4` running on `Docker version 19.03.13`
|
||||
|
||||
To run, update the configs above if necessary, build the image by running `docker-compose build`, then run `docker-compose run`
|
||||
|
||||
### Simple Deployment, accessed as http://localhost
|
||||
|
||||
The main container runs migrations, mediacms_web, celery_beat, celery_workers (celery_short and celery_long services), exposed on port 80 supported by redis and postgres database.
|
||||
|
||||
The FRONTEND_HOST in `deploy/docker/local_settings.py` is configured as http://localhost, on the docker host machine.
|
||||
|
||||
### Server with ssl certificate through letsencrypt service, accessed as https://my_domain.com
|
||||
Before trying this out make sure the ip points to my_domain.com.
|
||||
|
||||
With this method [this deployment](../docker-compose-letsencrypt.yaml) is used.
|
||||
|
||||
Edit this file and set `VIRTUAL_HOST` as my_domain.com, `LETSENCRYPT_HOST` as my_domain.com, and your email on `LETSENCRYPT_EMAIL`
|
||||
|
||||
Edit `deploy/docker/local_settings.py` and set https://my_domain.com as `FRONTEND_HOST`
|
||||
|
||||
Now run docker-compose -f docker-compose-letsencrypt.yaml up, when installation finishes you will be able to access https://my_domain.com using a valid Letsencrypt certificate!
|
||||
|
||||
### Advanced Deployment, accessed as http://localhost:8000
|
||||
|
||||
Here we can run 1 mediacms_web instance, with the FRONTEND_HOST in `deploy/docker/local_settings.py` configured as http://localhost:8000. This is bootstrapped by a single migrations instance and supported by a single celery_beat instance and 1 or more celery_worker instances. Redis and postgres containers are also used for persistence. Clients can access the service on http://localhost:8000, on the docker host machine. This is similar to [this deployment](../docker-compose.yaml), with a `port` defined in FRONTEND_HOST.
|
||||
|
||||
### Advanced Deployment, with reverse proxy, accessed as http://mediacms.io
|
||||
|
||||
Here we can use `jwilder/nginx-proxy` to reverse proxy to 1 or more instances of mediacms_web supported by other services as mentioned in the previous deployment. The FRONTEND_HOST in `deploy/docker/local_settings.py` is configured as http://mediacms.io, nginx-proxy has port 80 exposed. Clients can access the service on http://mediacms.io (Assuming DNS or the hosts file is setup correctly to point to the IP of the nginx-proxy instance). This is similar to [this deployment](../docker-compose-http-proxy.yaml).
|
||||
|
||||
### Advanced Deployment, with reverse proxy, accessed as https://localhost
|
||||
|
||||
The reverse proxy (`jwilder/nginx-proxy`) can be configured to provide SSL termination using self-signed certificates, letsencrypt or CA signed certificates (see: https://hub.docker.com/r/jwilder/nginx-proxy or [LetsEncrypt Example](https://www.singularaspect.com/use-nginx-proxy-and-letsencrypt-companion-to-host-multiple-websites/) ). In this case the FRONTEND_HOST should be set to https://mediacms.io. This is similar to [this deployment](../docker-compose-http-proxy.yaml).
|
||||
|
||||
### A Scaleable Deployment Architecture (Docker, Swarm, Kubernetes)
|
||||
|
||||
The architecture below generalises all the deployment scenarios above, and provides a conceptual design for other deployments based on kubernetes and docker swarm. It allows for horizontal scaleability through the use of multiple mediacms_web instances and celery_workers. For large deployments, managed postgres, redis and storage may be adopted.
|
||||
|
||||

|
||||
|
||||
|
||||
## 5. Configuration
|
||||
Several options are available on `cms/settings.py`, most of the things that are allowed or should be disallowed are described there.
|
||||
|
||||
It is advisable to override any of them by adding it to `local_settings.py` .
|
||||
|
||||
In case of a the single server installation, add to `cms/local_settings.py` .
|
||||
|
||||
In case of a docker compose installation, add to `deploy/docker/local_settings.py` . This will automatically overwrite `cms/local_settings.py` .
|
||||
|
||||
Any change needs restart of MediaCMS in order to take effect.
|
||||
|
||||
Single server installation: edit `cms/local_settings.py`, make a change and restart MediaCMS
|
||||
|
||||
```bash
|
||||
#systemctl restart mediacms
|
||||
```
|
||||
|
||||
Docker Compose installation: edit `deploy/docker/local_settings.py`, make a change and restart MediaCMS containers
|
||||
|
||||
```bash
|
||||
#docker-compose restart web celery_worker celery_beat
|
||||
```
|
||||
|
||||
### 5.1 Change portal logo
|
||||
|
||||
Set a new svg file for the white theme (`static/images/logo_dark.svg`) or the dark theme (`static/images/logo_light.svg`)
|
||||
|
||||
### 5.2 Set global portal title
|
||||
|
||||
set `PORTAL_NAME`, eg
|
||||
|
||||
```
|
||||
PORTAL_NAME = 'my awesome portal'
|
||||
```
|
||||
|
||||
### 5.3 Control who can add media
|
||||
|
||||
By default `CAN_ADD_MEDIA = "all"` means that all registered users can add media. Other valid options are:
|
||||
|
||||
- **email_verified**, a user not only has to register an account but also verify the email (by clicking the link sent upon registration). Apparently email configuration need to work, otherise users won't receive emails.
|
||||
|
||||
- **advancedUser**, only users that are marked as advanced users can add media. Admins or MediaCMS managers can make users advanced users by editing their profile and selecting advancedUser.
|
||||
|
||||
### 5.4 What is the portal workflow
|
||||
|
||||
The `PORTAL_WORKFLOW` variable specifies what happens to newly uploaded media, whether they appear on listings (as the index page, or search)
|
||||
|
||||
- **public** is the default option and means that a media can appear on listings. If media type is video, it will appear once at least a task that produces an encoded version of the file has finished succesfully. For other type of files, as image/audio they appear instantly
|
||||
|
||||
- **private** means that newly uploaded content is private - only users can see it or MediaCMS editors, managers and admins. Those can also set the status to public or unlisted
|
||||
|
||||
- **unlisted** means that items are unlisted. However if a user visits the url of an unlisted media, it will be shown (as opposed to private)
|
||||
|
||||
|
||||
### 5.5 Show or hide the Sign in button
|
||||
|
||||
to show button:
|
||||
```
|
||||
LOGIN_ALLOWED = True
|
||||
```
|
||||
|
||||
to hide button:
|
||||
|
||||
```
|
||||
LOGIN_ALLOWED = False
|
||||
```
|
||||
|
||||
### 5.6 Show or hide the Register button
|
||||
|
||||
to show button:
|
||||
```
|
||||
REGISTER_ALLOWED = True
|
||||
```
|
||||
|
||||
to hide button:
|
||||
|
||||
```
|
||||
REGISTER_ALLOWED = False
|
||||
```
|
||||
|
||||
|
||||
### 5.7 Show or hide the upload media button
|
||||
|
||||
To show:
|
||||
|
||||
```
|
||||
UPLOAD_MEDIA_ALLOWED = True
|
||||
```
|
||||
|
||||
To hide:
|
||||
|
||||
```
|
||||
UPLOAD_MEDIA_ALLOWED = False
|
||||
```
|
||||
|
||||
### 5.8 Show or hide the actions buttons (like/dislike/report)
|
||||
|
||||
Make changes (True/False) to any of the following:
|
||||
|
||||
```
|
||||
- CAN_LIKE_MEDIA = True # whether the like media appears
|
||||
- CAN_DISLIKE_MEDIA = True # whether the dislike media appears
|
||||
- CAN_REPORT_MEDIA = True # whether the report media appears
|
||||
- CAN_SHARE_MEDIA = True # whether the share media appears
|
||||
```
|
||||
|
||||
### 5.9 Show or hide the download option on a media
|
||||
|
||||
Edit `templates/config/installation/features.html` and set
|
||||
|
||||
```
|
||||
download: false
|
||||
```
|
||||
|
||||
### 5.10 Automatically hide media upon being reported
|
||||
|
||||
set a low number for variable `REPORTED_TIMES_THRESHOLD`
|
||||
eg
|
||||
|
||||
```
|
||||
REPORTED_TIMES_THRESHOLD = 2
|
||||
```
|
||||
|
||||
once the limit is reached, media goes to private state and an email is sent to admins
|
||||
|
||||
### 5.11 Set a custom message on the media upload page
|
||||
|
||||
this message will appear below the media drag and drop form
|
||||
|
||||
```
|
||||
PRE_UPLOAD_MEDIA_MESSAGE = 'custom message'
|
||||
```
|
||||
|
||||
### 5.12 Set email settings
|
||||
|
||||
Set correct settings per provider
|
||||
|
||||
```
|
||||
DEFAULT_FROM_EMAIL = 'info@mediacms.io'
|
||||
EMAIL_HOST_PASSWORD = 'xyz'
|
||||
EMAIL_HOST_USER = 'info@mediacms.io'
|
||||
EMAIL_USE_TLS = True
|
||||
SERVER_EMAIL = DEFAULT_FROM_EMAIL
|
||||
EMAIL_HOST = 'mediacms.io'
|
||||
EMAIL_PORT = 587
|
||||
ADMIN_EMAIL_LIST = ['info@mediacms.io']
|
||||
```
|
||||
|
||||
### 5.13 Disallow user registrations from specific domains
|
||||
|
||||
set domains that are not valid for registration via this variable:
|
||||
|
||||
```
|
||||
RESTRICTED_DOMAINS_FOR_USER_REGISTRATION = [
|
||||
'xxx.com', 'emaildomainwhatever.com']
|
||||
```
|
||||
|
||||
### 5.14 Require a review by MediaCMS editors/managers/admins
|
||||
|
||||
set value
|
||||
|
||||
```
|
||||
MEDIA_IS_REVIEWED = False
|
||||
```
|
||||
|
||||
any uploaded media now needs to be reviewed before it can appear to the listings.
|
||||
MediaCMS editors/managers/admins can visit the media page and edit it, where they can see the option to mark media as reviewed. By default this is set to True, so all media don't require to be reviewed
|
||||
|
||||
### 5.15 Specify maximum number of media for a playlist
|
||||
|
||||
set a different threshold on variable `MAX_MEDIA_PER_PLAYLIST`
|
||||
|
||||
eg
|
||||
|
||||
```
|
||||
MAX_MEDIA_PER_PLAYLIST = 14
|
||||
```
|
||||
|
||||
### 5.16 Specify maximum size of a media that can be uploaded
|
||||
|
||||
change `UPLOAD_MAX_SIZE`.
|
||||
|
||||
default is 4GB
|
||||
|
||||
```
|
||||
UPLOAD_MAX_SIZE = 800 * 1024 * 1000 * 5
|
||||
```
|
||||
|
||||
### 5.17 Specify maximum size of comments
|
||||
|
||||
change `MAX_CHARS_FOR_COMMENT`
|
||||
|
||||
default:
|
||||
|
||||
```
|
||||
MAX_CHARS_FOR_COMMENT = 10000
|
||||
```
|
||||
|
||||
### 5.18 How many files to upload in parallel
|
||||
|
||||
set a different threshold for `UPLOAD_MAX_FILES_NUMBER`
|
||||
default:
|
||||
|
||||
```
|
||||
UPLOAD_MAX_FILES_NUMBER = 100
|
||||
```
|
||||
|
||||
### 5.18 force users confirm their email upon registrations
|
||||
|
||||
default option for email confirmation is optional. Set this to mandatory in order to force users confirm their email before they can login
|
||||
|
||||
```
|
||||
ACCOUNT_EMAIL_VERIFICATION = 'optional'
|
||||
```
|
||||
|
||||
### 5.20 Rate limit account login attempts
|
||||
|
||||
after this number is reached
|
||||
|
||||
```
|
||||
ACCOUNT_LOGIN_ATTEMPTS_LIMIT = 20
|
||||
```
|
||||
|
||||
sets a timeout (in seconds)
|
||||
|
||||
```
|
||||
ACCOUNT_LOGIN_ATTEMPTS_TIMEOUT = 5
|
||||
```
|
||||
|
||||
### 5.21 Disallow user registration
|
||||
|
||||
set the following variable to False
|
||||
|
||||
```
|
||||
USERS_CAN_SELF_REGISTER = True
|
||||
```
|
||||
|
||||
### 5.22 Configure notifications
|
||||
|
||||
Global notifications that are implemented are controlled by the following options:
|
||||
|
||||
```
|
||||
USERS_NOTIFICATIONS = {
|
||||
'MEDIA_ADDED': True,
|
||||
}
|
||||
```
|
||||
|
||||
If you want to disable notification for new media, set to False
|
||||
|
||||
Admins also receive notifications on different events, set any of the following to False to disable
|
||||
|
||||
```
|
||||
ADMINS_NOTIFICATIONS = {
|
||||
'NEW_USER': True,
|
||||
'MEDIA_ADDED': True,
|
||||
'MEDIA_REPORTED': True,
|
||||
}
|
||||
```
|
||||
|
||||
- NEW_USER: a new user is added
|
||||
- MEDIA_ADDED: a media is added
|
||||
- MEDIA_REPORTED: the report for a media was hit
|
||||
|
||||
|
||||
|
||||
## 6. Manage pages
|
||||
to be written
|
||||
|
||||
## 7. Django admin dashboard
|
||||
|
||||
## 8. On portal workflow
|
||||
Who can publish content, how content appears on public listings.Difference between statuses (private, unlisted, public)
|
||||
|
||||
## 9. On user roles
|
||||
Differences over MediaCMS manager, MediaCMS editor, logged in user
|
||||
|
||||
## 10. Adding languages for Captions and subtitles
|
||||
to be written
|
||||
|
||||
## 11. Add/delete categories and tags
|
||||
Through the admin section - http://your_installation/admin/
|
||||
|
||||
## 12. Video transcoding
|
||||
Add / remove resolutions and profiles through http://your_installation/admin/encodeprofile
|
||||
|
||||
## 13. How To Add A Static Page To The Sidebar
|
||||
|
||||
### 1. Create your html page in templates/cms/
|
||||
e.g. duplicate and rename about.html
|
||||
```
|
||||
sudo cp templates/cms/about.html templates/cms/volunteer.html
|
||||
```
|
||||
|
||||
### 2. Create your css file in static/css/
|
||||
```
|
||||
touch static/css/volunteer.css
|
||||
```
|
||||
|
||||
### 3. In your html file, update block headermeta to reflect your new page
|
||||
```
|
||||
{% block headermeta %}
|
||||
<meta property="og:title" content="Volunteer - {{PORTAL_NAME}}">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:description" content="">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": [{
|
||||
"@type": "ListItem",
|
||||
"position": 1,
|
||||
"name": "{{PORTAL_NAME}}",
|
||||
"item": {
|
||||
"@type": "WebPage",
|
||||
"@id": "{{FRONTEND_HOST}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 2,
|
||||
"name": "Volunteer",
|
||||
"item": {
|
||||
"@type": "VolunteerPage",
|
||||
"@id": "{{FRONTEND_HOST}}/volunteer"
|
||||
}
|
||||
}]
|
||||
}
|
||||
</script>
|
||||
<link href="{% static "css/volunteer.css" %}" rel="stylesheet"/>
|
||||
{% endblock headermeta %}
|
||||
```
|
||||
|
||||
### 4. In your html file, update block innercontent to reflect your actual content
|
||||
Write whatever you like.
|
||||
|
||||
### 5. In your css file, write matching styles for you html file.
|
||||
Write whatever you like.
|
||||
|
||||
### 6. Add your view to files/views.py
|
||||
```
|
||||
def volunteer(request):
|
||||
"""Volunteer view"""
|
||||
context = {}
|
||||
return render(request, "cms/volunteer.html", context)
|
||||
```
|
||||
|
||||
### 7. Add your url pattern to files/urls.py
|
||||
```
|
||||
urlpatterns = [
|
||||
url(r"^$", views.index),
|
||||
url(r"^about", views.about, name="about"),
|
||||
url(r"^volunteer", views.volunteer, name="volunteer"),
|
||||
```
|
||||
|
||||
### 8. Add your page to the left sidebar
|
||||
To add a link to your page as a menu item in the left sidebar,
|
||||
add the following code after the last line in _commons.js
|
||||
```
|
||||
/* Checks that a given selector has loaded. */
|
||||
const checkElement = async selector => {
|
||||
while ( document.querySelector(selector) === null) {
|
||||
await new Promise( resolve => requestAnimationFrame(resolve) )
|
||||
}
|
||||
return document.querySelector(selector);
|
||||
};
|
||||
|
||||
/* Checks that sidebar nav menu has loaded, then adds menu item. */
|
||||
checkElement('.nav-menu')
|
||||
.then((element) => {
|
||||
(function(){
|
||||
var a = document.createElement('a');
|
||||
a.href = "/volunteer";
|
||||
a.title = "Volunteer";
|
||||
|
||||
var s = document.createElement('span');
|
||||
s.className = "menu-item-icon";
|
||||
|
||||
var icon = document.createElement('i');
|
||||
icon.className = "material-icons";
|
||||
icon.setAttribute("data-icon", "people");
|
||||
|
||||
s.appendChild(icon);
|
||||
a.appendChild(s);
|
||||
|
||||
var linkText = document.createTextNode("Volunteer");
|
||||
var t = document.createElement('span');
|
||||
|
||||
t.appendChild(linkText);
|
||||
a.appendChild(t);
|
||||
|
||||
var listItem = document.createElement('li');
|
||||
listItem.className = "link-item";
|
||||
listItem.appendChild(a);
|
||||
|
||||
//if signed out use 3rd nav-menu
|
||||
var elem = document.querySelector(".nav-menu:nth-child(3) nav ul");
|
||||
var loc = elem.innerText;
|
||||
if (loc.includes("About")){
|
||||
elem.insertBefore(listItem, elem.children[2]);
|
||||
} else { //if signed in use 4th nav-menu
|
||||
elem = document.querySelector(".nav-menu:nth-child(4) nav ul");
|
||||
elem.insertBefore(listItem, elem.children[2]);
|
||||
}
|
||||
})();
|
||||
});
|
||||
```
|
||||
|
||||
### 9. Restart the mediacms web server
|
||||
On docker:
|
||||
```
|
||||
sudo docker stop mediacms_web_1 && sudo docker start mediacms_web_1
|
||||
```
|
||||
|
||||
Otherwise
|
||||
```
|
||||
sudo systemctl restart mediacms
|
||||
```
|
||||
|
||||
|
||||
## 14. Add Google Analytics
|
||||
Instructions contributed by @alberto98fx
|
||||
|
||||
1. Create a file:
|
||||
|
||||
``` touch $DIR/mediacms/templates/tracking.html ```
|
||||
|
||||
2. Add the Gtag/Analytics script
|
||||
|
||||
3. Inside ``` $DIR/mediacms/templates/root.html``` you'll see a file like this one:
|
||||
|
||||
```
|
||||
<head>
|
||||
{% block head %}
|
||||
|
||||
<title>{% block headtitle %}{{PORTAL_NAME}}{% endblock headtitle %}</title>
|
||||
|
||||
{% include "common/head-meta.html" %}
|
||||
|
||||
{% block headermeta %}
|
||||
|
||||
<meta property="og:title" content="{{PORTAL_NAME}}">
|
||||
<meta property="og:type" content="website">
|
||||
|
||||
{%endblock headermeta %}
|
||||
|
||||
{% block externallinks %}{% endblock externallinks %}
|
||||
|
||||
{% include "common/head-links.html" %}
|
||||
|
||||
{% block topimports %}{%endblock topimports %}
|
||||
|
||||
{% include "config/index.html" %}
|
||||
|
||||
{% endblock head %}
|
||||
|
||||
</head>
|
||||
```
|
||||
|
||||
4. Add ``` {% include "tracking.html" %} ``` at the end inside the section ```<head>```
|
||||
|
||||
5. If you are using Docker and didn't mount the entire dir you need to bind a new volume:
|
||||
```
|
||||
|
||||
web:
|
||||
image: mediacms/mediacms:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
deploy:
|
||||
replicas: 1
|
||||
volumes:
|
||||
- ./templates/root.html:/home/mediacms.io/mediacms/templates/root.html
|
||||
- ./templates/tracking.html://home/mediacms.io/mediacms/templates/tracking.html
|
||||
|
||||
```
|
||||
128
docs/developers_docs.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Developers documentation
|
||||
|
||||
## Table of contents
|
||||
- [1. Welcome](#1-welcome)
|
||||
- [2. System architecture](#2-system-architecture)
|
||||
- [3. API documentation](#3-api-documentation)
|
||||
- [4. How to contribute](#4-how-to-contribute)
|
||||
- [5. Working with Docker tips](#5-working-with-docker-tips)
|
||||
- [6. Working with the automated tests](#6-working-with-the-automated-tests)
|
||||
- [7. How video is transcoded](#7-how-video-is-transcoded)
|
||||
|
||||
## 1. Welcome
|
||||
This page is created for MediaCMS developers and contains related information.
|
||||
|
||||
## 2. System architecture
|
||||
to be written
|
||||
|
||||
## 3. API documentation
|
||||
API is documented using Swagger - checkout ot http://your_installation/swagger - example https://demo.mediacms.io/swagger/
|
||||
This page allows you to login to perform authenticated actions - it will also use your session if logged in.
|
||||
|
||||
## 4. How to contribute
|
||||
Before you send a PR, make sure your code is properly formatted. For that, use `pre-commit install` to install a pre-commit hook and run `pre-commit run --all` and fix everything before you commit. This pre-commit will check for your code lint everytime you commit a code.
|
||||
|
||||
Checkout the [Code of conduct page](../CODE_OF_CONDUCT.md) if you want to contribute to this repository
|
||||
|
||||
|
||||
## 5. Working with Docker tips
|
||||
|
||||
To perform the Docker installation, follow instructions to install Docker + Docker compose (docs/Docker_Compose.md) and then build/start docker-compose-dev.yaml . This will run the frontend application on port 8088 on top of all other containers (including the Django web application on port 80)
|
||||
|
||||
```
|
||||
docker-compose -f docker-compose-dev.yaml build
|
||||
docker-compose -f docker-compose-dev.yaml up
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
```
|
||||
docker-compose -f docker-compose-dev.yaml exec -T frontend npm run dist
|
||||
```
|
||||
|
||||
And then in order for the changes to be visible on the application while served through nginx,
|
||||
|
||||
```
|
||||
cp -r frontend/dist/static/* static/
|
||||
```
|
||||
|
||||
POST calls: cannot be performed through the dev server, you have to make through the normal application (port 80) and then see changes on the dev application on port 8088.
|
||||
Make sure the urls are set on `frontend/.env` if different than localhost
|
||||
|
||||
|
||||
Media page: need to upload content through the main application (nginx/port 80), and then use an id for page media.html, for example `http://localhost:8088/media.html?m=nc9rotyWP`
|
||||
|
||||
There are some issues with CORS too to resolve, in order for some pages to function, eg the manage comments page
|
||||
|
||||
```
|
||||
http://localhost:8088/manage-media.html manage_media
|
||||
```
|
||||
|
||||
### Backend application changes
|
||||
After I make changes to the django application (eg make a change on `files/forms.py`) in order to see the changes I have to restart the web container
|
||||
|
||||
```
|
||||
docker-compose -f docker-compose-dev.yaml restart web
|
||||
```
|
||||
|
||||
## How video is transcoded
|
||||
|
||||
Original files get uploaded to the application server, and they get stored there as FileFields.
|
||||
|
||||
If files are videos and the duration is greater than a number (defined on settings, I think 4minutes), they are also broken in chunks, so one Encode object per chunk, for all enabled EncodeProfiles.
|
||||
|
||||
Then the workers start picking Encode objects and they transcode the chunks, so if a chunk gets transcoded correctly, the original file (the small chunk) gets replaced by the transcoded file, and the Encode object status is marked as 'success'.
|
||||
|
||||
|
||||
original.mp4 (1G, 720px)--> Encode1 (100MB, 240px, chunk=True), Encode2 (100MB, 240px, chunk=True)...EncodeXX (100MB, 720px, chunk=True) ---> when all Encode objects are success, for a resolution, they get concatenated to the original_resolution.mp4 file and this gets stored as Encode object (chunk=False). This is what is available for download.
|
||||
|
||||
Apparently the Encode object is used to store Encoded files that are served eventually (chunk=False, status='success'), but also files while they are on their way to get transcoded (chunk=True, status='pending/etc')
|
||||
|
||||
(Parenthesis opening)
|
||||
there is also an experimental small service (not commited to the repo currently) that speaks only through API and a) gets tasks to run, b) returns results. So it makes a request and receives an ffmpeg command, plus a file, it runs the ffmpeg command, and returns the result.I've used this mechanism on a number of installations to migrate existing videos through more servers/cpu and has worked with only one problem, some temporary files needed to be removed from the servers (through a periodic task, not so big problem)
|
||||
(Parenthesis closing)
|
||||
|
||||
|
||||
When the Encode object is marked as success and chunk=False, and thus is available for download/stream, there is a task that gets started and saves an HLS version of the file (1 mp4-->x number of small .ts chunks). This would be FILES_C
|
||||
|
||||
This mechanism allows for workers that have access on the same filesystem (either localhost, or through a shared network filesystem, eg NFS/EFS) to work on the same time and produce results.
|
||||
|
||||
## 6. Working with the automated tests
|
||||
|
||||
This instructions assume that you're using the docker installation
|
||||
|
||||
1. start docker-compose
|
||||
|
||||
```
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
2. Install the requirements on `requirements-dev.txt ` on web container (we'll use the web container for this)
|
||||
|
||||
```
|
||||
docker-compose exec -T web pip install -r requirements-dev.txt
|
||||
```
|
||||
|
||||
3. Now you can run the existing tests
|
||||
|
||||
```
|
||||
docker-compose exec --env TESTING=True -T web pytest
|
||||
```
|
||||
|
||||
The `TESTING=True` is passed for Django to be aware this is a testing environment (so that it runs Celery tasks as functions for example and not as background tasks, since Celery is not started in the case of pytest)
|
||||
|
||||
|
||||
4. You may try a single test, by specifying the path, for example
|
||||
|
||||
```
|
||||
docker-compose exec --env TESTING=True -T web pytest tests/test_fixtures.py
|
||||
```
|
||||
|
||||
5. You can also see the coverage
|
||||
|
||||
```
|
||||
docker-compose exec --env TESTING=True -T web pytest --cov=. --cov-report=html
|
||||
```
|
||||
|
||||
and of course...you are very welcome to help us increase it ;)
|
||||
BIN
docs/images/CC-display.png
Executable file
|
After Width: | Height: | Size: 567 KiB |
BIN
docs/images/Click-ADD-button.png
Executable file
|
After Width: | Height: | Size: 17 KiB |
BIN
docs/images/Click-Browse-button.png
Executable file
|
After Width: | Height: | Size: 22 KiB |
BIN
docs/images/Click-Download-Button.png
Executable file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
docs/images/Click-EDIT-SUBTITLE.png
Executable file
|
After Width: | Height: | Size: 509 KiB |
BIN
docs/images/Click-Edit-Media-button.png
Executable file
|
After Width: | Height: | Size: 12 KiB |
BIN
docs/images/Click-Subtitle-Language-Menu.png
Executable file
|
After Width: | Height: | Size: 18 KiB |
BIN
docs/images/Click-Upload-Media-button.png
Executable file
|
After Width: | Height: | Size: 502 KiB |
BIN
docs/images/Click-View-media-button.png
Executable file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
docs/images/Click-version-download.png
Executable file
|
After Width: | Height: | Size: 22 KiB |
BIN
docs/images/Continue-button.png
Executable file
|
After Width: | Height: | Size: 11 KiB |
BIN
docs/images/Edit-Media-Metadata-1.png
Executable file
|
After Width: | Height: | Size: 76 KiB |
BIN
docs/images/Edit-Media-Metadata-2.png
Executable file
|
After Width: | Height: | Size: 56 KiB |
BIN
docs/images/Pause-button.png
Executable file
|
After Width: | Height: | Size: 10 KiB |
BIN
docs/images/Processing.png
Executable file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
docs/images/Save-File.png
Executable file
|
After Width: | Height: | Size: 55 KiB |
BIN
docs/images/Select-Media-File-Click-Open.png
Executable file
|
After Width: | Height: | Size: 76 KiB |
BIN
docs/images/Subtitles-captions1.png
Executable file
|
After Width: | Height: | Size: 24 KiB |
BIN
docs/images/Subtitles-captions2.png
Executable file
|
After Width: | Height: | Size: 40 KiB |
BIN
docs/images/Subtitles-captions3.png
Executable file
|
After Width: | Height: | Size: 89 KiB |
BIN
docs/images/Uploading.png
Executable file
|
After Width: | Height: | Size: 9.5 KiB |
@@ -1,55 +0,0 @@
|
||||
# Google Analytics
|
||||
|
||||
1. Create a file:
|
||||
|
||||
``` touch $DIR/mediacms/templates/tracking.html ```
|
||||
|
||||
2. Add the Gtag/Analytics script
|
||||
|
||||
3. Inside ``` $DIR/mediacms/templates/root.html``` you'll see a file like this one:
|
||||
|
||||
```
|
||||
<head>
|
||||
{% block head %}
|
||||
|
||||
<title>{% block headtitle %}{{PORTAL_NAME}}{% endblock headtitle %}</title>
|
||||
|
||||
{% include "common/head-meta.html" %}
|
||||
|
||||
{% block headermeta %}
|
||||
|
||||
<meta property="og:title" content="{{PORTAL_NAME}}">
|
||||
<meta property="og:type" content="website">
|
||||
|
||||
{%endblock headermeta %}
|
||||
|
||||
{% block externallinks %}{% endblock externallinks %}
|
||||
|
||||
{% include "common/head-links.html" %}
|
||||
|
||||
{% block topimports %}{%endblock topimports %}
|
||||
|
||||
{% include "config/index.html" %}
|
||||
|
||||
{% endblock head %}
|
||||
|
||||
</head>
|
||||
```
|
||||
|
||||
4. Add ``` {% include "tracking.html" %} ``` at the end inside the section ```<head>```
|
||||
|
||||
5. If you are using Docker and didn't mount the entire dir you need to bind a new volume:
|
||||
```
|
||||
|
||||
web:
|
||||
image: mediacms/mediacms:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
deploy:
|
||||
replicas: 1
|
||||
volumes:
|
||||
- ./templates/root.html:/home/mediacms.io/mediacms/templates/root.html
|
||||
- ./templates/tracking.html://home/mediacms.io/mediacms/templates/tracking.html
|
||||
|
||||
```
|
||||
208
docs/user_docs.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# Users documentation
|
||||
|
||||
## Table of contents
|
||||
- [Uploading media](#uploading-media)
|
||||
- [Downloading media](#downloading-media)
|
||||
- [Adding captions/subtitles](#adding-captionssubtitles)
|
||||
- [Search media](#search-media)
|
||||
- [Share media](#share-media)
|
||||
- [Embed media](#embed-media)
|
||||
- [Customize my profile options](#customize-my-profile-options)
|
||||
|
||||
## Uploading media
|
||||
|
||||
### How to Upload Media
|
||||
|
||||
Uploading media is as simple as clicking the _Upload Media_ button, waiting for media to upload, and then clicking the media to add metadata (title, description etc.) by filling out a form.
|
||||
|
||||
#### Click Upload Button
|
||||
|
||||
Click the _Upload Media_ button from the right-side of the screen at the top:
|
||||
|
||||
<p align="left">
|
||||
<img src="./images/Click-Upload-Media-button.png"/>
|
||||
</p>
|
||||
|
||||
#### Upload Page
|
||||
|
||||
Clicking the _Upload Media_ button takes you to the upload page at a URL like this:
|
||||
|
||||
https://demo.mediacms.io/upload
|
||||
|
||||
#### Click Browse Button
|
||||
|
||||
Here you should click the _"Browse your files"_ button (or drag and drop a file from your desktop):
|
||||
|
||||
<p align="left">
|
||||
<img src="./images/Click-Browse-button.png"/>
|
||||
</p>
|
||||
|
||||
#### Select media file and click Open button
|
||||
|
||||
Select the media file that you want to upload and click the _"Open"_ button:
|
||||
|
||||
<p align="left">
|
||||
<img src="./images/Select-Media-File-Click-Open.png"/>
|
||||
</p>
|
||||
|
||||
#### Wait for file to upload
|
||||
|
||||
Wait for the file to finish uploading:
|
||||
|
||||
<p align="left">
|
||||
<img src="./images/Uploading.png"/>
|
||||
</p>
|
||||
|
||||
#### Pause uploading
|
||||
|
||||
If you want you can pause upload by clicking _Pause button_:
|
||||
|
||||
<p align="left">
|
||||
<img src="./images/Pause-button.png"/>
|
||||
</p>
|
||||
|
||||
#### Continue uploading
|
||||
|
||||
Continue upload by clicking _Continue button_:
|
||||
|
||||
<p align="left">
|
||||
<img src="./images/Continue-button.png"/>
|
||||
</p>
|
||||
|
||||
#### Wait for media to finish Processing
|
||||
|
||||
Wait for the media file to finish Processing:
|
||||
|
||||
<p align="left">
|
||||
<img src="./images/Processing.png"/>
|
||||
</p>
|
||||
|
||||
#### Click View media button
|
||||
|
||||
Click the View media button to open the media page:
|
||||
|
||||
<p align="left">
|
||||
<img src="./images/Click-View-media-button.png"/>
|
||||
</p>
|
||||
|
||||
#### Media will be in the encoding queue
|
||||
|
||||
The media will take some time to finish encoding (MediaCMS will transcode the file to several formats and resolutions). Meanwhile you can edit the media file to add metadata.
|
||||
|
||||
#### Click Edit Media button
|
||||
|
||||
Click the EDIT MEDIA button to add metadata and configure the poster image:
|
||||
|
||||
<p align="left">
|
||||
<img src="./images/Click-Edit-Media-button.png"/>
|
||||
</p>
|
||||
|
||||
#### Add Metadata (Summary, Description etc.)
|
||||
|
||||
Make sure you fill in all the required fields, and try to complete as many of the non-required fields as possible. This ensures the database is populated with useful meta-data to help others access useful information about you and your video.
|
||||
|
||||
<p align="left">
|
||||
<img src="./images/Edit-Media-Metadata-1.png"/>
|
||||
</p>
|
||||
|
||||
<p align="left">
|
||||
<img src="./images/Edit-Media-Metadata-2.png"/>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
## Downloading media
|
||||
|
||||
MediaCMS offers a configurable option whereby users can make their media files available for download. Downloads are available for transcoded files, and the original file.
|
||||
|
||||
#### How To Enable Download
|
||||
|
||||
Visit the media view page and choose the EDIT MEDIA button.
|
||||
|
||||
Select the checkbox for "Allow Downloads"
|
||||
|
||||
#### How To Download Media
|
||||
|
||||
Visit the media view page and click the DOWNLOAD button below the video player.
|
||||
|
||||
<p align="left">
|
||||
<img src="./images/Click-Download-Button.png">
|
||||
</p>
|
||||
|
||||
Choose the version you wish to download - a transcoded file or the original file:
|
||||
|
||||
<p align="left">
|
||||
<img src="./images/Click-version-download.png">
|
||||
</p>
|
||||
|
||||
Choose Save File and click the OK button.
|
||||
|
||||
<p align="left">
|
||||
<img src="./images/Save-File.png">
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
## Adding captions/subtitles
|
||||
|
||||
With MediaCMS you can add subtitles/captions to your video by uploading a subtitles file in the popular .vtt format.
|
||||
|
||||
### Visit Media Page & Click EDIT SUBTITLE Button
|
||||
|
||||
Visit the "single media page" for the media you wish to add subtitles/captions to and click the EDIT SUBTITLES button:
|
||||
|
||||
<p align="left">
|
||||
<img src="./images/Click-EDIT-SUBTITLE.png"/>
|
||||
</p>
|
||||
|
||||
### Upload Subtitles in .vtt Format
|
||||
|
||||
Click the Language menu to select the correct language for your Subtitles/Captions file:
|
||||
|
||||
<p align="left">
|
||||
<img src="./images/Click-Subtitle-Language-Menu.png"/>
|
||||
</p>
|
||||
|
||||
Choose the correct Language for your file:
|
||||
|
||||
<p align="left">
|
||||
<img src="./images/Subtitles-captions1.png"/>
|
||||
</p>
|
||||
|
||||
Click Browse to find a subtitles/captions file on your computer (if your file is not in the .vtt format, you may find a conversion service on the Internet):
|
||||
|
||||
<p align="left">
|
||||
<img src="./images/Subtitles-captions2.png"/>
|
||||
</p>
|
||||
|
||||
Choose a .vtt subtitles/captions file from your computer:
|
||||
|
||||
<p align="left">
|
||||
<img src="./images/Subtitles-captions3.png"/>
|
||||
</p>
|
||||
|
||||
Click the Add button to add the file to your media:
|
||||
|
||||
<p align="left">
|
||||
<img src="./images/Click-ADD-button.png"/>
|
||||
</p>
|
||||
|
||||
### View Subtitles/Captions in Video Player
|
||||
|
||||
You can now watch the captions/subtitles play back in the video player - and toggle display on/off by clicking the CC button:
|
||||
|
||||
<p align="left">
|
||||
<img src="./images/CC-display.png"/>
|
||||
</p>
|
||||
|
||||
## Search media
|
||||
How search can be used
|
||||
|
||||
## Share media
|
||||
How to share media
|
||||
|
||||
## Embed media
|
||||
How to use the embed media option
|
||||
|
||||
## Customize my profile options
|
||||
Customize profile and channel
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
@@ -23,6 +22,8 @@ CRF_ENCODING_NUM_SECONDS = 2 # 0 * 60 # videos with greater duration will get
|
||||
# you should use CRF encoding.
|
||||
|
||||
MAX_RATE_MULTIPLIER = 1.5
|
||||
MIN_RATE_MULTIPLIER = 0.5
|
||||
|
||||
BUF_SIZE_MULTIPLIER = 1.5
|
||||
|
||||
# in seconds, anything between 2 and 6 makes sense
|
||||
@@ -238,7 +239,8 @@ def media_file_info(input_file):
|
||||
- `filename`: Filename
|
||||
- `file_size`: Size of the file in bytes
|
||||
- `video_duration`: Duration of the video in `s.msec`
|
||||
- `video_frame_rate`: Framerate in Hz
|
||||
- `video_frame_rate_d`: Framerate franction denominator
|
||||
- `video_frame_rate_n`: Framerate fraction nominator
|
||||
- `video_bitrate`: Bitrate of the video stream in kBit/s
|
||||
- `video_width`: Width in pixels
|
||||
- `video_height`: Height in pixels
|
||||
@@ -367,17 +369,30 @@ def media_file_info(input_file):
|
||||
stream_size = sum([int(line) for line in stdout.split("\n") if line != ""])
|
||||
video_bitrate = round((stream_size * 8 / 1024.0) / video_duration, 2)
|
||||
|
||||
if "r_frame_rate" in video_info.keys():
|
||||
video_frame_rate = video_info["r_frame_rate"].partition("/")
|
||||
video_frame_rate_n = video_frame_rate[0]
|
||||
video_frame_rate_d = video_frame_rate[2]
|
||||
|
||||
ret = {
|
||||
"filename": input_file,
|
||||
"file_size": file_size,
|
||||
"video_duration": video_duration,
|
||||
"video_frame_rate": float(Fraction(video_info["r_frame_rate"])),
|
||||
"video_frame_rate_n": video_frame_rate_n,
|
||||
"video_frame_rate_d": video_frame_rate_d,
|
||||
"video_bitrate": video_bitrate,
|
||||
"video_width": video_info["width"],
|
||||
"video_height": video_info["height"],
|
||||
"video_codec": video_info["codec_name"],
|
||||
"has_video": has_video,
|
||||
"has_audio": has_audio,
|
||||
"color_range": video_info.get("color_range"),
|
||||
"color_space": video_info.get("color_space"),
|
||||
"color_transfer": video_info.get("color_space"),
|
||||
"color_primaries": video_info.get("color_primaries"),
|
||||
"field_order": video_info.get("field_order"),
|
||||
"display_aspect_ratio": video_info.get("display_aspect_ratio"),
|
||||
"sample_aspect_ratio": video_info.get("sample_aspect_ratio"),
|
||||
}
|
||||
|
||||
if has_audio:
|
||||
@@ -492,7 +507,7 @@ def get_base_ffmpeg_command(
|
||||
codec {str} -- video codec
|
||||
encoder {str} -- video encoder
|
||||
audio_encoder {str} -- audio encoder
|
||||
target_fps {int} -- target FPS
|
||||
target_fps {fractions.Fraction} -- target FPS
|
||||
target_height {int} -- height
|
||||
target_rate {int} -- target bitrate in kbps
|
||||
target_rate_audio {int} -- audio target bitrate
|
||||
@@ -501,10 +516,12 @@ def get_base_ffmpeg_command(
|
||||
enc_type {str} -- encoding type (twopass or crf)
|
||||
"""
|
||||
|
||||
target_fps = int(target_fps)
|
||||
# avoid Frame rate very high for a muxer not efficiently supporting it.
|
||||
if target_fps > 90:
|
||||
target_fps = 90
|
||||
# avoid very high frame rates
|
||||
while target_fps > 60:
|
||||
target_fps = target_fps / 2
|
||||
|
||||
if target_fps < 1:
|
||||
target_fps = 1
|
||||
|
||||
base_cmd = [
|
||||
settings.FFMPEG_COMMAND,
|
||||
@@ -622,6 +639,8 @@ def get_base_ffmpeg_command(
|
||||
str(keyframe_distance),
|
||||
"-maxrate",
|
||||
str(int(int(target_rate) * MAX_RATE_MULTIPLIER)) + "k",
|
||||
"-minrate",
|
||||
str(int(int(target_rate) * MIN_RATE_MULTIPLIER)) + "k",
|
||||
"-bufsize",
|
||||
str(int(int(target_rate) * BUF_SIZE_MULTIPLIER)) + "k",
|
||||
"-speed",
|
||||
@@ -669,8 +688,8 @@ def produce_ffmpeg_commands(media_file, media_info, resolution, codec, output_fi
|
||||
else:
|
||||
return False
|
||||
|
||||
src_framerate = media_info.get("video_frame_rate", 30)
|
||||
if src_framerate <= 30:
|
||||
target_fps = Fraction(int(media_info.get("video_frame_rate_n", 30)), int(media_info.get("video_frame_rate_d", 1)))
|
||||
if target_fps <= 30:
|
||||
target_rate = VIDEO_BITRATES[codec][25].get(resolution)
|
||||
else:
|
||||
target_rate = VIDEO_BITRATES[codec][60].get(resolution)
|
||||
@@ -687,9 +706,6 @@ def produce_ffmpeg_commands(media_file, media_info, resolution, codec, output_fi
|
||||
# target_fps = 25
|
||||
# else:
|
||||
|
||||
# adjust the target frame rate if the input is fractional
|
||||
target_fps = src_framerate if isinstance(src_framerate, int) else math.ceil(src_framerate)
|
||||
|
||||
if media_info.get("video_duration") > CRF_ENCODING_NUM_SECONDS:
|
||||
enc_type = "crf"
|
||||
else:
|
||||
|
||||
1
files/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .user_utils import create_account # noqa
|
||||
24
files/tests/user_utils.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from faker import Factory
|
||||
|
||||
from users.models import User
|
||||
|
||||
faker = Factory.create()
|
||||
|
||||
|
||||
def create_account(username=None, email=None, password=None, name=None, **kwargs):
|
||||
"Allow to create accounts by passing None or specific arguements"
|
||||
email = email or faker.email()
|
||||
username = username or email.split('@')[0]
|
||||
password = password or faker.password()
|
||||
name = name or faker.name()
|
||||
|
||||
description = kwargs.get('description') or faker.text()
|
||||
is_superuser = kwargs.get('is_superuser') or False
|
||||
is_manager = kwargs.get('is_manager') or False
|
||||
is_editor = kwargs.get('is_editor') or False
|
||||
|
||||
user = User.objects.create(username=username, email=email, name=name, description=description, is_superuser=is_superuser, is_staff=is_superuser, is_editor=is_editor, is_manager=is_manager)
|
||||
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
return user
|
||||
100
files/urls.py
@@ -1,5 +1,5 @@
|
||||
from django.conf import settings
|
||||
from django.conf.urls import include, url
|
||||
from django.conf.urls import include, re_path
|
||||
from django.conf.urls.static import static
|
||||
from django.urls import path
|
||||
|
||||
@@ -7,85 +7,85 @@ from . import management_views, views
|
||||
from .feeds import IndexRSSFeed, SearchRSSFeed
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^$", views.index),
|
||||
url(r"^about", views.about, name="about"),
|
||||
url(r"^add_subtitle", views.add_subtitle, name="add_subtitle"),
|
||||
url(r"^categories$", views.categories, name="categories"),
|
||||
url(r"^contact$", views.contact, name="contact"),
|
||||
url(r"^edit", views.edit_media, name="edit_media"),
|
||||
url(r"^embed", views.embed_media, name="get_embed"),
|
||||
url(r"^featured$", views.featured_media),
|
||||
url(r"^fu/", include(("uploader.urls", "uploader"), namespace="uploader")),
|
||||
url(r"^history$", views.history, name="history"),
|
||||
url(r"^liked$", views.liked_media, name="liked_media"),
|
||||
url(r"^latest$", views.latest_media),
|
||||
url(r"^members", views.members, name="members"),
|
||||
url(
|
||||
re_path(r"^$", views.index),
|
||||
re_path(r"^about", views.about, name="about"),
|
||||
re_path(r"^add_subtitle", views.add_subtitle, name="add_subtitle"),
|
||||
re_path(r"^categories$", views.categories, name="categories"),
|
||||
re_path(r"^contact$", views.contact, name="contact"),
|
||||
re_path(r"^edit", views.edit_media, name="edit_media"),
|
||||
re_path(r"^embed", views.embed_media, name="get_embed"),
|
||||
re_path(r"^featured$", views.featured_media),
|
||||
re_path(r"^fu/", include(("uploader.urls", "uploader"), namespace="uploader")),
|
||||
re_path(r"^history$", views.history, name="history"),
|
||||
re_path(r"^liked$", views.liked_media, name="liked_media"),
|
||||
re_path(r"^latest$", views.latest_media),
|
||||
re_path(r"^members", views.members, name="members"),
|
||||
re_path(
|
||||
r"^playlist/(?P<friendly_token>[\w]*)$",
|
||||
views.view_playlist,
|
||||
name="get_playlist",
|
||||
),
|
||||
url(
|
||||
re_path(
|
||||
r"^playlists/(?P<friendly_token>[\w]*)$",
|
||||
views.view_playlist,
|
||||
name="get_playlist",
|
||||
),
|
||||
url(r"^popular$", views.recommended_media),
|
||||
url(r"^recommended$", views.recommended_media),
|
||||
re_path(r"^popular$", views.recommended_media),
|
||||
re_path(r"^recommended$", views.recommended_media),
|
||||
path("rss/", IndexRSSFeed()),
|
||||
url("^rss/search", SearchRSSFeed()),
|
||||
url(r"^search", views.search, name="search"),
|
||||
url(r"^scpublisher", views.upload_media, name="upload_media"),
|
||||
url(r"^tags", views.tags, name="tags"),
|
||||
url(r"^tos$", views.tos, name="terms_of_service"),
|
||||
url(r"^view", views.view_media, name="get_media"),
|
||||
url(r"^upload", views.upload_media, name="upload_media"),
|
||||
re_path("^rss/search", SearchRSSFeed()),
|
||||
re_path(r"^search", views.search, name="search"),
|
||||
re_path(r"^scpublisher", views.upload_media, name="upload_media"),
|
||||
re_path(r"^tags", views.tags, name="tags"),
|
||||
re_path(r"^tos$", views.tos, name="terms_of_service"),
|
||||
re_path(r"^view", views.view_media, name="get_media"),
|
||||
re_path(r"^upload", views.upload_media, name="upload_media"),
|
||||
# API VIEWS
|
||||
url(r"^api/v1/media$", views.MediaList.as_view()),
|
||||
url(r"^api/v1/media/$", views.MediaList.as_view()),
|
||||
url(
|
||||
re_path(r"^api/v1/media$", views.MediaList.as_view()),
|
||||
re_path(r"^api/v1/media/$", views.MediaList.as_view()),
|
||||
re_path(
|
||||
r"^api/v1/media/(?P<friendly_token>[\w]*)$",
|
||||
views.MediaDetail.as_view(),
|
||||
name="api_get_media",
|
||||
),
|
||||
url(
|
||||
re_path(
|
||||
r"^api/v1/media/encoding/(?P<encoding_id>[\w]*)$",
|
||||
views.EncodingDetail.as_view(),
|
||||
name="api_get_encoding",
|
||||
),
|
||||
url(r"^api/v1/search$", views.MediaSearch.as_view()),
|
||||
url(
|
||||
re_path(r"^api/v1/search$", views.MediaSearch.as_view()),
|
||||
re_path(
|
||||
r"^api/v1/media/(?P<friendly_token>[\w]*)/actions$",
|
||||
views.MediaActions.as_view(),
|
||||
),
|
||||
url(r"^api/v1/categories$", views.CategoryList.as_view()),
|
||||
url(r"^api/v1/tags$", views.TagList.as_view()),
|
||||
url(r"^api/v1/comments$", views.CommentList.as_view()),
|
||||
url(
|
||||
re_path(r"^api/v1/categories$", views.CategoryList.as_view()),
|
||||
re_path(r"^api/v1/tags$", views.TagList.as_view()),
|
||||
re_path(r"^api/v1/comments$", views.CommentList.as_view()),
|
||||
re_path(
|
||||
r"^api/v1/media/(?P<friendly_token>[\w]*)/comments$",
|
||||
views.CommentDetail.as_view(),
|
||||
),
|
||||
url(
|
||||
re_path(
|
||||
r"^api/v1/media/(?P<friendly_token>[\w]*)/comments/(?P<uid>[\w-]*)$",
|
||||
views.CommentDetail.as_view(),
|
||||
),
|
||||
url(r"^api/v1/playlists$", views.PlaylistList.as_view()),
|
||||
url(r"^api/v1/playlists/$", views.PlaylistList.as_view()),
|
||||
url(
|
||||
re_path(r"^api/v1/playlists$", views.PlaylistList.as_view()),
|
||||
re_path(r"^api/v1/playlists/$", views.PlaylistList.as_view()),
|
||||
re_path(
|
||||
r"^api/v1/playlists/(?P<friendly_token>[\w]*)$",
|
||||
views.PlaylistDetail.as_view(),
|
||||
name="api_get_playlist",
|
||||
),
|
||||
url(r"^api/v1/user/action/(?P<action>[\w]*)$", views.UserActions.as_view()),
|
||||
re_path(r"^api/v1/user/action/(?P<action>[\w]*)$", views.UserActions.as_view()),
|
||||
# ADMIN VIEWS
|
||||
url(r"^api/v1/encode_profiles/$", views.EncodeProfileList.as_view()),
|
||||
url(r"^api/v1/manage_media$", management_views.MediaList.as_view()),
|
||||
url(r"^api/v1/manage_comments$", management_views.CommentList.as_view()),
|
||||
url(r"^api/v1/manage_users$", management_views.UserList.as_view()),
|
||||
url(r"^api/v1/tasks$", views.TasksList.as_view()),
|
||||
url(r"^api/v1/tasks/$", views.TasksList.as_view()),
|
||||
url(r"^api/v1/tasks/(?P<friendly_token>[\w|\W]*)$", views.TaskDetail.as_view()),
|
||||
url(r"^manage/comments$", views.manage_comments, name="manage_comments"),
|
||||
url(r"^manage/media$", views.manage_media, name="manage_media"),
|
||||
url(r"^manage/users$", views.manage_users, name="manage_users"),
|
||||
re_path(r"^api/v1/encode_profiles/$", views.EncodeProfileList.as_view()),
|
||||
re_path(r"^api/v1/manage_media$", management_views.MediaList.as_view()),
|
||||
re_path(r"^api/v1/manage_comments$", management_views.CommentList.as_view()),
|
||||
re_path(r"^api/v1/manage_users$", management_views.UserList.as_view()),
|
||||
re_path(r"^api/v1/tasks$", views.TasksList.as_view()),
|
||||
re_path(r"^api/v1/tasks/$", views.TasksList.as_view()),
|
||||
re_path(r"^api/v1/tasks/(?P<friendly_token>[\w|\W]*)$", views.TaskDetail.as_view()),
|
||||
re_path(r"^manage/comments$", views.manage_comments, name="manage_comments"),
|
||||
re_path(r"^manage/media$", views.manage_media, name="manage_media"),
|
||||
re_path(r"^manage/users$", views.manage_users, name="manage_users"),
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
||||
23
install.sh
@@ -21,21 +21,24 @@ It is expected to run on a new system **with no running instances of any these s
|
||||
done
|
||||
|
||||
|
||||
if [[ `lsb_release -d` == *"Ubuntu 20"* ]]; then
|
||||
osVersion=$(lsb_release -d)
|
||||
if [[ $osVersion == *"Ubuntu 20"* ]] || [[ $osVersion == *"Ubuntu 18"* ]] || [[ $osVersion == *"buster"* ]]; then
|
||||
echo 'Performing system update and dependency installation, this will take a few minutes'
|
||||
apt-get update && apt-get -y upgrade && apt install python3-venv python3-dev virtualenv redis-server postgresql nginx git gcc vim unzip ffmpeg imagemagick python3-certbot-nginx certbot wget -y
|
||||
elif [[ `lsb_release -d` = *"Ubuntu 18"* ]]; then
|
||||
echo 'Performing system update and dependency installation, this will take a few minutes'
|
||||
apt-get update && apt-get -y upgrade && apt install python3-venv python3-dev virtualenv redis-server postgresql nginx git gcc vim unzip ffmpeg imagemagick python3-certbot-nginx certbot wget -y
|
||||
# added check for Debian 10 (buster)
|
||||
elif [[ `lsb_release -d` == *"buster"* ]]; then
|
||||
echo 'Performing system update and dependency installation, this will take a few minutes'
|
||||
apt-get update && apt-get -y upgrade && apt install python3-venv python3-dev virtualenv redis-server postgresql nginx git gcc vim unzip ffmpeg imagemagick python3-certbot-nginx certbot wget -y
|
||||
apt-get update && apt-get -y upgrade && apt-get install python3-venv python3-dev virtualenv redis-server postgresql nginx git gcc vim unzip imagemagick python3-certbot-nginx certbot wget xz-utils -y
|
||||
else
|
||||
echo "This script is tested for Ubuntu 18 and 20 versions only, if you want to try MediaCMS on another system you have to perform the manual installation"
|
||||
exit
|
||||
exit
|
||||
fi
|
||||
|
||||
# install ffmpeg
|
||||
echo "Downloading and installing ffmpeg"
|
||||
wget -q https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz
|
||||
mkdir -p tmp
|
||||
tar -xf ffmpeg-release-amd64-static.tar.xz --strip-components 1 -C tmp
|
||||
cp -v tmp/{ffmpeg,ffprobe,qt-faststart} /usr/local/bin
|
||||
rm -rf tmp ffmpeg-release-amd64-static.tar.xz
|
||||
echo "ffmpeg installed to /usr/local/bin"
|
||||
|
||||
read -p "Enter portal URL, or press enter for localhost : " FRONTEND_HOST
|
||||
read -p "Enter portal name, or press enter for 'MediaCMS : " PORTAL_NAME
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cms.settings")
|
||||
os.environ.setdefault("TESTING", "True")
|
||||
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Django==3.1.8
|
||||
Django==3.1.12
|
||||
djangorestframework==3.12.2
|
||||
django-allauth==0.44.0
|
||||
|
||||
@@ -30,4 +30,4 @@ m3u8
|
||||
django-ckeditor
|
||||
django-debug-toolbar
|
||||
|
||||
django-login-required-middleware==0.6.1
|
||||
django-login-required-middleware==0.6.1
|
||||
|
||||
10
tests/api/test_manage_apis_TOWRITE.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestX(TestCase):
|
||||
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
|
||||
|
||||
def test_X(self):
|
||||
# check only managers/editors/admins have access here!!!
|
||||
|
||||
pass
|
||||
10
tests/api/test_media_listings_TOWRITE.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestX(TestCase):
|
||||
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
|
||||
|
||||
def test_X(self):
|
||||
# test a number of listings works (index, featured, user etc)
|
||||
|
||||
pass
|
||||
9
tests/api/test_new_file_TOWRITE.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestX(TestCase):
|
||||
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
|
||||
|
||||
def test_X(self):
|
||||
# add new file, check it is added and more (eg for videos it is transcoded etc)
|
||||
pass
|
||||
10
tests/api/test_search_TOWRITE.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestX(TestCase):
|
||||
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
|
||||
|
||||
def test_X(self):
|
||||
# add a few files, check search different cases that work
|
||||
|
||||
pass
|
||||
54
tests/api/test_user_login.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from django.test import Client, TestCase
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
from files.tests import create_account
|
||||
|
||||
API_V1_LOGIN_URL = '/api/v1/login'
|
||||
|
||||
|
||||
class TestUserLogin(TestCase):
|
||||
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
|
||||
|
||||
def setUp(self):
|
||||
self.password = 'this_is_a_fake_password'
|
||||
self.user = create_account(password=self.password)
|
||||
|
||||
def test_login_endpoint(self):
|
||||
client = Client()
|
||||
response = client.get(API_V1_LOGIN_URL)
|
||||
self.assertEqual(
|
||||
response.status_code,
|
||||
405,
|
||||
"GET not allowed here",
|
||||
)
|
||||
|
||||
response = client.post(API_V1_LOGIN_URL, {'username': 'fake', 'password': 'fake'})
|
||||
self.assertTrue('User not found' in str(response.content), 'Expected user not to be there')
|
||||
|
||||
user = self.user
|
||||
response = client.post(API_V1_LOGIN_URL, {'username': user.username, 'password': self.password})
|
||||
|
||||
self.assertEqual(
|
||||
response.status_code,
|
||||
200,
|
||||
"Expected 200",
|
||||
)
|
||||
data = response.data
|
||||
|
||||
self.assertEqual(
|
||||
data.get('email'),
|
||||
user.email,
|
||||
"Expected user email",
|
||||
)
|
||||
self.assertEqual(
|
||||
data.get('username'),
|
||||
user.username,
|
||||
"Expected username",
|
||||
)
|
||||
|
||||
token = Token.objects.filter(user=user).first()
|
||||
self.assertEqual(
|
||||
data.get('token'),
|
||||
token.key,
|
||||
"Expected valid token",
|
||||
)
|
||||
49
tests/api/test_user_token.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from django.test import Client, TestCase
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
from files.tests import create_account
|
||||
|
||||
API_V1_USER_TOKEN_URL = '/api/v1/user/token'
|
||||
|
||||
|
||||
class TestUserToken(TestCase):
|
||||
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
|
||||
|
||||
def setUp(self):
|
||||
self.password = 'this_is_a_fake_password'
|
||||
self.user = create_account(password=self.password)
|
||||
|
||||
def test_user_token_endpoint(self):
|
||||
client = Client()
|
||||
response = client.get(API_V1_USER_TOKEN_URL)
|
||||
self.assertEqual(
|
||||
response.status_code,
|
||||
403,
|
||||
"FORBIDDEN",
|
||||
)
|
||||
|
||||
user = self.user
|
||||
client.force_login(user=user)
|
||||
|
||||
response = client.post(API_V1_USER_TOKEN_URL)
|
||||
self.assertEqual(
|
||||
response.status_code,
|
||||
405,
|
||||
"method not allowed here",
|
||||
)
|
||||
|
||||
response = client.get(API_V1_USER_TOKEN_URL)
|
||||
data = response.data
|
||||
|
||||
self.assertEqual(
|
||||
response.status_code,
|
||||
200,
|
||||
"expected 200",
|
||||
)
|
||||
|
||||
token = Token.objects.filter(user=user).first()
|
||||
self.assertEqual(
|
||||
data.get('token'),
|
||||
token.key,
|
||||
"Expected valid token",
|
||||
)
|
||||
41
tests/api/test_user_whoami.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from files.tests import create_account
|
||||
|
||||
API_V1_LOGIN_URL = '/api/v1/whoami'
|
||||
|
||||
|
||||
class TestUserWhoami(TestCase):
|
||||
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
|
||||
|
||||
def setUp(self):
|
||||
self.user = create_account()
|
||||
|
||||
def test_whoami_endpoint(self):
|
||||
client = Client()
|
||||
response = client.get(API_V1_LOGIN_URL)
|
||||
self.assertEqual(
|
||||
response.status_code,
|
||||
403,
|
||||
"Expected 403",
|
||||
)
|
||||
|
||||
user = self.user
|
||||
client.force_login(user=user)
|
||||
response = client.get(API_V1_LOGIN_URL)
|
||||
self.assertEqual(
|
||||
response.status_code,
|
||||
200,
|
||||
"Expected 200",
|
||||
)
|
||||
data = response.data
|
||||
self.assertEqual(
|
||||
data.get('description'),
|
||||
user.description,
|
||||
"Expected user description",
|
||||
)
|
||||
self.assertEqual(
|
||||
data.get('username'),
|
||||
user.username,
|
||||
"Expected username",
|
||||
)
|
||||
9
tests/forms/test_contact_TOWRITE.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestX(TestCase):
|
||||
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
|
||||
|
||||
def test_X(self):
|
||||
# test contact form, send a message and check on email box that it is received
|
||||
pass
|
||||
12
tests/forms/test_edit_media_TOWRITE.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestX(TestCase):
|
||||
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
|
||||
|
||||
def test_X(self):
|
||||
# test edit media that it works, by setting different values. then check on the API response
|
||||
# check that a user cannot set status upon different scenarios (eg on settings PORTAL_WORKFLOW)
|
||||
# check that changint thumbnail time produces a different thumbnail?
|
||||
# check adding custom poster, and that it appears instead of the autogenerated?
|
||||
pass
|
||||
9
tests/forms/test_edit_profile_TOWRITE.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestX(TestCase):
|
||||
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
|
||||
|
||||
def test_X(self):
|
||||
# test edit profile page that it stores correctly the data
|
||||
pass
|
||||
12
tests/forms/test_login_TOWRITE.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestX(TestCase):
|
||||
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
|
||||
|
||||
def test_X(self):
|
||||
# test login form that allows user to login
|
||||
# test login form that redirect has worked (if on a page that required redirect for login, eg edit profile?)
|
||||
# check upon setting change that it is not allowed
|
||||
|
||||
pass
|
||||
13
tests/forms/test_media_upload_TOWRITE.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestX(TestCase):
|
||||
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
|
||||
|
||||
def test_X(self):
|
||||
# test media upload, using the form.
|
||||
# 1. upload a file and then check that it appears on the listing
|
||||
# 2. check that the page has been created
|
||||
# 3. check that encodings are ok for videos
|
||||
# 4. check other things (eg pdf/audio/image)
|
||||
pass
|
||||
10
tests/forms/test_register_TOWRITE.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestX(TestCase):
|
||||
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
|
||||
|
||||
def test_X(self):
|
||||
# test register form that stores user data correctly
|
||||
# check upon setting change that it is not allowed
|
||||
pass
|
||||
9
tests/forms/test_subtitles_TOWRITE.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestX(TestCase):
|
||||
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
|
||||
|
||||
def test_X(self):
|
||||
# test that adding a subtitle works for a video - needs to have a language set on Admin
|
||||
pass
|
||||
9
tests/notifications/test_check_comment_add_TOWRITE.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestX(TestCase):
|
||||
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
|
||||
|
||||
def test_X(self):
|
||||
# add comment, check user is notified
|
||||
pass
|
||||
10
tests/notifications/test_check_media_add_TOWRITE.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestX(TestCase):
|
||||
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
|
||||
|
||||
def test_X(self):
|
||||
# add media, check on mailbox that admins are notified
|
||||
|
||||
pass
|
||||
10
tests/notifications/test_reported_media_add_TOWRITE.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestX(TestCase):
|
||||
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
|
||||
|
||||
def test_X(self):
|
||||
# report media, check on mailbox that admin/user are notified
|
||||
|
||||
pass
|
||||
11
tests/pages/test_index_page_TODO.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestX(TestCase):
|
||||
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
|
||||
|
||||
def test_X(self):
|
||||
# check that all links exist, on anonymous user
|
||||
# check that all links exist, on logged in user, plus the user specific
|
||||
# for editor/manager/admin, check that the related links exist
|
||||
pass
|
||||
9
tests/pages/test_manage_pages_TODO.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestX(TestCase):
|
||||
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
|
||||
|
||||
def test_X(self):
|
||||
# check that links exist from index page and pages are loading and only admins/editors/managers can see them
|
||||
pass
|
||||
11
tests/settings/test_portal_workflow_TOWRITE.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestX(TestCase):
|
||||
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
|
||||
|
||||
def test_X(self):
|
||||
# test what is the default portal workflow
|
||||
# change it and make sure nothing strange happens (public/unlisted/private)
|
||||
|
||||
pass
|
||||
13
tests/settings/test_user_roles_TOWRITE.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestX(TestCase):
|
||||
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
|
||||
|
||||
def test_X(self):
|
||||
# test extra things editor can do
|
||||
# test extra things manager can do
|
||||
# test extra things admin can do
|
||||
# test what single user cannot do (eg mess with other users content!)
|
||||
|
||||
pass
|
||||
35
tests/test_fixtures.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from files.models import Category, EncodeProfile
|
||||
|
||||
|
||||
class TestFixtures(TestCase):
|
||||
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
|
||||
|
||||
def test_categories_fixtures(self):
|
||||
categories = Category.objects.all()
|
||||
self.assertEqual(
|
||||
categories.count(),
|
||||
6,
|
||||
"Problem with category fixtures",
|
||||
)
|
||||
categories = Category.objects.filter().order_by('id')
|
||||
self.assertEqual(
|
||||
categories.first().title,
|
||||
'Art',
|
||||
"Problem with category fixtures",
|
||||
)
|
||||
|
||||
def test_encodeprofile_fixtures(self):
|
||||
profiles = EncodeProfile.objects.all()
|
||||
self.assertEqual(
|
||||
profiles.count(),
|
||||
21,
|
||||
"Problem with Encode Profile fixtures",
|
||||
)
|
||||
profiles = EncodeProfile.objects.filter(active=True)
|
||||
self.assertEqual(
|
||||
profiles.count(),
|
||||
6,
|
||||
"Problem with Encode Profile fixtures, not as active as expected",
|
||||
)
|
||||
@@ -1,13 +1,13 @@
|
||||
from django.test import TestCase
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
|
||||
|
||||
|
||||
class SeleniumTest(TestCase):
|
||||
def setUp(self):
|
||||
self.chrome = webdriver.Remote(command_executor='http://selenium_hub:4444/wd/hub', desired_capabilities=DesiredCapabilities.CHROME)
|
||||
self.chrome.implicitly_wait(10)
|
||||
|
||||
def test_visit_site_with_chrome(self):
|
||||
self.chrome.get('http://web/admin')
|
||||
self.assertIn(self.chrome.title, "Log in | Django site admin")
|
||||
# from django.test import TestCase
|
||||
# from selenium import webdriver
|
||||
# from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
|
||||
#
|
||||
#
|
||||
# class SeleniumTest(TestCase):
|
||||
# def setUp(self):
|
||||
# self.chrome = webdriver.Remote(command_executor='http://selenium_hub:4444/wd/hub', desired_capabilities=DesiredCapabilities.CHROME)
|
||||
# self.chrome.implicitly_wait(10)
|
||||
#
|
||||
# def test_visit_site_with_chrome(self):
|
||||
# self.chrome.get('http://web/admin')
|
||||
# self.assertIn(self.chrome.title, "Log in | Django site admin")
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
def test_new_user(user_factory):
|
||||
print(user_factory.name)
|
||||
print(user_factory.description)
|
||||
assert True
|
||||
@@ -1,10 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.conf.urls import url
|
||||
from django.conf.urls import re_path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = "uploader"
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^upload/$", views.FineUploaderView.as_view(), name="upload"),
|
||||
re_path(r"^upload/$", views.FineUploaderView.as_view(), name="upload"),
|
||||
]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from django import forms
|
||||
|
||||
from files.methods import is_mediacms_manager
|
||||
|
||||
from .models import Channel, User
|
||||
|
||||
|
||||
@@ -17,7 +19,6 @@ class UserForm(forms.ModelForm):
|
||||
fields = (
|
||||
"name",
|
||||
"description",
|
||||
"email",
|
||||
"logo",
|
||||
"notification_on_comments",
|
||||
"is_featured",
|
||||
@@ -39,7 +40,7 @@ class UserForm(forms.ModelForm):
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
super(UserForm, self).__init__(*args, **kwargs)
|
||||
self.fields.pop("is_featured")
|
||||
if not user.is_superuser:
|
||||
if not is_mediacms_manager(user):
|
||||
self.fields.pop("advancedUser")
|
||||
self.fields.pop("is_manager")
|
||||
self.fields.pop("is_editor")
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import authenticate
|
||||
from rest_framework import serializers
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
from .models import User
|
||||
|
||||
@@ -77,3 +80,46 @@ class UserDetailSerializer(serializers.ModelSerializer):
|
||||
"default_channel_edit_url",
|
||||
)
|
||||
extra_kwargs = {"name": {"required": False}}
|
||||
|
||||
|
||||
class LoginSerializer(serializers.Serializer):
|
||||
email = serializers.CharField(max_length=255, required=False)
|
||||
username = serializers.CharField(max_length=255, required=False)
|
||||
password = serializers.CharField(max_length=128, write_only=True)
|
||||
token = serializers.CharField(max_length=255, required=False)
|
||||
|
||||
def validate(self, data):
|
||||
email = data.get('email', None)
|
||||
username = data.get('username', None)
|
||||
password = data.get('password', None)
|
||||
|
||||
if settings.ACCOUNT_AUTHENTICATION_METHOD == 'username' and not username:
|
||||
raise serializers.ValidationError('username is required to log in.')
|
||||
else:
|
||||
username_or_email = username
|
||||
if settings.ACCOUNT_AUTHENTICATION_METHOD == 'email' and not email:
|
||||
raise serializers.ValidationError('email is required to log in.')
|
||||
else:
|
||||
username_or_email = email
|
||||
|
||||
if settings.ACCOUNT_AUTHENTICATION_METHOD == 'username_email' and not (username or email):
|
||||
raise serializers.ValidationError('username or email is required to log in.')
|
||||
else:
|
||||
username_or_email = username or email
|
||||
|
||||
if password is None:
|
||||
raise serializers.ValidationError('password is required to log in.')
|
||||
|
||||
user = authenticate(username=username_or_email, password=password)
|
||||
|
||||
if user is None:
|
||||
raise serializers.ValidationError('User not found.')
|
||||
|
||||
if not user.is_active:
|
||||
raise serializers.ValidationError('User has been deactivated.')
|
||||
|
||||
token = Token.objects.filter(user=user).first()
|
||||
if not token:
|
||||
token = Token.objects.create(user=user)
|
||||
|
||||
return {'email': user.email, 'username': user.username, 'token': token.key}
|
||||
|
||||
@@ -1,41 +1,45 @@
|
||||
from django.conf.urls import url
|
||||
from django.conf.urls import re_path
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^user/(?P<username>[\w@._-]*)$", views.view_user, name="get_user"),
|
||||
url(r"^user/(?P<username>[\w@._-]*)/$", views.view_user, name="get_user"),
|
||||
url(
|
||||
re_path(r"^user/(?P<username>[\w@._-]*)$", views.view_user, name="get_user"),
|
||||
re_path(r"^user/(?P<username>[\w@._-]*)/$", views.view_user, name="get_user"),
|
||||
re_path(
|
||||
r"^user/(?P<username>[\w@.]*)/media$",
|
||||
views.view_user_media,
|
||||
name="get_user_media",
|
||||
),
|
||||
url(
|
||||
re_path(
|
||||
r"^user/(?P<username>[\w@.]*)/playlists$",
|
||||
views.view_user_playlists,
|
||||
name="get_user_playlists",
|
||||
),
|
||||
url(
|
||||
re_path(
|
||||
r"^user/(?P<username>[\w@.]*)/about$",
|
||||
views.view_user_about,
|
||||
name="get_user_about",
|
||||
),
|
||||
url(r"^user/(?P<username>[\w@.]*)/edit$", views.edit_user, name="edit_user"),
|
||||
url(r"^channel/(?P<friendly_token>[\w]*)$", views.view_channel, name="view_channel"),
|
||||
url(
|
||||
re_path(r"^user/(?P<username>[\w@.]*)/edit$", views.edit_user, name="edit_user"),
|
||||
re_path(r"^channel/(?P<friendly_token>[\w]*)$", views.view_channel, name="view_channel"),
|
||||
re_path(
|
||||
r"^channel/(?P<friendly_token>[\w]*)/edit$",
|
||||
views.edit_channel,
|
||||
name="edit_channel",
|
||||
),
|
||||
# API VIEWS
|
||||
url(r"^api/v1/users$", views.UserList.as_view(), name="api_users"),
|
||||
url(r"^api/v1/users/$", views.UserList.as_view()),
|
||||
url(
|
||||
path('api/v1/whoami', views.UserWhoami.as_view(), name='user-whoami'),
|
||||
path('api/v1/user/token', views.UserToken.as_view(), name='user-token'),
|
||||
path('api/v1/login', views.LoginView.as_view(), name='user-login'),
|
||||
re_path(r"^api/v1/users$", views.UserList.as_view(), name="api_users"),
|
||||
re_path(r"^api/v1/users/$", views.UserList.as_view()),
|
||||
re_path(
|
||||
r"^api/v1/users/(?P<username>[\w@._-]*)$",
|
||||
views.UserDetail.as_view(),
|
||||
name="api_get_user",
|
||||
),
|
||||
url(
|
||||
re_path(
|
||||
r"^api/v1/users/(?P<username>[\w@._-]*)/contact",
|
||||
views.contact_user,
|
||||
name="api_contact_user",
|
||||
|
||||
@@ -5,7 +5,8 @@ from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from drf_yasg import openapi as openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework import generics, permissions, status
|
||||
from rest_framework.authtoken.models import Token
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.parsers import (
|
||||
@@ -23,7 +24,7 @@ from files.methods import is_mediacms_editor, is_mediacms_manager
|
||||
|
||||
from .forms import ChannelForm, UserForm
|
||||
from .models import Channel, User
|
||||
from .serializers import UserDetailSerializer, UserSerializer
|
||||
from .serializers import LoginSerializer, UserDetailSerializer, UserSerializer
|
||||
|
||||
|
||||
def get_user(username):
|
||||
@@ -305,3 +306,65 @@ class UserDetail(APIView):
|
||||
|
||||
user.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class UserWhoami(generics.RetrieveAPIView):
|
||||
parser_classes = (JSONParser, FormParser, MultiPartParser)
|
||||
queryset = User.objects.all()
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
serializer_class = UserDetailSerializer
|
||||
|
||||
def get_object(self):
|
||||
return User.objects.get(id=self.request.user.id)
|
||||
|
||||
@swagger_auto_schema(
|
||||
tags=['Users'],
|
||||
operation_summary='Whoami user information',
|
||||
operation_description='Whoami user information',
|
||||
responses={200: openapi.Response('response description', UserDetailSerializer), 403: 'Forbidden'},
|
||||
)
|
||||
def get(self, request, *args, **kwargs):
|
||||
return super(UserWhoami, self).get(request, *args, **kwargs)
|
||||
|
||||
|
||||
class UserToken(APIView):
|
||||
parser_classes = (JSONParser,)
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
|
||||
@swagger_auto_schema(
|
||||
tags=['Users'],
|
||||
operation_summary='Get a user token',
|
||||
operation_description="Returns an authenticated user's token",
|
||||
responses={200: 'token', 403: 'Forbidden'},
|
||||
)
|
||||
def get(self, request, *args, **kwargs):
|
||||
token = Token.objects.filter(user=request.user).first()
|
||||
if not token:
|
||||
token = Token.objects.create(user=request.user)
|
||||
|
||||
return Response({'token': str(token)}, status=200)
|
||||
|
||||
|
||||
class LoginView(APIView):
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
serializer_class = LoginSerializer
|
||||
parser_classes = (MultiPartParser, FormParser, FileUploadParser)
|
||||
|
||||
@swagger_auto_schema(
|
||||
tags=['Users'],
|
||||
operation_summary='Login url',
|
||||
operation_description="Login url endpoint. According to what the portal provides, you may provide username and/or email, plus the password",
|
||||
manual_parameters=[
|
||||
openapi.Parameter(name="username", in_=openapi.IN_FORM, type=openapi.TYPE_STRING, required=False, description="username"),
|
||||
openapi.Parameter(name="email", in_=openapi.IN_FORM, type=openapi.TYPE_STRING, required=False, description="email"),
|
||||
openapi.Parameter(name="password", in_=openapi.IN_FORM, type=openapi.TYPE_STRING, required=True, description="password"),
|
||||
],
|
||||
responses={200: openapi.Response('user details', LoginSerializer), 404: 'Bad request'},
|
||||
)
|
||||
def post(self, request):
|
||||
data = request.data
|
||||
|
||||
serializer = self.serializer_class(data=data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
1
version.py
Normal file
@@ -0,0 +1 @@
|
||||
1.6
|
||||