Compare commits

...

11 Commits

Author SHA1 Message Date
Markos Gogoulos
94c646fdb8 update metadata only, on API call 2024-09-20 19:26:13 +03:00
Markos Gogoulos
d665058b80 speed up docker start 2024-09-20 13:02:00 +03:00
Markos Gogoulos
986c7d1074 add validation to files uploading to avoid client side pushing arbitrary data (#1057) 2024-09-20 13:01:33 +03:00
thau0x01
1adee8c156 fix #943 (#1052) 2024-09-20 12:53:56 +03:00
makerduck
ffd7a52863 Fix postgres role output (#1029) 2024-09-20 12:52:50 +03:00
Kyle Maas
c5047d8df8 Fix null bug in More Options button (#913) 2023-11-14 09:24:05 +02:00
Markos Gogoulos
dcbfaca91c Developer Experience (#911)
local dev environment
2023-11-13 11:13:08 +02:00
Kyle Maas
918df010f5 Fix bug that crashes page if an encoding has a null URL (#912) 2023-11-13 11:09:16 +02:00
Markos Gogoulos
e9739bab45 Feat celery run (#860)
* avoid calling post save signals
* remove stale celery ids that prevent new services from starting
2023-11-10 16:06:17 +02:00
Kyle Maas
e7ce9ef5c0 Add admin action to generate missing encodings for a particular Media (#883)
* Add admin action to generate missing encodings for a particular Media
* Only regenerate the encodings that are missing
2023-11-10 15:41:20 +02:00
Kyle Maas
4829adf110 Add useful fields to the Encodings admin screen (#885) 2023-11-10 15:37:40 +02:00
23 changed files with 235 additions and 36 deletions

View File

@@ -26,7 +26,7 @@ jobs:
shell: bash
- name: Run Django Tests
run: docker-compose -f docker-compose-dev.yaml exec --env TESTING=True -T web pytest
run: docker-compose -f docker-compose-dev.yaml exec --env TESTING=True -T web pytest
# Run with coverage, saves report on htmlcov dir
# run: docker-compose -f docker-compose-dev.yaml exec --env TESTING=True -T web pytest --cov --cov-report=html --cov-config=.coveragerc

View File

@@ -1,5 +1 @@
Yiannis Stergiou - ys.stergiou@gmail.com
Markos Gogoulos - mgogoulos@gmail.com
Swift Ugandan - swiftugandan@gmail.com
Please see https://github.com/mediacms-io/mediacms/graphs/contributors for complete list of contributors to this repository!

View File

@@ -11,6 +11,7 @@ RUN mkdir -p /home/mediacms.io/mediacms/{logs} && cd /home/mediacms.io && python
# Install dependencies:
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . /home/mediacms.io/mediacms

View File

@@ -10,10 +10,12 @@ ENV PIP_NO_CACHE_DIR=1
RUN mkdir -p /home/mediacms.io/mediacms/{logs} && cd /home/mediacms.io && python3 -m venv $VIRTUAL_ENV
# Install dependencies:
COPY requirements.txt .
COPY requirements.txt .
COPY requirements-dev.txt .
RUN pip install -r requirements-dev.txt
RUN pip install -r requirements.txt
RUN pip install -r requirements-dev.txt
COPY . /home/mediacms.io/mediacms
WORKDIR /home/mediacms.io/mediacms

View File

@@ -73,7 +73,7 @@ We provide custom installations, development of extra functionality, migration f
## Hardware dependencies
## Hardware considerations
For a small to medium installation, with a few hours of video uploaded daily, and a few hundreds of active daily users viewing content, 4GB Ram / 2-4 CPUs as minimum is ok. For a larger installation with many hours of video uploaded daily, consider adding more CPUs and more Ram.
@@ -99,6 +99,10 @@ There are two ways to run MediaCMS, through Docker Compose and through installin
Visit [Configuration](docs/admins_docs.md#5-configuration) page.
## Information for developers
Check out the new section on the [Developer Experience](docs/dev_exp.md) page
## Documentation
* [Users documentation](docs/user_docs.md) page
@@ -115,7 +119,7 @@ This software uses the following list of awesome technologies: Python, Django, D
- **Cinemata** non-profit media, technology and culture organization - https://cinemata.org
- **Critical Commons** public media archive and fair use advocacy network - https://criticalcommons.org
- **Heritales** International Heritage Film Festival - https://stage.heritales.org
- **American Association of Gynecologic Laparoscopists** - https://surgeryu.aagl.org/
## How to contribute
@@ -125,7 +129,8 @@ If you like the project, here's a few things you can do
- Suggest us to others that are interested to hire us
- Write a blog post/article about MediaCMS
- Share on social media about the project
- Open issues, participate on discussions, report bugs, suggest ideas
- Open issues, participate on [discussions](https://github.com/mediacms-io/mediacms/discussions), report bugs, suggest ideas
- [Show and tell](https://github.com/mediacms-io/mediacms/discussions/categories/show-and-tell) how you are using the project
- Star the project
- Add functionality, work on a PR, fix an issue!

48
cms/dev_settings.py Normal file
View File

@@ -0,0 +1,48 @@
# Development settings, used in docker-compose-dev.yaml
import os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'allauth',
'allauth.account',
'allauth.socialaccount',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'rest_framework',
'rest_framework.authtoken',
'imagekit',
'files.apps.FilesConfig',
'users.apps.UsersConfig',
'actions.apps.ActionsConfig',
'debug_toolbar',
'mptt',
'crispy_forms',
'uploader.apps.UploaderConfig',
'djcelery_email',
'ckeditor',
'drf_yasg',
'corsheaders',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'debug_toolbar.middleware.DebugToolbarMiddleware',
]
DEBUG = True
CORS_ORIGIN_ALLOW_ALL = True
STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static/'),)
STATIC_ROOT = None

View File

@@ -493,3 +493,13 @@ if GLOBAL_LOGIN_REQUIRED:
DO_NOT_TRANSCODE_VIDEO = False
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
# the following is related to local development using docker
# and docker-compose-dev.yaml
try:
DEVELOPMENT_MODE = os.environ.get("DEVELOPMENT_MODE")
if DEVELOPMENT_MODE:
# keep a dev_settings.py file for local overrides
from .dev_settings import * # noqa
except ImportError:
pass

View File

@@ -28,7 +28,8 @@ else
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 {} +
# Also ignoring .git folder to fix this issue https://github.com/mediacms-io/mediacms/issues/934
find /home/mediacms.io/mediacms ! \( -path "*.git*" \) -exec chown www-data:$TARGET_GID {} +
chmod +x /home/mediacms.io/mediacms/deploy/docker/start.sh /home/mediacms.io/mediacms/deploy/docker/prestart.sh

View File

@@ -7,7 +7,7 @@ if [ X"$ENABLE_MIGRATIONS" = X"yes" ]; then
echo "Running migrations service"
python manage.py migrate
EXISTING_INSTALLATION=`echo "from users.models import User; print(User.objects.exists())" |python manage.py shell`
if [ "$EXISTING_INSTALLATION" = "True" ]; then
if [ "$EXISTING_INSTALLATION" = "True" ]; then
echo "Loaddata has already run"
else
echo "Running loaddata and creating admin user"
@@ -67,4 +67,5 @@ fi
if [ X"$ENABLE_CELERY_LONG" = X"yes" ] ; then
echo "Enabling celery-long task worker"
cp deploy/docker/supervisord/supervisord-celery_long.conf /etc/supervisor/conf.d/supervisord-celery_long.conf
rm /var/run/mediacms/* -f # remove any stale id, so that on forced restarts of celery workers there are no stale processes that prevent new ones
fi

View File

@@ -1,6 +1,22 @@
version: "3"
services:
migrations:
build:
context: .
dockerfile: ./Dockerfile-dev
image: mediacms/mediacms-dev:latest
volumes:
- ./:/home/mediacms.io/mediacms/
command: "python manage.py migrate"
environment:
DEVELOPMENT_MODE: "True"
restart: on-failure
depends_on:
redis:
condition: service_healthy
db:
condition: service_healthy
frontend:
image: node:14
volumes:
@@ -18,7 +34,9 @@ services:
context: .
dockerfile: ./Dockerfile-dev
image: mediacms/mediacms-dev:latest
command: "python manage.py runserver 0.0.0.0:80"
environment:
DEVELOPMENT_MODE: "True"
ADMIN_USER: 'admin'
ADMIN_PASSWORD: 'admin'
ADMIN_EMAIL: 'admin@localhost'
@@ -27,10 +45,7 @@ services:
volumes:
- ./:/home/mediacms.io/mediacms/
depends_on:
redis:
condition: service_healthy
db:
condition: service_healthy
- migrations
db:
image: postgres:15.2-alpine
volumes:
@@ -42,7 +57,7 @@ services:
POSTGRES_DB: mediacms
TZ: Europe/London
healthcheck:
test: ["CMD-SHELL", "pg_isready", "--host=db", "--dbname=$POSTGRES_DB", "--username=$POSTGRES_USER"]
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}", "--host=db", "--dbname=$POSTGRES_DB", "--username=$POSTGRES_USER"]
interval: 10s
timeout: 5s
retries: 5
@@ -54,3 +69,16 @@ services:
interval: 30s
timeout: 10s
retries: 3
celery_worker:
image: mediacms/mediacms-dev:latest
deploy:
replicas: 1
volumes:
- ./:/home/mediacms.io/mediacms/
environment:
ENABLE_UWSGI: 'no'
ENABLE_NGINX: 'no'
ENABLE_CELERY_BEAT: 'no'
ENABLE_MIGRATIONS: 'no'
depends_on:
- web

View File

@@ -78,7 +78,7 @@ services:
POSTGRES_DB: mediacms
TZ: Europe/London
healthcheck:
test: ["CMD-SHELL", "pg_isready", "--host=db", "--dbname=$POSTGRES_DB", "--username=$POSTGRES_USER"]
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}", "--host=db", "--dbname=$POSTGRES_DB", "--username=$POSTGRES_USER"]
interval: 10s
timeout: 5s
retries: 5

View File

@@ -80,7 +80,7 @@ services:
POSTGRES_DB: mediacms
TZ: Europe/London
healthcheck:
test: ["CMD-SHELL", "pg_isready", "--host=db", "--dbname=$POSTGRES_DB", "--username=$POSTGRES_USER"]
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}", "--host=db", "--dbname=$POSTGRES_DB", "--username=$POSTGRES_USER"]
interval: 10s
timeout: 5s
retries: 5

View File

@@ -100,7 +100,7 @@ services:
POSTGRES_DB: mediacms
TZ: Europe/London
healthcheck:
test: ["CMD-SHELL", "pg_isready", "--host=db", "--dbname=$POSTGRES_DB", "--username=$POSTGRES_USER"]
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}", "--host=db", "--dbname=$POSTGRES_DB", "--username=$POSTGRES_USER"]
interval: 30s
timeout: 10s
retries: 5

View File

@@ -76,7 +76,7 @@ services:
POSTGRES_DB: mediacms
TZ: Europe/London
healthcheck:
test: ["CMD-SHELL", "pg_isready", "--host=db", "--dbname=$POSTGRES_DB", "--username=$POSTGRES_USER"]
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}", "--host=db", "--dbname=$POSTGRES_DB", "--username=$POSTGRES_USER"]
interval: 30s
timeout: 10s
retries: 5

View File

@@ -72,7 +72,7 @@ services:
POSTGRES_DB: mediacms
TZ: Europe/London
healthcheck:
test: ["CMD-SHELL", "pg_isready", "--host=db", "--dbname=$POSTGRES_DB", "--username=$POSTGRES_USER"]
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}", "--host=db", "--dbname=$POSTGRES_DB", "--username=$POSTGRES_USER"]
interval: 10s
timeout: 5s
retries: 5

60
docs/dev_exp.md Normal file
View File

@@ -0,0 +1,60 @@
# Developer Experience
There is ongoing effort to provide a better developer experience and document it.
## How to develop locally with Docker
First install a recent version of [Docker](https://docs.docker.com/get-docker/), and [Docker Compose](https://docs.docker.com/compose/install/).
Then run `docker-compose -f docker-compose-dev.yaml up`
```
user@user:~/mediacms$ docker-compose -f docker-compose-dev.yaml up
```
In a few minutes the app will be available at http://localhost . Login via admin/admin
### What does docker-compose-dev.yaml do?
It build the two images used for backend and frontend.
* Backend: `mediacms/mediacms-dev:latest`
* Frontend: `frontend`
and will start all services required for MediaCMS, as Celery/Redis for asynchronous tasks, PostgreSQL database, Django and React
For Django, the changes from the image produced by docker-compose.yaml are these:
* Django runs in debug mode, with `python manage.py runserver`
* uwsgi and nginx are not run
* Django runs in Debug mode, with Debug Toolbar
* Static files (js/css) are loaded from static/ folder
* corsheaders is installed and configured to allow all origins
For React, it will run `npm start` in the frontend folder, which will start the development server.
Check it on http://localhost:8088/
### How to develop in Django
Django starts at http://localhost and is reloading automatically. Making any change to the python code should refresh Django.
### How to develop in React
React is started on http://localhost:8088/ , code is located in frontend/ , so making changes there should have instant effect on the page. Keep in mind that React is loading data from Django, and that it has to be built so that Django can serve it.
### Making changes to the frontend
The way React is added is more complicated than the usual SPA project and this is because React is used as a library loaded by Django Templates, so it is not a standalone project and is not handling routes etc.
The two directories to consider are:
* frontend/src , for the React files
* templates/, for the Django templates.
Django is using a highly intuitive hierarchical templating system (https://docs.djangoproject.com/en/4.2/ref/templates/), where the base template is templates/root.html and all other templates are extending it.
React is called through the Django templates, eg templates/cms/media.html is loading js/media.js
In order to make changes to React code, edit code on frontend/src and check it's effect on http://localhost:8088/ . Once ready, build it and copy it to the Django static folder, so that it is served by Django.
### Development workflow with the frontend
1. Edit frontend/src/ files
2. Check changes on http://localhost:8088/
3. Build frontend with `docker-compose -f docker-compose-dev.yaml exec frontend npm run dist`
4. Copy static files to Django static folder with`cp -r frontend/dist/static/* static/`
5. Restart Django - `docker-compose -f docker-compose-dev.yaml restart web` so that it uses the new static files
6. Commit the changes

View File

@@ -40,6 +40,12 @@ class MediaAdmin(admin.ModelAdmin):
def get_comments_count(self, obj):
return obj.comments.count()
@admin.action(description="Generate missing encoding(s)", permissions=["change"])
def generate_missing_encodings(modeladmin, request, queryset):
for m in queryset:
m.encode(force=False)
actions = [generate_missing_encodings]
get_comments_count.short_description = "Comments count"
@@ -74,7 +80,18 @@ class SubtitleAdmin(admin.ModelAdmin):
class EncodingAdmin(admin.ModelAdmin):
pass
list_display = ["get_title", "chunk", "profile", "progress", "status", "has_file"]
list_filter = ["chunk", "profile", "status"]
def get_title(self, obj):
return str(obj)
get_title.short_description = "Encoding"
def has_file(self, obj):
return obj.media_encoding_url is not None
has_file.short_description = "Has file"
admin.site.register(EncodeProfile, EncodeProfileAdmin)

View File

@@ -644,7 +644,11 @@ def save_user_action(user_or_session, friendly_token=None, action="watch", extra
if action == "watch":
media.views += 1
media.save(update_fields=["views"])
Media.objects.filter(friendly_token=friendly_token).update(views=media.views)
# update field without calling save, to avoid post_save signals being triggered
# same in other actions
elif action == "report":
media.reported_times += 1
@@ -659,10 +663,10 @@ def save_user_action(user_or_session, friendly_token=None, action="watch", extra
)
elif action == "like":
media.likes += 1
media.save(update_fields=["likes"])
Media.objects.filter(friendly_token=friendly_token).update(likes=media.likes)
elif action == "dislike":
media.dislikes += 1
media.save(update_fields=["dislikes"])
Media.objects.filter(friendly_token=friendly_token).update(dislikes=media.dislikes)
return True

View File

@@ -598,14 +598,15 @@ class MediaDetail(APIView):
media = self.get_object(friendly_token)
if isinstance(media, Response):
return media
serializer = MediaSerializer(media, data=request.data, context={"request": request})
if serializer.is_valid():
if request.data.get('media_file'):
media_file = request.data["media_file"]
serializer.save(user=request.user, media_file=media_file)
else:
serializer.save(user=request.user)
serializer.save(user=request.user)
# no need to update the media file itself, only the metadata
#if request.data.get('media_file'):
# media_file = request.data["media_file"]
# serializer.save(user=request.user, media_file=media_file)
#else:
# serializer.save(user=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

View File

@@ -22,7 +22,7 @@ function downloadOptions(mediaData, allowDownload) {
if (Object.keys(encodingsInfo[k]).length) {
for (g in encodingsInfo[k]) {
if (encodingsInfo[k].hasOwnProperty(g)) {
if ('success' === encodingsInfo[k][g].status && 100 === encodingsInfo[k][g].progress) {
if ('success' === encodingsInfo[k][g].status && 100 === encodingsInfo[k][g].progress && null !== encodingsInfo[k][g].url) {
options[encodingsInfo[k][g].title] = {
text: k + ' - ' + g.toUpperCase() + ' (' + encodingsInfo[k][g].size + ')',
link: formatInnerLink(encodingsInfo[k][g].url, site.url),

View File

@@ -19,7 +19,7 @@ function downloadOptionsList() {
if (Object.keys(encodings_info[k]).length) {
for (g in encodings_info[k]) {
if (encodings_info[k].hasOwnProperty(g)) {
if ('success' === encodings_info[k][g].status && 100 === encodings_info[k][g].progress) {
if ('success' === encodings_info[k][g].status && 100 === encodings_info[k][g].progress && null !== encodings_info[k][g].url) {
optionsList[encodings_info[k][g].title] = {
text: k + ' - ' + g.toUpperCase() + ' (' + encodings_info[k][g].size + ')',
link: formatInnerLink(encodings_info[k][g].url, SiteContext._currentValue.url),

View File

@@ -12,3 +12,5 @@ pytest-cov
pytest-django
pytest-factoryboy
Faker
django-cors-headers

View File

@@ -1,4 +1,7 @@
import re
import shutil
import os
import uuid
from io import StringIO
from os.path import join
@@ -7,16 +10,31 @@ from django.conf import settings
from . import utils
def is_valid_uuid_format(uuid_string):
pattern = re.compile(r'^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$', re.IGNORECASE)
return bool(pattern.match(uuid_string))
class BaseFineUploader(object):
def __init__(self, data, *args, **kwargs):
self.data = data
self.total_filesize = data.get("qqtotalfilesize")
self.filename = data.get("qqfilename")
self.uuid = data.get("qquuid")
if not is_valid_uuid_format(self.uuid):
# something nasty client side could be happening here
# generate new uuid to ensure this is uuid
# not sure if this will work with the chunked uploads though
self.uuid = uuid.uuid4()
self.filename = os.path.basename(self.filename)
# avoid possibility of passing a fake path here
self.file = data.get("qqfile")
self.storage_class = settings.FILE_STORAGE
self.real_path = None
@property
def finished(self):
return self.real_path is not None
@@ -50,7 +68,11 @@ class ChunkedFineUploader(BaseFineUploader):
self.total_parts = data.get("qqtotalparts")
if not isinstance(self.total_parts, int):
self.total_parts = 1
self.part_index = data.get("qqpartindex")
qqpartindex = data.get("qqpartindex")
if not isinstance(qqpartindex, int):
# something nasty client side could be happening here
qqpartindex = 0
self.part_index = qqpartindex
@property
def chunks_path(self):
@@ -75,6 +97,7 @@ class ChunkedFineUploader(BaseFineUploader):
def combine_chunks(self):
# implement the same behaviour.
self.real_path = self.storage.save(self._full_file_path, StringIO())
with self.storage.open(self.real_path, "wb") as final_file:
for i in range(self.total_parts):
part = join(self.chunks_path, str(i))