mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-11-21 22:07:59 -05:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5602422d29 | ||
|
|
110695ae2f | ||
|
|
6df942ac4e | ||
|
|
2d49b1df29 | ||
|
|
8f228d6844 | ||
|
|
94b26a8781 | ||
|
|
9002930994 | ||
|
|
1e6ee280ca | ||
|
|
cf278211fb | ||
|
|
2226b6afbf | ||
|
|
adf3d4377f | ||
|
|
be41c6876d | ||
|
|
784a18ad27 | ||
|
|
6a5c57f2b2 | ||
|
|
10f198fff3 | ||
|
|
6b89d9722b | ||
|
|
04f59ffbb8 | ||
|
|
632db06ca3 | ||
|
|
0129ab6732 | ||
|
|
2b65afc8dd | ||
|
|
b27e3ca6f6 | ||
|
|
1a7adb80da | ||
|
|
2552551662 | ||
|
|
3b35ce0262 | ||
|
|
883af9bb4a |
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: [mgogoulos]
|
||||||
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
name: Issue report
|
||||||
|
about: Create a report to help us improve MediaCMS
|
||||||
|
title: ''
|
||||||
|
labels: 'issue: bug'
|
||||||
|
assignees: mgogoulos
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the issue**
|
||||||
|
A clear and concise description of what the issue is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the issue:
|
||||||
|
1. Go to ...
|
||||||
|
2. Perform action ...
|
||||||
|
3. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Environment (please complete the following information):**
|
||||||
|
- OS: [e.g. Ubuntu Linux]
|
||||||
|
- Installation method: [Docker install, or single server install]
|
||||||
|
- Browser, if applicable
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea
|
||||||
|
title: ''
|
||||||
|
labels: 'issue: enhancement'
|
||||||
|
assignees: mgogoulos
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the feature you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
10
.github/pull_request_template.md
vendored
Normal file
10
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
## Description
|
||||||
|
<!-- Describe the changes introduced by this PR for the reviewers to fully understand. -->
|
||||||
|
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
<!-- Actions to be done pre and post deployment -->
|
||||||
|
*Pre-deploy*
|
||||||
|
|
||||||
|
*Post-deploy*
|
||||||
|
|
||||||
15
.github/workflows/lint_test.yml
vendored
Normal file
15
.github/workflows/lint_test.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pre-commit:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
- uses: pre-commit/action@v2.0.0
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,6 +1,8 @@
|
|||||||
media_files/encoded/
|
media_files/encoded/
|
||||||
media_files/original/
|
media_files/original/
|
||||||
media_files/hls/
|
media_files/hls/
|
||||||
|
media_files/chunks/
|
||||||
|
media_files/uploads/
|
||||||
postgres_data/
|
postgres_data/
|
||||||
celerybeat-schedule
|
celerybeat-schedule
|
||||||
logs/
|
logs/
|
||||||
@@ -11,3 +13,4 @@ static/debug_toolbar/
|
|||||||
static/mptt/
|
static/mptt/
|
||||||
static/rest_framework/
|
static/rest_framework/
|
||||||
cms/local_settings.py
|
cms/local_settings.py
|
||||||
|
deploy/docker/local_settings.py
|
||||||
|
|||||||
15
.pre-commit-config.yaml
Normal file
15
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://gitlab.com/pycqa/flake8
|
||||||
|
rev: 3.7.9
|
||||||
|
hooks:
|
||||||
|
- id: flake8
|
||||||
|
- repo: https://github.com/pycqa/isort
|
||||||
|
rev: 5.5.4
|
||||||
|
hooks:
|
||||||
|
- id: isort
|
||||||
|
args: ["--profile", "black"]
|
||||||
|
- repo: https://github.com/psf/black
|
||||||
|
rev: 20.8b1
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
language_version: python3
|
||||||
13
CODE_OF_CONDUCT.md
Normal file
13
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Contributor Code of Conduct
|
||||||
|
|
||||||
|
As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
|
||||||
|
|
||||||
|
We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at https://www.contributor-covenant.org/version/1/0/0/code-of-conduct.html
|
||||||
12
Dockerfile
12
Dockerfile
@@ -16,12 +16,12 @@ RUN pip install -r requirements.txt
|
|||||||
COPY . /home/mediacms.io/mediacms
|
COPY . /home/mediacms.io/mediacms
|
||||||
WORKDIR /home/mediacms.io/mediacms
|
WORKDIR /home/mediacms.io/mediacms
|
||||||
|
|
||||||
RUN wget -q http://zebulon.bok.net/Bento4/binaries/Bento4-SDK-1-6-0-632.x86_64-unknown-linux.zip && \
|
RUN wget -q http://zebulon.bok.net/Bento4/binaries/Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip && \
|
||||||
unzip Bento4-SDK-1-6-0-632.x86_64-unknown-linux.zip -d ../bento4 && \
|
unzip Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip -d ../bento4 && \
|
||||||
mv ../bento4/Bento4-SDK-1-6-0-632.x86_64-unknown-linux/* ../bento4/ && \
|
mv ../bento4/Bento4-SDK-1-6-0-637.x86_64-unknown-linux/* ../bento4/ && \
|
||||||
rm -rf ../bento4/Bento4-SDK-1-6-0-632.x86_64-unknown-linux && \
|
rm -rf ../bento4/Bento4-SDK-1-6-0-637.x86_64-unknown-linux && \
|
||||||
rm -rf ../bento4/docs && \
|
rm -rf ../bento4/docs && \
|
||||||
rm Bento4-SDK-1-6-0-632.x86_64-unknown-linux.zip
|
rm Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip
|
||||||
|
|
||||||
############ RUNTIME IMAGE ############
|
############ RUNTIME IMAGE ############
|
||||||
FROM python:3.8-slim-buster as runtime-image
|
FROM python:3.8-slim-buster as runtime-image
|
||||||
@@ -47,7 +47,7 @@ ENV ENABLE_MIGRATIONS='yes'
|
|||||||
ENV VIRTUAL_ENV=/home/mediacms.io
|
ENV VIRTUAL_ENV=/home/mediacms.io
|
||||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||||
|
|
||||||
COPY --from=compile-image /home/mediacms.io /home/mediacms.io
|
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 \
|
RUN apt-get update -y && apt-get -y upgrade && apt-get install --no-install-recommends \
|
||||||
supervisor nginx ffmpeg imagemagick procps -y && \
|
supervisor nginx ffmpeg imagemagick procps -y && \
|
||||||
|
|||||||
49
README.md
49
README.md
@@ -9,18 +9,11 @@ A demo is available at https://demo.mediacms.io
|
|||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||

|
<p align="center">
|
||||||
|
<img src="https://raw.githubusercontent.com/mediacms-io/mediacms/main/docs/images/index.jpg" width="340">
|
||||||
Vanilla MediaCMS index page
|
<img src="https://raw.githubusercontent.com/mediacms-io/mediacms/main/docs/images/video.jpg" width="340">
|
||||||
|
<img src="https://raw.githubusercontent.com/mediacms-io/mediacms/main/docs/images/embed.jpg" width="340">
|
||||||

|
</p>
|
||||||
|
|
||||||
Video page with player different options
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Embed video page
|
|
||||||
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- **Complete control over your data**: host it yourself!
|
- **Complete control over your data**: host it yourself!
|
||||||
@@ -94,15 +87,15 @@ git clone https://github.com/mediacms-io/mediacms
|
|||||||
cd mediacms
|
cd mediacms
|
||||||
```
|
```
|
||||||
|
|
||||||
The default option to serve MediaCMS is on http://localhost. If you want to set a url and have it served there, set the `FRONTEND_HOST` variable on file `deploy/docker/local_settings.py`.
|
The default option is to serve MediaCMS on all ips available of the server (including localhost).
|
||||||
|
|
||||||
Now run
|
Now run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose build && docker-compose up
|
docker-compose up
|
||||||
```
|
```
|
||||||
|
|
||||||
This will build an image, download and setup necessary Docker images and start all containers. Once it finishes, MediaCMS will be installed and available on http://localhost (or the url you've set as `FRONTEND_HOST` on file `deploy/docker/local_settings.py`)
|
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
|
||||||
|
|
||||||
For more instructions, checkout the docs on the [Docker deployment](docs/Docker_deployment.md) page. Docker Compose support has been contributed by @swiftugandan.
|
For more instructions, checkout the docs on the [Docker deployment](docs/Docker_deployment.md) page. Docker Compose support has been contributed by @swiftugandan.
|
||||||
|
|
||||||
@@ -114,7 +107,7 @@ The core dependencies are Python3, Django3, Celery, PostgreSQL, Redis, ffmpeg. A
|
|||||||
Installation on a Ubuntu 18 or 20 system with git utility installed should be completed in a few minutes with the following steps.
|
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.
|
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 - to run on Ubuntu 18 or Ubuntu 20 flavors only!
|
Automated script - tested on Ubuntu 18, Ubuntu 20, and Debian Buster
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir /home/mediacms.io && cd /home/mediacms.io/
|
mkdir /home/mediacms.io && cd /home/mediacms.io/
|
||||||
@@ -165,28 +158,13 @@ This software uses the following list of awesome technologies:
|
|||||||
|
|
||||||
## Who is using it
|
## Who is using it
|
||||||
|
|
||||||
- **EngageMedia** non-profit media, technology and culture organization - https://video.engagemedia.org
|
- **Cinemata** non-profit media, technology and culture organization - https://cinemata.org
|
||||||
|
|
||||||
- **Critical Commons** public media archive and fair use advocacy network - https://criticalcommons.org
|
- **Critical Commons** public media archive and fair use advocacy network - https://criticalcommons.org
|
||||||
|
|
||||||
- **Heritales** International Heritage Film Festival - https://stage.heritales.org
|
- **Heritales** International Heritage Film Festival - https://stage.heritales.org
|
||||||
|
|
||||||
|
|
||||||
## Thanks To
|
|
||||||
|
|
||||||
- **Anna Helme**, for such a great partnership all these years!
|
|
||||||
|
|
||||||
- **Steve Anderson**, for trusting us and helping the Wordgames team make this real.
|
|
||||||
|
|
||||||
- **Andrew Lowenthal, King Catoy, Rezwan Islam** and the rest of the great team of [Engage Media](https://engagemedia.org).
|
|
||||||
|
|
||||||
- **Ioannis Korovesis, Ioannis Maistros, Diomidis Spinellis and Theodoros Karounos**, for their mentorship all these years, their contribution to science and the promotion of open source and free software technologies.
|
|
||||||
|
|
||||||
- **Antonis Ikonomou**, for hosting us on the excellent [Innovathens](https://www.innovathens.gr) space.
|
|
||||||
|
|
||||||
- **Werner Robitza**, for helping us with ffmpeg related stuff.
|
|
||||||
|
|
||||||
|
|
||||||
## How to contribute
|
## How to contribute
|
||||||
|
|
||||||
If you like the project, here's a few things you can do
|
If you like the project, here's a few things you can do
|
||||||
@@ -198,5 +176,12 @@ If you like the project, here's a few things you can do
|
|||||||
- Star the project
|
- Star the project
|
||||||
- Add functionality, work on a PR, fix an issue!
|
- Add functionality, work on a PR, fix an issue!
|
||||||
|
|
||||||
|
## Developers info
|
||||||
|
|
||||||
|
- API documentation available under /swagger URL (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
|
## Contact
|
||||||
info@mediacms.io
|
info@mediacms.io
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Generated by Django 3.1.4 on 2020-12-01 07:12
|
# Generated by Django 3.1.4 on 2020-12-01 07:12
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Generated by Django 3.1.4 on 2020-12-01 07:12
|
# Generated by Django 3.1.4 on 2020-12-01 07:12
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@@ -35,8 +35,6 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
migrations.AddIndex(
|
migrations.AddIndex(
|
||||||
model_name="mediaaction",
|
model_name="mediaaction",
|
||||||
index=models.Index(
|
index=models.Index(fields=["session_key", "action"], name="actions_med_session_fac55a_idx"),
|
||||||
fields=["session_key", "action"], name="actions_med_session_fac55a_idx"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from users.models import User
|
|
||||||
from files.models import Media
|
from files.models import Media
|
||||||
|
from users.models import User
|
||||||
|
|
||||||
USER_MEDIA_ACTIONS = (
|
USER_MEDIA_ACTIONS = (
|
||||||
("like", "Like"),
|
("like", "Like"),
|
||||||
@@ -30,15 +31,11 @@ class MediaAction(models.Model):
|
|||||||
help_text="for not logged in users",
|
help_text="for not logged in users",
|
||||||
)
|
)
|
||||||
|
|
||||||
action = models.CharField(
|
action = models.CharField(max_length=20, choices=USER_MEDIA_ACTIONS, default="watch")
|
||||||
max_length=20, choices=USER_MEDIA_ACTIONS, default="watch"
|
|
||||||
)
|
|
||||||
# keeps extra info, eg on report action, why it is reported
|
# keeps extra info, eg on report action, why it is reported
|
||||||
extra_info = models.TextField(blank=True, null=True)
|
extra_info = models.TextField(blank=True, null=True)
|
||||||
|
|
||||||
media = models.ForeignKey(
|
media = models.ForeignKey(Media, on_delete=models.CASCADE, related_name="mediaactions")
|
||||||
Media, on_delete=models.CASCADE, related_name="mediaactions"
|
|
||||||
)
|
|
||||||
action_date = models.DateTimeField(auto_now_add=True)
|
action_date = models.DateTimeField(auto_now_add=True)
|
||||||
remote_ip = models.CharField(max_length=40, blank=True, null=True)
|
remote_ip = models.CharField(max_length=40, blank=True, null=True)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
from .celery import app as celery_app
|
from .celery import app as celery_app
|
||||||
|
|
||||||
__all__ = ["celery_app"]
|
__all__ = ["celery_app"]
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from celery import Celery
|
from celery import Celery
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cms.settings")
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cms.settings")
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from rest_framework.pagination import PageNumberPagination
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from collections import OrderedDict # requires Python 2.7 or later
|
from collections import OrderedDict # requires Python 2.7 or later
|
||||||
|
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
from rest_framework.pagination import PageNumberPagination
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
|
||||||
class FasterDjangoPaginator(Paginator):
|
class FasterDjangoPaginator(Paginator):
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from rest_framework import permissions
|
from rest_framework import permissions
|
||||||
|
|
||||||
from files.methods import is_mediacms_editor, is_mediacms_manager
|
from files.methods import is_mediacms_editor, is_mediacms_manager
|
||||||
|
|
||||||
|
|
||||||
@@ -24,7 +25,10 @@ class IsUserOrManager(permissions.BasePermission):
|
|||||||
if is_mediacms_manager(request.user):
|
if is_mediacms_manager(request.user):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return obj.user == request.user
|
if hasattr(obj, 'user'):
|
||||||
|
return obj.user == request.user
|
||||||
|
else:
|
||||||
|
return obj == request.user
|
||||||
|
|
||||||
|
|
||||||
class IsUserOrEditor(permissions.BasePermission):
|
class IsUserOrEditor(permissions.BasePermission):
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from celery.schedules import crontab
|
from celery.schedules import crontab
|
||||||
|
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
@@ -16,7 +17,8 @@ CAN_ADD_MEDIA = "all"
|
|||||||
# valid choices here are 'public', 'private', 'unlisted
|
# valid choices here are 'public', 'private', 'unlisted
|
||||||
PORTAL_WORKFLOW = "public"
|
PORTAL_WORKFLOW = "public"
|
||||||
|
|
||||||
DEFAULT_THEME = "black" # this is not taken under consideration currently
|
# valid values: 'light', 'dark'.
|
||||||
|
DEFAULT_THEME = "light"
|
||||||
|
|
||||||
|
|
||||||
# These are passed on every request
|
# These are passed on every request
|
||||||
@@ -42,7 +44,11 @@ ALLOW_RATINGS_CONFIRMED_EMAIL_ONLY = True
|
|||||||
|
|
||||||
# ip of the server should be part of this
|
# ip of the server should be part of this
|
||||||
ALLOWED_HOSTS = ["*", "mediacms.io", "127.0.0.1", "localhost"]
|
ALLOWED_HOSTS = ["*", "mediacms.io", "127.0.0.1", "localhost"]
|
||||||
|
|
||||||
FRONTEND_HOST = "http://localhost"
|
FRONTEND_HOST = "http://localhost"
|
||||||
|
# this variable - along with SSL_FRONTEND_HOST is used on several places
|
||||||
|
# as email where a URL need appear etc
|
||||||
|
|
||||||
# FRONTEND_HOST needs an http prefix - at the end of the file
|
# FRONTEND_HOST needs an http prefix - at the end of the file
|
||||||
# there's a conversion to https with the SSL_FRONTEND_HOST env
|
# there's a conversion to https with the SSL_FRONTEND_HOST env
|
||||||
INTERNAL_IPS = "127.0.0.1"
|
INTERNAL_IPS = "127.0.0.1"
|
||||||
@@ -208,9 +214,7 @@ POST_UPLOAD_AUTHOR_MESSAGE_UNLISTED_NO_COMMENTARY = ""
|
|||||||
CANNOT_ADD_MEDIA_MESSAGE = ""
|
CANNOT_ADD_MEDIA_MESSAGE = ""
|
||||||
|
|
||||||
# mp4hls command, part of Bendo4
|
# mp4hls command, part of Bendo4
|
||||||
MP4HLS_COMMAND = (
|
MP4HLS_COMMAND = "/home/mediacms.io/mediacms/Bento4-SDK-1-6-0-637.x86_64-unknown-linux/bin/mp4hls"
|
||||||
"/home/mediacms.io/mediacms/Bento4-SDK-1-6-0-632.x86_64-unknown-linux/bin/mp4hls"
|
|
||||||
)
|
|
||||||
|
|
||||||
# highly experimental, related with remote workers
|
# highly experimental, related with remote workers
|
||||||
ADMIN_TOKEN = "c2b8e1838b6128asd333ddc5e24"
|
ADMIN_TOKEN = "c2b8e1838b6128asd333ddc5e24"
|
||||||
@@ -288,6 +292,7 @@ INSTALLED_APPS = [
|
|||||||
"uploader.apps.UploaderConfig",
|
"uploader.apps.UploaderConfig",
|
||||||
"djcelery_email",
|
"djcelery_email",
|
||||||
"ckeditor",
|
"ckeditor",
|
||||||
|
"drf_yasg",
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
@@ -419,6 +424,7 @@ CELERY_BEAT_SCHEDULE = {
|
|||||||
# TODO: beat, delete chunks from media root
|
# TODO: beat, delete chunks from media root
|
||||||
# chunks_dir after xx days...(also uploads_dir)
|
# chunks_dir after xx days...(also uploads_dir)
|
||||||
|
|
||||||
|
|
||||||
LOCAL_INSTALL = False
|
LOCAL_INSTALL = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
19
cms/urls.py
19
cms/urls.py
@@ -1,7 +1,17 @@
|
|||||||
from django.contrib import admin
|
|
||||||
from django.urls import path
|
|
||||||
from django.conf.urls import url, include
|
|
||||||
import debug_toolbar
|
import debug_toolbar
|
||||||
|
from django.conf.urls import include, url
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import path, re_path
|
||||||
|
from drf_yasg import openapi
|
||||||
|
from drf_yasg.views import get_schema_view
|
||||||
|
from rest_framework.permissions import AllowAny
|
||||||
|
|
||||||
|
schema_view = get_schema_view(
|
||||||
|
openapi.Info(title="MediaCMS API", default_version='v1', contact=openapi.Contact(url="https://mediacms.io"), x_logo={"url": "../../static/images/logo_dark.svg"}),
|
||||||
|
public=True,
|
||||||
|
permission_classes=(AllowAny,),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r"^__debug__/", include(debug_toolbar.urls)),
|
url(r"^__debug__/", include(debug_toolbar.urls)),
|
||||||
@@ -10,4 +20,7 @@ urlpatterns = [
|
|||||||
url(r"^accounts/", include("allauth.urls")),
|
url(r"^accounts/", include("allauth.urls")),
|
||||||
url(r"^api-auth/", include("rest_framework.urls")),
|
url(r"^api-auth/", include("rest_framework.urls")),
|
||||||
path("admin/", admin.site.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'),
|
||||||
|
path('docs/api/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ set -e
|
|||||||
# forward request and error logs to docker log collector
|
# forward request and error logs to docker log collector
|
||||||
ln -sf /dev/stdout /var/log/nginx/access.log && ln -sf /dev/stderr /var/log/nginx/error.log && \
|
ln -sf /dev/stdout /var/log/nginx/access.log && ln -sf /dev/stderr /var/log/nginx/error.log && \
|
||||||
ln -sf /dev/stdout /var/log/nginx/mediacms.io.access.log && ln -sf /dev/stderr /var/log/nginx/mediacms.io.error.log
|
ln -sf /dev/stdout /var/log/nginx/mediacms.io.access.log && ln -sf /dev/stderr /var/log/nginx/mediacms.io.error.log
|
||||||
|
|
||||||
cp /home/mediacms.io/mediacms/deploy/docker/local_settings.py /home/mediacms.io/mediacms/cms/local_settings.py
|
cp /home/mediacms.io/mediacms/deploy/docker/local_settings.py /home/mediacms.io/mediacms/cms/local_settings.py
|
||||||
|
|
||||||
mkdir -p /home/mediacms.io/mediacms/{logs,pids,media_files/hls}
|
mkdir -p /home/mediacms.io/mediacms/{logs,pids,media_files/hls}
|
||||||
@@ -12,8 +13,6 @@ touch /home/mediacms.io/mediacms/logs/debug.log
|
|||||||
# Remove any dangling pids
|
# Remove any dangling pids
|
||||||
rm -rf /home/mediacms.io/mediacms/pids/*
|
rm -rf /home/mediacms.io/mediacms/pids/*
|
||||||
|
|
||||||
chown -R www-data. /home/mediacms.io/
|
|
||||||
|
|
||||||
TARGET_GID=$(stat -c "%g" /home/mediacms.io/mediacms/)
|
TARGET_GID=$(stat -c "%g" /home/mediacms.io/mediacms/)
|
||||||
|
|
||||||
EXISTS=$(cat /etc/group | grep $TARGET_GID | wc -l)
|
EXISTS=$(cat /etc/group | grep $TARGET_GID | wc -l)
|
||||||
@@ -28,6 +27,9 @@ else
|
|||||||
usermod -a -G $GROUP www-data
|
usermod -a -G $GROUP www-data
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# We should do this only for folders that have a different owner, since it is an expensive operation
|
||||||
|
find /home/mediacms.io/ ! \( -user www-data -group $TARGET_GID \) -exec chown www-data:$TARGET_GID {} +
|
||||||
|
|
||||||
chmod +x /home/mediacms.io/mediacms/deploy/docker/start.sh /home/mediacms.io/mediacms/deploy/docker/prestart.sh
|
chmod +x /home/mediacms.io/mediacms/deploy/docker/start.sh /home/mediacms.io/mediacms/deploy/docker/prestart.sh
|
||||||
|
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|||||||
@@ -29,8 +29,6 @@ CACHES = {
|
|||||||
BROKER_URL = REDIS_LOCATION
|
BROKER_URL = REDIS_LOCATION
|
||||||
CELERY_RESULT_BACKEND = BROKER_URL
|
CELERY_RESULT_BACKEND = BROKER_URL
|
||||||
|
|
||||||
MP4HLS_COMMAND = (
|
MP4HLS_COMMAND = "/home/mediacms.io/bento4/bin/mp4hls"
|
||||||
"/home/mediacms.io/bento4/bin/mp4hls"
|
|
||||||
)
|
|
||||||
|
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ server {
|
|||||||
|
|
||||||
error_log /var/log/nginx/mediacms.io.error.log warn;
|
error_log /var/log/nginx/mediacms.io.error.log warn;
|
||||||
|
|
||||||
# redirect to https if logged in
|
# # redirect to https if logged in
|
||||||
if ($http_cookie ~* "sessionid") {
|
# if ($http_cookie ~* "sessionid") {
|
||||||
rewrite ^/(.*)$ https://localhost/$1 permanent;
|
# rewrite ^/(.*)$ https://localhost/$1 permanent;
|
||||||
}
|
# }
|
||||||
|
|
||||||
# redirect basic forms to https
|
# # redirect basic forms to https
|
||||||
location ~ (login|login_form|register|mail_password_form)$ {
|
# location ~ (login|login_form|register|mail_password_form)$ {
|
||||||
rewrite ^/(.*)$ https://localhost/$1 permanent;
|
# rewrite ^/(.*)$ https://localhost/$1 permanent;
|
||||||
}
|
# }
|
||||||
|
|
||||||
location /static {
|
location /static {
|
||||||
alias /home/mediacms.io/mediacms/static ;
|
alias /home/mediacms.io/mediacms/static ;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ services:
|
|||||||
- /var/run/docker.sock:/tmp/docker.sock:ro
|
- /var/run/docker.sock:/tmp/docker.sock:ro
|
||||||
- ./deploy/docker/reverse_proxy/client_max_body_size.conf:/etc/nginx/conf.d/client_max_body_size.conf:ro
|
- ./deploy/docker/reverse_proxy/client_max_body_size.conf:/etc/nginx/conf.d/client_max_body_size.conf:ro
|
||||||
migrations:
|
migrations:
|
||||||
image: mediacms:latest
|
image: mediacms/mediacms:latest
|
||||||
volumes:
|
volumes:
|
||||||
- ./:/home/mediacms.io/mediacms/
|
- ./:/home/mediacms.io/mediacms/
|
||||||
environment:
|
environment:
|
||||||
@@ -24,10 +24,7 @@ services:
|
|||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
web:
|
web:
|
||||||
build:
|
image: mediacms/mediacms:latest
|
||||||
context: .
|
|
||||||
target: runtime-image
|
|
||||||
image: mediacms:latest
|
|
||||||
deploy:
|
deploy:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
volumes:
|
volumes:
|
||||||
@@ -41,7 +38,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- migrations
|
- migrations
|
||||||
celery_beat:
|
celery_beat:
|
||||||
image: mediacms:latest
|
image: mediacms/mediacms:latest
|
||||||
volumes:
|
volumes:
|
||||||
- ./:/home/mediacms.io/mediacms/
|
- ./:/home/mediacms.io/mediacms/
|
||||||
environment:
|
environment:
|
||||||
@@ -53,7 +50,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
celery_worker:
|
celery_worker:
|
||||||
image: mediacms:latest
|
image: mediacms/mediacms:latest
|
||||||
deploy:
|
deploy:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ services:
|
|||||||
- ./deploy/docker/reverse_proxy/certs/:/etc/nginx/certs/
|
- ./deploy/docker/reverse_proxy/certs/:/etc/nginx/certs/
|
||||||
- ./deploy/docker/reverse_proxy/client_max_body_size.conf:/etc/nginx/conf.d/client_max_body_size.conf:ro
|
- ./deploy/docker/reverse_proxy/client_max_body_size.conf:/etc/nginx/conf.d/client_max_body_size.conf:ro
|
||||||
migrations:
|
migrations:
|
||||||
image: mediacms:latest
|
image: mediacms/mediacms:latest
|
||||||
volumes:
|
volumes:
|
||||||
- ./:/home/mediacms.io/mediacms/
|
- ./:/home/mediacms.io/mediacms/
|
||||||
environment:
|
environment:
|
||||||
@@ -26,10 +26,7 @@ services:
|
|||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
web:
|
web:
|
||||||
build:
|
image: mediacms/mediacms:latest
|
||||||
context: .
|
|
||||||
target: runtime-image
|
|
||||||
image: mediacms:latest
|
|
||||||
deploy:
|
deploy:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
volumes:
|
volumes:
|
||||||
@@ -43,7 +40,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- migrations
|
- migrations
|
||||||
celery_beat:
|
celery_beat:
|
||||||
image: mediacms:latest
|
image: mediacms/mediacms:latest
|
||||||
volumes:
|
volumes:
|
||||||
- ./:/home/mediacms.io/mediacms/
|
- ./:/home/mediacms.io/mediacms/
|
||||||
environment:
|
environment:
|
||||||
@@ -55,7 +52,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
celery_worker:
|
celery_worker:
|
||||||
image: mediacms:latest
|
image: mediacms/mediacms:latest
|
||||||
deploy:
|
deploy:
|
||||||
replicas: 2
|
replicas: 2
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ version: "3"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
migrations:
|
migrations:
|
||||||
image: mediacms:latest
|
image: mediacms/mediacms:latest
|
||||||
volumes:
|
volumes:
|
||||||
- ./deploy/docker/local_settings.py:/home/mediacms.io/mediacms/deploy/docker/local_settings.py
|
- ./deploy/docker/local_settings.py:/home/mediacms.io/mediacms/deploy/docker/local_settings.py
|
||||||
environment:
|
environment:
|
||||||
@@ -17,10 +17,7 @@ services:
|
|||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
web:
|
web:
|
||||||
build:
|
image: mediacms/mediacms:latest
|
||||||
context: .
|
|
||||||
target: runtime-image
|
|
||||||
image: mediacms:latest
|
|
||||||
deploy:
|
deploy:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
ports:
|
ports:
|
||||||
@@ -37,7 +34,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- migrations
|
- migrations
|
||||||
celery_beat:
|
celery_beat:
|
||||||
image: mediacms:latest
|
image: mediacms/mediacms:latest
|
||||||
volumes:
|
volumes:
|
||||||
- ./deploy/docker/local_settings.py:/home/mediacms.io/mediacms/deploy/docker/local_settings.py
|
- ./deploy/docker/local_settings.py:/home/mediacms.io/mediacms/deploy/docker/local_settings.py
|
||||||
environment:
|
environment:
|
||||||
@@ -49,7 +46,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
celery_worker:
|
celery_worker:
|
||||||
image: mediacms:latest
|
image: mediacms/mediacms:latest
|
||||||
deploy:
|
deploy:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ version: "3"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
migrations:
|
migrations:
|
||||||
image: mediacms:latest
|
image: mediacms/mediacms:latest
|
||||||
volumes:
|
volumes:
|
||||||
- ./:/home/mediacms.io/mediacms/
|
- ./:/home/mediacms.io/mediacms/
|
||||||
environment:
|
environment:
|
||||||
@@ -17,10 +17,7 @@ services:
|
|||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
web:
|
web:
|
||||||
build:
|
image: mediacms/mediacms:latest
|
||||||
context: .
|
|
||||||
target: runtime-image
|
|
||||||
image: mediacms:latest
|
|
||||||
deploy:
|
deploy:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
ports:
|
ports:
|
||||||
@@ -35,7 +32,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- migrations
|
- migrations
|
||||||
celery_beat:
|
celery_beat:
|
||||||
image: mediacms:latest
|
image: mediacms/mediacms:latest
|
||||||
volumes:
|
volumes:
|
||||||
- ./:/home/mediacms.io/mediacms/
|
- ./:/home/mediacms.io/mediacms/
|
||||||
environment:
|
environment:
|
||||||
@@ -47,7 +44,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
celery_worker:
|
celery_worker:
|
||||||
image: mediacms:latest
|
image: mediacms/mediacms:latest
|
||||||
deploy:
|
deploy:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
volumes:
|
volumes:
|
||||||
@@ -80,4 +77,4 @@ services:
|
|||||||
test: ["CMD", "redis-cli","ping"]
|
test: ["CMD", "redis-cli","ping"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|||||||
@@ -2,7 +2,11 @@
|
|||||||
|
|
||||||
A number of options are available on `cms/settings.py`.
|
A number of options are available on `cms/settings.py`.
|
||||||
|
|
||||||
It is advisable to override any of them by adding it to `cms/local_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. So edit `cms/local_settings.py`, make a change and restart MediaCMS
|
Any change needs restart of MediaCMS in order to take effect. So edit `cms/local_settings.py`, make a change and restart MediaCMS
|
||||||
|
|
||||||
@@ -94,6 +98,14 @@ Make changes (True/False) to any of the following:
|
|||||||
- CAN_SHARE_MEDIA = True # whether the share 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
|
### automatically hide media upon being reported
|
||||||
|
|
||||||
set a low number for variable `REPORTED_TIMES_THRESHOLD`
|
set a low number for variable `REPORTED_TIMES_THRESHOLD`
|
||||||
|
|||||||
@@ -37,4 +37,4 @@ The reverse proxy (`jwilder/nginx-proxy`) can be configured to provide SSL termi
|
|||||||
|
|
||||||
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.
|
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,14 +1,14 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Media,
|
|
||||||
Encoding,
|
|
||||||
EncodeProfile,
|
|
||||||
Category,
|
Category,
|
||||||
Comment,
|
Comment,
|
||||||
Tag,
|
EncodeProfile,
|
||||||
|
Encoding,
|
||||||
Language,
|
Language,
|
||||||
|
Media,
|
||||||
Subtitle,
|
Subtitle,
|
||||||
|
Tag,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# ffmpeg only backend
|
# ffmpeg only backend
|
||||||
|
|
||||||
from subprocess import PIPE, Popen
|
|
||||||
import locale
|
import locale
|
||||||
import re
|
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
|
from subprocess import PIPE, Popen
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from .methods import is_mediacms_editor, is_mediacms_manager
|
from .methods import is_mediacms_editor, is_mediacms_manager
|
||||||
|
|
||||||
|
|
||||||
def stuff(request):
|
def stuff(request):
|
||||||
"""Pass settings to the frontend"""
|
"""Pass settings to the frontend"""
|
||||||
ret = {}
|
ret = {}
|
||||||
if request.is_secure():
|
ret["FRONTEND_HOST"] = request.build_absolute_uri('/')
|
||||||
# in case session is https, pass this setting so
|
|
||||||
# that the frontend uses https too
|
|
||||||
ret["FRONTEND_HOST"] = settings.SSL_FRONTEND_HOST
|
|
||||||
else:
|
|
||||||
ret["FRONTEND_HOST"] = settings.FRONTEND_HOST
|
|
||||||
ret["DEFAULT_THEME"] = settings.DEFAULT_THEME
|
ret["DEFAULT_THEME"] = settings.DEFAULT_THEME
|
||||||
ret["PORTAL_NAME"] = settings.PORTAL_NAME
|
ret["PORTAL_NAME"] = settings.PORTAL_NAME
|
||||||
ret["LOAD_FROM_CDN"] = settings.LOAD_FROM_CDN
|
ret["LOAD_FROM_CDN"] = settings.LOAD_FROM_CDN
|
||||||
@@ -24,18 +20,12 @@ def stuff(request):
|
|||||||
ret["UPLOAD_MAX_SIZE"] = settings.UPLOAD_MAX_SIZE
|
ret["UPLOAD_MAX_SIZE"] = settings.UPLOAD_MAX_SIZE
|
||||||
ret["UPLOAD_MAX_FILES_NUMBER"] = settings.UPLOAD_MAX_FILES_NUMBER
|
ret["UPLOAD_MAX_FILES_NUMBER"] = settings.UPLOAD_MAX_FILES_NUMBER
|
||||||
ret["PRE_UPLOAD_MEDIA_MESSAGE"] = settings.PRE_UPLOAD_MEDIA_MESSAGE
|
ret["PRE_UPLOAD_MEDIA_MESSAGE"] = settings.PRE_UPLOAD_MEDIA_MESSAGE
|
||||||
ret[
|
ret["POST_UPLOAD_AUTHOR_MESSAGE_UNLISTED_NO_COMMENTARY"] = settings.POST_UPLOAD_AUTHOR_MESSAGE_UNLISTED_NO_COMMENTARY
|
||||||
"POST_UPLOAD_AUTHOR_MESSAGE_UNLISTED_NO_COMMENTARY"
|
|
||||||
] = settings.POST_UPLOAD_AUTHOR_MESSAGE_UNLISTED_NO_COMMENTARY
|
|
||||||
ret["IS_MEDIACMS_ADMIN"] = request.user.is_superuser
|
ret["IS_MEDIACMS_ADMIN"] = request.user.is_superuser
|
||||||
ret["IS_MEDIACMS_EDITOR"] = is_mediacms_editor(request.user)
|
ret["IS_MEDIACMS_EDITOR"] = is_mediacms_editor(request.user)
|
||||||
ret["IS_MEDIACMS_MANAGER"] = is_mediacms_manager(request.user)
|
ret["IS_MEDIACMS_MANAGER"] = is_mediacms_manager(request.user)
|
||||||
ret["ALLOW_RATINGS"] = settings.ALLOW_RATINGS
|
ret["ALLOW_RATINGS"] = settings.ALLOW_RATINGS
|
||||||
ret[
|
ret["ALLOW_RATINGS_CONFIRMED_EMAIL_ONLY"] = settings.ALLOW_RATINGS_CONFIRMED_EMAIL_ONLY
|
||||||
"ALLOW_RATINGS_CONFIRMED_EMAIL_ONLY"
|
ret["VIDEO_PLAYER_FEATURED_VIDEO_ON_INDEX_PAGE"] = settings.VIDEO_PLAYER_FEATURED_VIDEO_ON_INDEX_PAGE
|
||||||
] = settings.ALLOW_RATINGS_CONFIRMED_EMAIL_ONLY
|
|
||||||
ret[
|
|
||||||
"VIDEO_PLAYER_FEATURED_VIDEO_ON_INDEX_PAGE"
|
|
||||||
] = settings.VIDEO_PLAYER_FEATURED_VIDEO_ON_INDEX_PAGE
|
|
||||||
ret["RSS_URL"] = "/rss"
|
ret["RSS_URL"] = "/rss"
|
||||||
return ret
|
return ret
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
from django.contrib.syndication.views import Feed
|
|
||||||
from django.utils.feedgenerator import Rss201rev2Feed
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.db.models import Q
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.postgres.search import SearchQuery
|
from django.contrib.postgres.search import SearchQuery
|
||||||
|
from django.contrib.syndication.views import Feed
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.feedgenerator import Rss201rev2Feed
|
||||||
|
|
||||||
from .models import Media, Category
|
|
||||||
from . import helpers
|
from . import helpers
|
||||||
|
from .models import Category, Media
|
||||||
from .stop_words import STOP_WORDS
|
from .stop_words import STOP_WORDS
|
||||||
|
|
||||||
|
|
||||||
@@ -119,11 +119,7 @@ class SearchRSSFeed(Feed):
|
|||||||
elif query:
|
elif query:
|
||||||
# same as on files.views.MediaSearch: move this processing to a prepare_query function
|
# same as on files.views.MediaSearch: move this processing to a prepare_query function
|
||||||
query = helpers.clean_query(query)
|
query = helpers.clean_query(query)
|
||||||
q_parts = [
|
q_parts = [q_part.rstrip("y") for q_part in query.split() if q_part not in STOP_WORDS]
|
||||||
q_part.rstrip("y")
|
|
||||||
for q_part in query.split()
|
|
||||||
if q_part not in STOP_WORDS
|
|
||||||
]
|
|
||||||
if q_parts:
|
if q_parts:
|
||||||
query = SearchQuery(q_parts[0] + ":*", search_type="raw")
|
query = SearchQuery(q_parts[0] + ":*", search_type="raw")
|
||||||
for part in q_parts[1:]:
|
for part in q_parts[1:]:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
|
from .methods import get_next_state, is_mediacms_editor
|
||||||
from .models import Media, Subtitle
|
from .models import Media, Subtitle
|
||||||
from .methods import is_mediacms_editor, get_next_state
|
|
||||||
|
|
||||||
|
|
||||||
class MultipleSelect(forms.CheckboxSelectMultiple):
|
class MultipleSelect(forms.CheckboxSelectMultiple):
|
||||||
@@ -8,9 +9,7 @@ class MultipleSelect(forms.CheckboxSelectMultiple):
|
|||||||
|
|
||||||
|
|
||||||
class MediaForm(forms.ModelForm):
|
class MediaForm(forms.ModelForm):
|
||||||
new_tags = forms.CharField(
|
new_tags = forms.CharField(label="Tags", help_text="a comma separated list of new tags.", required=False)
|
||||||
label="Tags", help_text="a comma separated list of new tags.", required=False
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Media
|
model = Media
|
||||||
@@ -27,7 +26,7 @@ class MediaForm(forms.ModelForm):
|
|||||||
"thumbnail_time",
|
"thumbnail_time",
|
||||||
"reported_times",
|
"reported_times",
|
||||||
"is_reviewed",
|
"is_reviewed",
|
||||||
"allow_download"
|
"allow_download",
|
||||||
)
|
)
|
||||||
widgets = {
|
widgets = {
|
||||||
"tags": MultipleSelect(),
|
"tags": MultipleSelect(),
|
||||||
@@ -42,9 +41,7 @@ class MediaForm(forms.ModelForm):
|
|||||||
self.fields.pop("featured")
|
self.fields.pop("featured")
|
||||||
self.fields.pop("reported_times")
|
self.fields.pop("reported_times")
|
||||||
self.fields.pop("is_reviewed")
|
self.fields.pop("is_reviewed")
|
||||||
self.fields["new_tags"].initial = ", ".join(
|
self.fields["new_tags"].initial = ", ".join([tag.title for tag in self.instance.tags.all()])
|
||||||
[tag.title for tag in self.instance.tags.all()]
|
|
||||||
)
|
|
||||||
|
|
||||||
def clean_uploaded_poster(self):
|
def clean_uploaded_poster(self):
|
||||||
image = self.cleaned_data.get("uploaded_poster", False)
|
image = self.cleaned_data.get("uploaded_poster", False)
|
||||||
@@ -57,9 +54,7 @@ class MediaForm(forms.ModelForm):
|
|||||||
data = self.cleaned_data
|
data = self.cleaned_data
|
||||||
state = data.get("state")
|
state = data.get("state")
|
||||||
if state != self.initial["state"]:
|
if state != self.initial["state"]:
|
||||||
self.instance.state = get_next_state(
|
self.instance.state = get_next_state(self.user, self.initial["state"], self.instance.state)
|
||||||
self.user, self.initial["state"], self.instance.state
|
|
||||||
)
|
|
||||||
|
|
||||||
media = super(MediaForm, self).save(*args, **kwargs)
|
media = super(MediaForm, self).save(*args, **kwargs)
|
||||||
return media
|
return media
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
# Kudos to Werner Robitza, AVEQ GmbH, for helping with ffmpeg
|
# Kudos to Werner Robitza, AVEQ GmbH, for helping with ffmpeg
|
||||||
# related content
|
# related content
|
||||||
|
|
||||||
import os
|
|
||||||
import math
|
|
||||||
import shutil
|
|
||||||
import tempfile
|
|
||||||
import random
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import subprocess
|
|
||||||
import json
|
import json
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
from fractions import Fraction
|
from fractions import Fraction
|
||||||
|
|
||||||
import filetype
|
import filetype
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
|
||||||
CRF_ENCODING_NUM_SECONDS = 2 # 0 * 60 # videos with greater duration will get
|
CRF_ENCODING_NUM_SECONDS = 2 # 0 * 60 # videos with greater duration will get
|
||||||
@@ -168,9 +168,7 @@ def rm_dir(directory):
|
|||||||
|
|
||||||
def url_from_path(filename):
|
def url_from_path(filename):
|
||||||
# TODO: find a way to preserver http - https ...
|
# TODO: find a way to preserver http - https ...
|
||||||
return "{0}{1}".format(
|
return "{0}{1}".format(settings.MEDIA_URL, filename.replace(settings.MEDIA_ROOT, ""))
|
||||||
settings.MEDIA_URL, filename.replace(settings.MEDIA_ROOT, "")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def create_temp_file(suffix=None, dir=settings.TEMP_DIRECTORY):
|
def create_temp_file(suffix=None, dir=settings.TEMP_DIRECTORY):
|
||||||
@@ -210,9 +208,7 @@ def run_command(cmd, cwd=None):
|
|||||||
cmd = cmd.split()
|
cmd = cmd.split()
|
||||||
ret = {}
|
ret = {}
|
||||||
if cwd:
|
if cwd:
|
||||||
process = subprocess.Popen(
|
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd)
|
||||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
stdout, stderr = process.communicate()
|
stdout, stderr = process.communicate()
|
||||||
@@ -331,9 +327,7 @@ def media_file_info(input_file):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
hms, msec = duration_str.split(",")
|
hms, msec = duration_str.split(",")
|
||||||
|
|
||||||
total_dur = sum(
|
total_dur = sum(int(x) * 60 ** i for i, x in enumerate(reversed(hms.split(":"))))
|
||||||
int(x) * 60 ** i for i, x in enumerate(reversed(hms.split(":")))
|
|
||||||
)
|
|
||||||
video_duration = total_dur + float("0." + msec)
|
video_duration = total_dur + float("0." + msec)
|
||||||
else:
|
else:
|
||||||
# fallback to format, eg for webm
|
# fallback to format, eg for webm
|
||||||
@@ -370,7 +364,7 @@ def media_file_info(input_file):
|
|||||||
input_file,
|
input_file,
|
||||||
]
|
]
|
||||||
stdout = run_command(cmd).get("out")
|
stdout = run_command(cmd).get("out")
|
||||||
stream_size = sum([int(l) for l in stdout.split("\n") if l != ""])
|
stream_size = sum([int(line) for line in stdout.split("\n") if line != ""])
|
||||||
video_bitrate = round((stream_size * 8 / 1024.0) / video_duration, 2)
|
video_bitrate = round((stream_size * 8 / 1024.0) / video_duration, 2)
|
||||||
|
|
||||||
ret = {
|
ret = {
|
||||||
@@ -396,9 +390,7 @@ def media_file_info(input_file):
|
|||||||
hms, msec = duration_str.split(".")
|
hms, msec = duration_str.split(".")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
hms, msec = duration_str.split(",")
|
hms, msec = duration_str.split(",")
|
||||||
total_dur = sum(
|
total_dur = sum(int(x) * 60 ** i for i, x in enumerate(reversed(hms.split(":"))))
|
||||||
int(x) * 60 ** i for i, x in enumerate(reversed(hms.split(":")))
|
|
||||||
)
|
|
||||||
audio_duration = total_dur + float("0." + msec)
|
audio_duration = total_dur + float("0." + msec)
|
||||||
else:
|
else:
|
||||||
# fallback to format, eg for webm
|
# fallback to format, eg for webm
|
||||||
@@ -432,7 +424,7 @@ def media_file_info(input_file):
|
|||||||
input_file,
|
input_file,
|
||||||
]
|
]
|
||||||
stdout = run_command(cmd).get("out")
|
stdout = run_command(cmd).get("out")
|
||||||
stream_size = sum([int(l) for l in stdout.split("\n") if l != ""])
|
stream_size = sum([int(line) for line in stdout.split("\n") if line != ""])
|
||||||
audio_bitrate = round((stream_size * 8 / 1024.0) / audio_duration, 2)
|
audio_bitrate = round((stream_size * 8 / 1024.0) / audio_duration, 2)
|
||||||
|
|
||||||
ret.update(
|
ret.update(
|
||||||
@@ -660,9 +652,7 @@ def get_base_ffmpeg_command(
|
|||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
def produce_ffmpeg_commands(
|
def produce_ffmpeg_commands(media_file, media_info, resolution, codec, output_filename, pass_file, chunk=False):
|
||||||
media_file, media_info, resolution, codec, output_filename, pass_file, chunk=False
|
|
||||||
):
|
|
||||||
try:
|
try:
|
||||||
media_info = json.loads(media_info)
|
media_info = json.loads(media_info)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
@@ -670,13 +660,13 @@ def produce_ffmpeg_commands(
|
|||||||
|
|
||||||
if codec == "h264":
|
if codec == "h264":
|
||||||
encoder = "libx264"
|
encoder = "libx264"
|
||||||
ext = "mp4"
|
# ext = "mp4"
|
||||||
elif codec in ["h265", "hevc"]:
|
elif codec in ["h265", "hevc"]:
|
||||||
encoder = "libx265"
|
encoder = "libx265"
|
||||||
ext = "mp4"
|
# ext = "mp4"
|
||||||
elif codec == "vp9":
|
elif codec == "vp9":
|
||||||
encoder = "libvpx-vp9"
|
encoder = "libvpx-vp9"
|
||||||
ext = "webm"
|
# ext = "webm"
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -699,9 +689,7 @@ def produce_ffmpeg_commands(
|
|||||||
# else:
|
# else:
|
||||||
|
|
||||||
# adjust the target frame rate if the input is fractional
|
# adjust the target frame rate if the input is fractional
|
||||||
target_fps = (
|
target_fps = src_framerate if isinstance(src_framerate, int) else math.ceil(src_framerate)
|
||||||
src_framerate if isinstance(src_framerate, int) else math.ceil(src_framerate)
|
|
||||||
)
|
|
||||||
|
|
||||||
if media_info.get("video_duration") > CRF_ENCODING_NUM_SECONDS:
|
if media_info.get("video_duration") > CRF_ENCODING_NUM_SECONDS:
|
||||||
enc_type = "crf"
|
enc_type = "crf"
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
from rest_framework.views import APIView
|
from drf_yasg import openapi as openapi
|
||||||
from rest_framework.parsers import JSONParser
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from rest_framework.settings import api_settings
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
from rest_framework.parsers import JSONParser
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.settings import api_settings
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
from users.models import User
|
from users.models import User
|
||||||
from users.serializers import UserSerializer
|
from users.serializers import UserSerializer
|
||||||
from .permissions import IsMediacmsEditor
|
|
||||||
from .models import Media, Comment
|
|
||||||
from .methods import is_mediacms_manager
|
|
||||||
|
|
||||||
from .serializers import MediaSerializer, CommentSerializer
|
from .methods import is_mediacms_manager
|
||||||
|
from .models import Comment, Media
|
||||||
|
from .permissions import IsMediacmsEditor
|
||||||
|
from .serializers import CommentSerializer, MediaSerializer
|
||||||
|
|
||||||
|
|
||||||
class MediaList(APIView):
|
class MediaList(APIView):
|
||||||
@@ -23,6 +25,17 @@ class MediaList(APIView):
|
|||||||
permission_classes = (IsMediacmsEditor,)
|
permission_classes = (IsMediacmsEditor,)
|
||||||
parser_classes = (JSONParser,)
|
parser_classes = (JSONParser,)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[
|
||||||
|
openapi.Parameter(name='sort_by', type=openapi.TYPE_STRING, in_=openapi.IN_QUERY, description='Sort by any of: title, add_date, edit_date, views, likes, reported_times'),
|
||||||
|
openapi.Parameter(name='ordering', type=openapi.TYPE_STRING, in_=openapi.IN_QUERY, description='Order by: asc, desc'),
|
||||||
|
openapi.Parameter(name='state', type=openapi.TYPE_STRING, in_=openapi.IN_QUERY, description='Media state, options: private", "public", "unlisted'),
|
||||||
|
openapi.Parameter(name='encoding_status', type=openapi.TYPE_STRING, in_=openapi.IN_QUERY, description='Encoding status, options "pending", "running", "fail", "success"'),
|
||||||
|
],
|
||||||
|
tags=['Manage'],
|
||||||
|
operation_summary='Manage Media',
|
||||||
|
operation_description='Manage media for MediaCMS managers and reviewers',
|
||||||
|
)
|
||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
params = self.request.query_params
|
params = self.request.query_params
|
||||||
ordering = params.get("ordering", "").strip()
|
ordering = params.get("ordering", "").strip()
|
||||||
@@ -94,6 +107,12 @@ class MediaList(APIView):
|
|||||||
serializer = MediaSerializer(page, many=True, context={"request": request})
|
serializer = MediaSerializer(page, many=True, context={"request": request})
|
||||||
return paginator.get_paginated_response(serializer.data)
|
return paginator.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[],
|
||||||
|
tags=['Manage'],
|
||||||
|
operation_summary='Delete Media',
|
||||||
|
operation_description='Delete media for MediaCMS managers and reviewers',
|
||||||
|
)
|
||||||
def delete(self, request, format=None):
|
def delete(self, request, format=None):
|
||||||
tokens = request.GET.get("tokens")
|
tokens = request.GET.get("tokens")
|
||||||
if tokens:
|
if tokens:
|
||||||
@@ -112,6 +131,12 @@ class CommentList(APIView):
|
|||||||
permission_classes = (IsMediacmsEditor,)
|
permission_classes = (IsMediacmsEditor,)
|
||||||
parser_classes = (JSONParser,)
|
parser_classes = (JSONParser,)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[],
|
||||||
|
tags=['Manage'],
|
||||||
|
operation_summary='Manage Comments',
|
||||||
|
operation_description='Manage comments for MediaCMS managers and reviewers',
|
||||||
|
)
|
||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
params = self.request.query_params
|
params = self.request.query_params
|
||||||
ordering = params.get("ordering", "").strip()
|
ordering = params.get("ordering", "").strip()
|
||||||
@@ -137,6 +162,12 @@ class CommentList(APIView):
|
|||||||
serializer = CommentSerializer(page, many=True, context={"request": request})
|
serializer = CommentSerializer(page, many=True, context={"request": request})
|
||||||
return paginator.get_paginated_response(serializer.data)
|
return paginator.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[],
|
||||||
|
tags=['Manage'],
|
||||||
|
operation_summary='Delete Comments',
|
||||||
|
operation_description='Delete comments for MediaCMS managers and reviewers',
|
||||||
|
)
|
||||||
def delete(self, request, format=None):
|
def delete(self, request, format=None):
|
||||||
comment_ids = request.GET.get("comment_ids")
|
comment_ids = request.GET.get("comment_ids")
|
||||||
if comment_ids:
|
if comment_ids:
|
||||||
@@ -156,6 +187,12 @@ class UserList(APIView):
|
|||||||
permission_classes = (IsMediacmsEditor,)
|
permission_classes = (IsMediacmsEditor,)
|
||||||
parser_classes = (JSONParser,)
|
parser_classes = (JSONParser,)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[],
|
||||||
|
tags=['Manage'],
|
||||||
|
operation_summary='Manage Users',
|
||||||
|
operation_description='Manage users for MediaCMS managers and reviewers',
|
||||||
|
)
|
||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
params = self.request.query_params
|
params = self.request.query_params
|
||||||
ordering = params.get("ordering", "").strip()
|
ordering = params.get("ordering", "").strip()
|
||||||
@@ -187,11 +224,15 @@ class UserList(APIView):
|
|||||||
serializer = UserSerializer(page, many=True, context={"request": request})
|
serializer = UserSerializer(page, many=True, context={"request": request})
|
||||||
return paginator.get_paginated_response(serializer.data)
|
return paginator.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[],
|
||||||
|
tags=['Manage'],
|
||||||
|
operation_summary='Delete Users',
|
||||||
|
operation_description='Delete users for MediaCMS managers',
|
||||||
|
)
|
||||||
def delete(self, request, format=None):
|
def delete(self, request, format=None):
|
||||||
if not is_mediacms_manager(request.user):
|
if not is_mediacms_manager(request.user):
|
||||||
return Response(
|
return Response({"detail": "bad permissions"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
{"detail": "bad permissions"}, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
tokens = request.GET.get("tokens")
|
tokens = request.GET.get("tokens")
|
||||||
if tokens:
|
if tokens:
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
# Kudos to Werner Robitza, AVEQ GmbH, for helping with ffmpeg
|
# Kudos to Werner Robitza, AVEQ GmbH, for helping with ffmpeg
|
||||||
# related content
|
# related content
|
||||||
|
|
||||||
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
import itertools
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from cms import celery_app
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db.models import Q
|
|
||||||
from django.core.mail import EmailMessage
|
from django.core.mail import EmailMessage
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
from cms import celery_app
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
from .helpers import mask_ip
|
from .helpers import mask_ip
|
||||||
@@ -48,9 +50,7 @@ def pre_save_action(media, user, session_key, action, remote_ip):
|
|||||||
if user:
|
if user:
|
||||||
query = MediaAction.objects.filter(media=media, action=action, user=user)
|
query = MediaAction.objects.filter(media=media, action=action, user=user)
|
||||||
else:
|
else:
|
||||||
query = MediaAction.objects.filter(
|
query = MediaAction.objects.filter(media=media, action=action, session_key=session_key)
|
||||||
media=media, action=action, session_key=session_key
|
|
||||||
)
|
|
||||||
query = query.order_by("-action_date")
|
query = query.order_by("-action_date")
|
||||||
|
|
||||||
if query:
|
if query:
|
||||||
@@ -71,11 +71,7 @@ def pre_save_action(media, user, session_key, action, remote_ip):
|
|||||||
# perform some checking for requests where no session
|
# perform some checking for requests where no session
|
||||||
# id is specified (and user is anonymous) to avoid spam
|
# id is specified (and user is anonymous) to avoid spam
|
||||||
# eg allow for the same remote_ip for a specific number of actions
|
# eg allow for the same remote_ip for a specific number of actions
|
||||||
query = (
|
query = MediaAction.objects.filter(media=media, action=action, remote_ip=remote_ip).filter(user=None).order_by("-action_date")
|
||||||
MediaAction.objects.filter(media=media, action=action, remote_ip=remote_ip)
|
|
||||||
.filter(user=None)
|
|
||||||
.order_by("-action_date")
|
|
||||||
)
|
|
||||||
if query:
|
if query:
|
||||||
query = query.first()
|
query = query.first()
|
||||||
now = datetime.now(query.action_date.tzinfo)
|
now = datetime.now(query.action_date.tzinfo)
|
||||||
@@ -204,11 +200,8 @@ URL: %s
|
|||||||
d["to"] = [media.user.email]
|
d["to"] = [media.user.email]
|
||||||
notify_items.append(d)
|
notify_items.append(d)
|
||||||
|
|
||||||
|
|
||||||
for item in notify_items:
|
for item in notify_items:
|
||||||
email = EmailMessage(
|
email = EmailMessage(item["title"], item["msg"], settings.DEFAULT_FROM_EMAIL, item["to"])
|
||||||
item["title"], item["msg"], settings.DEFAULT_FROM_EMAIL, item["to"]
|
|
||||||
)
|
|
||||||
email.send(fail_silently=True)
|
email.send(fail_silently=True)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -222,17 +215,9 @@ def show_recommended_media(request, limit=100):
|
|||||||
pmi = cache.get("popular_media_ids")
|
pmi = cache.get("popular_media_ids")
|
||||||
# produced by task get_list_of_popular_media and cached
|
# produced by task get_list_of_popular_media and cached
|
||||||
if pmi:
|
if pmi:
|
||||||
media = list(
|
media = list(models.Media.objects.filter(friendly_token__in=pmi).filter(basic_query).prefetch_related("user")[:limit])
|
||||||
models.Media.objects.filter(friendly_token__in=pmi)
|
|
||||||
.filter(basic_query)
|
|
||||||
.prefetch_related("user")[:limit]
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
media = list(
|
media = list(models.Media.objects.filter(basic_query).order_by("-views", "-likes").prefetch_related("user")[:limit])
|
||||||
models.Media.objects.filter(basic_query)
|
|
||||||
.order_by("-views", "-likes")
|
|
||||||
.prefetch_related("user")[:limit]
|
|
||||||
)
|
|
||||||
random.shuffle(media)
|
random.shuffle(media)
|
||||||
return media
|
return media
|
||||||
|
|
||||||
@@ -257,11 +242,7 @@ def show_related_media_content(media, request, limit):
|
|||||||
# and include author videos in any case
|
# and include author videos in any case
|
||||||
|
|
||||||
q_author = Q(listable=True, user=media.user)
|
q_author = Q(listable=True, user=media.user)
|
||||||
m = list(
|
m = list(models.Media.objects.filter(q_author).order_by().prefetch_related("user")[:limit])
|
||||||
models.Media.objects.filter(q_author)
|
|
||||||
.order_by()
|
|
||||||
.prefetch_related("user")[:limit]
|
|
||||||
)
|
|
||||||
|
|
||||||
# order by random criteria so that it doesn't bring the same results
|
# order by random criteria so that it doesn't bring the same results
|
||||||
# attention: only fields that are indexed make sense here! also need
|
# attention: only fields that are indexed make sense here! also need
|
||||||
@@ -282,20 +263,12 @@ def show_related_media_content(media, request, limit):
|
|||||||
category = media.category.first()
|
category = media.category.first()
|
||||||
if category:
|
if category:
|
||||||
q_category = Q(listable=True, category=category)
|
q_category = Q(listable=True, category=category)
|
||||||
q_res = (
|
q_res = models.Media.objects.filter(q_category).order_by(order_criteria[random.randint(0, len(order_criteria) - 1)]).prefetch_related("user")[: limit - media.user.media_count]
|
||||||
models.Media.objects.filter(q_category)
|
|
||||||
.order_by(order_criteria[random.randint(0, len(order_criteria) - 1)])
|
|
||||||
.prefetch_related("user")[: limit - media.user.media_count]
|
|
||||||
)
|
|
||||||
m = list(itertools.chain(m, q_res))
|
m = list(itertools.chain(m, q_res))
|
||||||
|
|
||||||
if len(m) < limit:
|
if len(m) < limit:
|
||||||
q_generic = Q(listable=True)
|
q_generic = Q(listable=True)
|
||||||
q_res = (
|
q_res = models.Media.objects.filter(q_generic).order_by(order_criteria[random.randint(0, len(order_criteria) - 1)]).prefetch_related("user")[: limit - media.user.media_count]
|
||||||
models.Media.objects.filter(q_generic)
|
|
||||||
.order_by(order_criteria[random.randint(0, len(order_criteria) - 1)])
|
|
||||||
.prefetch_related("user")[: limit - media.user.media_count]
|
|
||||||
)
|
|
||||||
m = list(itertools.chain(m, q_res))
|
m = list(itertools.chain(m, q_res))
|
||||||
|
|
||||||
m = list(set(m[:limit])) # remove duplicates
|
m = list(set(m[:limit])) # remove duplicates
|
||||||
@@ -313,11 +286,7 @@ def show_related_media_author(media, request, limit):
|
|||||||
"""Return a list of related media form the same author"""
|
"""Return a list of related media form the same author"""
|
||||||
|
|
||||||
q_author = Q(listable=True, user=media.user)
|
q_author = Q(listable=True, user=media.user)
|
||||||
m = list(
|
m = list(models.Media.objects.filter(q_author).order_by().prefetch_related("user")[:limit])
|
||||||
models.Media.objects.filter(q_author)
|
|
||||||
.order_by()
|
|
||||||
.prefetch_related("user")[:limit]
|
|
||||||
)
|
|
||||||
|
|
||||||
# order by random criteria so that it doesn't bring the same results
|
# order by random criteria so that it doesn't bring the same results
|
||||||
# attention: only fields that are indexed make sense here! also need
|
# attention: only fields that are indexed make sense here! also need
|
||||||
@@ -347,13 +316,7 @@ def update_user_ratings(user, media, user_ratings):
|
|||||||
"""Populate user ratings for a media"""
|
"""Populate user ratings for a media"""
|
||||||
|
|
||||||
for rating in user_ratings:
|
for rating in user_ratings:
|
||||||
user_rating = (
|
user_rating = models.Rating.objects.filter(user=user, media_id=media, rating_category_id=rating.get("category_id")).only("score").first()
|
||||||
models.Rating.objects.filter(
|
|
||||||
user=user, media_id=media, rating_category_id=rating.get("category_id")
|
|
||||||
)
|
|
||||||
.only("score")
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
if user_rating:
|
if user_rating:
|
||||||
rating["score"] = user_rating.score
|
rating["score"] = user_rating.score
|
||||||
return user_ratings
|
return user_ratings
|
||||||
@@ -379,9 +342,7 @@ View it on %s
|
|||||||
media.title,
|
media.title,
|
||||||
media_url,
|
media_url,
|
||||||
)
|
)
|
||||||
email = EmailMessage(
|
email = EmailMessage(title, msg, settings.DEFAULT_FROM_EMAIL, [media.user.email])
|
||||||
title, msg, settings.DEFAULT_FROM_EMAIL, [media.user.email]
|
|
||||||
)
|
|
||||||
email.send(fail_silently=True)
|
email.send(fail_silently=True)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -420,27 +381,17 @@ def list_tasks():
|
|||||||
friendly_token = task_args.split()[0]
|
friendly_token = task_args.split()[0]
|
||||||
profile_id = task_args.split()[1]
|
profile_id = task_args.split()[1]
|
||||||
|
|
||||||
media = models.Media.objects.filter(
|
media = models.Media.objects.filter(friendly_token=friendly_token).first()
|
||||||
friendly_token=friendly_token
|
|
||||||
).first()
|
|
||||||
if media:
|
if media:
|
||||||
profile = models.EncodeProfile.objects.filter(
|
profile = models.EncodeProfile.objects.filter(id=profile_id).first()
|
||||||
id=profile_id
|
|
||||||
).first()
|
|
||||||
if profile:
|
if profile:
|
||||||
media_profile_pairs.append(
|
media_profile_pairs.append((media.friendly_token, profile.id))
|
||||||
(media.friendly_token, profile.id)
|
|
||||||
)
|
|
||||||
task_dict["info"] = {}
|
task_dict["info"] = {}
|
||||||
task_dict["info"]["profile name"] = profile.name
|
task_dict["info"]["profile name"] = profile.name
|
||||||
task_dict["info"]["media title"] = media.title
|
task_dict["info"]["media title"] = media.title
|
||||||
encoding = models.Encoding.objects.filter(
|
encoding = models.Encoding.objects.filter(task_id=task.get("id")).first()
|
||||||
task_id=task.get("id")
|
|
||||||
).first()
|
|
||||||
if encoding:
|
if encoding:
|
||||||
task_dict["info"][
|
task_dict["info"]["encoding progress"] = encoding.progress
|
||||||
"encoding progress"
|
|
||||||
] = encoding.progress
|
|
||||||
|
|
||||||
ret[state]["tasks"].append(task_dict)
|
ret[state]["tasks"].append(task_dict)
|
||||||
ret["task_ids"] = task_ids
|
ret["task_ids"] = task_ids
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
# Generated by Django 3.1.4 on 2020-12-01 07:12
|
# Generated by Django 3.1.4 on 2020-12-01 07:12
|
||||||
|
|
||||||
import django.contrib.postgres.search
|
|
||||||
from django.db import migrations, models
|
|
||||||
import files.models
|
|
||||||
import imagekit.models.fields
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
import django.contrib.postgres.search
|
||||||
|
import imagekit.models.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import files.models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
@@ -32,9 +34,7 @@ class Migration(migrations.Migration):
|
|||||||
("description", models.TextField(blank=True)),
|
("description", models.TextField(blank=True)),
|
||||||
(
|
(
|
||||||
"is_global",
|
"is_global",
|
||||||
models.BooleanField(
|
models.BooleanField(default=False, help_text="global categories or user specific"),
|
||||||
default=False, help_text="global categories or user specific"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"media_count",
|
"media_count",
|
||||||
@@ -42,9 +42,7 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
"thumbnail",
|
"thumbnail",
|
||||||
imagekit.models.fields.ProcessedImageField(
|
imagekit.models.fields.ProcessedImageField(blank=True, upload_to=files.models.category_thumb_path),
|
||||||
blank=True, upload_to=files.models.category_thumb_path
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"listings_thumbnail",
|
"listings_thumbnail",
|
||||||
@@ -153,9 +151,7 @@ class Migration(migrations.Migration):
|
|||||||
("commands", models.TextField(blank=True, help_text="commands run")),
|
("commands", models.TextField(blank=True, help_text="commands run")),
|
||||||
(
|
(
|
||||||
"chunk",
|
"chunk",
|
||||||
models.BooleanField(
|
models.BooleanField(db_index=True, default=False, help_text="is chunk?"),
|
||||||
db_index=True, default=False, help_text="is chunk?"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
("chunk_file_path", models.CharField(blank=True, max_length=400)),
|
("chunk_file_path", models.CharField(blank=True, max_length=400)),
|
||||||
("chunks_info", models.TextField(blank=True)),
|
("chunks_info", models.TextField(blank=True)),
|
||||||
@@ -317,9 +313,7 @@ class Migration(migrations.Migration):
|
|||||||
("likes", models.IntegerField(db_index=True, default=1)),
|
("likes", models.IntegerField(db_index=True, default=1)),
|
||||||
(
|
(
|
||||||
"listable",
|
"listable",
|
||||||
models.BooleanField(
|
models.BooleanField(default=False, help_text="Whether it will appear on listings"),
|
||||||
default=False, help_text="Whether it will appear on listings"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"md5sum",
|
"md5sum",
|
||||||
@@ -341,9 +335,7 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
"media_info",
|
"media_info",
|
||||||
models.TextField(
|
models.TextField(blank=True, help_text="extracted media metadata info"),
|
||||||
blank=True, help_text="extracted media metadata info"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"media_type",
|
"media_type",
|
||||||
@@ -387,9 +379,7 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
"reported_times",
|
"reported_times",
|
||||||
models.IntegerField(
|
models.IntegerField(default=0, help_text="how many time a Medis is reported"),
|
||||||
default=0, help_text="how many time a Medis is reported"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"search",
|
"search",
|
||||||
@@ -485,9 +475,7 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
"user_featured",
|
"user_featured",
|
||||||
models.BooleanField(
|
models.BooleanField(default=False, help_text="Featured by the user"),
|
||||||
default=False, help_text="Featured by the user"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
("video_height", models.IntegerField(default=1)),
|
("video_height", models.IntegerField(default=1)),
|
||||||
("views", models.IntegerField(db_index=True, default=1)),
|
("views", models.IntegerField(db_index=True, default=1)),
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# Generated by Django 3.1.4 on 2020-12-01 07:12
|
# Generated by Django 3.1.4 on 2020-12-01 07:12
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
import django.contrib.postgres.indexes
|
import django.contrib.postgres.indexes
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import mptt.fields
|
import mptt.fields
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@@ -31,9 +31,7 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="subtitle",
|
model_name="subtitle",
|
||||||
name="language",
|
name="language",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="files.language"),
|
||||||
on_delete=django.db.models.deletion.CASCADE, to="files.language"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="subtitle",
|
model_name="subtitle",
|
||||||
@@ -47,9 +45,7 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="subtitle",
|
model_name="subtitle",
|
||||||
name="user",
|
name="user",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||||
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="rating",
|
model_name="rating",
|
||||||
@@ -63,37 +59,27 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="rating",
|
model_name="rating",
|
||||||
name="rating_category",
|
name="rating_category",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="files.ratingcategory"),
|
||||||
on_delete=django.db.models.deletion.CASCADE, to="files.ratingcategory"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="rating",
|
model_name="rating",
|
||||||
name="user",
|
name="user",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||||
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="playlistmedia",
|
model_name="playlistmedia",
|
||||||
name="media",
|
name="media",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="files.media"),
|
||||||
on_delete=django.db.models.deletion.CASCADE, to="files.media"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="playlistmedia",
|
model_name="playlistmedia",
|
||||||
name="playlist",
|
name="playlist",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="files.playlist"),
|
||||||
on_delete=django.db.models.deletion.CASCADE, to="files.playlist"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="playlist",
|
model_name="playlist",
|
||||||
name="media",
|
name="media",
|
||||||
field=models.ManyToManyField(
|
field=models.ManyToManyField(blank=True, through="files.PlaylistMedia", to="files.Media"),
|
||||||
blank=True, through="files.PlaylistMedia", to="files.Media"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="playlist",
|
model_name="playlist",
|
||||||
@@ -173,9 +159,7 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="encoding",
|
model_name="encoding",
|
||||||
name="profile",
|
name="profile",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="files.encodeprofile"),
|
||||||
on_delete=django.db.models.deletion.CASCADE, to="files.encodeprofile"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="comment",
|
model_name="comment",
|
||||||
@@ -200,9 +184,7 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="comment",
|
model_name="comment",
|
||||||
name="user",
|
name="user",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||||
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="category",
|
model_name="category",
|
||||||
@@ -216,9 +198,7 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
migrations.AddIndex(
|
migrations.AddIndex(
|
||||||
model_name="rating",
|
model_name="rating",
|
||||||
index=models.Index(
|
index=models.Index(fields=["user", "media"], name="files_ratin_user_id_72ca6a_idx"),
|
||||||
fields=["user", "media"], name="files_ratin_user_id_72ca6a_idx"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
name="rating",
|
name="rating",
|
||||||
@@ -226,8 +206,6 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
migrations.AddIndex(
|
migrations.AddIndex(
|
||||||
model_name="media",
|
model_name="media",
|
||||||
index=django.contrib.postgres.indexes.GinIndex(
|
index=django.contrib.postgres.indexes.GinIndex(fields=["search"], name="files_media_search_7194c6_gin"),
|
||||||
fields=["search"], name="files_media_search_7194c6_gin"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
295
files/models.py
295
files/models.py
@@ -1,28 +1,27 @@
|
|||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
|
||||||
import os
|
import os
|
||||||
|
import random
|
||||||
import re
|
import re
|
||||||
import tempfile
|
import tempfile
|
||||||
import random
|
import uuid
|
||||||
import json
|
|
||||||
import m3u8
|
import m3u8
|
||||||
from django.utils import timezone
|
|
||||||
from django.db import connection
|
|
||||||
from django.db import models
|
|
||||||
from django.template.defaultfilters import slugify
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.postgres.indexes import GinIndex
|
from django.contrib.postgres.indexes import GinIndex
|
||||||
from django.db.models.signals import pre_delete, post_delete, post_save, m2m_changed
|
|
||||||
from django.core.files import File
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.dispatch import receiver
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.html import strip_tags
|
|
||||||
from django.contrib.postgres.search import SearchVectorField
|
from django.contrib.postgres.search import SearchVectorField
|
||||||
from mptt.models import MPTTModel, TreeForeignKey
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.core.files import File
|
||||||
from imagekit.processors import ResizeToFit
|
from django.db import connection, models
|
||||||
|
from django.db.models.signals import m2m_changed, post_delete, post_save, pre_delete
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.template.defaultfilters import slugify
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.html import strip_tags
|
||||||
from imagekit.models import ProcessedImageField
|
from imagekit.models import ProcessedImageField
|
||||||
|
from imagekit.processors import ResizeToFit
|
||||||
|
from mptt.models import MPTTModel, TreeForeignKey
|
||||||
|
|
||||||
from . import helpers
|
from . import helpers
|
||||||
from .methods import notify_users
|
from .methods import notify_users
|
||||||
@@ -88,36 +87,26 @@ ENCODE_RESOLUTIONS_KEYS = [resolution for resolution, name in ENCODE_RESOLUTIONS
|
|||||||
def original_media_file_path(instance, filename):
|
def original_media_file_path(instance, filename):
|
||||||
"""Helper function to place original media file"""
|
"""Helper function to place original media file"""
|
||||||
file_name = "{0}.{1}".format(instance.uid.hex, helpers.get_file_name(filename))
|
file_name = "{0}.{1}".format(instance.uid.hex, helpers.get_file_name(filename))
|
||||||
return settings.MEDIA_UPLOAD_DIR + "user/{0}/{1}".format(
|
return settings.MEDIA_UPLOAD_DIR + "user/{0}/{1}".format(instance.user.username, file_name)
|
||||||
instance.user.username, file_name
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def encoding_media_file_path(instance, filename):
|
def encoding_media_file_path(instance, filename):
|
||||||
"""Helper function to place encoded media file"""
|
"""Helper function to place encoded media file"""
|
||||||
|
|
||||||
file_name = "{0}.{1}".format(
|
file_name = "{0}.{1}".format(instance.media.uid.hex, helpers.get_file_name(filename))
|
||||||
instance.media.uid.hex, helpers.get_file_name(filename)
|
return settings.MEDIA_ENCODING_DIR + "{0}/{1}/{2}".format(instance.profile.id, instance.media.user.username, file_name)
|
||||||
)
|
|
||||||
return settings.MEDIA_ENCODING_DIR + "{0}/{1}/{2}".format(
|
|
||||||
instance.profile.id, instance.media.user.username, file_name
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def original_thumbnail_file_path(instance, filename):
|
def original_thumbnail_file_path(instance, filename):
|
||||||
"""Helper function to place original media thumbnail file"""
|
"""Helper function to place original media thumbnail file"""
|
||||||
|
|
||||||
return settings.THUMBNAIL_UPLOAD_DIR + "user/{0}/{1}".format(
|
return settings.THUMBNAIL_UPLOAD_DIR + "user/{0}/{1}".format(instance.user.username, filename)
|
||||||
instance.user.username, filename
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def subtitles_file_path(instance, filename):
|
def subtitles_file_path(instance, filename):
|
||||||
"""Helper function to place subtitle file"""
|
"""Helper function to place subtitle file"""
|
||||||
|
|
||||||
return settings.SUBTITLES_UPLOAD_DIR + "user/{0}/{1}".format(
|
return settings.SUBTITLES_UPLOAD_DIR + "user/{0}/{1}".format(instance.media.user.username, filename)
|
||||||
instance.media.user.username, filename
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def category_thumb_path(instance, filename):
|
def category_thumb_path(instance, filename):
|
||||||
@@ -130,17 +119,11 @@ def category_thumb_path(instance, filename):
|
|||||||
class Media(models.Model):
|
class Media(models.Model):
|
||||||
"""The most important model for MediaCMS"""
|
"""The most important model for MediaCMS"""
|
||||||
|
|
||||||
add_date = models.DateTimeField(
|
add_date = models.DateTimeField("Date produced", blank=True, null=True, db_index=True)
|
||||||
"Date produced", blank=True, null=True, db_index=True
|
|
||||||
)
|
|
||||||
|
|
||||||
allow_download = models.BooleanField(
|
allow_download = models.BooleanField(default=True, help_text="Whether option to download media is shown")
|
||||||
default=True, help_text="Whether option to download media is shown"
|
|
||||||
)
|
|
||||||
|
|
||||||
category = models.ManyToManyField(
|
category = models.ManyToManyField("Category", blank=True, help_text="Media can be part of one or more categories")
|
||||||
"Category", blank=True, help_text="Media can be part of one or more categories"
|
|
||||||
)
|
|
||||||
|
|
||||||
channel = models.ForeignKey(
|
channel = models.ForeignKey(
|
||||||
"users.Channel",
|
"users.Channel",
|
||||||
@@ -158,13 +141,9 @@ class Media(models.Model):
|
|||||||
|
|
||||||
edit_date = models.DateTimeField(auto_now=True)
|
edit_date = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
enable_comments = models.BooleanField(
|
enable_comments = models.BooleanField(default=True, help_text="Whether comments will be allowed for this media")
|
||||||
default=True, help_text="Whether comments will be allowed for this media"
|
|
||||||
)
|
|
||||||
|
|
||||||
encoding_status = models.CharField(
|
encoding_status = models.CharField(max_length=20, choices=MEDIA_ENCODING_STATUS, default="pending", db_index=True)
|
||||||
max_length=20, choices=MEDIA_ENCODING_STATUS, default="pending", db_index=True
|
|
||||||
)
|
|
||||||
|
|
||||||
featured = models.BooleanField(
|
featured = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
@@ -172,13 +151,9 @@ class Media(models.Model):
|
|||||||
help_text="Whether media is globally featured by a MediaCMS editor",
|
help_text="Whether media is globally featured by a MediaCMS editor",
|
||||||
)
|
)
|
||||||
|
|
||||||
friendly_token = models.CharField(
|
friendly_token = models.CharField(blank=True, max_length=12, db_index=True, help_text="Identifier for the Media")
|
||||||
blank=True, max_length=12, db_index=True, help_text="Identifier for the Media"
|
|
||||||
)
|
|
||||||
|
|
||||||
hls_file = models.CharField(
|
hls_file = models.CharField(max_length=1000, blank=True, help_text="Path to HLS file for videos")
|
||||||
max_length=1000, blank=True, help_text="Path to HLS file for videos"
|
|
||||||
)
|
|
||||||
|
|
||||||
is_reviewed = models.BooleanField(
|
is_reviewed = models.BooleanField(
|
||||||
default=settings.MEDIA_IS_REVIEWED,
|
default=settings.MEDIA_IS_REVIEWED,
|
||||||
@@ -186,19 +161,13 @@ class Media(models.Model):
|
|||||||
help_text="Whether media is reviewed, so it can appear on public listings",
|
help_text="Whether media is reviewed, so it can appear on public listings",
|
||||||
)
|
)
|
||||||
|
|
||||||
license = models.ForeignKey(
|
license = models.ForeignKey("License", on_delete=models.CASCADE, db_index=True, blank=True, null=True)
|
||||||
"License", on_delete=models.CASCADE, db_index=True, blank=True, null=True
|
|
||||||
)
|
|
||||||
|
|
||||||
likes = models.IntegerField(db_index=True, default=1)
|
likes = models.IntegerField(db_index=True, default=1)
|
||||||
|
|
||||||
listable = models.BooleanField(
|
listable = models.BooleanField(default=False, help_text="Whether it will appear on listings")
|
||||||
default=False, help_text="Whether it will appear on listings"
|
|
||||||
)
|
|
||||||
|
|
||||||
md5sum = models.CharField(
|
md5sum = models.CharField(max_length=50, blank=True, null=True, help_text="Not exposed, used internally")
|
||||||
max_length=50, blank=True, null=True, help_text="Not exposed, used internally"
|
|
||||||
)
|
|
||||||
|
|
||||||
media_file = models.FileField(
|
media_file = models.FileField(
|
||||||
"media file",
|
"media file",
|
||||||
@@ -217,9 +186,7 @@ class Media(models.Model):
|
|||||||
default="video",
|
default="video",
|
||||||
)
|
)
|
||||||
|
|
||||||
password = models.CharField(
|
password = models.CharField(max_length=100, blank=True, help_text="password for private media")
|
||||||
max_length=100, blank=True, help_text="password for private media"
|
|
||||||
)
|
|
||||||
|
|
||||||
preview_file_path = models.CharField(
|
preview_file_path = models.CharField(
|
||||||
max_length=500,
|
max_length=500,
|
||||||
@@ -243,9 +210,7 @@ class Media(models.Model):
|
|||||||
help_text="Rating category, if media Rating is allowed",
|
help_text="Rating category, if media Rating is allowed",
|
||||||
)
|
)
|
||||||
|
|
||||||
reported_times = models.IntegerField(
|
reported_times = models.IntegerField(default=0, help_text="how many time a Medis is reported")
|
||||||
default=0, help_text="how many time a Medis is reported"
|
|
||||||
)
|
|
||||||
|
|
||||||
search = SearchVectorField(
|
search = SearchVectorField(
|
||||||
null=True,
|
null=True,
|
||||||
@@ -274,13 +239,9 @@ class Media(models.Model):
|
|||||||
help_text="state of Media",
|
help_text="state of Media",
|
||||||
)
|
)
|
||||||
|
|
||||||
tags = models.ManyToManyField(
|
tags = models.ManyToManyField("Tag", blank=True, help_text="select one or more out of the existing tags")
|
||||||
"Tag", blank=True, help_text="select one or more out of the existing tags"
|
|
||||||
)
|
|
||||||
|
|
||||||
title = models.CharField(
|
title = models.CharField(max_length=100, help_text="media title", blank=True, db_index=True)
|
||||||
max_length=100, help_text="media title", blank=True, db_index=True
|
|
||||||
)
|
|
||||||
|
|
||||||
thumbnail = ProcessedImageField(
|
thumbnail = ProcessedImageField(
|
||||||
upload_to=original_thumbnail_file_path,
|
upload_to=original_thumbnail_file_path,
|
||||||
@@ -292,13 +253,9 @@ class Media(models.Model):
|
|||||||
help_text="media extracted small thumbnail, shown on listings",
|
help_text="media extracted small thumbnail, shown on listings",
|
||||||
)
|
)
|
||||||
|
|
||||||
thumbnail_time = models.FloatField(
|
thumbnail_time = models.FloatField(blank=True, null=True, help_text="Time on video that a thumbnail will be taken")
|
||||||
blank=True, null=True, help_text="Time on video that a thumbnail will be taken"
|
|
||||||
)
|
|
||||||
|
|
||||||
uid = models.UUIDField(
|
uid = models.UUIDField(unique=True, default=uuid.uuid4, help_text="A unique identifier for the Media")
|
||||||
unique=True, default=uuid.uuid4, help_text="A unique identifier for the Media"
|
|
||||||
)
|
|
||||||
|
|
||||||
uploaded_thumbnail = ProcessedImageField(
|
uploaded_thumbnail = ProcessedImageField(
|
||||||
upload_to=original_thumbnail_file_path,
|
upload_to=original_thumbnail_file_path,
|
||||||
@@ -321,9 +278,7 @@ class Media(models.Model):
|
|||||||
max_length=500,
|
max_length=500,
|
||||||
)
|
)
|
||||||
|
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey("users.User", on_delete=models.CASCADE, help_text="user that uploads the media")
|
||||||
"users.User", on_delete=models.CASCADE, help_text="user that uploads the media"
|
|
||||||
)
|
|
||||||
|
|
||||||
user_featured = models.BooleanField(default=False, help_text="Featured by the user")
|
user_featured = models.BooleanField(default=False, help_text="Featured by the user")
|
||||||
|
|
||||||
@@ -406,11 +361,7 @@ class Media(models.Model):
|
|||||||
self.state = helpers.get_default_state(user=self.user)
|
self.state = helpers.get_default_state(user=self.user)
|
||||||
|
|
||||||
# condition to appear on listings
|
# condition to appear on listings
|
||||||
if (
|
if self.state == "public" and self.encoding_status == "success" and self.is_reviewed is True:
|
||||||
self.state == "public"
|
|
||||||
and self.encoding_status == "success"
|
|
||||||
and self.is_reviewed == True
|
|
||||||
):
|
|
||||||
self.listable = True
|
self.listable = True
|
||||||
else:
|
else:
|
||||||
self.listable = False
|
self.listable = False
|
||||||
@@ -419,10 +370,7 @@ class Media(models.Model):
|
|||||||
|
|
||||||
# produce a thumbnail out of an uploaded poster
|
# produce a thumbnail out of an uploaded poster
|
||||||
# will run only when a poster is uploaded for the first time
|
# will run only when a poster is uploaded for the first time
|
||||||
if (
|
if self.uploaded_poster and self.uploaded_poster != self.__original_uploaded_poster:
|
||||||
self.uploaded_poster
|
|
||||||
and self.uploaded_poster != self.__original_uploaded_poster
|
|
||||||
):
|
|
||||||
with open(self.uploaded_poster.path, "rb") as f:
|
with open(self.uploaded_poster.path, "rb") as f:
|
||||||
|
|
||||||
# set this otherwise gets to infinite loop
|
# set this otherwise gets to infinite loop
|
||||||
@@ -458,9 +406,7 @@ class Media(models.Model):
|
|||||||
]
|
]
|
||||||
items = [item for item in items if item]
|
items = [item for item in items if item]
|
||||||
text = " ".join(items)
|
text = " ".join(items)
|
||||||
text = " ".join(
|
text = " ".join([token for token in text.lower().split(" ") if token not in STOP_WORDS])
|
||||||
[token for token in text.lower().split(" ") if token not in STOP_WORDS]
|
|
||||||
)
|
|
||||||
|
|
||||||
sql_code = """
|
sql_code = """
|
||||||
UPDATE {db_table} SET search = to_tsvector(
|
UPDATE {db_table} SET search = to_tsvector(
|
||||||
@@ -561,9 +507,7 @@ class Media(models.Model):
|
|||||||
if self.media_type == "image":
|
if self.media_type == "image":
|
||||||
with open(self.media_file.path, "rb") as f:
|
with open(self.media_file.path, "rb") as f:
|
||||||
myfile = File(f)
|
myfile = File(f)
|
||||||
thumbnail_name = (
|
thumbnail_name = helpers.get_file_name(self.media_file.path) + ".jpg"
|
||||||
helpers.get_file_name(self.media_file.path) + ".jpg"
|
|
||||||
)
|
|
||||||
self.thumbnail.save(content=myfile, name=thumbnail_name)
|
self.thumbnail.save(content=myfile, name=thumbnail_name)
|
||||||
self.poster.save(content=myfile, name=thumbnail_name)
|
self.poster.save(content=myfile, name=thumbnail_name)
|
||||||
return True
|
return True
|
||||||
@@ -585,9 +529,7 @@ class Media(models.Model):
|
|||||||
command = [
|
command = [
|
||||||
settings.FFMPEG_COMMAND,
|
settings.FFMPEG_COMMAND,
|
||||||
"-ss",
|
"-ss",
|
||||||
str(
|
str(thumbnail_time), # -ss need to be firt here otherwise time taken is huge
|
||||||
thumbnail_time
|
|
||||||
), # -ss need to be firt here otherwise time taken is huge
|
|
||||||
"-i",
|
"-i",
|
||||||
self.media_file.path,
|
self.media_file.path,
|
||||||
"-vframes",
|
"-vframes",
|
||||||
@@ -595,7 +537,7 @@ class Media(models.Model):
|
|||||||
"-y",
|
"-y",
|
||||||
tf,
|
tf,
|
||||||
]
|
]
|
||||||
ret = helpers.run_command(command)
|
helpers.run_command(command)
|
||||||
|
|
||||||
if os.path.exists(tf) and helpers.get_file_type(tf) == "image":
|
if os.path.exists(tf) and helpers.get_file_type(tf) == "image":
|
||||||
with open(tf, "rb") as f:
|
with open(tf, "rb") as f:
|
||||||
@@ -650,10 +592,7 @@ class Media(models.Model):
|
|||||||
for profile in profiles:
|
for profile in profiles:
|
||||||
if profile.extension != "gif":
|
if profile.extension != "gif":
|
||||||
if self.video_height and self.video_height < profile.resolution:
|
if self.video_height and self.video_height < profile.resolution:
|
||||||
if (
|
if profile.resolution not in settings.MINIMUM_RESOLUTIONS_TO_ENCODE:
|
||||||
profile.resolution
|
|
||||||
not in settings.MINIMUM_RESOLUTIONS_TO_ENCODE
|
|
||||||
):
|
|
||||||
continue
|
continue
|
||||||
encoding = Encoding(media=self, profile=profile)
|
encoding = Encoding(media=self, profile=profile)
|
||||||
encoding.save()
|
encoding.save()
|
||||||
@@ -688,12 +627,7 @@ class Media(models.Model):
|
|||||||
|
|
||||||
self.save(update_fields=["encoding_status", "listable"])
|
self.save(update_fields=["encoding_status", "listable"])
|
||||||
|
|
||||||
if (
|
if encoding and encoding.status == "success" and encoding.profile.codec == "h264" and action == "add":
|
||||||
encoding
|
|
||||||
and encoding.status == "success"
|
|
||||||
and encoding.profile.codec == "h264"
|
|
||||||
and action == "add"
|
|
||||||
):
|
|
||||||
from . import tasks
|
from . import tasks
|
||||||
|
|
||||||
tasks.create_hls(self.friendly_token)
|
tasks.create_hls(self.friendly_token)
|
||||||
@@ -704,10 +638,7 @@ class Media(models.Model):
|
|||||||
"""Set encoding_status for videos
|
"""Set encoding_status for videos
|
||||||
Set success if at least one mp4 exists
|
Set success if at least one mp4 exists
|
||||||
"""
|
"""
|
||||||
mp4_statuses = set(
|
mp4_statuses = set(encoding.status for encoding in self.encodings.filter(profile__extension="mp4", chunk=False))
|
||||||
encoding.status
|
|
||||||
for encoding in self.encodings.filter(profile__extension="mp4", chunk=False)
|
|
||||||
)
|
|
||||||
|
|
||||||
if not mp4_statuses:
|
if not mp4_statuses:
|
||||||
encoding_status = "pending"
|
encoding_status = "pending"
|
||||||
@@ -726,7 +657,6 @@ class Media(models.Model):
|
|||||||
"""Property used on serializers"""
|
"""Property used on serializers"""
|
||||||
|
|
||||||
ret = {}
|
ret = {}
|
||||||
chunks_ret = {}
|
|
||||||
|
|
||||||
if self.media_type not in ["video"]:
|
if self.media_type not in ["video"]:
|
||||||
return ret
|
return ret
|
||||||
@@ -752,12 +682,8 @@ class Media(models.Model):
|
|||||||
extra.append(encoding.profile.codec)
|
extra.append(encoding.profile.codec)
|
||||||
for codec in extra:
|
for codec in extra:
|
||||||
ret[resolution][codec] = {}
|
ret[resolution][codec] = {}
|
||||||
v = self.encodings.filter(chunk=True, profile__codec=codec).values(
|
v = self.encodings.filter(chunk=True, profile__codec=codec).values("progress")
|
||||||
"progress"
|
ret[resolution][codec]["progress"] = sum([p["progress"] for p in v]) / v.count()
|
||||||
)
|
|
||||||
ret[resolution][codec]["progress"] = (
|
|
||||||
sum([p["progress"] for p in v]) / v.count()
|
|
||||||
)
|
|
||||||
# TODO; status/logs/errors
|
# TODO; status/logs/errors
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@@ -897,19 +823,13 @@ class Media(models.Model):
|
|||||||
for iframe_playlist in m3u8_obj.iframe_playlists:
|
for iframe_playlist in m3u8_obj.iframe_playlists:
|
||||||
uri = os.path.join(p, iframe_playlist.uri)
|
uri = os.path.join(p, iframe_playlist.uri)
|
||||||
if os.path.exists(uri):
|
if os.path.exists(uri):
|
||||||
resolution = iframe_playlist.iframe_stream_info.resolution[
|
resolution = iframe_playlist.iframe_stream_info.resolution[1]
|
||||||
1
|
res["{}_iframe".format(resolution)] = helpers.url_from_path(uri)
|
||||||
]
|
|
||||||
res["{}_iframe".format(resolution)] = helpers.url_from_path(
|
|
||||||
uri
|
|
||||||
)
|
|
||||||
for playlist in m3u8_obj.playlists:
|
for playlist in m3u8_obj.playlists:
|
||||||
uri = os.path.join(p, playlist.uri)
|
uri = os.path.join(p, playlist.uri)
|
||||||
if os.path.exists(uri):
|
if os.path.exists(uri):
|
||||||
resolution = playlist.stream_info.resolution[1]
|
resolution = playlist.stream_info.resolution[1]
|
||||||
res[
|
res["{}_playlist".format(resolution)] = helpers.url_from_path(uri)
|
||||||
"{}_playlist".format(resolution)
|
|
||||||
] = helpers.url_from_path(uri)
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -930,9 +850,7 @@ class Media(models.Model):
|
|||||||
if edit:
|
if edit:
|
||||||
return reverse("edit_media") + "?m={0}".format(self.friendly_token)
|
return reverse("edit_media") + "?m={0}".format(self.friendly_token)
|
||||||
if api:
|
if api:
|
||||||
return reverse(
|
return reverse("api_get_media", kwargs={"friendly_token": self.friendly_token})
|
||||||
"api_get_media", kwargs={"friendly_token": self.friendly_token}
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
return reverse("get_media") + "?m={0}".format(self.friendly_token)
|
return reverse("get_media") + "?m={0}".format(self.friendly_token)
|
||||||
|
|
||||||
@@ -988,13 +906,9 @@ class Category(models.Model):
|
|||||||
|
|
||||||
description = models.TextField(blank=True)
|
description = models.TextField(blank=True)
|
||||||
|
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey("users.User", on_delete=models.CASCADE, blank=True, null=True)
|
||||||
"users.User", on_delete=models.CASCADE, blank=True, null=True
|
|
||||||
)
|
|
||||||
|
|
||||||
is_global = models.BooleanField(
|
is_global = models.BooleanField(default=False, help_text="global categories or user specific")
|
||||||
default=False, help_text="global categories or user specific"
|
|
||||||
)
|
|
||||||
|
|
||||||
media_count = models.IntegerField(default=0, help_text="number of media")
|
media_count = models.IntegerField(default=0, help_text="number of media")
|
||||||
|
|
||||||
@@ -1006,9 +920,7 @@ class Category(models.Model):
|
|||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
listings_thumbnail = models.CharField(
|
listings_thumbnail = models.CharField(max_length=400, blank=True, null=True, help_text="Thumbnail to show on listings")
|
||||||
max_length=400, blank=True, null=True, help_text="Thumbnail to show on listings"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
@@ -1039,11 +951,7 @@ class Category(models.Model):
|
|||||||
if self.thumbnail:
|
if self.thumbnail:
|
||||||
return helpers.url_from_path(self.thumbnail.path)
|
return helpers.url_from_path(self.thumbnail.path)
|
||||||
|
|
||||||
media = (
|
media = Media.objects.filter(category=self, state="public").order_by("-views").first()
|
||||||
Media.objects.filter(category=self, state="public")
|
|
||||||
.order_by("-views")
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
if media:
|
if media:
|
||||||
return media.thumbnail_url
|
return media.thumbnail_url
|
||||||
|
|
||||||
@@ -1061,9 +969,7 @@ class Tag(models.Model):
|
|||||||
|
|
||||||
title = models.CharField(max_length=100, unique=True, db_index=True)
|
title = models.CharField(max_length=100, unique=True, db_index=True)
|
||||||
|
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey("users.User", on_delete=models.CASCADE, blank=True, null=True)
|
||||||
"users.User", on_delete=models.CASCADE, blank=True, null=True
|
|
||||||
)
|
|
||||||
|
|
||||||
media_count = models.IntegerField(default=0, help_text="number of media")
|
media_count = models.IntegerField(default=0, help_text="number of media")
|
||||||
|
|
||||||
@@ -1085,9 +991,7 @@ class Tag(models.Model):
|
|||||||
return reverse("search") + "?t={0}".format(self.title)
|
return reverse("search") + "?t={0}".format(self.title)
|
||||||
|
|
||||||
def update_tag_media(self):
|
def update_tag_media(self):
|
||||||
self.media_count = Media.objects.filter(
|
self.media_count = Media.objects.filter(state="public", is_reviewed=True, tags=self).count()
|
||||||
state="public", is_reviewed=True, tags=self
|
|
||||||
).count()
|
|
||||||
self.save(update_fields=["media_count"])
|
self.save(update_fields=["media_count"])
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -1102,9 +1006,7 @@ class Tag(models.Model):
|
|||||||
def thumbnail_url(self):
|
def thumbnail_url(self):
|
||||||
if self.listings_thumbnail:
|
if self.listings_thumbnail:
|
||||||
return self.listings_thumbnail
|
return self.listings_thumbnail
|
||||||
media = (
|
media = Media.objects.filter(tags=self, state="public").order_by("-views").first()
|
||||||
Media.objects.filter(tags=self, state="public").order_by("-views").first()
|
|
||||||
)
|
|
||||||
if media:
|
if media:
|
||||||
return media.thumbnail_url
|
return media.thumbnail_url
|
||||||
|
|
||||||
@@ -1154,9 +1056,7 @@ class Encoding(models.Model):
|
|||||||
|
|
||||||
media = models.ForeignKey(Media, on_delete=models.CASCADE, related_name="encodings")
|
media = models.ForeignKey(Media, on_delete=models.CASCADE, related_name="encodings")
|
||||||
|
|
||||||
media_file = models.FileField(
|
media_file = models.FileField("encoding file", upload_to=encoding_media_file_path, blank=True, max_length=500)
|
||||||
"encoding file", upload_to=encoding_media_file_path, blank=True, max_length=500
|
|
||||||
)
|
|
||||||
|
|
||||||
profile = models.ForeignKey(EncodeProfile, on_delete=models.CASCADE)
|
profile = models.ForeignKey(EncodeProfile, on_delete=models.CASCADE)
|
||||||
|
|
||||||
@@ -1168,9 +1068,7 @@ class Encoding(models.Model):
|
|||||||
|
|
||||||
size = models.CharField(max_length=20, blank=True)
|
size = models.CharField(max_length=20, blank=True)
|
||||||
|
|
||||||
status = models.CharField(
|
status = models.CharField(max_length=20, choices=MEDIA_ENCODING_STATUS, default="pending")
|
||||||
max_length=20, choices=MEDIA_ENCODING_STATUS, default="pending"
|
|
||||||
)
|
|
||||||
|
|
||||||
temp_file = models.CharField(max_length=400, blank=True)
|
temp_file = models.CharField(max_length=400, blank=True)
|
||||||
|
|
||||||
@@ -1305,9 +1203,7 @@ class Rating(models.Model):
|
|||||||
unique_together = ("user", "media", "rating_category")
|
unique_together = ("user", "media", "rating_category")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{0}, rate for {1} for category {2}".format(
|
return "{0}, rate for {1} for category {2}".format(self.user.username, self.media.title, self.rating_category.title)
|
||||||
self.user.username, self.media.title, self.rating_category.title
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Playlist(models.Model):
|
class Playlist(models.Model):
|
||||||
@@ -1325,9 +1221,7 @@ class Playlist(models.Model):
|
|||||||
|
|
||||||
uid = models.UUIDField(unique=True, default=uuid.uuid4)
|
uid = models.UUIDField(unique=True, default=uuid.uuid4)
|
||||||
|
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey("users.User", on_delete=models.CASCADE, db_index=True, related_name="playlists")
|
||||||
"users.User", on_delete=models.CASCADE, db_index=True, related_name="playlists"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
@@ -1338,13 +1232,9 @@ class Playlist(models.Model):
|
|||||||
|
|
||||||
def get_absolute_url(self, api=False):
|
def get_absolute_url(self, api=False):
|
||||||
if api:
|
if api:
|
||||||
return reverse(
|
return reverse("api_get_playlist", kwargs={"friendly_token": self.friendly_token})
|
||||||
"api_get_playlist", kwargs={"friendly_token": self.friendly_token}
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
return reverse(
|
return reverse("get_playlist", kwargs={"friendly_token": self.friendly_token})
|
||||||
"get_playlist", kwargs={"friendly_token": self.friendly_token}
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self):
|
||||||
@@ -1386,7 +1276,7 @@ class Playlist(models.Model):
|
|||||||
@property
|
@property
|
||||||
def thumbnail_url(self):
|
def thumbnail_url(self):
|
||||||
pm = self.playlistmedia_set.first()
|
pm = self.playlistmedia_set.first()
|
||||||
if pm:
|
if pm and pm.media.thumbnail:
|
||||||
return helpers.url_from_path(pm.media.thumbnail.path)
|
return helpers.url_from_path(pm.media.thumbnail.path)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -1411,13 +1301,9 @@ class Comment(MPTTModel):
|
|||||||
|
|
||||||
add_date = models.DateTimeField(auto_now_add=True)
|
add_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
media = models.ForeignKey(
|
media = models.ForeignKey(Media, on_delete=models.CASCADE, db_index=True, related_name="comments")
|
||||||
Media, on_delete=models.CASCADE, db_index=True, related_name="comments"
|
|
||||||
)
|
|
||||||
|
|
||||||
parent = TreeForeignKey(
|
parent = TreeForeignKey("self", on_delete=models.CASCADE, null=True, blank=True, related_name="children")
|
||||||
"self", on_delete=models.CASCADE, null=True, blank=True, related_name="children"
|
|
||||||
)
|
|
||||||
|
|
||||||
text = models.TextField(help_text="text")
|
text = models.TextField(help_text="text")
|
||||||
|
|
||||||
@@ -1566,13 +1452,9 @@ def encoding_file_save(sender, instance, created, **kwargs):
|
|||||||
# concatenate chunks and create final encoding file
|
# concatenate chunks and create final encoding file
|
||||||
chunks_paths = [f.media_file.path for f in chunks]
|
chunks_paths = [f.media_file.path for f in chunks]
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory(
|
with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as temp_dir:
|
||||||
dir=settings.TEMP_DIRECTORY
|
|
||||||
) as temp_dir:
|
|
||||||
seg_file = helpers.create_temp_file(suffix=".txt", dir=temp_dir)
|
seg_file = helpers.create_temp_file(suffix=".txt", dir=temp_dir)
|
||||||
tf = helpers.create_temp_file(
|
tf = helpers.create_temp_file(suffix=".{0}".format(instance.profile.extension), dir=temp_dir)
|
||||||
suffix=".{0}".format(instance.profile.extension), dir=temp_dir
|
|
||||||
)
|
|
||||||
with open(seg_file, "w") as ff:
|
with open(seg_file, "w") as ff:
|
||||||
for f in chunks_paths:
|
for f in chunks_paths:
|
||||||
ff.write("file {}\n".format(f))
|
ff.write("file {}\n".format(f))
|
||||||
@@ -1602,9 +1484,7 @@ def encoding_file_save(sender, instance, created, **kwargs):
|
|||||||
progress=100,
|
progress=100,
|
||||||
)
|
)
|
||||||
all_logs = "\n".join([st.logs for st in chunks])
|
all_logs = "\n".join([st.logs for st in chunks])
|
||||||
encoding.logs = "{0}\n{1}\n{2}".format(
|
encoding.logs = "{0}\n{1}\n{2}".format(chunks_paths, stdout, all_logs)
|
||||||
chunks_paths, stdout, all_logs
|
|
||||||
)
|
|
||||||
workers = list(set([st.worker for st in chunks]))
|
workers = list(set([st.worker for st in chunks]))
|
||||||
encoding.worker = json.dumps({"workers": workers})
|
encoding.worker = json.dumps({"workers": workers})
|
||||||
|
|
||||||
@@ -1635,9 +1515,7 @@ def encoding_file_save(sender, instance, created, **kwargs):
|
|||||||
):
|
):
|
||||||
# if two chunks are finished at the same time, this
|
# if two chunks are finished at the same time, this
|
||||||
# will be changed
|
# will be changed
|
||||||
who = Encoding.objects.filter(
|
who = Encoding.objects.filter(media=encoding.media, profile=encoding.profile).exclude(id=encoding.id)
|
||||||
media=encoding.media, profile=encoding.profile
|
|
||||||
).exclude(id=encoding.id)
|
|
||||||
who.delete()
|
who.delete()
|
||||||
else:
|
else:
|
||||||
encoding.delete()
|
encoding.delete()
|
||||||
@@ -1652,13 +1530,9 @@ def encoding_file_save(sender, instance, created, **kwargs):
|
|||||||
instance.media.post_encode_actions(encoding=instance, action="add")
|
instance.media.post_encode_actions(encoding=instance, action="add")
|
||||||
|
|
||||||
elif instance.chunk and instance.status == "fail":
|
elif instance.chunk and instance.status == "fail":
|
||||||
encoding = Encoding(
|
encoding = Encoding(media=instance.media, profile=instance.profile, status="fail", progress=100)
|
||||||
media=instance.media, profile=instance.profile, status="fail", progress=100
|
|
||||||
)
|
|
||||||
|
|
||||||
chunks = Encoding.objects.filter(
|
chunks = Encoding.objects.filter(media=instance.media, chunks_info=instance.chunks_info, chunk=True).order_by("add_date")
|
||||||
media=instance.media, chunks_info=instance.chunks_info, chunk=True
|
|
||||||
).order_by("add_date")
|
|
||||||
|
|
||||||
chunks_paths = [f.media_file.path for f in chunks]
|
chunks_paths = [f.media_file.path for f in chunks]
|
||||||
|
|
||||||
@@ -1671,9 +1545,7 @@ def encoding_file_save(sender, instance, created, **kwargs):
|
|||||||
encoding.total_run_time = (end_date - start_date).seconds
|
encoding.total_run_time = (end_date - start_date).seconds
|
||||||
encoding.save()
|
encoding.save()
|
||||||
|
|
||||||
who = Encoding.objects.filter(
|
who = Encoding.objects.filter(media=encoding.media, profile=encoding.profile).exclude(id=encoding.id)
|
||||||
media=encoding.media, profile=encoding.profile
|
|
||||||
).exclude(id=encoding.id)
|
|
||||||
|
|
||||||
who.delete()
|
who.delete()
|
||||||
pass # TODO: merge with above if, do not repeat code
|
pass # TODO: merge with above if, do not repeat code
|
||||||
@@ -1681,22 +1553,10 @@ def encoding_file_save(sender, instance, created, **kwargs):
|
|||||||
if instance.status in ["fail", "success"]:
|
if instance.status in ["fail", "success"]:
|
||||||
instance.media.post_encode_actions(encoding=instance, action="add")
|
instance.media.post_encode_actions(encoding=instance, action="add")
|
||||||
|
|
||||||
encodings = set(
|
encodings = set([encoding.status for encoding in Encoding.objects.filter(media=instance.media)])
|
||||||
[
|
|
||||||
encoding.status
|
|
||||||
for encoding in Encoding.objects.filter(media=instance.media)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
if ("running" in encodings) or ("pending" in encodings):
|
if ("running" in encodings) or ("pending" in encodings):
|
||||||
return
|
return
|
||||||
workers = list(
|
workers = list(set([encoding.worker for encoding in Encoding.objects.filter(media=instance.media)]))
|
||||||
set(
|
|
||||||
[
|
|
||||||
encoding.worker
|
|
||||||
for encoding in Encoding.objects.filter(media=instance.media)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_delete, sender=Encoding)
|
@receiver(post_delete, sender=Encoding)
|
||||||
@@ -1712,4 +1572,3 @@ def encoding_file_delete(sender, instance, **kwargs):
|
|||||||
instance.media.post_encode_actions(encoding=instance, action="delete")
|
instance.media.post_encode_actions(encoding=instance, action="delete")
|
||||||
# delete local chunks, and remote chunks + media file. Only when the
|
# delete local chunks, and remote chunks + media file. Only when the
|
||||||
# last encoding of a media is complete
|
# last encoding of a media is complete
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from rest_framework import permissions
|
from rest_framework import permissions
|
||||||
|
|
||||||
from .methods import is_mediacms_editor
|
from .methods import is_mediacms_editor
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from .models import Media, EncodeProfile, Playlist, Comment, Category, Tag
|
from .models import Category, Comment, EncodeProfile, Media, Playlist, Tag
|
||||||
|
|
||||||
# TODO: put them in a more DRY way
|
# TODO: put them in a more DRY way
|
||||||
|
|
||||||
@@ -18,9 +18,7 @@ class MediaSerializer(serializers.ModelSerializer):
|
|||||||
return self.context["request"].build_absolute_uri(obj.get_absolute_url())
|
return self.context["request"].build_absolute_uri(obj.get_absolute_url())
|
||||||
|
|
||||||
def get_api_url(self, obj):
|
def get_api_url(self, obj):
|
||||||
return self.context["request"].build_absolute_uri(
|
return self.context["request"].build_absolute_uri(obj.get_absolute_url(api=True))
|
||||||
obj.get_absolute_url(api=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_thumbnail_url(self, obj):
|
def get_thumbnail_url(self, obj):
|
||||||
if obj.thumbnail_url:
|
if obj.thumbnail_url:
|
||||||
@@ -210,16 +208,7 @@ class PlaylistSerializer(serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Playlist
|
model = Playlist
|
||||||
read_only_fields = ("add_date", "user")
|
read_only_fields = ("add_date", "user")
|
||||||
fields = (
|
fields = ("add_date", "title", "description", "user", "media_count", "url", "api_url", "thumbnail_url")
|
||||||
"add_date",
|
|
||||||
"title",
|
|
||||||
"description",
|
|
||||||
"user",
|
|
||||||
"media_count",
|
|
||||||
"url",
|
|
||||||
"api_url",
|
|
||||||
"thumbnail_url"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PlaylistDetailSerializer(serializers.ModelSerializer):
|
class PlaylistDetailSerializer(serializers.ModelSerializer):
|
||||||
@@ -228,16 +217,7 @@ class PlaylistDetailSerializer(serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Playlist
|
model = Playlist
|
||||||
read_only_fields = ("add_date", "user")
|
read_only_fields = ("add_date", "user")
|
||||||
fields = (
|
fields = ("title", "add_date", "user_thumbnail_url", "description", "user", "media_count", "url", "thumbnail_url")
|
||||||
"title",
|
|
||||||
"add_date",
|
|
||||||
"user_thumbnail_url",
|
|
||||||
"description",
|
|
||||||
"user",
|
|
||||||
"media_count",
|
|
||||||
"url",
|
|
||||||
"thumbnail_url"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CommentSerializer(serializers.ModelSerializer):
|
class CommentSerializer(serializers.ModelSerializer):
|
||||||
|
|||||||
156
files/tasks.py
156
files/tasks.py
@@ -1,41 +1,40 @@
|
|||||||
import re
|
|
||||||
import os
|
|
||||||
import json
|
import json
|
||||||
import subprocess
|
import os
|
||||||
from datetime import datetime, timedelta
|
import re
|
||||||
import tempfile
|
|
||||||
import shutil
|
import shutil
|
||||||
from django.core.cache import cache
|
import subprocess
|
||||||
from django.conf import settings
|
import tempfile
|
||||||
from django.core.files import File
|
from datetime import datetime, timedelta
|
||||||
from django.db.models import Q
|
|
||||||
|
|
||||||
from celery import Task
|
from celery import Task
|
||||||
from celery.decorators import task
|
from celery.decorators import task
|
||||||
from celery.utils.log import get_task_logger
|
|
||||||
from celery.exceptions import SoftTimeLimitExceeded
|
from celery.exceptions import SoftTimeLimitExceeded
|
||||||
|
|
||||||
from celery.task.control import revoke
|
|
||||||
from celery.signals import task_revoked
|
from celery.signals import task_revoked
|
||||||
|
from celery.task.control import revoke
|
||||||
|
from celery.utils.log import get_task_logger
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.cache import cache
|
||||||
|
from django.core.files import File
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
from actions.models import USER_MEDIA_ACTIONS, MediaAction
|
||||||
|
from users.models import User
|
||||||
|
|
||||||
from .backends import FFmpegBackend
|
from .backends import FFmpegBackend
|
||||||
from .exceptions import VideoEncodingError
|
from .exceptions import VideoEncodingError
|
||||||
from .helpers import (
|
from .helpers import (
|
||||||
calculate_seconds,
|
calculate_seconds,
|
||||||
rm_file,
|
|
||||||
create_temp_file,
|
create_temp_file,
|
||||||
get_file_name,
|
get_file_name,
|
||||||
get_file_type,
|
get_file_type,
|
||||||
media_file_info,
|
media_file_info,
|
||||||
run_command,
|
|
||||||
produce_ffmpeg_commands,
|
produce_ffmpeg_commands,
|
||||||
produce_friendly_token,
|
produce_friendly_token,
|
||||||
|
rm_file,
|
||||||
|
run_command,
|
||||||
)
|
)
|
||||||
|
from .methods import list_tasks, notify_users, pre_save_action
|
||||||
from actions.models import MediaAction, USER_MEDIA_ACTIONS
|
from .models import Category, EncodeProfile, Encoding, Media, Rating, Tag
|
||||||
from users.models import User
|
|
||||||
from .models import Encoding, EncodeProfile, Media, Category, Rating, Tag
|
|
||||||
from .methods import list_tasks, pre_save_action, notify_users
|
|
||||||
|
|
||||||
logger = get_task_logger(__name__)
|
logger = get_task_logger(__name__)
|
||||||
|
|
||||||
@@ -83,10 +82,7 @@ def chunkize_media(self, friendly_token, profiles, force=True):
|
|||||||
chunks.append(ch[0])
|
chunks.append(ch[0])
|
||||||
if not chunks:
|
if not chunks:
|
||||||
# command completely failed to segment file.putting to normal encode
|
# command completely failed to segment file.putting to normal encode
|
||||||
logger.info(
|
logger.info("Failed to break file {0} in chunks." " Putting to normal encode queue".format(friendly_token))
|
||||||
"Failed to break file {0} in chunks."
|
|
||||||
" Putting to normal encode queue".format(friendly_token)
|
|
||||||
)
|
|
||||||
for profile in profiles:
|
for profile in profiles:
|
||||||
if media.video_height and media.video_height < profile.resolution:
|
if media.video_height and media.video_height < profile.resolution:
|
||||||
if profile.resolution not in settings.MINIMUM_RESOLUTIONS_TO_ENCODE:
|
if profile.resolution not in settings.MINIMUM_RESOLUTIONS_TO_ENCODE:
|
||||||
@@ -94,9 +90,7 @@ def chunkize_media(self, friendly_token, profiles, force=True):
|
|||||||
encoding = Encoding(media=media, profile=profile)
|
encoding = Encoding(media=media, profile=profile)
|
||||||
encoding.save()
|
encoding.save()
|
||||||
enc_url = settings.SSL_FRONTEND_HOST + encoding.get_absolute_url()
|
enc_url = settings.SSL_FRONTEND_HOST + encoding.get_absolute_url()
|
||||||
encode_media.delay(
|
encode_media.delay(friendly_token, profile.id, encoding.id, enc_url, force=force)
|
||||||
friendly_token, profile.id, encoding.id, enc_url, force=force
|
|
||||||
)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
chunks = [os.path.join(cwd, ch) for ch in chunks]
|
chunks = [os.path.join(cwd, ch) for ch in chunks]
|
||||||
@@ -137,11 +131,7 @@ def chunkize_media(self, friendly_token, profiles, force=True):
|
|||||||
priority=priority,
|
priority=priority,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info("got {0} chunks and will encode to {1} profiles".format(len(chunks), to_profiles))
|
||||||
"got {0} chunks and will encode to {1} profiles".format(
|
|
||||||
len(chunks), to_profiles
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -180,11 +170,7 @@ def encode_media(
|
|||||||
):
|
):
|
||||||
"""Encode a media to given profile, using ffmpeg, storing progress"""
|
"""Encode a media to given profile, using ffmpeg, storing progress"""
|
||||||
|
|
||||||
logger.info(
|
logger.info("Encode Media started, friendly token {0}, profile id {1}, force {2}".format(friendly_token, profile_id, force))
|
||||||
"Encode Media started, friendly token {0}, profile id {1}, force {2}".format(
|
|
||||||
friendly_token, profile_id, force
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.request.id:
|
if self.request.id:
|
||||||
task_id = self.request.id
|
task_id = self.request.id
|
||||||
@@ -202,13 +188,7 @@ def encode_media(
|
|||||||
# TODO: in case a video is chunkized and this enters here many times
|
# TODO: in case a video is chunkized and this enters here many times
|
||||||
# it will always run since chunk_file_path is always different
|
# it will always run since chunk_file_path is always different
|
||||||
# thus find a better way for this check
|
# thus find a better way for this check
|
||||||
if (
|
if Encoding.objects.filter(media=media, profile=profile, chunk_file_path=chunk_file_path).count() > 1 and force is False:
|
||||||
Encoding.objects.filter(
|
|
||||||
media=media, profile=profile, chunk_file_path=chunk_file_path
|
|
||||||
).count()
|
|
||||||
> 1
|
|
||||||
and force == False
|
|
||||||
):
|
|
||||||
Encoding.objects.filter(id=encoding_id).delete()
|
Encoding.objects.filter(id=encoding_id).delete()
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
@@ -230,19 +210,14 @@ def encode_media(
|
|||||||
chunk_file_path=chunk_file_path,
|
chunk_file_path=chunk_file_path,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if (
|
if Encoding.objects.filter(media=media, profile=profile).count() > 1 and force is False:
|
||||||
Encoding.objects.filter(media=media, profile=profile).count() > 1
|
|
||||||
and force is False
|
|
||||||
):
|
|
||||||
Encoding.objects.filter(id=encoding_id).delete()
|
Encoding.objects.filter(id=encoding_id).delete()
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
encoding = Encoding.objects.get(id=encoding_id)
|
encoding = Encoding.objects.get(id=encoding_id)
|
||||||
encoding.status = "running"
|
encoding.status = "running"
|
||||||
Encoding.objects.filter(media=media, profile=profile).exclude(
|
Encoding.objects.filter(media=media, profile=profile).exclude(id=encoding_id).delete()
|
||||||
id=encoding_id
|
|
||||||
).delete()
|
|
||||||
except BaseException:
|
except BaseException:
|
||||||
encoding = Encoding(media=media, profile=profile, status="running")
|
encoding = Encoding(media=media, profile=profile, status="running")
|
||||||
|
|
||||||
@@ -287,7 +262,7 @@ def encode_media(
|
|||||||
else:
|
else:
|
||||||
original_media_path = media.media_file.path
|
original_media_path = media.media_file.path
|
||||||
|
|
||||||
#if not media.duration:
|
# if not media.duration:
|
||||||
# encoding.status = "fail"
|
# encoding.status = "fail"
|
||||||
# encoding.save(update_fields=["status"])
|
# encoding.save(update_fields=["status"])
|
||||||
# return False
|
# return False
|
||||||
@@ -337,9 +312,7 @@ def encode_media(
|
|||||||
if n_times % 60 == 0:
|
if n_times % 60 == 0:
|
||||||
encoding.progress = percent
|
encoding.progress = percent
|
||||||
try:
|
try:
|
||||||
encoding.save(
|
encoding.save(update_fields=["progress", "update_date"])
|
||||||
update_fields=["progress", "update_date"]
|
|
||||||
)
|
|
||||||
logger.info("Saved {0}".format(round(percent, 2)))
|
logger.info("Saved {0}".format(round(percent, 2)))
|
||||||
except BaseException:
|
except BaseException:
|
||||||
pass
|
pass
|
||||||
@@ -383,18 +356,12 @@ def encode_media(
|
|||||||
|
|
||||||
with open(tf, "rb") as f:
|
with open(tf, "rb") as f:
|
||||||
myfile = File(f)
|
myfile = File(f)
|
||||||
output_name = "{0}.{1}".format(
|
output_name = "{0}.{1}".format(get_file_name(original_media_path), profile.extension)
|
||||||
get_file_name(original_media_path), profile.extension
|
|
||||||
)
|
|
||||||
encoding.media_file.save(content=myfile, name=output_name)
|
encoding.media_file.save(content=myfile, name=output_name)
|
||||||
encoding.total_run_time = (
|
encoding.total_run_time = (encoding.update_date - encoding.add_date).seconds
|
||||||
encoding.update_date - encoding.add_date
|
|
||||||
).seconds
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
encoding.save(
|
encoding.save(update_fields=["status", "logs", "progress", "total_run_time"])
|
||||||
update_fields=["status", "logs", "progress", "total_run_time"]
|
|
||||||
)
|
|
||||||
# this will raise a django.db.utils.DatabaseError error when task is revoked,
|
# this will raise a django.db.utils.DatabaseError error when task is revoked,
|
||||||
# since we delete the encoding at that stage
|
# since we delete the encoding at that stage
|
||||||
except BaseException:
|
except BaseException:
|
||||||
@@ -424,7 +391,7 @@ def produce_sprite_from_video(friendly_token):
|
|||||||
tmpdirname,
|
tmpdirname,
|
||||||
output_name,
|
output_name,
|
||||||
)
|
)
|
||||||
ret = subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
|
subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
|
||||||
if os.path.exists(output_name) and get_file_type(output_name) == "image":
|
if os.path.exists(output_name) and get_file_type(output_name) == "image":
|
||||||
with open(output_name, "rb") as f:
|
with open(output_name, "rb") as f:
|
||||||
myfile = File(f)
|
myfile = File(f)
|
||||||
@@ -457,23 +424,19 @@ def create_hls(friendly_token):
|
|||||||
|
|
||||||
p = media.uid.hex
|
p = media.uid.hex
|
||||||
output_dir = os.path.join(settings.HLS_DIR, p)
|
output_dir = os.path.join(settings.HLS_DIR, p)
|
||||||
encodings = media.encodings.filter(
|
encodings = media.encodings.filter(profile__extension="mp4", status="success", chunk=False, profile__codec="h264")
|
||||||
profile__extension="mp4", status="success", chunk=False, profile__codec="h264"
|
|
||||||
)
|
|
||||||
if encodings:
|
if encodings:
|
||||||
existing_output_dir = None
|
existing_output_dir = None
|
||||||
if os.path.exists(output_dir):
|
if os.path.exists(output_dir):
|
||||||
existing_output_dir = output_dir
|
existing_output_dir = output_dir
|
||||||
output_dir = os.path.join(settings.HLS_DIR, p + produce_friendly_token())
|
output_dir = os.path.join(settings.HLS_DIR, p + produce_friendly_token())
|
||||||
files = " ".join([f.media_file.path for f in encodings if f.media_file])
|
files = " ".join([f.media_file.path for f in encodings if f.media_file])
|
||||||
cmd = "{0} --segment-duration=4 --output-dir={1} {2}".format(
|
cmd = "{0} --segment-duration=4 --output-dir={1} {2}".format(settings.MP4HLS_COMMAND, output_dir, files)
|
||||||
settings.MP4HLS_COMMAND, output_dir, files
|
subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
|
||||||
)
|
|
||||||
ret = subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
|
|
||||||
if existing_output_dir:
|
if existing_output_dir:
|
||||||
# override content with -T !
|
# override content with -T !
|
||||||
cmd = "cp -rT {0} {1}".format(output_dir, existing_output_dir)
|
cmd = "cp -rT {0} {1}".format(output_dir, existing_output_dir)
|
||||||
ret = subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
|
subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
|
||||||
shutil.rmtree(output_dir)
|
shutil.rmtree(output_dir)
|
||||||
output_dir = existing_output_dir
|
output_dir = existing_output_dir
|
||||||
pp = os.path.join(output_dir, "master.m3u8")
|
pp = os.path.join(output_dir, "master.m3u8")
|
||||||
@@ -515,11 +478,7 @@ def check_running_states():
|
|||||||
def check_media_states():
|
def check_media_states():
|
||||||
# Experimental - unused
|
# Experimental - unused
|
||||||
# check encoding status of not success media
|
# check encoding status of not success media
|
||||||
media = Media.objects.filter(
|
media = Media.objects.filter(Q(encoding_status="running") | Q(encoding_status="fail") | Q(encoding_status="pending"))
|
||||||
Q(encoding_status="running")
|
|
||||||
| Q(encoding_status="fail")
|
|
||||||
| Q(encoding_status="pending")
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info("got {0} media that are not in state success".format(media.count()))
|
logger.info("got {0} media that are not in state success".format(media.count()))
|
||||||
|
|
||||||
@@ -564,11 +523,7 @@ def check_pending_states():
|
|||||||
media.encode(profiles=[profile], force=False)
|
media.encode(profiles=[profile], force=False)
|
||||||
changed += 1
|
changed += 1
|
||||||
if changed:
|
if changed:
|
||||||
logger.info(
|
logger.info("set to the encode queue {0} encodings that were on pending state".format(changed))
|
||||||
"set to the encode queue {0} encodings that were on pending state".format(
|
|
||||||
changed
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -602,6 +557,7 @@ def clear_sessions():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
engine = import_module(settings.SESSION_ENGINE)
|
engine = import_module(settings.SESSION_ENGINE)
|
||||||
@@ -612,9 +568,7 @@ def clear_sessions():
|
|||||||
|
|
||||||
|
|
||||||
@task(name="save_user_action", queue="short_tasks")
|
@task(name="save_user_action", queue="short_tasks")
|
||||||
def save_user_action(
|
def save_user_action(user_or_session, friendly_token=None, action="watch", extra_info=None):
|
||||||
user_or_session, friendly_token=None, action="watch", extra_info=None
|
|
||||||
):
|
|
||||||
"""Short task that saves a user action"""
|
"""Short task that saves a user action"""
|
||||||
|
|
||||||
if action not in VALID_USER_ACTIONS:
|
if action not in VALID_USER_ACTIONS:
|
||||||
@@ -652,9 +606,7 @@ def save_user_action(
|
|||||||
if user:
|
if user:
|
||||||
MediaAction.objects.filter(user=user, media=media, action="watch").delete()
|
MediaAction.objects.filter(user=user, media=media, action="watch").delete()
|
||||||
else:
|
else:
|
||||||
MediaAction.objects.filter(
|
MediaAction.objects.filter(session_key=session_key, media=media, action="watch").delete()
|
||||||
session_key=session_key, media=media, action="watch"
|
|
||||||
).delete()
|
|
||||||
if action == "rate":
|
if action == "rate":
|
||||||
try:
|
try:
|
||||||
score = extra_info.get("score")
|
score = extra_info.get("score")
|
||||||
@@ -663,9 +615,7 @@ def save_user_action(
|
|||||||
# TODO: better error handling?
|
# TODO: better error handling?
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
rating = Rating.objects.filter(
|
rating = Rating.objects.filter(user=user, media=media, rating_category_id=rating_category).first()
|
||||||
user=user, media=media, rating_category_id=rating_category
|
|
||||||
).first()
|
|
||||||
if rating:
|
if rating:
|
||||||
rating.score = score
|
rating.score = score
|
||||||
rating.save(update_fields=["score"])
|
rating.save(update_fields=["score"])
|
||||||
@@ -676,7 +626,7 @@ def save_user_action(
|
|||||||
rating_category_id=rating_category,
|
rating_category_id=rating_category,
|
||||||
score=score,
|
score=score,
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception:
|
||||||
# TODO: more specific handling, for errors in score, or
|
# TODO: more specific handling, for errors in score, or
|
||||||
# rating_category?
|
# rating_category?
|
||||||
return False
|
return False
|
||||||
@@ -735,14 +685,10 @@ def get_list_of_popular_media():
|
|||||||
|
|
||||||
for media in media_x:
|
for media in media_x:
|
||||||
ft = media["friendly_token"]
|
ft = media["friendly_token"]
|
||||||
num = MediaAction.objects.filter(
|
num = MediaAction.objects.filter(action_date__gte=period_x, action="watch", media__friendly_token=ft).count()
|
||||||
action_date__gte=period_x, action="watch", media__friendly_token=ft
|
|
||||||
).count()
|
|
||||||
if num:
|
if num:
|
||||||
valid_media_x[ft] = num
|
valid_media_x[ft] = num
|
||||||
num = MediaAction.objects.filter(
|
num = MediaAction.objects.filter(action_date__gte=period_y, action="like", media__friendly_token=ft).count()
|
||||||
action_date__gte=period_y, action="like", media__friendly_token=ft
|
|
||||||
).count()
|
|
||||||
if num:
|
if num:
|
||||||
valid_media_y[ft] = num
|
valid_media_y[ft] = num
|
||||||
|
|
||||||
@@ -767,12 +713,7 @@ def update_listings_thumbnails():
|
|||||||
saved = 0
|
saved = 0
|
||||||
qs = Category.objects.filter().order_by("-media_count")
|
qs = Category.objects.filter().order_by("-media_count")
|
||||||
for object in qs:
|
for object in qs:
|
||||||
media = (
|
media = Media.objects.exclude(friendly_token__in=used_media).filter(category=object, state="public", is_reviewed=True).order_by("-views").first()
|
||||||
Media.objects.exclude(friendly_token__in=used_media)
|
|
||||||
.filter(category=object, state="public", is_reviewed=True)
|
|
||||||
.order_by("-views")
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
if media:
|
if media:
|
||||||
object.listings_thumbnail = media.thumbnail_url
|
object.listings_thumbnail = media.thumbnail_url
|
||||||
object.save(update_fields=["listings_thumbnail"])
|
object.save(update_fields=["listings_thumbnail"])
|
||||||
@@ -785,12 +726,7 @@ def update_listings_thumbnails():
|
|||||||
saved = 0
|
saved = 0
|
||||||
qs = Tag.objects.filter().order_by("-media_count")
|
qs = Tag.objects.filter().order_by("-media_count")
|
||||||
for object in qs:
|
for object in qs:
|
||||||
media = (
|
media = Media.objects.exclude(friendly_token__in=used_media).filter(tags=object, state="public", is_reviewed=True).order_by("-views").first()
|
||||||
Media.objects.exclude(friendly_token__in=used_media)
|
|
||||||
.filter(tags=object, state="public", is_reviewed=True)
|
|
||||||
.order_by("-views")
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
if media:
|
if media:
|
||||||
object.listings_thumbnail = media.thumbnail_url
|
object.listings_thumbnail = media.thumbnail_url
|
||||||
object.save(update_fields=["listings_thumbnail"])
|
object.save(update_fields=["listings_thumbnail"])
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
from django.conf.urls.static import static
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import include, url
|
||||||
|
from django.conf.urls.static import static
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from . import views
|
from . import management_views, views
|
||||||
from . import management_views
|
|
||||||
from .feeds import IndexRSSFeed, SearchRSSFeed
|
from .feeds import IndexRSSFeed, SearchRSSFeed
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|||||||
459
files/views.py
459
files/views.py
@@ -1,69 +1,69 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from django.shortcuts import render
|
|
||||||
from django.http import HttpResponseRedirect
|
|
||||||
from django.conf import settings
|
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from django.db.models import Q
|
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
from django.contrib import messages
|
|
||||||
from django.template.defaultfilters import slugify
|
|
||||||
from django.core.mail import EmailMessage
|
|
||||||
from django.contrib.postgres.search import SearchQuery
|
|
||||||
|
|
||||||
from rest_framework import permissions
|
|
||||||
from rest_framework.views import APIView
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.settings import api_settings
|
|
||||||
from rest_framework.exceptions import PermissionDenied
|
|
||||||
from rest_framework import status
|
|
||||||
from rest_framework.parsers import (
|
|
||||||
JSONParser,
|
|
||||||
MultiPartParser,
|
|
||||||
FileUploadParser,
|
|
||||||
FormParser,
|
|
||||||
)
|
|
||||||
|
|
||||||
from celery.task.control import revoke
|
from celery.task.control import revoke
|
||||||
from cms.permissions import IsAuthorizedToAdd, IsUserOrEditor
|
from django.conf import settings
|
||||||
from cms.permissions import user_allowed_to_upload
|
from django.contrib import messages
|
||||||
from cms.custom_pagination import FastPaginationWithoutCount
|
from django.contrib.auth.decorators import login_required
|
||||||
from actions.models import MediaAction, USER_MEDIA_ACTIONS
|
from django.contrib.postgres.search import SearchQuery
|
||||||
from users.models import User
|
from django.core.mail import EmailMessage
|
||||||
from .helpers import produce_ffmpeg_commands, clean_query
|
from django.db.models import Q
|
||||||
from .models import (
|
from django.http import HttpResponseRedirect
|
||||||
Media,
|
from django.shortcuts import get_object_or_404, render
|
||||||
EncodeProfile,
|
from django.template.defaultfilters import slugify
|
||||||
Encoding,
|
from drf_yasg import openapi as openapi
|
||||||
Playlist,
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
PlaylistMedia,
|
from rest_framework import permissions, status
|
||||||
Comment,
|
from rest_framework.exceptions import PermissionDenied
|
||||||
Category,
|
from rest_framework.parsers import (
|
||||||
Tag,
|
FileUploadParser,
|
||||||
|
FormParser,
|
||||||
|
JSONParser,
|
||||||
|
MultiPartParser,
|
||||||
)
|
)
|
||||||
from .forms import MediaForm, ContactForm, SubtitleForm
|
from rest_framework.response import Response
|
||||||
from .tasks import save_user_action
|
from rest_framework.settings import api_settings
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
from actions.models import USER_MEDIA_ACTIONS, MediaAction
|
||||||
|
from cms.custom_pagination import FastPaginationWithoutCount
|
||||||
|
from cms.permissions import IsAuthorizedToAdd, IsUserOrEditor, user_allowed_to_upload
|
||||||
|
from users.models import User
|
||||||
|
|
||||||
|
from .forms import ContactForm, MediaForm, SubtitleForm
|
||||||
|
from .helpers import clean_query, produce_ffmpeg_commands
|
||||||
from .methods import (
|
from .methods import (
|
||||||
list_tasks,
|
|
||||||
get_user_or_session,
|
get_user_or_session,
|
||||||
show_recommended_media,
|
|
||||||
show_related_media,
|
|
||||||
is_mediacms_editor,
|
is_mediacms_editor,
|
||||||
is_mediacms_manager,
|
is_mediacms_manager,
|
||||||
update_user_ratings,
|
list_tasks,
|
||||||
notify_user_on_comment,
|
notify_user_on_comment,
|
||||||
|
show_recommended_media,
|
||||||
|
show_related_media,
|
||||||
|
update_user_ratings,
|
||||||
|
)
|
||||||
|
from .models import (
|
||||||
|
Category,
|
||||||
|
Comment,
|
||||||
|
EncodeProfile,
|
||||||
|
Encoding,
|
||||||
|
Media,
|
||||||
|
Playlist,
|
||||||
|
PlaylistMedia,
|
||||||
|
Tag,
|
||||||
)
|
)
|
||||||
from .serializers import (
|
from .serializers import (
|
||||||
MediaSerializer,
|
|
||||||
CategorySerializer,
|
CategorySerializer,
|
||||||
TagSerializer,
|
CommentSerializer,
|
||||||
SingleMediaSerializer,
|
|
||||||
EncodeProfileSerializer,
|
EncodeProfileSerializer,
|
||||||
MediaSearchSerializer,
|
MediaSearchSerializer,
|
||||||
PlaylistSerializer,
|
MediaSerializer,
|
||||||
PlaylistDetailSerializer,
|
PlaylistDetailSerializer,
|
||||||
CommentSerializer,
|
PlaylistSerializer,
|
||||||
|
SingleMediaSerializer,
|
||||||
|
TagSerializer,
|
||||||
)
|
)
|
||||||
from .stop_words import STOP_WORDS
|
from .stop_words import STOP_WORDS
|
||||||
|
from .tasks import save_user_action
|
||||||
|
|
||||||
VALID_USER_ACTIONS = [action for action, name in USER_MEDIA_ACTIONS]
|
VALID_USER_ACTIONS = [action for action, name in USER_MEDIA_ACTIONS]
|
||||||
|
|
||||||
@@ -86,11 +86,7 @@ def add_subtitle(request):
|
|||||||
if not media:
|
if not media:
|
||||||
return HttpResponseRedirect("/")
|
return HttpResponseRedirect("/")
|
||||||
|
|
||||||
if not (
|
if not (request.user == media.user or is_mediacms_editor(request.user) or is_mediacms_manager(request.user)):
|
||||||
request.user == media.user
|
|
||||||
or is_mediacms_editor(request.user)
|
|
||||||
or is_mediacms_manager(request.user)
|
|
||||||
):
|
|
||||||
return HttpResponseRedirect("/")
|
return HttpResponseRedirect("/")
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
@@ -175,11 +171,7 @@ def edit_media(request):
|
|||||||
if not media:
|
if not media:
|
||||||
return HttpResponseRedirect("/")
|
return HttpResponseRedirect("/")
|
||||||
|
|
||||||
if not (
|
if not (request.user == media.user or is_mediacms_editor(request.user) or is_mediacms_manager(request.user)):
|
||||||
request.user == media.user
|
|
||||||
or is_mediacms_editor(request.user)
|
|
||||||
or is_mediacms_manager(request.user)
|
|
||||||
):
|
|
||||||
return HttpResponseRedirect("/")
|
return HttpResponseRedirect("/")
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = MediaForm(request.user, request.POST, request.FILES, instance=media)
|
form = MediaForm(request.user, request.POST, request.FILES, instance=media)
|
||||||
@@ -220,8 +212,6 @@ def embed_media(request):
|
|||||||
if not media:
|
if not media:
|
||||||
return HttpResponseRedirect("/")
|
return HttpResponseRedirect("/")
|
||||||
|
|
||||||
user_or_session = get_user_or_session(request)
|
|
||||||
|
|
||||||
context = {}
|
context = {}
|
||||||
context["media"] = friendly_token
|
context["media"] = friendly_token
|
||||||
return render(request, "cms/embed.html", context)
|
return render(request, "cms/embed.html", context)
|
||||||
@@ -342,9 +332,7 @@ def view_media(request):
|
|||||||
return render(request, "cms/media.html", context)
|
return render(request, "cms/media.html", context)
|
||||||
|
|
||||||
user_or_session = get_user_or_session(request)
|
user_or_session = get_user_or_session(request)
|
||||||
save_user_action.delay(
|
save_user_action.delay(user_or_session, friendly_token=friendly_token, action="watch")
|
||||||
user_or_session, friendly_token=friendly_token, action="watch"
|
|
||||||
)
|
|
||||||
context = {}
|
context = {}
|
||||||
context["media"] = friendly_token
|
context["media"] = friendly_token
|
||||||
context["media_object"] = media
|
context["media_object"] = media
|
||||||
@@ -354,11 +342,7 @@ def view_media(request):
|
|||||||
context["CAN_DELETE_COMMENTS"] = False
|
context["CAN_DELETE_COMMENTS"] = False
|
||||||
|
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
if (
|
if (media.user.id == request.user.id) or is_mediacms_editor(request.user) or is_mediacms_manager(request.user):
|
||||||
(media.user.id == request.user.id)
|
|
||||||
or is_mediacms_editor(request.user)
|
|
||||||
or is_mediacms_manager(request.user)
|
|
||||||
):
|
|
||||||
context["CAN_DELETE_MEDIA"] = True
|
context["CAN_DELETE_MEDIA"] = True
|
||||||
context["CAN_EDIT_MEDIA"] = True
|
context["CAN_EDIT_MEDIA"] = True
|
||||||
context["CAN_DELETE_COMMENTS"] = True
|
context["CAN_DELETE_COMMENTS"] = True
|
||||||
@@ -384,6 +368,12 @@ class MediaList(APIView):
|
|||||||
permission_classes = (IsAuthorizedToAdd,)
|
permission_classes = (IsAuthorizedToAdd,)
|
||||||
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)
|
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[],
|
||||||
|
tags=['Media'],
|
||||||
|
operation_summary='to_be_written',
|
||||||
|
operation_description='to_be_written',
|
||||||
|
)
|
||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
# Show media
|
# Show media
|
||||||
params = self.request.query_params
|
params = self.request.query_params
|
||||||
@@ -423,6 +413,12 @@ class MediaList(APIView):
|
|||||||
serializer = MediaSerializer(page, many=True, context={"request": request})
|
serializer = MediaSerializer(page, many=True, context={"request": request})
|
||||||
return paginator.get_paginated_response(serializer.data)
|
return paginator.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[],
|
||||||
|
tags=['Media'],
|
||||||
|
operation_summary='to_be_written',
|
||||||
|
operation_description='to_be_written',
|
||||||
|
)
|
||||||
def post(self, request, format=None):
|
def post(self, request, format=None):
|
||||||
# Add new media
|
# Add new media
|
||||||
serializer = MediaSerializer(data=request.data, context={"request": request})
|
serializer = MediaSerializer(data=request.data, context={"request": request})
|
||||||
@@ -443,39 +439,33 @@ class MediaDetail(APIView):
|
|||||||
|
|
||||||
def get_object(self, friendly_token, password=None):
|
def get_object(self, friendly_token, password=None):
|
||||||
try:
|
try:
|
||||||
media = (
|
media = Media.objects.select_related("user").prefetch_related("encodings__profile").get(friendly_token=friendly_token)
|
||||||
Media.objects.select_related("user")
|
|
||||||
.prefetch_related("encodings__profile")
|
|
||||||
.get(friendly_token=friendly_token)
|
|
||||||
)
|
|
||||||
|
|
||||||
# this need be explicitly called, and will call
|
# this need be explicitly called, and will call
|
||||||
# has_object_permission() after has_permission has succeeded
|
# has_object_permission() after has_permission has succeeded
|
||||||
self.check_object_permissions(self.request, media)
|
self.check_object_permissions(self.request, media)
|
||||||
|
|
||||||
if media.state == "private" and not (
|
if media.state == "private" and not (self.request.user == media.user or is_mediacms_editor(self.request.user)):
|
||||||
self.request.user == media.user or is_mediacms_editor(self.request.user)
|
if (not password) or (not media.password) or (password != media.password):
|
||||||
):
|
|
||||||
if (
|
|
||||||
(not password)
|
|
||||||
or (not media.password)
|
|
||||||
or (password != media.password)
|
|
||||||
):
|
|
||||||
return Response(
|
return Response(
|
||||||
{"detail": "media is private"},
|
{"detail": "media is private"},
|
||||||
status=status.HTTP_401_UNAUTHORIZED,
|
status=status.HTTP_401_UNAUTHORIZED,
|
||||||
)
|
)
|
||||||
return media
|
return media
|
||||||
except PermissionDenied:
|
except PermissionDenied:
|
||||||
return Response(
|
return Response({"detail": "bad permissions"}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
{"detail": "bad permissions"}, status=status.HTTP_401_UNAUTHORIZED
|
|
||||||
)
|
|
||||||
except BaseException:
|
except BaseException:
|
||||||
return Response(
|
return Response(
|
||||||
{"detail": "media file does not exist"},
|
{"detail": "media file does not exist"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[],
|
||||||
|
tags=['Media'],
|
||||||
|
operation_summary='to_be_written',
|
||||||
|
operation_description='to_be_written',
|
||||||
|
)
|
||||||
def get(self, request, friendly_token, format=None):
|
def get(self, request, friendly_token, format=None):
|
||||||
# Get media details
|
# Get media details
|
||||||
password = request.GET.get("password")
|
password = request.GET.get("password")
|
||||||
@@ -488,27 +478,25 @@ class MediaDetail(APIView):
|
|||||||
related_media = []
|
related_media = []
|
||||||
else:
|
else:
|
||||||
related_media = show_related_media(media, request=request, limit=100)
|
related_media = show_related_media(media, request=request, limit=100)
|
||||||
related_media_serializer = MediaSerializer(
|
related_media_serializer = MediaSerializer(related_media, many=True, context={"request": request})
|
||||||
related_media, many=True, context={"request": request}
|
|
||||||
)
|
|
||||||
related_media = related_media_serializer.data
|
related_media = related_media_serializer.data
|
||||||
ret = serializer.data
|
ret = serializer.data
|
||||||
|
|
||||||
# update rattings info with user specific ratings
|
# update rattings info with user specific ratings
|
||||||
# eg user has already rated for this media
|
# eg user has already rated for this media
|
||||||
# this only affects user rating and only if enabled
|
# this only affects user rating and only if enabled
|
||||||
if (
|
if settings.ALLOW_RATINGS and ret.get("ratings_info") and not request.user.is_anonymous:
|
||||||
settings.ALLOW_RATINGS
|
ret["ratings_info"] = update_user_ratings(request.user, media, ret.get("ratings_info"))
|
||||||
and ret.get("ratings_info")
|
|
||||||
and not request.user.is_anonymous
|
|
||||||
):
|
|
||||||
ret["ratings_info"] = update_user_ratings(
|
|
||||||
request.user, media, ret.get("ratings_info")
|
|
||||||
)
|
|
||||||
|
|
||||||
ret["related_media"] = related_media
|
ret["related_media"] = related_media
|
||||||
return Response(ret)
|
return Response(ret)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[],
|
||||||
|
tags=['Media'],
|
||||||
|
operation_summary='to_be_written',
|
||||||
|
operation_description='to_be_written',
|
||||||
|
)
|
||||||
def post(self, request, friendly_token, format=None):
|
def post(self, request, friendly_token, format=None):
|
||||||
"""superuser actions
|
"""superuser actions
|
||||||
Available only to MediaCMS editors and managers
|
Available only to MediaCMS editors and managers
|
||||||
@@ -521,9 +509,7 @@ class MediaDetail(APIView):
|
|||||||
return media
|
return media
|
||||||
|
|
||||||
if not (is_mediacms_editor(request.user) or is_mediacms_manager(request.user)):
|
if not (is_mediacms_editor(request.user) or is_mediacms_manager(request.user)):
|
||||||
return Response(
|
return Response({"detail": "not allowed"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
{"detail": "not allowed"}, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
action = request.data.get("type")
|
action = request.data.get("type")
|
||||||
profiles_list = request.data.get("encoding_profiles")
|
profiles_list = request.data.get("encoding_profiles")
|
||||||
@@ -544,44 +530,48 @@ class MediaDetail(APIView):
|
|||||||
valid_profiles.append(p)
|
valid_profiles.append(p)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{"detail": "encoding_profiles must be int or list of ints of valid encode profiles"},
|
||||||
"detail": "encoding_profiles must be int or list of ints of valid encode profiles"
|
|
||||||
},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
media.encode(profiles=valid_profiles)
|
media.encode(profiles=valid_profiles)
|
||||||
return Response(
|
return Response({"detail": "media will be encoded"}, status=status.HTTP_201_CREATED)
|
||||||
{"detail": "media will be encoded"}, status=status.HTTP_201_CREATED
|
|
||||||
)
|
|
||||||
elif action == "review":
|
elif action == "review":
|
||||||
if result:
|
if result:
|
||||||
media.is_reviewed = True
|
media.is_reviewed = True
|
||||||
elif result == False:
|
elif result is False:
|
||||||
media.is_reviewed = False
|
media.is_reviewed = False
|
||||||
media.save(update_fields=["is_reviewed"])
|
media.save(update_fields=["is_reviewed"])
|
||||||
return Response(
|
return Response({"detail": "media reviewed set"}, status=status.HTTP_201_CREATED)
|
||||||
{"detail": "media reviewed set"}, status=status.HTTP_201_CREATED
|
|
||||||
)
|
|
||||||
return Response(
|
return Response(
|
||||||
{"detail": "not valid action or no action specified"},
|
{"detail": "not valid action or no action specified"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[],
|
||||||
|
tags=['Media'],
|
||||||
|
operation_summary='to_be_written',
|
||||||
|
operation_description='to_be_written',
|
||||||
|
)
|
||||||
def put(self, request, friendly_token, format=None):
|
def put(self, request, friendly_token, format=None):
|
||||||
# Update a media object
|
# Update a media object
|
||||||
media = self.get_object(friendly_token)
|
media = self.get_object(friendly_token)
|
||||||
if isinstance(media, Response):
|
if isinstance(media, Response):
|
||||||
return media
|
return media
|
||||||
|
|
||||||
serializer = MediaSerializer(
|
serializer = MediaSerializer(media, data=request.data, context={"request": request})
|
||||||
media, data=request.data, context={"request": request}
|
|
||||||
)
|
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
media_file = request.data["media_file"]
|
media_file = request.data["media_file"]
|
||||||
serializer.save(user=request.user, media_file=media_file)
|
serializer.save(user=request.user, media_file=media_file)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[],
|
||||||
|
tags=['Media'],
|
||||||
|
operation_summary='to_be_written',
|
||||||
|
operation_description='to_be_written',
|
||||||
|
)
|
||||||
def delete(self, request, friendly_token, format=None):
|
def delete(self, request, friendly_token, format=None):
|
||||||
# Delete a media object
|
# Delete a media object
|
||||||
media = self.get_object(friendly_token)
|
media = self.get_object(friendly_token)
|
||||||
@@ -601,26 +591,24 @@ class MediaActions(APIView):
|
|||||||
|
|
||||||
def get_object(self, friendly_token):
|
def get_object(self, friendly_token):
|
||||||
try:
|
try:
|
||||||
media = (
|
media = Media.objects.select_related("user").prefetch_related("encodings__profile").get(friendly_token=friendly_token)
|
||||||
Media.objects.select_related("user")
|
|
||||||
.prefetch_related("encodings__profile")
|
|
||||||
.get(friendly_token=friendly_token)
|
|
||||||
)
|
|
||||||
if media.state == "private" and self.request.user != media.user:
|
if media.state == "private" and self.request.user != media.user:
|
||||||
return Response(
|
return Response({"detail": "media is private"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
{"detail": "media is private"}, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
return media
|
return media
|
||||||
except PermissionDenied:
|
except PermissionDenied:
|
||||||
return Response(
|
return Response({"detail": "bad permissions"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
{"detail": "bad permissions"}, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
except BaseException:
|
except BaseException:
|
||||||
return Response(
|
return Response(
|
||||||
{"detail": "media file does not exist"},
|
{"detail": "media file does not exist"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[],
|
||||||
|
tags=['Media'],
|
||||||
|
operation_summary='to_be_written',
|
||||||
|
operation_description='to_be_written',
|
||||||
|
)
|
||||||
def get(self, request, friendly_token, format=None):
|
def get(self, request, friendly_token, format=None):
|
||||||
# show date and reason for each time media was reported
|
# show date and reason for each time media was reported
|
||||||
media = self.get_object(friendly_token)
|
media = self.get_object(friendly_token)
|
||||||
@@ -636,6 +624,12 @@ class MediaActions(APIView):
|
|||||||
|
|
||||||
return Response(ret, status=status.HTTP_200_OK)
|
return Response(ret, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[],
|
||||||
|
tags=['Media'],
|
||||||
|
operation_summary='to_be_written',
|
||||||
|
operation_description='to_be_written',
|
||||||
|
)
|
||||||
def post(self, request, friendly_token, format=None):
|
def post(self, request, friendly_token, format=None):
|
||||||
# perform like/dislike/report actions
|
# perform like/dislike/report actions
|
||||||
media = self.get_object(friendly_token)
|
media = self.get_object(friendly_token)
|
||||||
@@ -661,23 +655,23 @@ class MediaActions(APIView):
|
|||||||
extra_info=extra,
|
extra_info=extra,
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(
|
return Response({"detail": "action received"}, status=status.HTTP_201_CREATED)
|
||||||
{"detail": "action received"}, status=status.HTTP_201_CREATED
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
return Response(
|
return Response({"detail": "no action specified"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
{"detail": "no action specified"}, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[],
|
||||||
|
tags=['Media'],
|
||||||
|
operation_summary='to_be_written',
|
||||||
|
operation_description='to_be_written',
|
||||||
|
)
|
||||||
def delete(self, request, friendly_token, format=None):
|
def delete(self, request, friendly_token, format=None):
|
||||||
media = self.get_object(friendly_token)
|
media = self.get_object(friendly_token)
|
||||||
if isinstance(media, Response):
|
if isinstance(media, Response):
|
||||||
return media
|
return media
|
||||||
|
|
||||||
if not request.user.is_superuser:
|
if not request.user.is_superuser:
|
||||||
return Response(
|
return Response({"detail": "not allowed"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
{"detail": "not allowed"}, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
action = request.data.get("type")
|
action = request.data.get("type")
|
||||||
if action:
|
if action:
|
||||||
@@ -690,9 +684,7 @@ class MediaActions(APIView):
|
|||||||
status=status.HTTP_201_CREATED,
|
status=status.HTTP_201_CREATED,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return Response(
|
return Response({"detail": "no action specified"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
{"detail": "no action specified"}, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MediaSearch(APIView):
|
class MediaSearch(APIView):
|
||||||
@@ -703,6 +695,12 @@ class MediaSearch(APIView):
|
|||||||
|
|
||||||
parser_classes = (JSONParser,)
|
parser_classes = (JSONParser,)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[],
|
||||||
|
tags=['Search'],
|
||||||
|
operation_summary='to_be_written',
|
||||||
|
operation_description='to_be_written',
|
||||||
|
)
|
||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
params = self.request.query_params
|
params = self.request.query_params
|
||||||
query = params.get("q", "").strip().lower()
|
query = params.get("q", "").strip().lower()
|
||||||
@@ -736,11 +734,7 @@ class MediaSearch(APIView):
|
|||||||
if query:
|
if query:
|
||||||
# move this processing to a prepare_query function
|
# move this processing to a prepare_query function
|
||||||
query = clean_query(query)
|
query = clean_query(query)
|
||||||
q_parts = [
|
q_parts = [q_part.rstrip("y") for q_part in query.split() if q_part not in STOP_WORDS]
|
||||||
q_part.rstrip("y")
|
|
||||||
for q_part in query.split()
|
|
||||||
if q_part not in STOP_WORDS
|
|
||||||
]
|
|
||||||
if q_parts:
|
if q_parts:
|
||||||
query = SearchQuery(q_parts[0] + ":*", search_type="raw")
|
query = SearchQuery(q_parts[0] + ":*", search_type="raw")
|
||||||
for part in q_parts[1:]:
|
for part in q_parts[1:]:
|
||||||
@@ -771,10 +765,10 @@ class MediaSearch(APIView):
|
|||||||
if upload_date == 'this_month':
|
if upload_date == 'this_month':
|
||||||
year = datetime.now().date().year
|
year = datetime.now().date().year
|
||||||
month = datetime.now().date().month
|
month = datetime.now().date().month
|
||||||
gte = datetime(year,month,1)
|
gte = datetime(year, month, 1)
|
||||||
if upload_date == 'this_year':
|
if upload_date == 'this_year':
|
||||||
year = datetime.now().date().year
|
year = datetime.now().date().year
|
||||||
gte = datetime(year,1,1)
|
gte = datetime(year, 1, 1)
|
||||||
if lte:
|
if lte:
|
||||||
media = media.filter(add_date__lte=lte)
|
media = media.filter(add_date__lte=lte)
|
||||||
if gte:
|
if gte:
|
||||||
@@ -794,9 +788,7 @@ class MediaSearch(APIView):
|
|||||||
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
|
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
|
||||||
paginator = pagination_class()
|
paginator = pagination_class()
|
||||||
page = paginator.paginate_queryset(media, request)
|
page = paginator.paginate_queryset(media, request)
|
||||||
serializer = MediaSearchSerializer(
|
serializer = MediaSearchSerializer(page, many=True, context={"request": request})
|
||||||
page, many=True, context={"request": request}
|
|
||||||
)
|
|
||||||
return paginator.get_paginated_response(serializer.data)
|
return paginator.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
@@ -806,6 +798,12 @@ class PlaylistList(APIView):
|
|||||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsAuthorizedToAdd)
|
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsAuthorizedToAdd)
|
||||||
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)
|
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[],
|
||||||
|
tags=['Playlists'],
|
||||||
|
operation_summary='to_be_written',
|
||||||
|
operation_description='to_be_written',
|
||||||
|
)
|
||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
|
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
|
||||||
paginator = pagination_class()
|
paginator = pagination_class()
|
||||||
@@ -820,6 +818,12 @@ class PlaylistList(APIView):
|
|||||||
serializer = PlaylistSerializer(page, many=True, context={"request": request})
|
serializer = PlaylistSerializer(page, many=True, context={"request": request})
|
||||||
return paginator.get_paginated_response(serializer.data)
|
return paginator.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[],
|
||||||
|
tags=['Playlists'],
|
||||||
|
operation_summary='to_be_written',
|
||||||
|
operation_description='to_be_written',
|
||||||
|
)
|
||||||
def post(self, request, format=None):
|
def post(self, request, format=None):
|
||||||
serializer = PlaylistSerializer(data=request.data, context={"request": request})
|
serializer = PlaylistSerializer(data=request.data, context={"request": request})
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
@@ -840,15 +844,19 @@ class PlaylistDetail(APIView):
|
|||||||
self.check_object_permissions(self.request, playlist)
|
self.check_object_permissions(self.request, playlist)
|
||||||
return playlist
|
return playlist
|
||||||
except PermissionDenied:
|
except PermissionDenied:
|
||||||
return Response(
|
return Response({"detail": "not enough permissions"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
{"detail": "not enough permissions"}, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
except BaseException:
|
except BaseException:
|
||||||
return Response(
|
return Response(
|
||||||
{"detail": "Playlist does not exist"},
|
{"detail": "Playlist does not exist"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[],
|
||||||
|
tags=['Playlists'],
|
||||||
|
operation_summary='to_be_written',
|
||||||
|
operation_description='to_be_written',
|
||||||
|
)
|
||||||
def get(self, request, friendly_token, format=None):
|
def get(self, request, friendly_token, format=None):
|
||||||
playlist = self.get_playlist(friendly_token)
|
playlist = self.get_playlist(friendly_token)
|
||||||
if isinstance(playlist, Response):
|
if isinstance(playlist, Response):
|
||||||
@@ -856,31 +864,37 @@ class PlaylistDetail(APIView):
|
|||||||
|
|
||||||
serializer = PlaylistDetailSerializer(playlist, context={"request": request})
|
serializer = PlaylistDetailSerializer(playlist, context={"request": request})
|
||||||
|
|
||||||
playlist_media = PlaylistMedia.objects.filter(
|
playlist_media = PlaylistMedia.objects.filter(playlist=playlist).prefetch_related("media__user")
|
||||||
playlist=playlist
|
|
||||||
).prefetch_related("media__user")
|
|
||||||
|
|
||||||
playlist_media = [c.media for c in playlist_media]
|
playlist_media = [c.media for c in playlist_media]
|
||||||
playlist_media_serializer = MediaSerializer(
|
playlist_media_serializer = MediaSerializer(playlist_media, many=True, context={"request": request})
|
||||||
playlist_media, many=True, context={"request": request}
|
|
||||||
)
|
|
||||||
ret = serializer.data
|
ret = serializer.data
|
||||||
ret["playlist_media"] = playlist_media_serializer.data
|
ret["playlist_media"] = playlist_media_serializer.data
|
||||||
|
|
||||||
return Response(ret)
|
return Response(ret)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[],
|
||||||
|
tags=['Playlists'],
|
||||||
|
operation_summary='to_be_written',
|
||||||
|
operation_description='to_be_written',
|
||||||
|
)
|
||||||
def post(self, request, friendly_token, format=None):
|
def post(self, request, friendly_token, format=None):
|
||||||
playlist = self.get_playlist(friendly_token)
|
playlist = self.get_playlist(friendly_token)
|
||||||
if isinstance(playlist, Response):
|
if isinstance(playlist, Response):
|
||||||
return playlist
|
return playlist
|
||||||
serializer = PlaylistDetailSerializer(
|
serializer = PlaylistDetailSerializer(playlist, data=request.data, context={"request": request})
|
||||||
playlist, data=request.data, context={"request": request}
|
|
||||||
)
|
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save(user=request.user)
|
serializer.save(user=request.user)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[],
|
||||||
|
tags=['Playlists'],
|
||||||
|
operation_summary='to_be_written',
|
||||||
|
operation_description='to_be_written',
|
||||||
|
)
|
||||||
def put(self, request, friendly_token, format=None):
|
def put(self, request, friendly_token, format=None):
|
||||||
playlist = self.get_playlist(friendly_token)
|
playlist = self.get_playlist(friendly_token)
|
||||||
if isinstance(playlist, Response):
|
if isinstance(playlist, Response):
|
||||||
@@ -895,14 +909,10 @@ class PlaylistDetail(APIView):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
if action in ["add", "remove", "ordering"]:
|
if action in ["add", "remove", "ordering"]:
|
||||||
media = Media.objects.filter(
|
media = Media.objects.filter(friendly_token=media_friendly_token).first()
|
||||||
friendly_token=media_friendly_token, state="public"
|
|
||||||
).first()
|
|
||||||
if media:
|
if media:
|
||||||
if action == "add":
|
if action == "add":
|
||||||
media_in_playlist = PlaylistMedia.objects.filter(
|
media_in_playlist = PlaylistMedia.objects.filter(playlist=playlist).count()
|
||||||
playlist=playlist
|
|
||||||
).count()
|
|
||||||
if media_in_playlist >= settings.MAX_MEDIA_PER_PLAYLIST:
|
if media_in_playlist >= settings.MAX_MEDIA_PER_PLAYLIST:
|
||||||
return Response(
|
return Response(
|
||||||
{"detail": "max number of media for a Playlist reached"},
|
{"detail": "max number of media for a Playlist reached"},
|
||||||
@@ -920,9 +930,7 @@ class PlaylistDetail(APIView):
|
|||||||
status=status.HTTP_201_CREATED,
|
status=status.HTTP_201_CREATED,
|
||||||
)
|
)
|
||||||
elif action == "remove":
|
elif action == "remove":
|
||||||
PlaylistMedia.objects.filter(
|
PlaylistMedia.objects.filter(playlist=playlist, media=media).delete()
|
||||||
playlist=playlist, media=media
|
|
||||||
).delete()
|
|
||||||
return Response(
|
return Response(
|
||||||
{"detail": "media removed from Playlist"},
|
{"detail": "media removed from Playlist"},
|
||||||
status=status.HTTP_201_CREATED,
|
status=status.HTTP_201_CREATED,
|
||||||
@@ -935,14 +943,18 @@ class PlaylistDetail(APIView):
|
|||||||
status=status.HTTP_201_CREATED,
|
status=status.HTTP_201_CREATED,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return Response(
|
return Response({"detail": "media is not valid"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
{"detail": "media is not valid"}, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
return Response(
|
return Response(
|
||||||
{"detail": "invalid or not specified action"},
|
{"detail": "invalid or not specified action"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[],
|
||||||
|
tags=['Playlists'],
|
||||||
|
operation_summary='to_be_written',
|
||||||
|
operation_description='to_be_written',
|
||||||
|
)
|
||||||
def delete(self, request, friendly_token, format=None):
|
def delete(self, request, friendly_token, format=None):
|
||||||
playlist = self.get_playlist(friendly_token)
|
playlist = self.get_playlist(friendly_token)
|
||||||
if isinstance(playlist, Response):
|
if isinstance(playlist, Response):
|
||||||
@@ -960,6 +972,7 @@ class EncodingDetail(APIView):
|
|||||||
permission_classes = (permissions.IsAdminUser,)
|
permission_classes = (permissions.IsAdminUser,)
|
||||||
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)
|
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)
|
||||||
|
|
||||||
|
@swagger_auto_schema(auto_schema=None)
|
||||||
def post(self, request, encoding_id):
|
def post(self, request, encoding_id):
|
||||||
ret = {}
|
ret = {}
|
||||||
force = request.data.get("force", False)
|
force = request.data.get("force", False)
|
||||||
@@ -993,7 +1006,7 @@ class EncodingDetail(APIView):
|
|||||||
chunk_file_path=chunk_file_path,
|
chunk_file_path=chunk_file_path,
|
||||||
).count()
|
).count()
|
||||||
> 1
|
> 1
|
||||||
and force == False
|
and force is False
|
||||||
):
|
):
|
||||||
Encoding.objects.filter(id=encoding_id).delete()
|
Encoding.objects.filter(id=encoding_id).delete()
|
||||||
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
@@ -1013,15 +1026,11 @@ class EncodingDetail(APIView):
|
|||||||
if chunk:
|
if chunk:
|
||||||
original_media_path = chunk_file_path
|
original_media_path = chunk_file_path
|
||||||
original_media_md5sum = encoding.md5sum
|
original_media_md5sum = encoding.md5sum
|
||||||
original_media_url = (
|
original_media_url = settings.SSL_FRONTEND_HOST + encoding.media_chunk_url
|
||||||
settings.SSL_FRONTEND_HOST + encoding.media_chunk_url
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
original_media_path = media.media_file.path
|
original_media_path = media.media_file.path
|
||||||
original_media_md5sum = media.md5sum
|
original_media_md5sum = media.md5sum
|
||||||
original_media_url = (
|
original_media_url = settings.SSL_FRONTEND_HOST + media.original_media_url
|
||||||
settings.SSL_FRONTEND_HOST + media.original_media_url
|
|
||||||
)
|
|
||||||
|
|
||||||
ret["original_media_url"] = original_media_url
|
ret["original_media_url"] = original_media_url
|
||||||
ret["original_media_path"] = original_media_path
|
ret["original_media_path"] = original_media_path
|
||||||
@@ -1089,6 +1098,7 @@ class EncodingDetail(APIView):
|
|||||||
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
return Response({"status": "success"}, status=status.HTTP_201_CREATED)
|
return Response({"status": "success"}, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
@swagger_auto_schema(auto_schema=None)
|
||||||
def put(self, request, encoding_id, format=None):
|
def put(self, request, encoding_id, format=None):
|
||||||
encoding_file = request.data["file"]
|
encoding_file = request.data["file"]
|
||||||
encoding = Encoding.objects.filter(id=encoding_id).first()
|
encoding = Encoding.objects.filter(id=encoding_id).first()
|
||||||
@@ -1106,6 +1116,15 @@ class CommentList(APIView):
|
|||||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsAuthorizedToAdd)
|
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsAuthorizedToAdd)
|
||||||
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)
|
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[
|
||||||
|
openapi.Parameter(name='page', type=openapi.TYPE_INTEGER, in_=openapi.IN_QUERY, description='Page number'),
|
||||||
|
openapi.Parameter(name='author', type=openapi.TYPE_STRING, in_=openapi.IN_QUERY, description='username'),
|
||||||
|
],
|
||||||
|
tags=['Comments'],
|
||||||
|
operation_summary='Lists Comments',
|
||||||
|
operation_description='Paginated listing of all comments',
|
||||||
|
)
|
||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
|
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
|
||||||
paginator = pagination_class()
|
paginator = pagination_class()
|
||||||
@@ -1137,25 +1156,25 @@ class CommentDetail(APIView):
|
|||||||
|
|
||||||
def get_object(self, friendly_token):
|
def get_object(self, friendly_token):
|
||||||
try:
|
try:
|
||||||
media = Media.objects.select_related("user").get(
|
media = Media.objects.select_related("user").get(friendly_token=friendly_token)
|
||||||
friendly_token=friendly_token
|
|
||||||
)
|
|
||||||
self.check_object_permissions(self.request, media)
|
self.check_object_permissions(self.request, media)
|
||||||
if media.state == "private" and self.request.user != media.user:
|
if media.state == "private" and self.request.user != media.user:
|
||||||
return Response(
|
return Response({"detail": "media is private"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
{"detail": "media is private"}, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
return media
|
return media
|
||||||
except PermissionDenied:
|
except PermissionDenied:
|
||||||
return Response(
|
return Response({"detail": "bad permissions"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
{"detail": "bad permissions"}, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
except BaseException:
|
except BaseException:
|
||||||
return Response(
|
return Response(
|
||||||
{"detail": "media file does not exist"},
|
{"detail": "media file does not exist"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[],
|
||||||
|
tags=['Media'],
|
||||||
|
operation_summary='to_be_written',
|
||||||
|
operation_description='to_be_written',
|
||||||
|
)
|
||||||
def get(self, request, friendly_token):
|
def get(self, request, friendly_token):
|
||||||
# list comments for a media
|
# list comments for a media
|
||||||
media = self.get_object(friendly_token)
|
media = self.get_object(friendly_token)
|
||||||
@@ -1168,6 +1187,12 @@ class CommentDetail(APIView):
|
|||||||
serializer = CommentSerializer(page, many=True, context={"request": request})
|
serializer = CommentSerializer(page, many=True, context={"request": request})
|
||||||
return paginator.get_paginated_response(serializer.data)
|
return paginator.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[],
|
||||||
|
tags=['Media'],
|
||||||
|
operation_summary='to_be_written',
|
||||||
|
operation_description='to_be_written',
|
||||||
|
)
|
||||||
def delete(self, request, friendly_token, uid=None):
|
def delete(self, request, friendly_token, uid=None):
|
||||||
"""Delete a comment
|
"""Delete a comment
|
||||||
Administrators, MediaCMS editors and managers,
|
Administrators, MediaCMS editors and managers,
|
||||||
@@ -1181,18 +1206,18 @@ class CommentDetail(APIView):
|
|||||||
{"detail": "comment does not exist"},
|
{"detail": "comment does not exist"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
if (
|
if (comment.user == self.request.user) or comment.media.user == self.request.user or is_mediacms_editor(self.request.user):
|
||||||
(comment.user == self.request.user)
|
|
||||||
or comment.media.user == self.request.user
|
|
||||||
or is_mediacms_editor(self.request.user)
|
|
||||||
):
|
|
||||||
comment.delete()
|
comment.delete()
|
||||||
else:
|
else:
|
||||||
return Response(
|
return Response({"detail": "bad permissions"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
{"detail": "bad permissions"}, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[],
|
||||||
|
tags=['Media'],
|
||||||
|
operation_summary='to_be_written',
|
||||||
|
operation_description='to_be_written',
|
||||||
|
)
|
||||||
def post(self, request, friendly_token):
|
def post(self, request, friendly_token):
|
||||||
"""Create a comment"""
|
"""Create a comment"""
|
||||||
media = self.get_object(friendly_token)
|
media = self.get_object(friendly_token)
|
||||||
@@ -1217,17 +1242,19 @@ class CommentDetail(APIView):
|
|||||||
class UserActions(APIView):
|
class UserActions(APIView):
|
||||||
parser_classes = (JSONParser,)
|
parser_classes = (JSONParser,)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[
|
||||||
|
openapi.Parameter(name='action', type=openapi.TYPE_STRING, in_=openapi.IN_PATH, description='action', required=True, enum=VALID_USER_ACTIONS),
|
||||||
|
],
|
||||||
|
tags=['Users'],
|
||||||
|
operation_summary='List user actions',
|
||||||
|
operation_description='Lists user actions',
|
||||||
|
)
|
||||||
def get(self, request, action):
|
def get(self, request, action):
|
||||||
media = []
|
media = []
|
||||||
if action in VALID_USER_ACTIONS:
|
if action in VALID_USER_ACTIONS:
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
media = (
|
media = Media.objects.select_related("user").filter(mediaactions__user=request.user, mediaactions__action=action).order_by("-mediaactions__action_date")
|
||||||
Media.objects.select_related("user")
|
|
||||||
.filter(
|
|
||||||
mediaactions__user=request.user, mediaactions__action=action
|
|
||||||
)
|
|
||||||
.order_by("-mediaactions__action_date")
|
|
||||||
)
|
|
||||||
elif request.session.session_key:
|
elif request.session.session_key:
|
||||||
media = (
|
media = (
|
||||||
Media.objects.select_related("user")
|
Media.objects.select_related("user")
|
||||||
@@ -1248,11 +1275,15 @@ class UserActions(APIView):
|
|||||||
class CategoryList(APIView):
|
class CategoryList(APIView):
|
||||||
"""List categories"""
|
"""List categories"""
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[],
|
||||||
|
tags=['Categories'],
|
||||||
|
operation_summary='Lists Categories',
|
||||||
|
operation_description='Lists all categories',
|
||||||
|
)
|
||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
categories = Category.objects.filter().order_by("title")
|
categories = Category.objects.filter().order_by("title")
|
||||||
serializer = CategorySerializer(
|
serializer = CategorySerializer(categories, many=True, context={"request": request})
|
||||||
categories, many=True, context={"request": request}
|
|
||||||
)
|
|
||||||
ret = serializer.data
|
ret = serializer.data
|
||||||
return Response(ret)
|
return Response(ret)
|
||||||
|
|
||||||
@@ -1260,6 +1291,14 @@ class CategoryList(APIView):
|
|||||||
class TagList(APIView):
|
class TagList(APIView):
|
||||||
"""List tags"""
|
"""List tags"""
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[
|
||||||
|
openapi.Parameter(name='page', type=openapi.TYPE_INTEGER, in_=openapi.IN_QUERY, description='Page number'),
|
||||||
|
],
|
||||||
|
tags=['Tags'],
|
||||||
|
operation_summary='Lists Tags',
|
||||||
|
operation_description='Paginated listing of all tags',
|
||||||
|
)
|
||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
tags = Tag.objects.filter().order_by("-media_count")
|
tags = Tag.objects.filter().order_by("-media_count")
|
||||||
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
|
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
|
||||||
@@ -1272,17 +1311,23 @@ class TagList(APIView):
|
|||||||
class EncodeProfileList(APIView):
|
class EncodeProfileList(APIView):
|
||||||
"""List encode profiles"""
|
"""List encode profiles"""
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[],
|
||||||
|
tags=['Encoding Profiles'],
|
||||||
|
operation_summary='to_be_written',
|
||||||
|
operation_description='to_be_written',
|
||||||
|
)
|
||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
profiles = EncodeProfile.objects.all()
|
profiles = EncodeProfile.objects.all()
|
||||||
serializer = EncodeProfileSerializer(
|
serializer = EncodeProfileSerializer(profiles, many=True, context={"request": request})
|
||||||
profiles, many=True, context={"request": request}
|
|
||||||
)
|
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
class TasksList(APIView):
|
class TasksList(APIView):
|
||||||
"""List tasks"""
|
"""List tasks"""
|
||||||
|
|
||||||
|
swagger_schema = None
|
||||||
|
|
||||||
permission_classes = (permissions.IsAdminUser,)
|
permission_classes = (permissions.IsAdminUser,)
|
||||||
|
|
||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
@@ -1293,6 +1338,8 @@ class TasksList(APIView):
|
|||||||
class TaskDetail(APIView):
|
class TaskDetail(APIView):
|
||||||
"""Cancel a task"""
|
"""Cancel a task"""
|
||||||
|
|
||||||
|
swagger_schema = None
|
||||||
|
|
||||||
permission_classes = (permissions.IsAdminUser,)
|
permission_classes = (permissions.IsAdminUser,)
|
||||||
|
|
||||||
def delete(self, request, uid, format=None):
|
def delete(self, request, uid, format=None):
|
||||||
|
|||||||
10
install.sh
10
install.sh
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# should be run as root and only on Ubuntu 18/20 versions!
|
# should be run as root and only on Ubuntu 18/20, Debian Buster versions!
|
||||||
echo "Welcome to the MediacMS installation!";
|
echo "Welcome to the MediacMS installation!";
|
||||||
|
|
||||||
if [ `id -u` -ne 0 ]
|
if [ `id -u` -ne 0 ]
|
||||||
@@ -27,6 +27,10 @@ if [[ `lsb_release -d` == *"Ubuntu 20"* ]]; then
|
|||||||
elif [[ `lsb_release -d` = *"Ubuntu 18"* ]]; then
|
elif [[ `lsb_release -d` = *"Ubuntu 18"* ]]; then
|
||||||
echo 'Performing system update and dependency installation, this will take a few minutes'
|
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 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
|
||||||
else
|
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"
|
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
|
||||||
@@ -115,8 +119,8 @@ fi
|
|||||||
# Bento4 utility installation, for HLS
|
# Bento4 utility installation, for HLS
|
||||||
|
|
||||||
cd /home/mediacms.io/mediacms
|
cd /home/mediacms.io/mediacms
|
||||||
wget http://zebulon.bok.net/Bento4/binaries/Bento4-SDK-1-6-0-632.x86_64-unknown-linux.zip
|
wget http://zebulon.bok.net/Bento4/binaries/Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip
|
||||||
unzip Bento4-SDK-1-6-0-632.x86_64-unknown-linux.zip
|
unzip Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip
|
||||||
mkdir /home/mediacms.io/mediacms/media_files/hls
|
mkdir /home/mediacms.io/mediacms/media_files/hls
|
||||||
|
|
||||||
# last, set default owner
|
# last, set default owner
|
||||||
|
|||||||
@@ -7,9 +7,5 @@ if __name__ == "__main__":
|
|||||||
try:
|
try:
|
||||||
from django.core.management import execute_from_command_line
|
from django.core.management import execute_from_command_line
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
raise ImportError(
|
raise ImportError("Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?") from exc
|
||||||
"Couldn't import Django. Are you sure it's installed and "
|
|
||||||
"available on your PYTHONPATH environment variable? Did you "
|
|
||||||
"forget to activate a virtual environment?"
|
|
||||||
) from exc
|
|
||||||
execute_from_command_line(sys.argv)
|
execute_from_command_line(sys.argv)
|
||||||
|
|||||||
17
pyproject.toml
Normal file
17
pyproject.toml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[tool.black]
|
||||||
|
line-length = 200
|
||||||
|
target-version = ['py36', 'py37', 'py38']
|
||||||
|
skip-string-normalization = true
|
||||||
|
include = '\.pyi?$'
|
||||||
|
exclude = '''
|
||||||
|
/(
|
||||||
|
\.*
|
||||||
|
| \.git
|
||||||
|
| \.mypy_cache
|
||||||
|
| \.venv
|
||||||
|
# The following are specific to Black, you probably don't want those.
|
||||||
|
| blib2to3
|
||||||
|
| tests/data
|
||||||
|
| profiling
|
||||||
|
)/
|
||||||
|
'''
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
Django==3.1.4
|
Django==3.1.8
|
||||||
djangorestframework==3.12.2
|
djangorestframework==3.12.2
|
||||||
django-allauth==0.44.0
|
django-allauth==0.44.0
|
||||||
|
|
||||||
@@ -9,14 +9,21 @@ uwsgi==2.0.19.1
|
|||||||
django-redis==4.12.1
|
django-redis==4.12.1
|
||||||
celery==4.4.7
|
celery==4.4.7
|
||||||
|
|
||||||
Pillow==8.0.1
|
drf-yasg==1.20.0
|
||||||
|
|
||||||
|
Pillow==8.1.1
|
||||||
django-imagekit
|
django-imagekit
|
||||||
|
|
||||||
markdown
|
markdown
|
||||||
django-filter
|
django-filter
|
||||||
|
|
||||||
filetype
|
filetype
|
||||||
django-mptt
|
django-mptt
|
||||||
|
|
||||||
django-crispy-forms
|
django-crispy-forms
|
||||||
|
|
||||||
requests==2.25.0
|
requests==2.25.0
|
||||||
|
|
||||||
django-celery-email
|
django-celery-email
|
||||||
m3u8
|
m3u8
|
||||||
|
|
||||||
@@ -30,3 +37,4 @@ flake8
|
|||||||
pep8
|
pep8
|
||||||
django-silk
|
django-silk
|
||||||
django-debug-toolbar
|
django-debug-toolbar
|
||||||
|
pre-commit
|
||||||
|
|||||||
4
setup.cfg
Normal file
4
setup.cfg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[flake8]
|
||||||
|
exclude = .git,*migrations*
|
||||||
|
max-line-length = 119
|
||||||
|
ignore=F401,F403,E501,W503
|
||||||
@@ -1,37 +1,37 @@
|
|||||||
MediaCMS.url = {
|
MediaCMS.url = {
|
||||||
home: "{{FRONTEND_HOST}}",
|
home: "/",
|
||||||
search: "{{FRONTEND_HOST}}/search",
|
search: "/search",
|
||||||
latestMedia: "{{FRONTEND_HOST}}/latest",
|
latestMedia: "/latest",
|
||||||
featuredMedia: "{{FRONTEND_HOST}}/featured",
|
featuredMedia: "/featured",
|
||||||
recommendedMedia: "{{FRONTEND_HOST}}/recommended",
|
recommendedMedia: "/recommended",
|
||||||
members: "{{FRONTEND_HOST}}/members",
|
members: "/members",
|
||||||
/* Error pages */
|
/* Error pages */
|
||||||
error404: "{{FRONTEND_HOST}}/error",
|
error404: "/error",
|
||||||
/* Taxonomies pages */
|
/* Taxonomies pages */
|
||||||
tags: "{{FRONTEND_HOST}}/tags",
|
tags: "/tags",
|
||||||
categories: "{{FRONTEND_HOST}}/categories",
|
categories: "/categories",
|
||||||
topics: "{{FRONTEND_HOST}}/topics",
|
topics: "/topics",
|
||||||
languages: "{{FRONTEND_HOST}}/languages",
|
languages: "/languages",
|
||||||
countries: "{{FRONTEND_HOST}}/countries",
|
countries: "/countries",
|
||||||
/* User pages */
|
/* User pages */
|
||||||
likedMedia: "{{FRONTEND_HOST}}/liked",
|
likedMedia: "/liked",
|
||||||
history: "{{FRONTEND_HOST}}/history",
|
history: "/history",
|
||||||
/* Add/edit pages */
|
/* Add/edit pages */
|
||||||
addMedia: "{{FRONTEND_HOST}}/upload",
|
addMedia: "/upload",
|
||||||
/* User account pages */
|
/* User account pages */
|
||||||
editProfile: "{{user.edit_url}}",
|
editProfile: "{{user.edit_url}}",
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
signout: "{{FRONTEND_HOST}}/accounts/logout/",
|
signout: "/accounts/logout/",
|
||||||
editChannel: "{{user.default_channel_edit_url}}",
|
editChannel: "{{user.default_channel_edit_url}}",
|
||||||
changePassword: "{{FRONTEND_HOST}}/accounts/password/change/",
|
changePassword: "/accounts/password/change/",
|
||||||
/* Administration pages */
|
/* Administration pages */
|
||||||
{% if IS_MEDIACMS_ADMIN %}admin: '/admin',{% endif %}
|
{% if IS_MEDIACMS_ADMIN %}admin: '/admin',{% endif %}
|
||||||
/* Management pages */
|
/* Management pages */
|
||||||
{% if IS_MEDIACMS_ADMIN or IS_MEDIACMS_MANAGER or IS_MEDIACMS_EDITOR %}manageMedia: "{{FRONTEND_HOST}}/manage/media",{% endif %}
|
{% if IS_MEDIACMS_EDITOR %}manageMedia: "/manage/media",{% endif %}
|
||||||
{% if IS_MEDIACMS_ADMIN or IS_MEDIACMS_MANAGER %}manageUsers: "{{FRONTEND_HOST}}/manage/users",{% endif %}
|
{% if IS_MEDIACMS_MANAGER %}manageUsers: "/manage/users",{% endif %}
|
||||||
{% if IS_MEDIACMS_ADMIN or IS_MEDIACMS_MANAGER or IS_MEDIACMS_EDITOR %}manageComments: "{{FRONTEND_HOST}}/manage/comments",{% endif %}
|
{% if IS_MEDIACMS_EDITOR %}manageComments: "/manage/comments",{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
signin: "{{FRONTEND_HOST}}/accounts/login/",
|
signin: "/accounts/login/",
|
||||||
register: "{{FRONTEND_HOST}}/accounts/signup/",
|
register: "/accounts/signup/",
|
||||||
{% endif %}
|
{% endif %}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ MediaCMS.user = {
|
|||||||
addComment: true,
|
addComment: true,
|
||||||
deleteComment: {% if CAN_DELETE_COMMENTS %}true{% else %}false{% endif %},
|
deleteComment: {% if CAN_DELETE_COMMENTS %}true{% else %}false{% endif %},
|
||||||
editProfile: {% if CAN_EDIT %}true{% else %}false{% endif %},
|
editProfile: {% if CAN_EDIT %}true{% else %}false{% endif %},
|
||||||
deleteProfile: {% if CAN_DELETE_PROFILE %}true{% else %}false{% endif %},
|
deleteProfile: {% if CAN_DELETE %}true{% else %}false{% endif %},
|
||||||
manageMedia: {% if IS_MEDIACMS_ADMIN or IS_MEDIACMS_MANAGER or IS_MEDIACMS_EDITOR %}true{% else %}false{% endif %},
|
manageMedia: {% if IS_MEDIACMS_ADMIN or IS_MEDIACMS_MANAGER or IS_MEDIACMS_EDITOR %}true{% else %}false{% endif %},
|
||||||
manageUsers: {% if IS_MEDIACMS_ADMIN or IS_MEDIACMS_MANAGER %}true{% else %}false{% endif %},
|
manageUsers: {% if IS_MEDIACMS_ADMIN or IS_MEDIACMS_MANAGER %}true{% else %}false{% endif %},
|
||||||
manageComments: {% if IS_MEDIACMS_ADMIN or IS_MEDIACMS_MANAGER or IS_MEDIACMS_EDITOR %}true{% else %}false{% endif %},
|
manageComments: {% if IS_MEDIACMS_ADMIN or IS_MEDIACMS_MANAGER or IS_MEDIACMS_EDITOR %}true{% else %}false{% endif %},
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
MediaCMS.site = {
|
MediaCMS.site = {
|
||||||
id: 'mediacms',
|
id: 'mediacms',
|
||||||
title: "{{PORTAL_NAME}}",
|
title: "{{PORTAL_NAME}}",
|
||||||
url: '{{FRONTEND_HOST}}/',
|
url: '{{FRONTEND_HOST}}',
|
||||||
api: '{{FRONTEND_HOST}}/api/v1',
|
api: '{{FRONTEND_HOST}}api/v1',
|
||||||
theme: {
|
theme: {
|
||||||
mode: 'light', // Valid values: 'light', 'dark'.
|
mode: '{{DEFAULT_THEME}}',
|
||||||
switch: {
|
switch: {
|
||||||
position: 'header', // Valid values: 'header', 'sidebar'.
|
position: 'header', // Valid values: 'header', 'sidebar'.
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
logo:{
|
logo:{
|
||||||
lightMode:{
|
lightMode:{
|
||||||
img: "{{FRONTEND_HOST}}/static/images/logo_dark.png",
|
img: "/static/images/logo_dark.png",
|
||||||
svg: "{{FRONTEND_HOST}}/static/images/logo_dark.svg",
|
svg: "/static/images/logo_dark.svg",
|
||||||
},
|
},
|
||||||
darkMode:{
|
darkMode:{
|
||||||
img: "{{FRONTEND_HOST}}/static/images/logo_light.png",
|
img: "/static/images/logo_light.png",
|
||||||
svg: "{{FRONTEND_HOST}}/static/images/logo_light.svg",
|
svg: "/static/images/logo_light.svg",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
pages: {
|
pages: {
|
||||||
@@ -49,4 +49,5 @@ MediaCMS.site = {
|
|||||||
title: 'Categories',
|
title: 'Categories',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from os.path import join
|
|
||||||
from io import StringIO
|
|
||||||
import shutil
|
import shutil
|
||||||
|
from io import StringIO
|
||||||
|
from os.path import join
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from . import utils
|
from . import utils
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from django.core.exceptions import ImproperlyConfigured
|
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
|
|
||||||
def import_class(path):
|
def import_class(path):
|
||||||
path_bits = path.split(".")
|
path_bits = path.split(".")
|
||||||
@@ -14,9 +15,7 @@ def import_class(path):
|
|||||||
module_itself = import_module(module_path)
|
module_itself = import_module(module_path)
|
||||||
|
|
||||||
if not hasattr(module_itself, class_name):
|
if not hasattr(module_itself, class_name):
|
||||||
message = "The Python module '{}' has no '{}' class.".format(
|
message = "The Python module '{}' has no '{}' class.".format(module_path, class_name)
|
||||||
module_path, class_name
|
|
||||||
)
|
|
||||||
raise ImportError(message)
|
raise ImportError(message)
|
||||||
|
|
||||||
return getattr(module_itself, class_name)
|
return getattr(module_itself, class_name)
|
||||||
|
|||||||
@@ -2,17 +2,18 @@
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.core.files import File
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.views import generic
|
from django.views import generic
|
||||||
from django.conf import settings
|
|
||||||
from django.core.files import File
|
|
||||||
from django.core.exceptions import PermissionDenied
|
|
||||||
|
|
||||||
from cms.permissions import user_allowed_to_upload
|
from cms.permissions import user_allowed_to_upload
|
||||||
from files.models import Media
|
|
||||||
from files.helpers import rm_file
|
from files.helpers import rm_file
|
||||||
from .forms import FineUploaderUploadForm, FineUploaderUploadSuccessForm
|
from files.models import Media
|
||||||
|
|
||||||
from .fineuploader import ChunkedFineUploader
|
from .fineuploader import ChunkedFineUploader
|
||||||
|
from .forms import FineUploaderUploadForm, FineUploaderUploadSuccessForm
|
||||||
|
|
||||||
|
|
||||||
class FineUploaderView(generic.FormView):
|
class FineUploaderView(generic.FormView):
|
||||||
@@ -67,9 +68,7 @@ class FineUploaderView(generic.FormView):
|
|||||||
new = Media.objects.create(media_file=myfile, user=self.request.user)
|
new = Media.objects.create(media_file=myfile, user=self.request.user)
|
||||||
rm_file(media_file)
|
rm_file(media_file)
|
||||||
shutil.rmtree(os.path.join(settings.MEDIA_ROOT, self.upload.file_path))
|
shutil.rmtree(os.path.join(settings.MEDIA_ROOT, self.upload.file_path))
|
||||||
return self.make_response(
|
return self.make_response({"success": True, "media_url": new.get_absolute_url()})
|
||||||
{"success": True, "media_url": new.get_absolute_url()}
|
|
||||||
)
|
|
||||||
|
|
||||||
def form_invalid(self, form):
|
def form_invalid(self, form):
|
||||||
data = {"success": False, "error": "%s" % repr(form.errors)}
|
data = {"success": False, "error": "%s" % repr(form.errors)}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from django.urls import reverse
|
|
||||||
from django.conf import settings
|
|
||||||
from allauth.account.adapter import DefaultAccountAdapter
|
from allauth.account.adapter import DefaultAccountAdapter
|
||||||
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
|
||||||
class MyAccountAdapter(DefaultAccountAdapter):
|
class MyAccountAdapter(DefaultAccountAdapter):
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ class UserAdmin(admin.ModelAdmin):
|
|||||||
"last_name",
|
"last_name",
|
||||||
"media_count",
|
"media_count",
|
||||||
"date_joined",
|
"date_joined",
|
||||||
"is_staff",
|
|
||||||
"is_active",
|
"is_active",
|
||||||
)
|
)
|
||||||
list_display = [
|
list_display = [
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from .models import User, Channel
|
|
||||||
|
from .models import Channel, User
|
||||||
|
|
||||||
|
|
||||||
class SignupForm(forms.Form):
|
class SignupForm(forms.Form):
|
||||||
@@ -23,7 +24,7 @@ class UserForm(forms.ModelForm):
|
|||||||
"advancedUser",
|
"advancedUser",
|
||||||
"is_manager",
|
"is_manager",
|
||||||
"is_editor",
|
"is_editor",
|
||||||
#"allow_contact",
|
# "allow_contact",
|
||||||
)
|
)
|
||||||
|
|
||||||
def clean_logo(self):
|
def clean_logo(self):
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# Generated by Django 3.1.4 on 2020-12-01 07:12
|
# Generated by Django 3.1.4 on 2020-12-01 07:12
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
import django.contrib.auth.models
|
import django.contrib.auth.models
|
||||||
import django.contrib.auth.validators
|
import django.contrib.auth.validators
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import django.utils.timezone
|
import django.utils.timezone
|
||||||
import imagekit.models.fields
|
import imagekit.models.fields
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@@ -33,9 +33,7 @@ class Migration(migrations.Migration):
|
|||||||
("password", models.CharField(max_length=128, verbose_name="password")),
|
("password", models.CharField(max_length=128, verbose_name="password")),
|
||||||
(
|
(
|
||||||
"last_login",
|
"last_login",
|
||||||
models.DateTimeField(
|
models.DateTimeField(blank=True, null=True, verbose_name="last login"),
|
||||||
blank=True, null=True, verbose_name="last login"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"is_superuser",
|
"is_superuser",
|
||||||
@@ -48,35 +46,25 @@ class Migration(migrations.Migration):
|
|||||||
(
|
(
|
||||||
"username",
|
"username",
|
||||||
models.CharField(
|
models.CharField(
|
||||||
error_messages={
|
error_messages={"unique": "A user with that username already exists."},
|
||||||
"unique": "A user with that username already exists."
|
|
||||||
},
|
|
||||||
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
|
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
|
||||||
max_length=150,
|
max_length=150,
|
||||||
unique=True,
|
unique=True,
|
||||||
validators=[
|
validators=[django.contrib.auth.validators.UnicodeUsernameValidator()],
|
||||||
django.contrib.auth.validators.UnicodeUsernameValidator()
|
|
||||||
],
|
|
||||||
verbose_name="username",
|
verbose_name="username",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"first_name",
|
"first_name",
|
||||||
models.CharField(
|
models.CharField(blank=True, max_length=150, verbose_name="first name"),
|
||||||
blank=True, max_length=150, verbose_name="first name"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"last_name",
|
"last_name",
|
||||||
models.CharField(
|
models.CharField(blank=True, max_length=150, verbose_name="last name"),
|
||||||
blank=True, max_length=150, verbose_name="last name"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"email",
|
"email",
|
||||||
models.EmailField(
|
models.EmailField(blank=True, max_length=254, verbose_name="email address"),
|
||||||
blank=True, max_length=254, verbose_name="email address"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"is_staff",
|
"is_staff",
|
||||||
@@ -96,9 +84,7 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
"date_joined",
|
"date_joined",
|
||||||
models.DateTimeField(
|
models.DateTimeField(default=django.utils.timezone.now, verbose_name="date joined"),
|
||||||
default=django.utils.timezone.now, verbose_name="date joined"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"logo",
|
"logo",
|
||||||
@@ -111,9 +97,7 @@ class Migration(migrations.Migration):
|
|||||||
("description", models.TextField(blank=True, verbose_name="About me")),
|
("description", models.TextField(blank=True, verbose_name="About me")),
|
||||||
(
|
(
|
||||||
"name",
|
"name",
|
||||||
models.CharField(
|
models.CharField(db_index=True, max_length=250, verbose_name="full name"),
|
||||||
db_index=True, max_length=250, verbose_name="full name"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"date_added",
|
"date_added",
|
||||||
@@ -125,9 +109,7 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
"is_featured",
|
"is_featured",
|
||||||
models.BooleanField(
|
models.BooleanField(db_index=True, default=False, verbose_name="Is featured"),
|
||||||
db_index=True, default=False, verbose_name="Is featured"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"title",
|
"title",
|
||||||
@@ -135,9 +117,7 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
"advancedUser",
|
"advancedUser",
|
||||||
models.BooleanField(
|
models.BooleanField(db_index=True, default=False, verbose_name="advanced user"),
|
||||||
db_index=True, default=False, verbose_name="advanced user"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
("media_count", models.IntegerField(default=0)),
|
("media_count", models.IntegerField(default=0)),
|
||||||
(
|
(
|
||||||
@@ -156,21 +136,15 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
"location",
|
"location",
|
||||||
models.CharField(
|
models.CharField(blank=True, max_length=250, verbose_name="Location"),
|
||||||
blank=True, max_length=250, verbose_name="Location"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"is_editor",
|
"is_editor",
|
||||||
models.BooleanField(
|
models.BooleanField(db_index=True, default=False, verbose_name="MediaCMS Editor"),
|
||||||
db_index=True, default=False, verbose_name="MediaCMS Editor"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"is_manager",
|
"is_manager",
|
||||||
models.BooleanField(
|
models.BooleanField(db_index=True, default=False, verbose_name="MediaCMS Manager"),
|
||||||
db_index=True, default=False, verbose_name="MediaCMS Manager"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"groups",
|
"groups",
|
||||||
@@ -218,9 +192,7 @@ class Migration(migrations.Migration):
|
|||||||
("notify", models.BooleanField(default=False)),
|
("notify", models.BooleanField(default=False)),
|
||||||
(
|
(
|
||||||
"method",
|
"method",
|
||||||
models.CharField(
|
models.CharField(choices=[("email", "Email")], default="email", max_length=20),
|
||||||
choices=[("email", "Email")], default="email", max_length=20
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"user",
|
"user",
|
||||||
@@ -276,8 +248,6 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
migrations.AddIndex(
|
migrations.AddIndex(
|
||||||
model_name="user",
|
model_name="user",
|
||||||
index=models.Index(
|
index=models.Index(fields=["-date_added", "name"], name="users_user_date_ad_4eb0b8_idx"),
|
||||||
fields=["-date_added", "name"], name="users_user_date_ad_4eb0b8_idx"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
from django.db import models
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.utils import timezone
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.dispatch import receiver
|
|
||||||
from django.db.models.signals import post_save, post_delete
|
|
||||||
from django.utils.html import strip_tags
|
|
||||||
from django.core.mail import EmailMessage
|
from django.core.mail import EmailMessage
|
||||||
|
from django.db import models
|
||||||
from imagekit.processors import ResizeToFill
|
from django.db.models.signals import post_delete, post_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.html import strip_tags
|
||||||
from imagekit.models import ProcessedImageField
|
from imagekit.models import ProcessedImageField
|
||||||
|
from imagekit.processors import ResizeToFill
|
||||||
|
|
||||||
import files.helpers as helpers
|
import files.helpers as helpers
|
||||||
from files.models import Media, Tag, Category
|
from files.models import Category, Media, Tag
|
||||||
|
|
||||||
|
|
||||||
class User(AbstractUser):
|
class User(AbstractUser):
|
||||||
@@ -40,9 +39,7 @@ class User(AbstractUser):
|
|||||||
location = models.CharField("Location", max_length=250, blank=True)
|
location = models.CharField("Location", max_length=250, blank=True)
|
||||||
is_editor = models.BooleanField("MediaCMS Editor", default=False, db_index=True)
|
is_editor = models.BooleanField("MediaCMS Editor", default=False, db_index=True)
|
||||||
is_manager = models.BooleanField("MediaCMS Manager", default=False, db_index=True)
|
is_manager = models.BooleanField("MediaCMS Manager", default=False, db_index=True)
|
||||||
allow_contact = models.BooleanField(
|
allow_contact = models.BooleanField("Whether allow contact will be shown on profile page", default=False)
|
||||||
"Whether allow contact will be shown on profile page", default=False
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["-date_added", "name"]
|
ordering = ["-date_added", "name"]
|
||||||
@@ -117,9 +114,7 @@ class User(AbstractUser):
|
|||||||
class Channel(models.Model):
|
class Channel(models.Model):
|
||||||
title = models.CharField(max_length=90, db_index=True)
|
title = models.CharField(max_length=90, db_index=True)
|
||||||
description = models.TextField(blank=True, help_text="description")
|
description = models.TextField(blank=True, help_text="description")
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(User, on_delete=models.CASCADE, db_index=True, related_name="channels")
|
||||||
User, on_delete=models.CASCADE, db_index=True, related_name="channels"
|
|
||||||
)
|
|
||||||
add_date = models.DateTimeField(auto_now_add=True, db_index=True)
|
add_date = models.DateTimeField(auto_now_add=True, db_index=True)
|
||||||
subscribers = models.ManyToManyField(User, related_name="subscriptions", blank=True)
|
subscribers = models.ManyToManyField(User, related_name="subscriptions", blank=True)
|
||||||
friendly_token = models.CharField(blank=True, max_length=12)
|
friendly_token = models.CharField(blank=True, max_length=12)
|
||||||
@@ -150,13 +145,9 @@ class Channel(models.Model):
|
|||||||
|
|
||||||
def get_absolute_url(self, edit=False):
|
def get_absolute_url(self, edit=False):
|
||||||
if edit:
|
if edit:
|
||||||
return reverse(
|
return reverse("edit_channel", kwargs={"friendly_token": self.friendly_token})
|
||||||
"edit_channel", kwargs={"friendly_token": self.friendly_token}
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
return reverse(
|
return reverse("view_channel", kwargs={"friendly_token": self.friendly_token})
|
||||||
"view_channel", kwargs={"friendly_token": self.friendly_token}
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def edit_url(self):
|
def edit_url(self):
|
||||||
@@ -178,9 +169,7 @@ Visit user profile page at %s
|
|||||||
instance.email,
|
instance.email,
|
||||||
settings.SSL_FRONTEND_HOST + instance.get_absolute_url(),
|
settings.SSL_FRONTEND_HOST + instance.get_absolute_url(),
|
||||||
)
|
)
|
||||||
email = EmailMessage(
|
email = EmailMessage(title, msg, settings.DEFAULT_FROM_EMAIL, settings.ADMIN_EMAIL_LIST)
|
||||||
title, msg, settings.DEFAULT_FROM_EMAIL, settings.ADMIN_EMAIL_LIST
|
|
||||||
)
|
|
||||||
email.send(fail_silently=True)
|
email.send(fail_silently=True)
|
||||||
|
|
||||||
|
|
||||||
@@ -193,14 +182,10 @@ class Notification(models.Model):
|
|||||||
Needs work
|
Needs work
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(User, on_delete=models.CASCADE, db_index=True, related_name="notifications")
|
||||||
User, on_delete=models.CASCADE, db_index=True, related_name="notifications"
|
|
||||||
)
|
|
||||||
action = models.CharField(max_length=30, blank=True)
|
action = models.CharField(max_length=30, blank=True)
|
||||||
notify = models.BooleanField(default=False)
|
notify = models.BooleanField(default=False)
|
||||||
method = models.CharField(
|
method = models.CharField(max_length=20, choices=NOTIFICATION_METHODS, default="email")
|
||||||
max_length=20, choices=NOTIFICATION_METHODS, default="email"
|
|
||||||
)
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
super(Notification, self).save(*args, **kwargs)
|
super(Notification, self).save(*args, **kwargs)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from .models import User
|
from .models import User
|
||||||
|
|
||||||
|
|
||||||
@@ -11,9 +12,7 @@ class UserSerializer(serializers.ModelSerializer):
|
|||||||
return self.context["request"].build_absolute_uri(obj.get_absolute_url())
|
return self.context["request"].build_absolute_uri(obj.get_absolute_url())
|
||||||
|
|
||||||
def get_api_url(self, obj):
|
def get_api_url(self, obj):
|
||||||
return self.context["request"].build_absolute_uri(
|
return self.context["request"].build_absolute_uri(obj.get_absolute_url(api=True))
|
||||||
obj.get_absolute_url(api=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_thumbnail_url(self, obj):
|
def get_thumbnail_url(self, obj):
|
||||||
return self.context["request"].build_absolute_uri(obj.thumbnail_url())
|
return self.context["request"].build_absolute_uri(obj.thumbnail_url())
|
||||||
@@ -55,9 +54,7 @@ class UserDetailSerializer(serializers.ModelSerializer):
|
|||||||
return self.context["request"].build_absolute_uri(obj.get_absolute_url())
|
return self.context["request"].build_absolute_uri(obj.get_absolute_url())
|
||||||
|
|
||||||
def get_api_url(self, obj):
|
def get_api_url(self, obj):
|
||||||
return self.context["request"].build_absolute_uri(
|
return self.context["request"].build_absolute_uri(obj.get_absolute_url(api=True))
|
||||||
obj.get_absolute_url(api=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_thumbnail_url(self, obj):
|
def get_thumbnail_url(self, obj):
|
||||||
return self.context["request"].build_absolute_uri(obj.thumbnail_url())
|
return self.context["request"].build_absolute_uri(obj.thumbnail_url())
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
urlpatterns = [
|
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(r"^user/(?P<username>[\w@._-]*)/$", views.view_user, name="get_user"),
|
||||||
url(
|
url(
|
||||||
r"^user/(?P<username>[\w@.]*)/media$",
|
r"^user/(?P<username>[\w@.]*)/media$",
|
||||||
views.view_user_media,
|
views.view_user_media,
|
||||||
@@ -19,9 +21,7 @@ urlpatterns = [
|
|||||||
name="get_user_about",
|
name="get_user_about",
|
||||||
),
|
),
|
||||||
url(r"^user/(?P<username>[\w@.]*)/edit$", views.edit_user, name="edit_user"),
|
url(r"^user/(?P<username>[\w@.]*)/edit$", views.edit_user, name="edit_user"),
|
||||||
url(
|
url(r"^channel/(?P<friendly_token>[\w]*)$", views.view_channel, name="view_channel"),
|
||||||
r"^channel/(?P<friendly_token>[\w]*)$", views.view_channel, name="view_channel"
|
|
||||||
),
|
|
||||||
url(
|
url(
|
||||||
r"^channel/(?P<friendly_token>[\w]*)/edit$",
|
r"^channel/(?P<friendly_token>[\w]*)/edit$",
|
||||||
views.edit_channel,
|
views.edit_channel,
|
||||||
|
|||||||
@@ -8,10 +8,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
@deconstructible
|
@deconstructible
|
||||||
class ASCIIUsernameValidator(validators.RegexValidator):
|
class ASCIIUsernameValidator(validators.RegexValidator):
|
||||||
regex = r"^[\w]+$"
|
regex = r"^[\w]+$"
|
||||||
message = _(
|
message = _("Enter a valid username. This value may contain only " "English letters and numbers")
|
||||||
"Enter a valid username. This value may contain only "
|
|
||||||
"English letters and numbers"
|
|
||||||
)
|
|
||||||
flags = re.ASCII
|
flags = re.ASCII
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
161
users/views.py
161
users/views.py
@@ -1,27 +1,29 @@
|
|||||||
from django.shortcuts import render
|
from django.conf import settings
|
||||||
from django.http import HttpResponseRedirect
|
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.core.mail import EmailMessage
|
from django.core.mail import EmailMessage
|
||||||
from django.conf import settings
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.shortcuts import render
|
||||||
from rest_framework import permissions
|
from drf_yasg import openapi as openapi
|
||||||
from rest_framework.views import APIView
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from rest_framework.response import Response
|
from rest_framework import permissions, status
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.decorators import api_view
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
from rest_framework import status
|
|
||||||
from rest_framework.parsers import (
|
from rest_framework.parsers import (
|
||||||
JSONParser,
|
|
||||||
MultiPartParser,
|
|
||||||
FileUploadParser,
|
FileUploadParser,
|
||||||
FormParser,
|
FormParser,
|
||||||
|
JSONParser,
|
||||||
|
MultiPartParser,
|
||||||
)
|
)
|
||||||
from rest_framework.decorators import api_view
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.settings import api_settings
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
from cms.permissions import IsUserOrManager
|
from cms.permissions import IsUserOrManager
|
||||||
from files.methods import is_mediacms_manager, is_mediacms_editor
|
from files.methods import is_mediacms_editor, is_mediacms_manager
|
||||||
from .models import User, Channel
|
|
||||||
from .forms import UserForm, ChannelForm
|
from .forms import ChannelForm, UserForm
|
||||||
from .serializers import UserSerializer, UserDetailSerializer
|
from .models import Channel, User
|
||||||
|
from .serializers import UserDetailSerializer, UserSerializer
|
||||||
|
|
||||||
|
|
||||||
def get_user(username):
|
def get_user(username):
|
||||||
@@ -38,15 +40,9 @@ def view_user(request, username):
|
|||||||
if not user:
|
if not user:
|
||||||
return HttpResponseRedirect("/members")
|
return HttpResponseRedirect("/members")
|
||||||
context["user"] = user
|
context["user"] = user
|
||||||
context["CAN_EDIT"] = (
|
context["CAN_EDIT"] = True if ((user and user == request.user) or is_mediacms_manager(request.user)) else False
|
||||||
True
|
|
||||||
if ((user and user == request.user) or is_mediacms_manager(request.user))
|
|
||||||
else False
|
|
||||||
)
|
|
||||||
context["CAN_DELETE"] = True if is_mediacms_manager(request.user) else False
|
context["CAN_DELETE"] = True if is_mediacms_manager(request.user) else False
|
||||||
context["SHOW_CONTACT_FORM"] = (
|
context["SHOW_CONTACT_FORM"] = True if (user.allow_contact or is_mediacms_editor(request.user)) else False
|
||||||
True if (user.allow_contact or is_mediacms_editor(request.user)) else False
|
|
||||||
)
|
|
||||||
return render(request, "cms/user.html", context)
|
return render(request, "cms/user.html", context)
|
||||||
|
|
||||||
|
|
||||||
@@ -57,15 +53,9 @@ def view_user_media(request, username):
|
|||||||
return HttpResponseRedirect("/members")
|
return HttpResponseRedirect("/members")
|
||||||
|
|
||||||
context["user"] = user
|
context["user"] = user
|
||||||
context["CAN_EDIT"] = (
|
context["CAN_EDIT"] = True if ((user and user == request.user) or is_mediacms_manager(request.user)) else False
|
||||||
True
|
context["CAN_DELETE"] = True if is_mediacms_manager(request.user) else False
|
||||||
if ((user and user == request.user) or request.user.is_superuser)
|
context["SHOW_CONTACT_FORM"] = True if (user.allow_contact or is_mediacms_editor(request.user)) else False
|
||||||
else False
|
|
||||||
)
|
|
||||||
context["CAN_DELETE"] = True if request.user.is_superuser else False
|
|
||||||
context["SHOW_CONTACT_FORM"] = (
|
|
||||||
True if (user.allow_contact or is_mediacms_editor(request.user)) else False
|
|
||||||
)
|
|
||||||
return render(request, "cms/user_media.html", context)
|
return render(request, "cms/user_media.html", context)
|
||||||
|
|
||||||
|
|
||||||
@@ -76,15 +66,9 @@ def view_user_playlists(request, username):
|
|||||||
return HttpResponseRedirect("/members")
|
return HttpResponseRedirect("/members")
|
||||||
|
|
||||||
context["user"] = user
|
context["user"] = user
|
||||||
context["CAN_EDIT"] = (
|
context["CAN_EDIT"] = True if ((user and user == request.user) or is_mediacms_manager(request.user)) else False
|
||||||
True
|
context["CAN_DELETE"] = True if is_mediacms_manager(request.user) else False
|
||||||
if ((user and user == request.user) or request.user.is_superuser)
|
context["SHOW_CONTACT_FORM"] = True if (user.allow_contact or is_mediacms_editor(request.user)) else False
|
||||||
else False
|
|
||||||
)
|
|
||||||
context["CAN_DELETE"] = True if request.user.is_superuser else False
|
|
||||||
context["SHOW_CONTACT_FORM"] = (
|
|
||||||
True if (user.allow_contact or is_mediacms_editor(request.user)) else False
|
|
||||||
)
|
|
||||||
|
|
||||||
return render(request, "cms/user_playlists.html", context)
|
return render(request, "cms/user_playlists.html", context)
|
||||||
|
|
||||||
@@ -96,15 +80,9 @@ def view_user_about(request, username):
|
|||||||
return HttpResponseRedirect("/members")
|
return HttpResponseRedirect("/members")
|
||||||
|
|
||||||
context["user"] = user
|
context["user"] = user
|
||||||
context["CAN_EDIT"] = (
|
context["CAN_EDIT"] = True if ((user and user == request.user) or is_mediacms_manager(request.user)) else False
|
||||||
True
|
context["CAN_DELETE"] = True if is_mediacms_manager(request.user) else False
|
||||||
if ((user and user == request.user) or request.user.is_superuser)
|
context["SHOW_CONTACT_FORM"] = True if (user.allow_contact or is_mediacms_editor(request.user)) else False
|
||||||
else False
|
|
||||||
)
|
|
||||||
context["CAN_DELETE"] = True if request.user.is_superuser else False
|
|
||||||
context["SHOW_CONTACT_FORM"] = (
|
|
||||||
True if (user.allow_contact or is_mediacms_editor(request.user)) else False
|
|
||||||
)
|
|
||||||
|
|
||||||
return render(request, "cms/user_about.html", context)
|
return render(request, "cms/user_about.html", context)
|
||||||
|
|
||||||
@@ -134,20 +112,14 @@ def view_channel(request, friendly_token):
|
|||||||
else:
|
else:
|
||||||
user = channel.user
|
user = channel.user
|
||||||
context["user"] = user
|
context["user"] = user
|
||||||
context["CAN_EDIT"] = (
|
context["CAN_EDIT"] = True if ((user and user == request.user) or is_mediacms_manager(request.user)) else False
|
||||||
True
|
|
||||||
if ((user and user == request.user) or request.user.is_superuser)
|
|
||||||
else False
|
|
||||||
)
|
|
||||||
return render(request, "cms/channel.html", context)
|
return render(request, "cms/channel.html", context)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def edit_channel(request, friendly_token):
|
def edit_channel(request, friendly_token):
|
||||||
channel = Channel.objects.filter(friendly_token=friendly_token).first()
|
channel = Channel.objects.filter(friendly_token=friendly_token).first()
|
||||||
if not (
|
if not (channel and request.user.is_authenticated and (request.user == channel.user)):
|
||||||
channel and request.user.is_authenticated and (request.user == channel.user)
|
|
||||||
):
|
|
||||||
return HttpResponseRedirect("/")
|
return HttpResponseRedirect("/")
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
@@ -161,6 +133,13 @@ def edit_channel(request, friendly_token):
|
|||||||
return render(request, "cms/channel_edit.html", {"form": form})
|
return render(request, "cms/channel_edit.html", {"form": form})
|
||||||
|
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
methods=['post'],
|
||||||
|
manual_parameters=[],
|
||||||
|
tags=['Users'],
|
||||||
|
operation_summary='Contact user',
|
||||||
|
operation_description='Contact user through email, if user has set this option',
|
||||||
|
)
|
||||||
@api_view(["POST"])
|
@api_view(["POST"])
|
||||||
def contact_user(request, username):
|
def contact_user(request, username):
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
@@ -197,9 +176,18 @@ Sender email: %s\n
|
|||||||
|
|
||||||
|
|
||||||
class UserList(APIView):
|
class UserList(APIView):
|
||||||
|
|
||||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
||||||
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)
|
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[
|
||||||
|
openapi.Parameter(name='page', type=openapi.TYPE_INTEGER, in_=openapi.IN_QUERY, description='Page number'),
|
||||||
|
],
|
||||||
|
tags=['Users'],
|
||||||
|
operation_summary='List users',
|
||||||
|
operation_description='Paginated listing of users',
|
||||||
|
)
|
||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
|
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
|
||||||
paginator = pagination_class()
|
paginator = pagination_class()
|
||||||
@@ -228,14 +216,18 @@ class UserDetail(APIView):
|
|||||||
self.check_object_permissions(self.request, user)
|
self.check_object_permissions(self.request, user)
|
||||||
return user
|
return user
|
||||||
except PermissionDenied:
|
except PermissionDenied:
|
||||||
return Response(
|
return Response({"detail": "not enough permissions"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
{"detail": "not enough permissions"}, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
return Response(
|
return Response({"detail": "user does not exist"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
{"detail": "user does not exist"}, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[
|
||||||
|
openapi.Parameter(name='username', type=openapi.TYPE_STRING, in_=openapi.IN_PATH, description='username', required=True),
|
||||||
|
],
|
||||||
|
tags=['Users'],
|
||||||
|
operation_summary='List user details',
|
||||||
|
operation_description='Get user details',
|
||||||
|
)
|
||||||
def get(self, request, username, format=None):
|
def get(self, request, username, format=None):
|
||||||
# Get user details
|
# Get user details
|
||||||
user = self.get_user(username)
|
user = self.get_user(username)
|
||||||
@@ -245,15 +237,28 @@ class UserDetail(APIView):
|
|||||||
serializer = UserDetailSerializer(user, context={"request": request})
|
serializer = UserDetailSerializer(user, context={"request": request})
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
def post(self, request, uid, format=None):
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[
|
||||||
|
openapi.Parameter(name='username', type=openapi.TYPE_STRING, in_=openapi.IN_PATH, description='username', required=True),
|
||||||
|
],
|
||||||
|
request_body=openapi.Schema(
|
||||||
|
type=openapi.TYPE_OBJECT,
|
||||||
|
properties={
|
||||||
|
'description': openapi.Schema(type=openapi.TYPE_STRING, description='description'),
|
||||||
|
'name': openapi.Schema(type=openapi.TYPE_STRING, description='name'),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
tags=['Users'],
|
||||||
|
operation_summary='Edit user details',
|
||||||
|
operation_description='Post user details - authenticated view',
|
||||||
|
)
|
||||||
|
def post(self, request, username, format=None):
|
||||||
# USER
|
# USER
|
||||||
user = self.get_user(uid)
|
user = self.get_user(username)
|
||||||
if isinstance(user, Response):
|
if isinstance(user, Response):
|
||||||
return user
|
return user
|
||||||
|
|
||||||
serializer = UserDetailSerializer(
|
serializer = UserDetailSerializer(user, data=request.data, context={"request": request})
|
||||||
user, data=request.data, context={"request": request}
|
|
||||||
)
|
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
logo = request.data.get("logo")
|
logo = request.data.get("logo")
|
||||||
if logo:
|
if logo:
|
||||||
@@ -264,6 +269,12 @@ class UserDetail(APIView):
|
|||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[],
|
||||||
|
tags=['Users'],
|
||||||
|
operation_summary='Xto_be_written',
|
||||||
|
operation_description='to_be_written',
|
||||||
|
)
|
||||||
def put(self, request, uid, format=None):
|
def put(self, request, uid, format=None):
|
||||||
# ADMIN
|
# ADMIN
|
||||||
user = self.get_user(uid)
|
user = self.get_user(uid)
|
||||||
@@ -271,9 +282,7 @@ class UserDetail(APIView):
|
|||||||
return user
|
return user
|
||||||
|
|
||||||
if not request.user.is_superuser:
|
if not request.user.is_superuser:
|
||||||
return Response(
|
return Response({"detail": "not allowed"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
{"detail": "not allowed"}, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
action = request.data.get("action")
|
action = request.data.get("action")
|
||||||
if action == "feature":
|
if action == "feature":
|
||||||
@@ -286,6 +295,12 @@ class UserDetail(APIView):
|
|||||||
serializer = UserDetailSerializer(user, context={"request": request})
|
serializer = UserDetailSerializer(user, context={"request": request})
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[],
|
||||||
|
tags=['Users'],
|
||||||
|
operation_summary='to_be_written',
|
||||||
|
operation_description='to_be_written',
|
||||||
|
)
|
||||||
def delete(self, request, username, format=None):
|
def delete(self, request, username, format=None):
|
||||||
# Delete a user
|
# Delete a user
|
||||||
user = self.get_user(username)
|
user = self.get_user(username)
|
||||||
|
|||||||
Reference in New Issue
Block a user