mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-08-19 17:15:25 +02:00
feat: bulk actions API
This commit is contained in:
2
Makefile
2
Makefile
@@ -1,7 +1,7 @@
|
||||
.PHONY: admin-shell build-frontend
|
||||
|
||||
admin-shell:
|
||||
@container_id=$$(docker-compose ps -q web); \
|
||||
@container_id=$$(docker compose ps -q web); \
|
||||
if [ -z "$$container_id" ]; then \
|
||||
echo "Web container not found"; \
|
||||
exit 1; \
|
||||
|
@@ -101,6 +101,7 @@ There are two ways to run MediaCMS, through Docker Compose and through installin
|
||||
* [Configuration](docs/admins_docs.md#5-configuration) page
|
||||
* [Transcoding](docs/transcoding.md) page
|
||||
* [Developer Experience](docs/dev_exp.md) page
|
||||
* [Media Permissions](docs/media_permissions.md) page
|
||||
|
||||
|
||||
## Technology
|
||||
|
@@ -498,6 +498,9 @@ ALLOW_VIDEO_TRIMMER = True
|
||||
|
||||
ALLOW_CUSTOM_MEDIA_URLS = False
|
||||
|
||||
# Whether to allow anonymous users to list all users
|
||||
ALLOW_ANONYMOUS_USER_LISTING = True
|
||||
|
||||
# ffmpeg options
|
||||
FFMPEG_DEFAULT_PRESET = "medium" # see https://trac.ffmpeg.org/wiki/Encode/H.264
|
||||
|
||||
|
@@ -1 +1 @@
|
||||
VERSION = "6.3.0"
|
||||
VERSION = "6.4.0"
|
||||
|
@@ -168,8 +168,6 @@ By default, all these services are enabled, but in order to create a scaleable d
|
||||
|
||||
Also see the `Dockerfile` for other environment variables which you may wish to override. Application settings, eg. `FRONTEND_HOST` can also be overridden by updating the `deploy/docker/local_settings.py` file.
|
||||
|
||||
See example deployments in the sections below. These example deployments have been tested on `docker-compose version 1.27.4` running on `Docker version 19.03.13`
|
||||
|
||||
To run, update the configs above if necessary, build the image by running `docker compose build`, then run `docker compose run`
|
||||
|
||||
### Simple Deployment, accessed as http://localhost
|
||||
@@ -502,6 +500,16 @@ By default `CAN_COMMENT = "all"` means that all registered users can add comment
|
||||
|
||||
- **advancedUser**, only users that are marked as advanced users can add comment. Admins or MediaCMS managers can make users advanced users by editing their profile and selecting advancedUser.
|
||||
|
||||
### 5.26 Control whether anonymous users can list all users
|
||||
|
||||
By default, anonymous users can view the list of all users on the platform. To restrict this to authenticated users only, set:
|
||||
|
||||
```
|
||||
ALLOW_ANONYMOUS_USER_LISTING = False
|
||||
```
|
||||
|
||||
When set to False, only logged-in users will be able to access the user listing API endpoint.
|
||||
|
||||
|
||||
## 6. Manage pages
|
||||
to be written
|
||||
|
@@ -4,10 +4,10 @@ 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`
|
||||
Then run `docker compose -f docker-compose-dev.yaml up`
|
||||
|
||||
```
|
||||
user@user:~/mediacms$ 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
|
||||
@@ -37,7 +37,7 @@ Django starts at http://localhost and is reloading automatically. Making any cha
|
||||
If Django breaks due to an error (eg SyntaxError, while editing the code), you might have to restart it
|
||||
|
||||
```
|
||||
docker-compose -f docker-compose-dev.yaml restart web
|
||||
docker compose -f docker-compose-dev.yaml restart web
|
||||
```
|
||||
|
||||
|
||||
@@ -62,9 +62,9 @@ In order to make changes to React code, edit code on frontend/src and check it's
|
||||
### 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`
|
||||
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
|
||||
5. Restart Django - `docker compose -f docker-compose-dev.yaml restart web` so that it uses the new static files
|
||||
6. Commit the changes
|
||||
|
||||
### Helper commands
|
||||
@@ -81,7 +81,7 @@ Build the frontend:
|
||||
|
||||
```
|
||||
user@user:~/mediacms$ make build-frontend
|
||||
docker-compose -f docker-compose-dev.yaml exec frontend npm run dist
|
||||
docker compose -f docker-compose-dev.yaml exec frontend npm run dist
|
||||
|
||||
> mediacms-frontend@0.9.1 dist /home/mediacms.io/mediacms/frontend
|
||||
> mediacms-scripts rimraf ./dist && mediacms-scripts build --config=./config/mediacms.config.js --env=dist
|
||||
|
@@ -50,8 +50,8 @@ Checkout the [Code of conduct page](../CODE_OF_CONDUCT.md) if you want to contri
|
||||
To perform the Docker installation, follow instructions to install Docker + Docker compose (docs/Docker_Compose.md) and then build/start docker-compose-dev.yaml . This will run the frontend application on port 8088 on top of all other containers (including the Django web application on port 80)
|
||||
|
||||
```
|
||||
docker-compose -f docker-compose-dev.yaml build
|
||||
docker-compose -f docker-compose-dev.yaml up
|
||||
docker compose -f docker-compose-dev.yaml build
|
||||
docker compose -f docker-compose-dev.yaml up
|
||||
```
|
||||
|
||||
An `admin` user is created during the installation process. Its attributes are defined in `docker-compose-dev.yaml`:
|
||||
@@ -65,7 +65,7 @@ ADMIN_EMAIL: 'admin@localhost'
|
||||
Eg change `frontend/src/static/js/pages/HomePage.tsx` , dev application refreshes in a number of seconds (hot reloading) and I see the changes, once I'm happy I can run
|
||||
|
||||
```
|
||||
docker-compose -f docker-compose-dev.yaml exec -T frontend npm run dist
|
||||
docker compose -f docker-compose-dev.yaml exec -T frontend npm run dist
|
||||
```
|
||||
|
||||
And then in order for the changes to be visible on the application while served through nginx,
|
||||
@@ -90,7 +90,7 @@ http://localhost:8088/manage-media.html manage_media
|
||||
After I make changes to the django application (eg make a change on `files/forms.py`) in order to see the changes I have to restart the web container
|
||||
|
||||
```
|
||||
docker-compose -f docker-compose-dev.yaml restart web
|
||||
docker compose -f docker-compose-dev.yaml restart web
|
||||
```
|
||||
|
||||
## How video is transcoded
|
||||
@@ -122,19 +122,19 @@ This instructions assume that you're using the docker installation
|
||||
1. start docker-compose
|
||||
|
||||
```
|
||||
docker-compose up
|
||||
docker compose up
|
||||
```
|
||||
|
||||
2. Install the requirements on `requirements-dev.txt ` on web container (we'll use the web container for this)
|
||||
|
||||
```
|
||||
docker-compose exec -T web pip install -r requirements-dev.txt
|
||||
docker compose exec -T web pip install -r requirements-dev.txt
|
||||
```
|
||||
|
||||
3. Now you can run the existing tests
|
||||
|
||||
```
|
||||
docker-compose exec --env TESTING=True -T web pytest
|
||||
docker compose exec --env TESTING=True -T web pytest
|
||||
```
|
||||
|
||||
The `TESTING=True` is passed for Django to be aware this is a testing environment (so that it runs Celery tasks as functions for example and not as background tasks, since Celery is not started in the case of pytest)
|
||||
@@ -143,13 +143,13 @@ The `TESTING=True` is passed for Django to be aware this is a testing environmen
|
||||
4. You may try a single test, by specifying the path, for example
|
||||
|
||||
```
|
||||
docker-compose exec --env TESTING=True -T web pytest tests/test_fixtures.py
|
||||
docker compose exec --env TESTING=True -T web pytest tests/test_fixtures.py
|
||||
```
|
||||
|
||||
5. You can also see the coverage
|
||||
|
||||
```
|
||||
docker-compose exec --env TESTING=True -T web pytest --cov=. --cov-report=html
|
||||
docker compose exec --env TESTING=True -T web pytest --cov=. --cov-report=html
|
||||
```
|
||||
|
||||
and of course...you are very welcome to help us increase it ;)
|
||||
|
166
docs/media_permissions.md
Normal file
166
docs/media_permissions.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# Media Permissions in MediaCMS
|
||||
|
||||
This document explains the permission system in MediaCMS, which controls who can view, edit, and manage media files.
|
||||
|
||||
## Overview
|
||||
|
||||
MediaCMS provides a flexible permission system that allows fine-grained control over media access. The system supports:
|
||||
|
||||
1. **Basic permissions** - Public, private, and unlisted media
|
||||
2. **User-specific permissions** - Direct permissions granted to specific users
|
||||
3. **Role-Based Access Control (RBAC)** - Category-based permissions through group membership
|
||||
|
||||
## Media States
|
||||
|
||||
Every media file has a state that determines its basic visibility:
|
||||
|
||||
- **Public** - Visible to everyone
|
||||
- **Private** - Only visible to the owner and users with explicit permissions
|
||||
- **Unlisted** - Not listed in public listings but accessible via direct link
|
||||
|
||||
|
||||
## User Roles
|
||||
|
||||
MediaCMS has several user roles that affect permissions:
|
||||
|
||||
- **Regular User** - Can upload and manage their own media
|
||||
- **Advanced User** - Additional capabilities (configurable)
|
||||
- **MediaCMS Editor** - Can edit and review content across the platform
|
||||
- **MediaCMS Manager** - Full management capabilities
|
||||
- **Admin** - Complete system access
|
||||
|
||||
## Direct Media Permissions
|
||||
|
||||
The `MediaPermission` model allows granting specific permissions to individual users:
|
||||
|
||||
### Permission Levels
|
||||
|
||||
- **Viewer** - Can view the media even if it's private
|
||||
- **Editor** - Can view and edit the media's metadata
|
||||
- **Owner** - Full control, including deletion
|
||||
|
||||
## Role-Based Access Control (RBAC)
|
||||
|
||||
When RBAC is enabled (`USE_RBAC` setting), permissions can be managed through categories and groups:
|
||||
|
||||
1. Categories can be marked as RBAC-controlled
|
||||
2. Users are assigned to RBAC groups with specific roles
|
||||
3. RBAC groups are associated with categories
|
||||
4. Users inherit permissions to media in those categories based on their role
|
||||
|
||||
### RBAC Roles
|
||||
|
||||
- **Member** - Can view media in the category
|
||||
- **Contributor** - Can view and edit media in the category
|
||||
- **Manager** - Full control over media in the category
|
||||
|
||||
## Permission Checking Methods
|
||||
|
||||
The User model provides several methods to check permissions:
|
||||
|
||||
```python
|
||||
# From users/models.py
|
||||
def has_member_access_to_media(self, media):
|
||||
# Check if user can view the media
|
||||
# ...
|
||||
|
||||
def has_contributor_access_to_media(self, media):
|
||||
# Check if user can edit the media
|
||||
# ...
|
||||
|
||||
def has_owner_access_to_media(self, media):
|
||||
# Check if user has full control over the media
|
||||
# ...
|
||||
```
|
||||
|
||||
## How Permissions Are Applied
|
||||
|
||||
When a user attempts to access media, the system checks permissions in this order:
|
||||
|
||||
1. Is the media public? If yes, allow access.
|
||||
2. Is the user the owner of the media? If yes, allow full access.
|
||||
3. Does the user have direct permissions through MediaPermission? If yes, grant the corresponding access level.
|
||||
4. If RBAC is enabled, does the user have access through category membership? If yes, grant the corresponding access level.
|
||||
5. If none of the above, deny access.
|
||||
|
||||
## Media Sharing
|
||||
|
||||
Users can share media with others by:
|
||||
|
||||
1. Making it public or unlisted
|
||||
2. Granting direct permissions to specific users
|
||||
3. Adding it to a category that's accessible to an RBAC group
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Media Listing
|
||||
|
||||
When listing media, the system filters based on permissions:
|
||||
|
||||
```python
|
||||
# Simplified example from files/views/media.py
|
||||
def _get_media_queryset(self, request, user=None):
|
||||
# 1. Public media
|
||||
listable_media = Media.objects.filter(listable=True)
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
return listable_media
|
||||
|
||||
# 2. User permissions for authenticated users
|
||||
user_media = Media.objects.filter(permissions__user=request.user)
|
||||
|
||||
# 3. RBAC for authenticated users
|
||||
if getattr(settings, 'USE_RBAC', False):
|
||||
rbac_categories = request.user.get_rbac_categories_as_member()
|
||||
rbac_media = Media.objects.filter(category__in=rbac_categories)
|
||||
|
||||
# Combine all accessible media
|
||||
return listable_media.union(user_media, rbac_media)
|
||||
```
|
||||
|
||||
### Permission Checking
|
||||
|
||||
The system uses helper methods to check permissions:
|
||||
|
||||
```python
|
||||
# From users/models.py
|
||||
def has_member_access_to_media(self, media):
|
||||
# First check if user is the owner
|
||||
if media.user == self:
|
||||
return True
|
||||
|
||||
# Then check RBAC permissions
|
||||
if getattr(settings, 'USE_RBAC', False):
|
||||
rbac_groups = RBACGroup.objects.filter(
|
||||
memberships__user=self,
|
||||
memberships__role__in=["member", "contributor", "manager"],
|
||||
categories__in=media.category.all()
|
||||
).distinct()
|
||||
if rbac_groups.exists():
|
||||
return True
|
||||
|
||||
# Then check MediaShare permissions for any access
|
||||
media_permission_exists = MediaPermission.objects.filter(
|
||||
user=self,
|
||||
media=media,
|
||||
).exists()
|
||||
|
||||
return media_permission_exists
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Default to Private** - Consider setting new uploads to private by default
|
||||
2. **Use Categories** - Organize media into categories for easier permission management
|
||||
3. **RBAC for Teams** - Use RBAC for team collaboration scenarios
|
||||
4. **Direct Permissions for Exceptions** - Use direct permissions for one-off sharing
|
||||
|
||||
## Configuration
|
||||
|
||||
The permission system can be configured through several settings:
|
||||
|
||||
- `USE_RBAC` - Enable/disable Role-Based Access Control
|
||||
|
||||
## Conclusion
|
||||
|
||||
MediaCMS provides a flexible and powerful permission system that can accommodate various use cases, from simple personal media libraries to complex team collaboration scenarios with fine-grained access control.
|
@@ -68,14 +68,18 @@ class MediaMetadataForm(forms.ModelForm):
|
||||
self.helper.form_method = 'post'
|
||||
self.helper.form_enctype = "multipart/form-data"
|
||||
self.helper.form_show_errors = False
|
||||
self.helper.layout = Layout(
|
||||
|
||||
layout_fields = [
|
||||
CustomField('title'),
|
||||
CustomField('new_tags'),
|
||||
CustomField('add_date'),
|
||||
CustomField('description'),
|
||||
CustomField('uploaded_poster'),
|
||||
CustomField('enable_comments'),
|
||||
)
|
||||
]
|
||||
if self.instance.media_type != "image":
|
||||
layout_fields.append(CustomField('uploaded_poster'))
|
||||
|
||||
self.helper.layout = Layout(*layout_fields)
|
||||
|
||||
if self.instance.media_type == "video":
|
||||
self.helper.layout.append(CustomField('thumbnail_time'))
|
||||
|
@@ -567,3 +567,42 @@ def handle_video_chapters(media, chapters):
|
||||
video_chapter = models.VideoChapterData.objects.create(media=media, data=chapters)
|
||||
|
||||
return media.chapter_data
|
||||
|
||||
|
||||
def change_media_owner(media_id, new_user):
|
||||
"""Change the owner of a media
|
||||
|
||||
Args:
|
||||
media_id: ID of the media to change owner
|
||||
new_user: New user object to set as owner
|
||||
|
||||
Returns:
|
||||
Media object or None if media not found
|
||||
"""
|
||||
media = models.Media.objects.filter(id=media_id).first()
|
||||
if not media:
|
||||
return None
|
||||
|
||||
# Change the owner
|
||||
media.user = new_user
|
||||
media.save(update_fields=["user"])
|
||||
|
||||
# Update any related permissions
|
||||
media_permissions = models.MediaPermission.objects.filter(media=media)
|
||||
for permission in media_permissions:
|
||||
permission.owner_user = new_user
|
||||
permission.save(update_fields=["owner_user"])
|
||||
|
||||
return media
|
||||
|
||||
|
||||
def copy_media(media_id):
|
||||
"""Create a copy of a media
|
||||
|
||||
Args:
|
||||
media_id: ID of the media to copy
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
pass
|
||||
|
29
files/migrations/0011_mediapermission.py
Normal file
29
files/migrations/0011_mediapermission.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 5.1.6 on 2025-07-08 19:15
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('files', '0010_alter_encodeprofile_resolution'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='MediaPermission',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('permission', models.CharField(choices=[('viewer', 'Viewer'), ('editor', 'Editor'), ('owner', 'Owner')], max_length=20)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('media', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='permissions', to='files.media')),
|
||||
('owner_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='granted_permissions', to=settings.AUTH_USER_MODEL)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('user', 'media')},
|
||||
},
|
||||
),
|
||||
]
|
25
files/models/__init__.py
Normal file
25
files/models/__init__.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Import all models for backward compatibility
|
||||
from .category import Category, Tag # noqa: F401
|
||||
from .comment import Comment # noqa: F401
|
||||
from .encoding import EncodeProfile, Encoding # noqa: F401
|
||||
from .license import License # noqa: F401
|
||||
from .media import Media, MediaPermission # noqa: F401
|
||||
from .playlist import Playlist, PlaylistMedia # noqa: F401
|
||||
from .rating import Rating, RatingCategory # noqa: F401
|
||||
from .subtitle import Language, Subtitle # noqa: F401
|
||||
from .utils import CODECS # noqa: F401
|
||||
from .utils import ENCODE_EXTENSIONS # noqa: F401
|
||||
from .utils import ENCODE_EXTENSIONS_KEYS # noqa: F401
|
||||
from .utils import ENCODE_RESOLUTIONS # noqa: F401
|
||||
from .utils import ENCODE_RESOLUTIONS_KEYS # noqa: F401
|
||||
from .utils import MEDIA_ENCODING_STATUS # noqa: F401
|
||||
from .utils import MEDIA_STATES # noqa: F401
|
||||
from .utils import MEDIA_TYPES_SUPPORTED # noqa: F401
|
||||
from .utils import category_thumb_path # noqa: F401
|
||||
from .utils import encoding_media_file_path # noqa: F401
|
||||
from .utils import generate_uid # noqa: F401
|
||||
from .utils import original_media_file_path # noqa: F401
|
||||
from .utils import original_thumbnail_file_path # noqa: F401
|
||||
from .utils import subtitles_file_path # noqa: F401
|
||||
from .utils import validate_rating # noqa: F401
|
||||
from .video_data import VideoChapterData, VideoTrimRequest # noqa: F401
|
156
files/models/category.py
Normal file
156
files/models/category.py
Normal file
@@ -0,0 +1,156 @@
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.html import strip_tags
|
||||
from imagekit.models import ProcessedImageField
|
||||
from imagekit.processors import ResizeToFit
|
||||
|
||||
from .. import helpers
|
||||
from .utils import category_thumb_path, generate_uid
|
||||
|
||||
|
||||
class Category(models.Model):
|
||||
"""A Category base model"""
|
||||
|
||||
uid = models.CharField(unique=True, max_length=36, default=generate_uid)
|
||||
|
||||
add_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
title = models.CharField(max_length=100, db_index=True)
|
||||
|
||||
description = models.TextField(blank=True)
|
||||
|
||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE, blank=True, null=True)
|
||||
|
||||
is_global = models.BooleanField(default=False, help_text="global categories or user specific")
|
||||
|
||||
media_count = models.IntegerField(default=0, help_text="number of media")
|
||||
|
||||
thumbnail = ProcessedImageField(
|
||||
upload_to=category_thumb_path,
|
||||
processors=[ResizeToFit(width=344, height=None)],
|
||||
format="JPEG",
|
||||
options={"quality": 85},
|
||||
blank=True,
|
||||
)
|
||||
|
||||
listings_thumbnail = models.CharField(max_length=400, blank=True, null=True, help_text="Thumbnail to show on listings")
|
||||
|
||||
is_rbac_category = models.BooleanField(default=False, db_index=True, help_text='If access to Category is controlled by role based membership of Groups')
|
||||
|
||||
identity_provider = models.ForeignKey(
|
||||
'socialaccount.SocialApp',
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='categories',
|
||||
help_text='If category is related with a specific Identity Provider',
|
||||
verbose_name='IDP Config Name',
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
class Meta:
|
||||
ordering = ["title"]
|
||||
verbose_name_plural = "Categories"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return f"{reverse('search')}?c={self.title}"
|
||||
|
||||
def update_category_media(self):
|
||||
"""Set media_count"""
|
||||
|
||||
# Always set number of Category the total number of media
|
||||
# Depending on how RBAC is set and Permissions etc it is
|
||||
# possible that users won't see all media in a Category
|
||||
# but it's worth to handle this on the UI level
|
||||
# (eg through a message that says that you see only files you have permissions to see)
|
||||
|
||||
self.media_count = Media.objects.filter(category=self).count()
|
||||
self.save(update_fields=["media_count"])
|
||||
|
||||
# OLD logic
|
||||
# if getattr(settings, 'USE_RBAC', False) and self.is_rbac_category:
|
||||
# self.media_count = Media.objects.filter(category=self).count()
|
||||
# else:
|
||||
# self.media_count = Media.objects.filter(listable=True, category=self).count()
|
||||
|
||||
self.save(update_fields=["media_count"])
|
||||
return True
|
||||
|
||||
@property
|
||||
def thumbnail_url(self):
|
||||
"""Return thumbnail for category
|
||||
prioritize processed value of listings_thumbnail
|
||||
then thumbnail
|
||||
"""
|
||||
|
||||
if self.thumbnail:
|
||||
return helpers.url_from_path(self.thumbnail.path)
|
||||
|
||||
if self.listings_thumbnail:
|
||||
return self.listings_thumbnail
|
||||
|
||||
if Media.objects.filter(category=self, state="public").exists():
|
||||
media = Media.objects.filter(category=self, state="public").order_by("-views").first()
|
||||
if media:
|
||||
return media.thumbnail_url
|
||||
|
||||
return None
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
strip_text_items = ["title", "description"]
|
||||
for item in strip_text_items:
|
||||
setattr(self, item, strip_tags(getattr(self, item, None)))
|
||||
super(Category, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class Tag(models.Model):
|
||||
"""A Tag model"""
|
||||
|
||||
title = models.CharField(max_length=100, unique=True, db_index=True)
|
||||
|
||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE, blank=True, null=True)
|
||||
|
||||
media_count = models.IntegerField(default=0, help_text="number of media")
|
||||
|
||||
listings_thumbnail = models.CharField(
|
||||
max_length=400,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text="Thumbnail to show on listings",
|
||||
db_index=True,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
class Meta:
|
||||
ordering = ["title"]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return f"{reverse('search')}?t={self.title}"
|
||||
|
||||
def update_tag_media(self):
|
||||
self.media_count = Media.objects.filter(state="public", is_reviewed=True, tags=self).count()
|
||||
self.save(update_fields=["media_count"])
|
||||
return True
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.title = helpers.get_alphanumeric_only(self.title)
|
||||
self.title = self.title[:99]
|
||||
super(Tag, self).save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def thumbnail_url(self):
|
||||
if self.listings_thumbnail:
|
||||
return self.listings_thumbnail
|
||||
media = Media.objects.filter(tags=self, state="public").order_by("-views").first()
|
||||
if media:
|
||||
return media.thumbnail_url
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# Import Media to avoid circular imports
|
||||
from .media import Media # noqa
|
46
files/models/comment.py
Normal file
46
files/models/comment.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.html import strip_tags
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
|
||||
|
||||
class Comment(MPTTModel):
|
||||
"""Comments model"""
|
||||
|
||||
add_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
media = models.ForeignKey("Media", on_delete=models.CASCADE, db_index=True, related_name="comments")
|
||||
|
||||
parent = TreeForeignKey("self", on_delete=models.CASCADE, null=True, blank=True, related_name="children")
|
||||
|
||||
text = models.TextField(help_text="text")
|
||||
|
||||
uid = models.UUIDField(unique=True, default=uuid.uuid4)
|
||||
|
||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE, db_index=True)
|
||||
|
||||
class MPTTMeta:
|
||||
order_insertion_by = ["add_date"]
|
||||
|
||||
def __str__(self):
|
||||
return f"On {self.media.title} by {self.user.username}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
strip_text_items = ["text"]
|
||||
for item in strip_text_items:
|
||||
setattr(self, item, strip_tags(getattr(self, item, None)))
|
||||
|
||||
if self.text:
|
||||
self.text = self.text[: settings.MAX_CHARS_FOR_COMMENT]
|
||||
|
||||
super(Comment, self).save(*args, **kwargs)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return f"{reverse('get_media')}?m={self.media.friendly_token}"
|
||||
|
||||
@property
|
||||
def media_url(self):
|
||||
return self.get_absolute_url()
|
303
files/models/encoding.py
Normal file
303
files/models/encoding.py
Normal file
@@ -0,0 +1,303 @@
|
||||
import json
|
||||
import tempfile
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files import File
|
||||
from django.db import models
|
||||
from django.db.models.signals import post_delete, post_save
|
||||
from django.dispatch import receiver
|
||||
from django.urls import reverse
|
||||
|
||||
from .. import helpers
|
||||
from .utils import (
|
||||
CODECS,
|
||||
ENCODE_EXTENSIONS,
|
||||
ENCODE_RESOLUTIONS,
|
||||
MEDIA_ENCODING_STATUS,
|
||||
encoding_media_file_path,
|
||||
)
|
||||
|
||||
|
||||
class EncodeProfile(models.Model):
|
||||
"""Encode Profile model
|
||||
keeps information for each profile
|
||||
"""
|
||||
|
||||
name = models.CharField(max_length=90)
|
||||
|
||||
extension = models.CharField(max_length=10, choices=ENCODE_EXTENSIONS)
|
||||
|
||||
resolution = models.IntegerField(choices=ENCODE_RESOLUTIONS, blank=True, null=True)
|
||||
|
||||
codec = models.CharField(max_length=10, choices=CODECS, blank=True, null=True)
|
||||
|
||||
description = models.TextField(blank=True, help_text="description")
|
||||
|
||||
active = models.BooleanField(default=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
ordering = ["resolution"]
|
||||
|
||||
|
||||
class Encoding(models.Model):
|
||||
"""Encoding Media Instances"""
|
||||
|
||||
add_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
commands = models.TextField(blank=True, help_text="commands run")
|
||||
|
||||
chunk = models.BooleanField(default=False, db_index=True, help_text="is chunk?")
|
||||
|
||||
chunk_file_path = models.CharField(max_length=400, blank=True)
|
||||
|
||||
chunks_info = models.TextField(blank=True)
|
||||
|
||||
logs = models.TextField(blank=True)
|
||||
|
||||
md5sum = models.CharField(max_length=50, blank=True, null=True)
|
||||
|
||||
media = models.ForeignKey("Media", on_delete=models.CASCADE, related_name="encodings")
|
||||
|
||||
media_file = models.FileField("encoding file", upload_to=encoding_media_file_path, blank=True, max_length=500)
|
||||
|
||||
profile = models.ForeignKey(EncodeProfile, on_delete=models.CASCADE)
|
||||
|
||||
progress = models.PositiveSmallIntegerField(default=0)
|
||||
|
||||
update_date = models.DateTimeField(auto_now=True)
|
||||
|
||||
retries = models.IntegerField(default=0)
|
||||
|
||||
size = models.CharField(max_length=20, blank=True)
|
||||
|
||||
status = models.CharField(max_length=20, choices=MEDIA_ENCODING_STATUS, default="pending")
|
||||
|
||||
temp_file = models.CharField(max_length=400, blank=True)
|
||||
|
||||
task_id = models.CharField(max_length=100, blank=True)
|
||||
|
||||
total_run_time = models.IntegerField(default=0)
|
||||
|
||||
worker = models.CharField(max_length=100, blank=True)
|
||||
|
||||
@property
|
||||
def media_encoding_url(self):
|
||||
if self.media_file:
|
||||
return helpers.url_from_path(self.media_file.path)
|
||||
return None
|
||||
|
||||
@property
|
||||
def media_chunk_url(self):
|
||||
if self.chunk_file_path:
|
||||
return helpers.url_from_path(self.chunk_file_path)
|
||||
return None
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.media_file:
|
||||
cmd = ["stat", "-c", "%s", self.media_file.path]
|
||||
stdout = helpers.run_command(cmd).get("out")
|
||||
if stdout:
|
||||
size = int(stdout.strip())
|
||||
self.size = helpers.show_file_size(size)
|
||||
if self.chunk_file_path and not self.md5sum:
|
||||
cmd = ["md5sum", self.chunk_file_path]
|
||||
stdout = helpers.run_command(cmd).get("out")
|
||||
if stdout:
|
||||
md5sum = stdout.strip().split()[0]
|
||||
self.md5sum = md5sum
|
||||
|
||||
super(Encoding, self).save(*args, **kwargs)
|
||||
|
||||
def update_size_without_save(self):
|
||||
"""Update the size of an encoding without saving to avoid calling signals"""
|
||||
if self.media_file:
|
||||
cmd = ["stat", "-c", "%s", self.media_file.path]
|
||||
stdout = helpers.run_command(cmd).get("out")
|
||||
if stdout:
|
||||
size = int(stdout.strip())
|
||||
size = helpers.show_file_size(size)
|
||||
Encoding.objects.filter(pk=self.pk).update(size=size)
|
||||
return True
|
||||
return False
|
||||
|
||||
def set_progress(self, progress, commit=True):
|
||||
if isinstance(progress, int):
|
||||
if 0 <= progress <= 100:
|
||||
self.progress = progress
|
||||
# save object with filter update
|
||||
# to avoid calling signals
|
||||
Encoding.objects.filter(pk=self.pk).update(progress=progress)
|
||||
return True
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.profile.name}-{self.media.title}"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("api_get_encoding", kwargs={"encoding_id": self.id})
|
||||
|
||||
|
||||
@receiver(post_save, sender=Encoding)
|
||||
def encoding_file_save(sender, instance, created, **kwargs):
|
||||
"""Performs actions on encoding file delete
|
||||
For example, if encoding is a chunk file, with encoding_status success,
|
||||
perform a check if this is the final chunk file of a media, then
|
||||
concatenate chunks, create final encoding file and delete chunks
|
||||
"""
|
||||
|
||||
if instance.chunk and instance.status == "success":
|
||||
# a chunk got completed
|
||||
|
||||
# check if all chunks are OK
|
||||
# then concatenate to new Encoding - and remove chunks
|
||||
# this should run only once!
|
||||
if instance.media_file:
|
||||
try:
|
||||
orig_chunks = json.loads(instance.chunks_info).keys()
|
||||
except BaseException:
|
||||
instance.delete()
|
||||
return False
|
||||
|
||||
chunks = Encoding.objects.filter(
|
||||
media=instance.media,
|
||||
profile=instance.profile,
|
||||
chunks_info=instance.chunks_info,
|
||||
chunk=True,
|
||||
).order_by("add_date")
|
||||
|
||||
complete = True
|
||||
|
||||
# perform validation, make sure everything is there
|
||||
for chunk in orig_chunks:
|
||||
if not chunks.filter(chunk_file_path=chunk):
|
||||
complete = False
|
||||
break
|
||||
|
||||
for chunk in chunks:
|
||||
if not (chunk.media_file and chunk.media_file.path):
|
||||
complete = False
|
||||
break
|
||||
|
||||
if complete:
|
||||
# concatenate chunks and create final encoding file
|
||||
chunks_paths = [f.media_file.path for f in chunks]
|
||||
|
||||
with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as temp_dir:
|
||||
seg_file = helpers.create_temp_file(suffix=".txt", dir=temp_dir)
|
||||
tf = helpers.create_temp_file(suffix=f".{instance.profile.extension}", dir=temp_dir)
|
||||
with open(seg_file, "w") as ff:
|
||||
for f in chunks_paths:
|
||||
ff.write(f"file {f}\n")
|
||||
cmd = [
|
||||
settings.FFMPEG_COMMAND,
|
||||
"-y",
|
||||
"-f",
|
||||
"concat",
|
||||
"-safe",
|
||||
"0",
|
||||
"-i",
|
||||
seg_file,
|
||||
"-c",
|
||||
"copy",
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
"-movflags",
|
||||
"faststart",
|
||||
tf,
|
||||
]
|
||||
stdout = helpers.run_command(cmd)
|
||||
|
||||
encoding = Encoding(
|
||||
media=instance.media,
|
||||
profile=instance.profile,
|
||||
status="success",
|
||||
progress=100,
|
||||
)
|
||||
all_logs = "\n".join([st.logs for st in chunks])
|
||||
encoding.logs = f"{chunks_paths}\n{stdout}\n{all_logs}"
|
||||
workers = list(set([st.worker for st in chunks]))
|
||||
encoding.worker = json.dumps({"workers": workers})
|
||||
|
||||
start_date = min([st.add_date for st in chunks])
|
||||
end_date = max([st.update_date for st in chunks])
|
||||
encoding.total_run_time = (end_date - start_date).seconds
|
||||
encoding.save()
|
||||
|
||||
with open(tf, "rb") as f:
|
||||
myfile = File(f)
|
||||
output_name = f"{helpers.get_file_name(instance.media.media_file.path)}.{instance.profile.extension}"
|
||||
encoding.media_file.save(content=myfile, name=output_name)
|
||||
|
||||
# encoding is saved, deleting chunks
|
||||
# and any other encoding that might exist
|
||||
# first perform one last validation
|
||||
# to avoid that this is run twice
|
||||
if (
|
||||
len(orig_chunks)
|
||||
== Encoding.objects.filter( # noqa
|
||||
media=instance.media,
|
||||
profile=instance.profile,
|
||||
chunks_info=instance.chunks_info,
|
||||
).count()
|
||||
):
|
||||
# if two chunks are finished at the same time, this
|
||||
# will be changed
|
||||
who = Encoding.objects.filter(media=encoding.media, profile=encoding.profile).exclude(id=encoding.id)
|
||||
who.delete()
|
||||
else:
|
||||
encoding.delete()
|
||||
if not Encoding.objects.filter(chunks_info=instance.chunks_info):
|
||||
# TODO: in case of remote workers, files should be deleted
|
||||
# example
|
||||
# for worker in workers:
|
||||
# for chunk in json.loads(instance.chunks_info).keys():
|
||||
# remove_media_file.delay(media_file=chunk)
|
||||
for chunk in json.loads(instance.chunks_info).keys():
|
||||
helpers.rm_file(chunk)
|
||||
instance.media.post_encode_actions(encoding=instance, action="add")
|
||||
|
||||
elif instance.chunk and instance.status == "fail":
|
||||
encoding = Encoding(media=instance.media, profile=instance.profile, status="fail", progress=100)
|
||||
|
||||
chunks = Encoding.objects.filter(media=instance.media, chunks_info=instance.chunks_info, chunk=True).order_by("add_date")
|
||||
|
||||
chunks_paths = [f.media_file.path for f in chunks]
|
||||
|
||||
all_logs = "\n".join([st.logs for st in chunks])
|
||||
encoding.logs = f"{chunks_paths}\n{all_logs}"
|
||||
workers = list(set([st.worker for st in chunks]))
|
||||
encoding.worker = json.dumps({"workers": workers})
|
||||
start_date = min([st.add_date for st in chunks])
|
||||
end_date = max([st.update_date for st in chunks])
|
||||
encoding.total_run_time = (end_date - start_date).seconds
|
||||
encoding.save()
|
||||
|
||||
who = Encoding.objects.filter(media=encoding.media, profile=encoding.profile).exclude(id=encoding.id)
|
||||
|
||||
who.delete()
|
||||
# TODO: merge with above if, do not repeat code
|
||||
else:
|
||||
if instance.status in ["fail", "success"]:
|
||||
instance.media.post_encode_actions(encoding=instance, action="add")
|
||||
|
||||
encodings = set([encoding.status for encoding in Encoding.objects.filter(media=instance.media)])
|
||||
if ("running" in encodings) or ("pending" in encodings):
|
||||
return
|
||||
|
||||
|
||||
@receiver(post_delete, sender=Encoding)
|
||||
def encoding_file_delete(sender, instance, **kwargs):
|
||||
"""
|
||||
Deletes file from filesystem
|
||||
when corresponding `Encoding` object is deleted.
|
||||
"""
|
||||
|
||||
if instance.media_file:
|
||||
helpers.rm_file(instance.media_file.path)
|
||||
if not instance.chunk:
|
||||
instance.media.post_encode_actions(encoding=instance, action="delete")
|
||||
# delete local chunks, and remote chunks + media file. Only when the
|
||||
# last encoding of a media is complete
|
11
files/models/license.py
Normal file
11
files/models/license.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class License(models.Model):
|
||||
"""A Base license model to be used in Media"""
|
||||
|
||||
title = models.CharField(max_length=100, unique=True)
|
||||
description = models.TextField(blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
@@ -3,15 +3,12 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import tempfile
|
||||
import uuid
|
||||
|
||||
import m3u8
|
||||
from django.conf import settings
|
||||
from django.contrib.postgres.indexes import GinIndex
|
||||
from django.contrib.postgres.search import SearchVectorField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files import File
|
||||
from django.db import models
|
||||
from django.db.models import Func, Value
|
||||
@@ -19,108 +16,25 @@ from django.db.models.signals import m2m_changed, post_delete, post_save, pre_de
|
||||
from django.dispatch import receiver
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.html import strip_tags
|
||||
from imagekit.models import ProcessedImageField
|
||||
from imagekit.processors import ResizeToFit
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
|
||||
from . import helpers
|
||||
from .stop_words import STOP_WORDS
|
||||
from .. import helpers
|
||||
from ..stop_words import STOP_WORDS
|
||||
from .encoding import EncodeProfile, Encoding
|
||||
from .utils import (
|
||||
ENCODE_RESOLUTIONS_KEYS,
|
||||
MEDIA_ENCODING_STATUS,
|
||||
MEDIA_STATES,
|
||||
MEDIA_TYPES_SUPPORTED,
|
||||
original_media_file_path,
|
||||
original_thumbnail_file_path,
|
||||
)
|
||||
from .video_data import VideoTrimRequest
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
RE_TIMECODE = re.compile(r"(\d+:\d+:\d+.\d+)")
|
||||
|
||||
# this is used by Media and Encoding models
|
||||
# reflects media encoding status for objects
|
||||
MEDIA_ENCODING_STATUS = (
|
||||
("pending", "Pending"),
|
||||
("running", "Running"),
|
||||
("fail", "Fail"),
|
||||
("success", "Success"),
|
||||
)
|
||||
|
||||
# the media state of a Media object
|
||||
# this is set by default according to the portal workflow
|
||||
MEDIA_STATES = (
|
||||
("private", "Private"),
|
||||
("public", "Public"),
|
||||
("unlisted", "Unlisted"),
|
||||
)
|
||||
|
||||
# each uploaded Media gets a media_type hint
|
||||
# by helpers.get_file_type
|
||||
|
||||
MEDIA_TYPES_SUPPORTED = (
|
||||
("video", "Video"),
|
||||
("image", "Image"),
|
||||
("pdf", "Pdf"),
|
||||
("audio", "Audio"),
|
||||
)
|
||||
|
||||
ENCODE_EXTENSIONS = (
|
||||
("mp4", "mp4"),
|
||||
("webm", "webm"),
|
||||
("gif", "gif"),
|
||||
)
|
||||
|
||||
ENCODE_RESOLUTIONS = (
|
||||
(2160, "2160"),
|
||||
(1440, "1440"),
|
||||
(1080, "1080"),
|
||||
(720, "720"),
|
||||
(480, "480"),
|
||||
(360, "360"),
|
||||
(240, "240"),
|
||||
(144, "144"),
|
||||
)
|
||||
|
||||
CODECS = (
|
||||
("h265", "h265"),
|
||||
("h264", "h264"),
|
||||
("vp9", "vp9"),
|
||||
)
|
||||
|
||||
ENCODE_EXTENSIONS_KEYS = [extension for extension, name in ENCODE_EXTENSIONS]
|
||||
ENCODE_RESOLUTIONS_KEYS = [resolution for resolution, name in ENCODE_RESOLUTIONS]
|
||||
|
||||
|
||||
def generate_uid():
|
||||
return get_random_string(length=16)
|
||||
|
||||
|
||||
def original_media_file_path(instance, filename):
|
||||
"""Helper function to place original media file"""
|
||||
file_name = f"{instance.uid.hex}.{helpers.get_file_name(filename)}"
|
||||
return settings.MEDIA_UPLOAD_DIR + f"user/{instance.user.username}/{file_name}"
|
||||
|
||||
|
||||
def encoding_media_file_path(instance, filename):
|
||||
"""Helper function to place encoded media file"""
|
||||
|
||||
file_name = f"{instance.media.uid.hex}.{helpers.get_file_name(filename)}"
|
||||
return settings.MEDIA_ENCODING_DIR + f"{instance.profile.id}/{instance.media.user.username}/{file_name}"
|
||||
|
||||
|
||||
def original_thumbnail_file_path(instance, filename):
|
||||
"""Helper function to place original media thumbnail file"""
|
||||
|
||||
return settings.THUMBNAIL_UPLOAD_DIR + f"user/{instance.user.username}/{filename}"
|
||||
|
||||
|
||||
def subtitles_file_path(instance, filename):
|
||||
"""Helper function to place subtitle file"""
|
||||
|
||||
return settings.SUBTITLES_UPLOAD_DIR + f"user/{instance.media.user.username}/{filename}"
|
||||
|
||||
|
||||
def category_thumb_path(instance, filename):
|
||||
"""Helper function to place category thumbnail file"""
|
||||
|
||||
file_name = f"{instance.uid}.{helpers.get_file_name(filename)}"
|
||||
return settings.MEDIA_UPLOAD_DIR + f"categories/{file_name}"
|
||||
|
||||
|
||||
class Media(models.Model):
|
||||
"""The most important model for MediaCMS"""
|
||||
@@ -138,7 +52,6 @@ class Media(models.Model):
|
||||
null=True,
|
||||
help_text="Media can exist in one or no Channels",
|
||||
)
|
||||
|
||||
description = models.TextField(blank=True)
|
||||
|
||||
dislikes = models.IntegerField(default=0)
|
||||
@@ -566,7 +479,7 @@ class Media(models.Model):
|
||||
To be used on the video player
|
||||
"""
|
||||
|
||||
from . import tasks
|
||||
from .. import tasks
|
||||
|
||||
tasks.produce_sprite_from_video.delay(self.friendly_token)
|
||||
return True
|
||||
@@ -582,7 +495,7 @@ class Media(models.Model):
|
||||
profiles = EncodeProfile.objects.filter(active=True)
|
||||
profiles = list(profiles)
|
||||
|
||||
from . import tasks
|
||||
from .. import tasks
|
||||
|
||||
# attempt to break media file in chunks
|
||||
if self.duration > settings.CHUNKIZE_VIDEO_DURATION and chunkize:
|
||||
@@ -638,7 +551,7 @@ class Media(models.Model):
|
||||
self.save(update_fields=["encoding_status", "listable", "preview_file_path"])
|
||||
|
||||
if encoding and encoding.status == "success" and encoding.profile.codec == "h264" and action == "add" and not encoding.chunk:
|
||||
from . import tasks
|
||||
from .. import tasks
|
||||
|
||||
tasks.create_hls.delay(self.friendly_token)
|
||||
|
||||
@@ -993,585 +906,26 @@ class Media(models.Model):
|
||||
return data
|
||||
|
||||
|
||||
class License(models.Model):
|
||||
"""A Base license model to be used in Media"""
|
||||
class MediaPermission(models.Model):
|
||||
"""Model to store user permissions for media"""
|
||||
|
||||
title = models.CharField(max_length=100, unique=True)
|
||||
description = models.TextField(blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
class Category(models.Model):
|
||||
"""A Category base model"""
|
||||
|
||||
uid = models.CharField(unique=True, max_length=36, default=generate_uid)
|
||||
|
||||
add_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
title = models.CharField(max_length=100, db_index=True)
|
||||
|
||||
description = models.TextField(blank=True)
|
||||
|
||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE, blank=True, null=True)
|
||||
|
||||
is_global = models.BooleanField(default=False, help_text="global categories or user specific")
|
||||
|
||||
media_count = models.IntegerField(default=0, help_text="number of media")
|
||||
|
||||
thumbnail = ProcessedImageField(
|
||||
upload_to=category_thumb_path,
|
||||
processors=[ResizeToFit(width=344, height=None)],
|
||||
format="JPEG",
|
||||
options={"quality": 85},
|
||||
blank=True,
|
||||
PERMISSION_CHOICES = (
|
||||
("viewer", "Viewer"),
|
||||
("editor", "Editor"),
|
||||
("owner", "Owner"),
|
||||
)
|
||||
|
||||
listings_thumbnail = models.CharField(max_length=400, blank=True, null=True, help_text="Thumbnail to show on listings")
|
||||
|
||||
is_rbac_category = models.BooleanField(default=False, db_index=True, help_text='If access to Category is controlled by role based membership of Groups')
|
||||
|
||||
identity_provider = models.ForeignKey(
|
||||
'socialaccount.SocialApp',
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='categories',
|
||||
help_text='If category is related with a specific Identity Provider',
|
||||
verbose_name='IDP Config Name',
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
owner_user = models.ForeignKey('users.User', on_delete=models.CASCADE, related_name='granted_permissions')
|
||||
user = models.ForeignKey('users.User', on_delete=models.CASCADE)
|
||||
media = models.ForeignKey('Media', on_delete=models.CASCADE, related_name='permissions')
|
||||
permission = models.CharField(max_length=20, choices=PERMISSION_CHOICES)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["title"]
|
||||
verbose_name_plural = "Categories"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return f"{reverse('search')}?c={self.title}"
|
||||
|
||||
def update_category_media(self):
|
||||
"""Set media_count"""
|
||||
|
||||
if getattr(settings, 'USE_RBAC', False) and self.is_rbac_category:
|
||||
self.media_count = Media.objects.filter(category=self).count()
|
||||
else:
|
||||
self.media_count = Media.objects.filter(listable=True, category=self).count()
|
||||
|
||||
self.save(update_fields=["media_count"])
|
||||
return True
|
||||
|
||||
@property
|
||||
def thumbnail_url(self):
|
||||
"""Return thumbnail for category
|
||||
prioritize processed value of listings_thumbnail
|
||||
then thumbnail
|
||||
"""
|
||||
|
||||
if self.listings_thumbnail:
|
||||
return self.listings_thumbnail
|
||||
if self.thumbnail:
|
||||
return helpers.url_from_path(self.thumbnail.path)
|
||||
|
||||
media = Media.objects.filter(category=self, state="public").order_by("-views").first()
|
||||
if media:
|
||||
return media.thumbnail_url
|
||||
|
||||
return None
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
strip_text_items = ["title", "description"]
|
||||
for item in strip_text_items:
|
||||
setattr(self, item, strip_tags(getattr(self, item, None)))
|
||||
super(Category, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class Tag(models.Model):
|
||||
"""A Tag model"""
|
||||
|
||||
title = models.CharField(max_length=100, unique=True, db_index=True)
|
||||
|
||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE, blank=True, null=True)
|
||||
|
||||
media_count = models.IntegerField(default=0, help_text="number of media")
|
||||
|
||||
listings_thumbnail = models.CharField(
|
||||
max_length=400,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text="Thumbnail to show on listings",
|
||||
db_index=True,
|
||||
)
|
||||
unique_together = ('user', 'media')
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
class Meta:
|
||||
ordering = ["title"]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return f"{reverse('search')}?t={self.title}"
|
||||
|
||||
def update_tag_media(self):
|
||||
self.media_count = Media.objects.filter(state="public", is_reviewed=True, tags=self).count()
|
||||
self.save(update_fields=["media_count"])
|
||||
return True
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.title = helpers.get_alphanumeric_only(self.title)
|
||||
self.title = self.title[:99]
|
||||
super(Tag, self).save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def thumbnail_url(self):
|
||||
if self.listings_thumbnail:
|
||||
return self.listings_thumbnail
|
||||
media = Media.objects.filter(tags=self, state="public").order_by("-views").first()
|
||||
if media:
|
||||
return media.thumbnail_url
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class EncodeProfile(models.Model):
|
||||
"""Encode Profile model
|
||||
keeps information for each profile
|
||||
"""
|
||||
|
||||
name = models.CharField(max_length=90)
|
||||
|
||||
extension = models.CharField(max_length=10, choices=ENCODE_EXTENSIONS)
|
||||
|
||||
resolution = models.IntegerField(choices=ENCODE_RESOLUTIONS, blank=True, null=True)
|
||||
|
||||
codec = models.CharField(max_length=10, choices=CODECS, blank=True, null=True)
|
||||
|
||||
description = models.TextField(blank=True, help_text="description")
|
||||
|
||||
active = models.BooleanField(default=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
ordering = ["resolution"]
|
||||
|
||||
|
||||
class Encoding(models.Model):
|
||||
"""Encoding Media Instances"""
|
||||
|
||||
add_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
commands = models.TextField(blank=True, help_text="commands run")
|
||||
|
||||
chunk = models.BooleanField(default=False, db_index=True, help_text="is chunk?")
|
||||
|
||||
chunk_file_path = models.CharField(max_length=400, blank=True)
|
||||
|
||||
chunks_info = models.TextField(blank=True)
|
||||
|
||||
logs = models.TextField(blank=True)
|
||||
|
||||
md5sum = models.CharField(max_length=50, blank=True, null=True)
|
||||
|
||||
media = models.ForeignKey(Media, on_delete=models.CASCADE, related_name="encodings")
|
||||
|
||||
media_file = models.FileField("encoding file", upload_to=encoding_media_file_path, blank=True, max_length=500)
|
||||
|
||||
profile = models.ForeignKey(EncodeProfile, on_delete=models.CASCADE)
|
||||
|
||||
progress = models.PositiveSmallIntegerField(default=0)
|
||||
|
||||
update_date = models.DateTimeField(auto_now=True)
|
||||
|
||||
retries = models.IntegerField(default=0)
|
||||
|
||||
size = models.CharField(max_length=20, blank=True)
|
||||
|
||||
status = models.CharField(max_length=20, choices=MEDIA_ENCODING_STATUS, default="pending")
|
||||
|
||||
temp_file = models.CharField(max_length=400, blank=True)
|
||||
|
||||
task_id = models.CharField(max_length=100, blank=True)
|
||||
|
||||
total_run_time = models.IntegerField(default=0)
|
||||
|
||||
worker = models.CharField(max_length=100, blank=True)
|
||||
|
||||
@property
|
||||
def media_encoding_url(self):
|
||||
if self.media_file:
|
||||
return helpers.url_from_path(self.media_file.path)
|
||||
return None
|
||||
|
||||
@property
|
||||
def media_chunk_url(self):
|
||||
if self.chunk_file_path:
|
||||
return helpers.url_from_path(self.chunk_file_path)
|
||||
return None
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.media_file:
|
||||
cmd = ["stat", "-c", "%s", self.media_file.path]
|
||||
stdout = helpers.run_command(cmd).get("out")
|
||||
if stdout:
|
||||
size = int(stdout.strip())
|
||||
self.size = helpers.show_file_size(size)
|
||||
if self.chunk_file_path and not self.md5sum:
|
||||
cmd = ["md5sum", self.chunk_file_path]
|
||||
stdout = helpers.run_command(cmd).get("out")
|
||||
if stdout:
|
||||
md5sum = stdout.strip().split()[0]
|
||||
self.md5sum = md5sum
|
||||
|
||||
super(Encoding, self).save(*args, **kwargs)
|
||||
|
||||
def update_size_without_save(self):
|
||||
"""Update the size of an encoding without saving to avoid calling signals"""
|
||||
if self.media_file:
|
||||
cmd = ["stat", "-c", "%s", self.media_file.path]
|
||||
stdout = helpers.run_command(cmd).get("out")
|
||||
if stdout:
|
||||
size = int(stdout.strip())
|
||||
size = helpers.show_file_size(size)
|
||||
Encoding.objects.filter(pk=self.pk).update(size=size)
|
||||
return True
|
||||
return False
|
||||
|
||||
def set_progress(self, progress, commit=True):
|
||||
if isinstance(progress, int):
|
||||
if 0 <= progress <= 100:
|
||||
self.progress = progress
|
||||
# save object with filter update
|
||||
# to avoid calling signals
|
||||
Encoding.objects.filter(pk=self.pk).update(progress=progress)
|
||||
return True
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.profile.name}-{self.media.title}"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("api_get_encoding", kwargs={"encoding_id": self.id})
|
||||
|
||||
|
||||
class Language(models.Model):
|
||||
"""Language model
|
||||
to be used with Subtitles
|
||||
"""
|
||||
|
||||
code = models.CharField(max_length=12, help_text="language code")
|
||||
|
||||
title = models.CharField(max_length=100, help_text="language code")
|
||||
|
||||
class Meta:
|
||||
ordering = ["id"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.code}-{self.title}"
|
||||
|
||||
|
||||
class Subtitle(models.Model):
|
||||
"""Subtitles model"""
|
||||
|
||||
language = models.ForeignKey(Language, on_delete=models.CASCADE)
|
||||
|
||||
media = models.ForeignKey(Media, on_delete=models.CASCADE, related_name="subtitles")
|
||||
|
||||
subtitle_file = models.FileField(
|
||||
"Subtitle/CC file",
|
||||
help_text="File has to be WebVTT format",
|
||||
upload_to=subtitles_file_path,
|
||||
max_length=500,
|
||||
)
|
||||
|
||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
ordering = ["language__title"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.media.title}-{self.language.title}"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return f"{reverse('edit_subtitle')}?id={self.id}"
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self.get_absolute_url()
|
||||
|
||||
def convert_to_srt(self):
|
||||
input_path = self.subtitle_file.path
|
||||
with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as tmpdirname:
|
||||
pysub = settings.PYSUBS_COMMAND
|
||||
|
||||
cmd = [pysub, input_path, "--to", "vtt", "-o", tmpdirname]
|
||||
stdout = helpers.run_command(cmd)
|
||||
|
||||
list_of_files = os.listdir(tmpdirname)
|
||||
if list_of_files:
|
||||
subtitles_file = os.path.join(tmpdirname, list_of_files[0])
|
||||
cmd = ["cp", subtitles_file, input_path]
|
||||
stdout = helpers.run_command(cmd) # noqa
|
||||
else:
|
||||
raise Exception("Could not convert to srt")
|
||||
return True
|
||||
|
||||
|
||||
class RatingCategory(models.Model):
|
||||
"""Rating Category
|
||||
Facilitate user ratings.
|
||||
One or more rating categories per Category can exist
|
||||
will be shown to the media if they are enabled
|
||||
"""
|
||||
|
||||
description = models.TextField(blank=True)
|
||||
|
||||
enabled = models.BooleanField(default=True)
|
||||
|
||||
title = models.CharField(max_length=200, unique=True, db_index=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Rating Categories"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.title}"
|
||||
|
||||
|
||||
def validate_rating(value):
|
||||
if -1 >= value or value > 5:
|
||||
raise ValidationError("score has to be between 0 and 5")
|
||||
|
||||
|
||||
class Rating(models.Model):
|
||||
"""User Rating"""
|
||||
|
||||
add_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
media = models.ForeignKey(Media, on_delete=models.CASCADE, related_name="ratings")
|
||||
|
||||
rating_category = models.ForeignKey(RatingCategory, on_delete=models.CASCADE)
|
||||
|
||||
score = models.IntegerField(validators=[validate_rating])
|
||||
|
||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Ratings"
|
||||
indexes = [
|
||||
models.Index(fields=["user", "media"]),
|
||||
]
|
||||
unique_together = ("user", "media", "rating_category")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username}, rate for {self.media.title} for category {self.rating_category.title}"
|
||||
|
||||
|
||||
class Playlist(models.Model):
|
||||
"""Playlists model"""
|
||||
|
||||
add_date = models.DateTimeField(auto_now_add=True, db_index=True)
|
||||
|
||||
description = models.TextField(blank=True, help_text="description")
|
||||
|
||||
friendly_token = models.CharField(blank=True, max_length=12, db_index=True)
|
||||
|
||||
media = models.ManyToManyField(Media, through="playlistmedia", blank=True)
|
||||
|
||||
title = models.CharField(max_length=100, db_index=True)
|
||||
|
||||
uid = models.UUIDField(unique=True, default=uuid.uuid4)
|
||||
|
||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE, db_index=True, related_name="playlists")
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
@property
|
||||
def media_count(self):
|
||||
return self.media.filter(listable=True).count()
|
||||
|
||||
def get_absolute_url(self, api=False):
|
||||
if api:
|
||||
return reverse("api_get_playlist", kwargs={"friendly_token": self.friendly_token})
|
||||
else:
|
||||
return reverse("get_playlist", kwargs={"friendly_token": self.friendly_token})
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self.get_absolute_url()
|
||||
|
||||
@property
|
||||
def api_url(self):
|
||||
return self.get_absolute_url(api=True)
|
||||
|
||||
def user_thumbnail_url(self):
|
||||
if self.user.logo:
|
||||
return helpers.url_from_path(self.user.logo.path)
|
||||
return None
|
||||
|
||||
def set_ordering(self, media, ordering):
|
||||
if media not in self.media.all():
|
||||
return False
|
||||
pm = PlaylistMedia.objects.filter(playlist=self, media=media).first()
|
||||
if pm and isinstance(ordering, int) and 0 < ordering:
|
||||
pm.ordering = ordering
|
||||
pm.save()
|
||||
return True
|
||||
return False
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
strip_text_items = ["title", "description"]
|
||||
for item in strip_text_items:
|
||||
setattr(self, item, strip_tags(getattr(self, item, None)))
|
||||
self.title = self.title[:99]
|
||||
|
||||
if not self.friendly_token:
|
||||
while True:
|
||||
friendly_token = helpers.produce_friendly_token()
|
||||
if not Playlist.objects.filter(friendly_token=friendly_token):
|
||||
self.friendly_token = friendly_token
|
||||
break
|
||||
super(Playlist, self).save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def thumbnail_url(self):
|
||||
pm = self.playlistmedia_set.filter(media__listable=True).first()
|
||||
if pm and pm.media.thumbnail:
|
||||
return helpers.url_from_path(pm.media.thumbnail.path)
|
||||
return None
|
||||
|
||||
|
||||
class PlaylistMedia(models.Model):
|
||||
"""Helper model to store playlist specific media"""
|
||||
|
||||
action_date = models.DateTimeField(auto_now=True)
|
||||
|
||||
media = models.ForeignKey(Media, on_delete=models.CASCADE)
|
||||
|
||||
playlist = models.ForeignKey(Playlist, on_delete=models.CASCADE)
|
||||
|
||||
ordering = models.IntegerField(default=1)
|
||||
|
||||
class Meta:
|
||||
ordering = ["ordering", "-action_date"]
|
||||
|
||||
|
||||
class Comment(MPTTModel):
|
||||
"""Comments model"""
|
||||
|
||||
add_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
media = models.ForeignKey(Media, on_delete=models.CASCADE, db_index=True, related_name="comments")
|
||||
|
||||
parent = TreeForeignKey("self", on_delete=models.CASCADE, null=True, blank=True, related_name="children")
|
||||
|
||||
text = models.TextField(help_text="text")
|
||||
|
||||
uid = models.UUIDField(unique=True, default=uuid.uuid4)
|
||||
|
||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE, db_index=True)
|
||||
|
||||
class MPTTMeta:
|
||||
order_insertion_by = ["add_date"]
|
||||
|
||||
def __str__(self):
|
||||
return f"On {self.media.title} by {self.user.username}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
strip_text_items = ["text"]
|
||||
for item in strip_text_items:
|
||||
setattr(self, item, strip_tags(getattr(self, item, None)))
|
||||
|
||||
if self.text:
|
||||
self.text = self.text[: settings.MAX_CHARS_FOR_COMMENT]
|
||||
|
||||
super(Comment, self).save(*args, **kwargs)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return f"{reverse('get_media')}?m={self.media.friendly_token}"
|
||||
|
||||
@property
|
||||
def media_url(self):
|
||||
return self.get_absolute_url()
|
||||
|
||||
|
||||
class VideoChapterData(models.Model):
|
||||
data = models.JSONField(null=False, blank=False, help_text="Chapter data")
|
||||
media = models.ForeignKey('Media', on_delete=models.CASCADE, related_name='chapters')
|
||||
|
||||
class Meta:
|
||||
unique_together = ['media']
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
from . import tasks
|
||||
|
||||
is_new = self.pk is None
|
||||
if is_new or (not is_new and self._check_data_changed()):
|
||||
super().save(*args, **kwargs)
|
||||
tasks.produce_video_chapters.delay(self.pk)
|
||||
else:
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def _check_data_changed(self):
|
||||
if self.pk:
|
||||
old_instance = VideoChapterData.objects.get(pk=self.pk)
|
||||
return old_instance.data != self.data
|
||||
return False
|
||||
|
||||
@property
|
||||
def chapter_data(self):
|
||||
# ensure response is consistent
|
||||
data = []
|
||||
for item in self.data:
|
||||
if item.get("start") and item.get("title"):
|
||||
thumbnail = item.get("thumbnail")
|
||||
if thumbnail:
|
||||
thumbnail = helpers.url_from_path(thumbnail)
|
||||
else:
|
||||
thumbnail = "static/images/chapter_default.jpg"
|
||||
data.append(
|
||||
{
|
||||
"start": item.get("start"),
|
||||
"title": item.get("title"),
|
||||
"thumbnail": thumbnail,
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
class VideoTrimRequest(models.Model):
|
||||
"""Model to handle video trimming requests"""
|
||||
|
||||
VIDEO_TRIM_STATUS = (
|
||||
("initial", "Initial"),
|
||||
("running", "Running"),
|
||||
("success", "Success"),
|
||||
("fail", "Fail"),
|
||||
)
|
||||
|
||||
VIDEO_ACTION_CHOICES = (
|
||||
("replace", "Replace Original"),
|
||||
("save_new", "Save as New"),
|
||||
("create_segments", "Create Segments"),
|
||||
)
|
||||
|
||||
TRIM_STYLE_CHOICES = (
|
||||
("no_encoding", "No Encoding"),
|
||||
("precise", "Precise"),
|
||||
)
|
||||
|
||||
media = models.ForeignKey('Media', on_delete=models.CASCADE, related_name='trim_requests')
|
||||
status = models.CharField(max_length=20, choices=VIDEO_TRIM_STATUS, default="initial")
|
||||
add_date = models.DateTimeField(auto_now_add=True)
|
||||
video_action = models.CharField(max_length=20, choices=VIDEO_ACTION_CHOICES)
|
||||
media_trim_style = models.CharField(max_length=20, choices=TRIM_STYLE_CHOICES, default="no_encoding")
|
||||
timestamps = models.JSONField(null=False, blank=False, help_text="Timestamps for trimming")
|
||||
|
||||
def __str__(self):
|
||||
return f"Trim request for {self.media.title} ({self.status})"
|
||||
return f"{self.user.username} - {self.media.title} ({self.permission})"
|
||||
|
||||
|
||||
@receiver(post_save, sender=Media)
|
||||
@@ -1585,7 +939,7 @@ def media_save(sender, instance, created, **kwargs):
|
||||
return False
|
||||
|
||||
if created:
|
||||
from .methods import notify_users
|
||||
from ..methods import notify_users
|
||||
|
||||
instance.media_init()
|
||||
notify_users(friendly_token=instance.friendly_token, action="media_added")
|
||||
@@ -1616,11 +970,6 @@ def media_file_pre_delete(sender, instance, **kwargs):
|
||||
tag.update_tag_media()
|
||||
|
||||
|
||||
@receiver(post_delete, sender=VideoChapterData)
|
||||
def videochapterdata_delete(sender, instance, **kwargs):
|
||||
helpers.rm_dir(instance.media.video_chapters_folder)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=Media)
|
||||
def media_file_delete(sender, instance, **kwargs):
|
||||
"""
|
||||
@@ -1661,166 +1010,3 @@ def media_m2m(sender, instance, **kwargs):
|
||||
if instance.tags.all():
|
||||
for tag in instance.tags.all():
|
||||
tag.update_tag_media()
|
||||
|
||||
|
||||
@receiver(post_save, sender=Encoding)
|
||||
def encoding_file_save(sender, instance, created, **kwargs):
|
||||
"""Performs actions on encoding file delete
|
||||
For example, if encoding is a chunk file, with encoding_status success,
|
||||
perform a check if this is the final chunk file of a media, then
|
||||
concatenate chunks, create final encoding file and delete chunks
|
||||
"""
|
||||
|
||||
if instance.chunk and instance.status == "success":
|
||||
# a chunk got completed
|
||||
|
||||
# check if all chunks are OK
|
||||
# then concatenate to new Encoding - and remove chunks
|
||||
# this should run only once!
|
||||
if instance.media_file:
|
||||
try:
|
||||
orig_chunks = json.loads(instance.chunks_info).keys()
|
||||
except BaseException:
|
||||
instance.delete()
|
||||
return False
|
||||
|
||||
chunks = Encoding.objects.filter(
|
||||
media=instance.media,
|
||||
profile=instance.profile,
|
||||
chunks_info=instance.chunks_info,
|
||||
chunk=True,
|
||||
).order_by("add_date")
|
||||
|
||||
complete = True
|
||||
|
||||
# perform validation, make sure everything is there
|
||||
for chunk in orig_chunks:
|
||||
if not chunks.filter(chunk_file_path=chunk):
|
||||
complete = False
|
||||
break
|
||||
|
||||
for chunk in chunks:
|
||||
if not (chunk.media_file and chunk.media_file.path):
|
||||
complete = False
|
||||
break
|
||||
|
||||
if complete:
|
||||
# concatenate chunks and create final encoding file
|
||||
chunks_paths = [f.media_file.path for f in chunks]
|
||||
|
||||
with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as temp_dir:
|
||||
seg_file = helpers.create_temp_file(suffix=".txt", dir=temp_dir)
|
||||
tf = helpers.create_temp_file(suffix=f".{instance.profile.extension}", dir=temp_dir)
|
||||
with open(seg_file, "w") as ff:
|
||||
for f in chunks_paths:
|
||||
ff.write(f"file {f}\n")
|
||||
cmd = [
|
||||
settings.FFMPEG_COMMAND,
|
||||
"-y",
|
||||
"-f",
|
||||
"concat",
|
||||
"-safe",
|
||||
"0",
|
||||
"-i",
|
||||
seg_file,
|
||||
"-c",
|
||||
"copy",
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
"-movflags",
|
||||
"faststart",
|
||||
tf,
|
||||
]
|
||||
stdout = helpers.run_command(cmd)
|
||||
|
||||
encoding = Encoding(
|
||||
media=instance.media,
|
||||
profile=instance.profile,
|
||||
status="success",
|
||||
progress=100,
|
||||
)
|
||||
all_logs = "\n".join([st.logs for st in chunks])
|
||||
encoding.logs = f"{chunks_paths}\n{stdout}\n{all_logs}"
|
||||
workers = list(set([st.worker for st in chunks]))
|
||||
encoding.worker = json.dumps({"workers": workers})
|
||||
|
||||
start_date = min([st.add_date for st in chunks])
|
||||
end_date = max([st.update_date for st in chunks])
|
||||
encoding.total_run_time = (end_date - start_date).seconds
|
||||
encoding.save()
|
||||
|
||||
with open(tf, "rb") as f:
|
||||
myfile = File(f)
|
||||
output_name = f"{helpers.get_file_name(instance.media.media_file.path)}.{instance.profile.extension}"
|
||||
encoding.media_file.save(content=myfile, name=output_name)
|
||||
|
||||
# encoding is saved, deleting chunks
|
||||
# and any other encoding that might exist
|
||||
# first perform one last validation
|
||||
# to avoid that this is run twice
|
||||
if (
|
||||
len(orig_chunks)
|
||||
== Encoding.objects.filter( # noqa
|
||||
media=instance.media,
|
||||
profile=instance.profile,
|
||||
chunks_info=instance.chunks_info,
|
||||
).count()
|
||||
):
|
||||
# if two chunks are finished at the same time, this
|
||||
# will be changed
|
||||
who = Encoding.objects.filter(media=encoding.media, profile=encoding.profile).exclude(id=encoding.id)
|
||||
who.delete()
|
||||
else:
|
||||
encoding.delete()
|
||||
if not Encoding.objects.filter(chunks_info=instance.chunks_info):
|
||||
# TODO: in case of remote workers, files should be deleted
|
||||
# example
|
||||
# for worker in workers:
|
||||
# for chunk in json.loads(instance.chunks_info).keys():
|
||||
# remove_media_file.delay(media_file=chunk)
|
||||
for chunk in json.loads(instance.chunks_info).keys():
|
||||
helpers.rm_file(chunk)
|
||||
instance.media.post_encode_actions(encoding=instance, action="add")
|
||||
|
||||
elif instance.chunk and instance.status == "fail":
|
||||
encoding = Encoding(media=instance.media, profile=instance.profile, status="fail", progress=100)
|
||||
|
||||
chunks = Encoding.objects.filter(media=instance.media, chunks_info=instance.chunks_info, chunk=True).order_by("add_date")
|
||||
|
||||
chunks_paths = [f.media_file.path for f in chunks]
|
||||
|
||||
all_logs = "\n".join([st.logs for st in chunks])
|
||||
encoding.logs = f"{chunks_paths}\n{all_logs}"
|
||||
workers = list(set([st.worker for st in chunks]))
|
||||
encoding.worker = json.dumps({"workers": workers})
|
||||
start_date = min([st.add_date for st in chunks])
|
||||
end_date = max([st.update_date for st in chunks])
|
||||
encoding.total_run_time = (end_date - start_date).seconds
|
||||
encoding.save()
|
||||
|
||||
who = Encoding.objects.filter(media=encoding.media, profile=encoding.profile).exclude(id=encoding.id)
|
||||
|
||||
who.delete()
|
||||
# TODO: merge with above if, do not repeat code
|
||||
else:
|
||||
if instance.status in ["fail", "success"]:
|
||||
instance.media.post_encode_actions(encoding=instance, action="add")
|
||||
|
||||
encodings = set([encoding.status for encoding in Encoding.objects.filter(media=instance.media)])
|
||||
if ("running" in encodings) or ("pending" in encodings):
|
||||
return
|
||||
|
||||
|
||||
@receiver(post_delete, sender=Encoding)
|
||||
def encoding_file_delete(sender, instance, **kwargs):
|
||||
"""
|
||||
Deletes file from filesystem
|
||||
when corresponding `Encoding` object is deleted.
|
||||
"""
|
||||
|
||||
if instance.media_file:
|
||||
helpers.rm_file(instance.media_file.path)
|
||||
if not instance.chunk:
|
||||
instance.media.post_encode_actions(encoding=instance, action="delete")
|
||||
# delete local chunks, and remote chunks + media file. Only when the
|
||||
# last encoding of a media is complete
|
97
files/models/playlist.py
Normal file
97
files/models/playlist.py
Normal file
@@ -0,0 +1,97 @@
|
||||
import uuid
|
||||
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.html import strip_tags
|
||||
|
||||
from .. import helpers
|
||||
|
||||
|
||||
class Playlist(models.Model):
|
||||
"""Playlists model"""
|
||||
|
||||
add_date = models.DateTimeField(auto_now_add=True, db_index=True)
|
||||
|
||||
description = models.TextField(blank=True, help_text="description")
|
||||
|
||||
friendly_token = models.CharField(blank=True, max_length=12, db_index=True)
|
||||
|
||||
media = models.ManyToManyField("Media", through="playlistmedia", blank=True)
|
||||
|
||||
title = models.CharField(max_length=100, db_index=True)
|
||||
|
||||
uid = models.UUIDField(unique=True, default=uuid.uuid4)
|
||||
|
||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE, db_index=True, related_name="playlists")
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
@property
|
||||
def media_count(self):
|
||||
return self.media.filter(listable=True).count()
|
||||
|
||||
def get_absolute_url(self, api=False):
|
||||
if api:
|
||||
return reverse("api_get_playlist", kwargs={"friendly_token": self.friendly_token})
|
||||
else:
|
||||
return reverse("get_playlist", kwargs={"friendly_token": self.friendly_token})
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self.get_absolute_url()
|
||||
|
||||
@property
|
||||
def api_url(self):
|
||||
return self.get_absolute_url(api=True)
|
||||
|
||||
def user_thumbnail_url(self):
|
||||
if self.user.logo:
|
||||
return helpers.url_from_path(self.user.logo.path)
|
||||
return None
|
||||
|
||||
def set_ordering(self, media, ordering):
|
||||
if media not in self.media.all():
|
||||
return False
|
||||
pm = PlaylistMedia.objects.filter(playlist=self, media=media).first()
|
||||
if pm and isinstance(ordering, int) and 0 < ordering:
|
||||
pm.ordering = ordering
|
||||
pm.save()
|
||||
return True
|
||||
return False
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
strip_text_items = ["title", "description"]
|
||||
for item in strip_text_items:
|
||||
setattr(self, item, strip_tags(getattr(self, item, None)))
|
||||
self.title = self.title[:99]
|
||||
|
||||
if not self.friendly_token:
|
||||
while True:
|
||||
friendly_token = helpers.produce_friendly_token()
|
||||
if not Playlist.objects.filter(friendly_token=friendly_token):
|
||||
self.friendly_token = friendly_token
|
||||
break
|
||||
super(Playlist, self).save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def thumbnail_url(self):
|
||||
pm = self.playlistmedia_set.filter(media__listable=True).first()
|
||||
if pm and pm.media.thumbnail:
|
||||
return helpers.url_from_path(pm.media.thumbnail.path)
|
||||
return None
|
||||
|
||||
|
||||
class PlaylistMedia(models.Model):
|
||||
"""Helper model to store playlist specific media"""
|
||||
|
||||
action_date = models.DateTimeField(auto_now=True)
|
||||
|
||||
media = models.ForeignKey("Media", on_delete=models.CASCADE)
|
||||
|
||||
playlist = models.ForeignKey(Playlist, on_delete=models.CASCADE)
|
||||
|
||||
ordering = models.IntegerField(default=1)
|
||||
|
||||
class Meta:
|
||||
ordering = ["ordering", "-action_date"]
|
47
files/models/rating.py
Normal file
47
files/models/rating.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from django.db import models
|
||||
|
||||
from .utils import validate_rating
|
||||
|
||||
|
||||
class RatingCategory(models.Model):
|
||||
"""Rating Category
|
||||
Facilitate user ratings.
|
||||
One or more rating categories per Category can exist
|
||||
will be shown to the media if they are enabled
|
||||
"""
|
||||
|
||||
description = models.TextField(blank=True)
|
||||
|
||||
enabled = models.BooleanField(default=True)
|
||||
|
||||
title = models.CharField(max_length=200, unique=True, db_index=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Rating Categories"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.title}"
|
||||
|
||||
|
||||
class Rating(models.Model):
|
||||
"""User Rating"""
|
||||
|
||||
add_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
media = models.ForeignKey("Media", on_delete=models.CASCADE, related_name="ratings")
|
||||
|
||||
rating_category = models.ForeignKey(RatingCategory, on_delete=models.CASCADE)
|
||||
|
||||
score = models.IntegerField(validators=[validate_rating])
|
||||
|
||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Ratings"
|
||||
indexes = [
|
||||
models.Index(fields=["user", "media"]),
|
||||
]
|
||||
unique_together = ("user", "media", "rating_category")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username}, rate for {self.media.title} for category {self.rating_category.title}"
|
72
files/models/subtitle.py
Normal file
72
files/models/subtitle.py
Normal file
@@ -0,0 +1,72 @@
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
|
||||
from .. import helpers
|
||||
from .utils import subtitles_file_path
|
||||
|
||||
|
||||
class Language(models.Model):
|
||||
"""Language model
|
||||
to be used with Subtitles
|
||||
"""
|
||||
|
||||
code = models.CharField(max_length=12, help_text="language code")
|
||||
|
||||
title = models.CharField(max_length=100, help_text="language code")
|
||||
|
||||
class Meta:
|
||||
ordering = ["id"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.code}-{self.title}"
|
||||
|
||||
|
||||
class Subtitle(models.Model):
|
||||
"""Subtitles model"""
|
||||
|
||||
language = models.ForeignKey(Language, on_delete=models.CASCADE)
|
||||
|
||||
media = models.ForeignKey("Media", on_delete=models.CASCADE, related_name="subtitles")
|
||||
|
||||
subtitle_file = models.FileField(
|
||||
"Subtitle/CC file",
|
||||
help_text="File has to be WebVTT format",
|
||||
upload_to=subtitles_file_path,
|
||||
max_length=500,
|
||||
)
|
||||
|
||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
ordering = ["language__title"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.media.title}-{self.language.title}"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return f"{reverse('edit_subtitle')}?id={self.id}"
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self.get_absolute_url()
|
||||
|
||||
def convert_to_srt(self):
|
||||
input_path = self.subtitle_file.path
|
||||
with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as tmpdirname:
|
||||
pysub = settings.PYSUBS_COMMAND
|
||||
|
||||
cmd = [pysub, input_path, "--to", "vtt", "-o", tmpdirname]
|
||||
stdout = helpers.run_command(cmd)
|
||||
|
||||
list_of_files = os.listdir(tmpdirname)
|
||||
if list_of_files:
|
||||
subtitles_file = os.path.join(tmpdirname, list_of_files[0])
|
||||
cmd = ["cp", subtitles_file, input_path]
|
||||
stdout = helpers.run_command(cmd) # noqa
|
||||
else:
|
||||
raise Exception("Could not convert to srt")
|
||||
return True
|
99
files/models/utils.py
Normal file
99
files/models/utils.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.crypto import get_random_string
|
||||
|
||||
from .. import helpers
|
||||
|
||||
# this is used by Media and Encoding models
|
||||
# reflects media encoding status for objects
|
||||
MEDIA_ENCODING_STATUS = (
|
||||
("pending", "Pending"),
|
||||
("running", "Running"),
|
||||
("fail", "Fail"),
|
||||
("success", "Success"),
|
||||
)
|
||||
|
||||
# the media state of a Media object
|
||||
# this is set by default according to the portal workflow
|
||||
MEDIA_STATES = (
|
||||
("private", "Private"),
|
||||
("public", "Public"),
|
||||
("unlisted", "Unlisted"),
|
||||
)
|
||||
|
||||
# each uploaded Media gets a media_type hint
|
||||
# by helpers.get_file_type
|
||||
|
||||
MEDIA_TYPES_SUPPORTED = (
|
||||
("video", "Video"),
|
||||
("image", "Image"),
|
||||
("pdf", "Pdf"),
|
||||
("audio", "Audio"),
|
||||
)
|
||||
|
||||
ENCODE_EXTENSIONS = (
|
||||
("mp4", "mp4"),
|
||||
("webm", "webm"),
|
||||
("gif", "gif"),
|
||||
)
|
||||
|
||||
ENCODE_RESOLUTIONS = (
|
||||
(2160, "2160"),
|
||||
(1440, "1440"),
|
||||
(1080, "1080"),
|
||||
(720, "720"),
|
||||
(480, "480"),
|
||||
(360, "360"),
|
||||
(240, "240"),
|
||||
(144, "144"),
|
||||
)
|
||||
|
||||
CODECS = (
|
||||
("h265", "h265"),
|
||||
("h264", "h264"),
|
||||
("vp9", "vp9"),
|
||||
)
|
||||
|
||||
ENCODE_EXTENSIONS_KEYS = [extension for extension, name in ENCODE_EXTENSIONS]
|
||||
ENCODE_RESOLUTIONS_KEYS = [resolution for resolution, name in ENCODE_RESOLUTIONS]
|
||||
|
||||
|
||||
def generate_uid():
|
||||
return get_random_string(length=16)
|
||||
|
||||
|
||||
def original_media_file_path(instance, filename):
|
||||
"""Helper function to place original media file"""
|
||||
file_name = f"{instance.uid.hex}.{helpers.get_file_name(filename)}"
|
||||
return settings.MEDIA_UPLOAD_DIR + f"user/{instance.user.username}/{file_name}"
|
||||
|
||||
|
||||
def encoding_media_file_path(instance, filename):
|
||||
"""Helper function to place encoded media file"""
|
||||
|
||||
file_name = f"{instance.media.uid.hex}.{helpers.get_file_name(filename)}"
|
||||
return settings.MEDIA_ENCODING_DIR + f"{instance.profile.id}/{instance.media.user.username}/{file_name}"
|
||||
|
||||
|
||||
def original_thumbnail_file_path(instance, filename):
|
||||
"""Helper function to place original media thumbnail file"""
|
||||
|
||||
return settings.THUMBNAIL_UPLOAD_DIR + f"user/{instance.user.username}/{filename}"
|
||||
|
||||
|
||||
def subtitles_file_path(instance, filename):
|
||||
"""Helper function to place subtitle file"""
|
||||
|
||||
return settings.SUBTITLES_UPLOAD_DIR + f"user/{instance.media.user.username}/{filename}"
|
||||
|
||||
|
||||
def category_thumb_path(instance, filename):
|
||||
"""Helper function to place category thumbnail file"""
|
||||
|
||||
file_name = f"{instance.uid}.{helpers.get_file_name(filename)}"
|
||||
return settings.MEDIA_UPLOAD_DIR + f"categories/{file_name}"
|
||||
|
||||
|
||||
def validate_rating(value):
|
||||
if -1 >= value or value > 5:
|
||||
raise ValidationError("score has to be between 0 and 5")
|
86
files/models/video_data.py
Normal file
86
files/models/video_data.py
Normal file
@@ -0,0 +1,86 @@
|
||||
from django.db import models
|
||||
from django.db.models.signals import post_delete
|
||||
from django.dispatch import receiver
|
||||
|
||||
from .. import helpers
|
||||
|
||||
|
||||
class VideoChapterData(models.Model):
|
||||
data = models.JSONField(null=False, blank=False, help_text="Chapter data")
|
||||
media = models.ForeignKey('Media', on_delete=models.CASCADE, related_name='chapters')
|
||||
|
||||
class Meta:
|
||||
unique_together = ['media']
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
from .. import tasks
|
||||
|
||||
is_new = self.pk is None
|
||||
if is_new or (not is_new and self._check_data_changed()):
|
||||
super().save(*args, **kwargs)
|
||||
tasks.produce_video_chapters.delay(self.pk)
|
||||
else:
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def _check_data_changed(self):
|
||||
if self.pk:
|
||||
old_instance = VideoChapterData.objects.get(pk=self.pk)
|
||||
return old_instance.data != self.data
|
||||
return False
|
||||
|
||||
@property
|
||||
def chapter_data(self):
|
||||
# ensure response is consistent
|
||||
data = []
|
||||
for item in self.data:
|
||||
if item.get("start") and item.get("title"):
|
||||
thumbnail = item.get("thumbnail")
|
||||
if thumbnail:
|
||||
thumbnail = helpers.url_from_path(thumbnail)
|
||||
else:
|
||||
thumbnail = "static/images/chapter_default.jpg"
|
||||
data.append(
|
||||
{
|
||||
"start": item.get("start"),
|
||||
"title": item.get("title"),
|
||||
"thumbnail": thumbnail,
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
class VideoTrimRequest(models.Model):
|
||||
"""Model to handle video trimming requests"""
|
||||
|
||||
VIDEO_TRIM_STATUS = (
|
||||
("initial", "Initial"),
|
||||
("running", "Running"),
|
||||
("success", "Success"),
|
||||
("fail", "Fail"),
|
||||
)
|
||||
|
||||
VIDEO_ACTION_CHOICES = (
|
||||
("replace", "Replace Original"),
|
||||
("save_new", "Save as New"),
|
||||
("create_segments", "Create Segments"),
|
||||
)
|
||||
|
||||
TRIM_STYLE_CHOICES = (
|
||||
("no_encoding", "No Encoding"),
|
||||
("precise", "Precise"),
|
||||
)
|
||||
|
||||
media = models.ForeignKey('Media', on_delete=models.CASCADE, related_name='trim_requests')
|
||||
status = models.CharField(max_length=20, choices=VIDEO_TRIM_STATUS, default="initial")
|
||||
add_date = models.DateTimeField(auto_now_add=True)
|
||||
video_action = models.CharField(max_length=20, choices=VIDEO_ACTION_CHOICES)
|
||||
media_trim_style = models.CharField(max_length=20, choices=TRIM_STYLE_CHOICES, default="no_encoding")
|
||||
timestamps = models.JSONField(null=False, blank=False, help_text="Timestamps for trimming")
|
||||
|
||||
def __str__(self):
|
||||
return f"Trim request for {self.media.title} ({self.status})"
|
||||
|
||||
|
||||
@receiver(post_delete, sender=VideoChapterData)
|
||||
def videochapterdata_delete(sender, instance, **kwargs):
|
||||
helpers.rm_dir(instance.media.video_chapters_folder)
|
@@ -820,7 +820,7 @@ def update_listings_thumbnails():
|
||||
# Categories
|
||||
used_media = []
|
||||
saved = 0
|
||||
qs = Category.objects.filter().order_by("-media_count")
|
||||
qs = Category.objects.filter()
|
||||
for object in qs:
|
||||
media = Media.objects.exclude(friendly_token__in=used_media).filter(category=object, state="public", is_reviewed=True).order_by("-views").first()
|
||||
if media:
|
||||
@@ -833,7 +833,7 @@ def update_listings_thumbnails():
|
||||
# Tags
|
||||
used_media = []
|
||||
saved = 0
|
||||
qs = Tag.objects.filter().order_by("-media_count")
|
||||
qs = Tag.objects.filter()
|
||||
for object in qs:
|
||||
media = Media.objects.exclude(friendly_token__in=used_media).filter(tags=object, state="public", is_reviewed=True).order_by("-views").first()
|
||||
if media:
|
||||
|
@@ -48,6 +48,8 @@ urlpatterns = [
|
||||
re_path(r"^view", views.view_media, name="get_media"),
|
||||
re_path(r"^upload", views.upload_media, name="upload_media"),
|
||||
# API VIEWS
|
||||
re_path(r"^api/v1/media/user/bulk_actions$", views.MediaBulkUserActions.as_view()),
|
||||
re_path(r"^api/v1/media/user/bulk_actions/$", views.MediaBulkUserActions.as_view()),
|
||||
re_path(r"^api/v1/media$", views.MediaList.as_view()),
|
||||
re_path(r"^api/v1/media/$", views.MediaList.as_view()),
|
||||
re_path(
|
||||
|
1744
files/views.py
1744
files/views.py
File diff suppressed because it is too large
Load Diff
43
files/views/__init__.py
Normal file
43
files/views/__init__.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# Import all views for backward compatibility
|
||||
from .auth import custom_login_view, saml_metadata # noqa: F401
|
||||
from .categories import CategoryList, TagList # noqa: F401
|
||||
from .comments import CommentDetail, CommentList # noqa: F401
|
||||
from .encoding import EncodeProfileList, EncodingDetail # noqa: F401
|
||||
from .media import MediaActions # noqa: F401
|
||||
from .media import MediaBulkUserActions # noqa: F401
|
||||
from .media import MediaDetail # noqa: F401
|
||||
from .media import MediaList # noqa: F401
|
||||
from .media import MediaSearch # noqa: F401
|
||||
from .pages import about # noqa: F401
|
||||
from .pages import add_subtitle # noqa: F401
|
||||
from .pages import categories # noqa: F401
|
||||
from .pages import contact # noqa: F401
|
||||
from .pages import edit_chapters # noqa: F401
|
||||
from .pages import edit_media # noqa: F401
|
||||
from .pages import edit_subtitle # noqa: F401
|
||||
from .pages import edit_video # noqa: F401
|
||||
from .pages import embed_media # noqa: F401
|
||||
from .pages import featured_media # noqa: F401
|
||||
from .pages import history # noqa: F401
|
||||
from .pages import index # noqa: F401
|
||||
from .pages import latest_media # noqa: F401
|
||||
from .pages import liked_media # noqa: F401
|
||||
from .pages import manage_comments # noqa: F401
|
||||
from .pages import manage_media # noqa: F401
|
||||
from .pages import manage_users # noqa: F401
|
||||
from .pages import members # noqa: F401
|
||||
from .pages import publish_media # noqa: F401
|
||||
from .pages import recommended_media # noqa: F401
|
||||
from .pages import search # noqa: F401
|
||||
from .pages import setlanguage # noqa: F401
|
||||
from .pages import sitemap # noqa: F401
|
||||
from .pages import tags # noqa: F401
|
||||
from .pages import tos # noqa: F401
|
||||
from .pages import trim_video # noqa: F401
|
||||
from .pages import upload_media # noqa: F401
|
||||
from .pages import video_chapters # noqa: F401
|
||||
from .pages import view_media # noqa: F401
|
||||
from .pages import view_playlist # noqa: F401
|
||||
from .playlists import PlaylistDetail, PlaylistList # noqa: F401
|
||||
from .tasks import TaskDetail, TasksList # noqa: F401
|
||||
from .user import UserActions # noqa: F401
|
42
files/views/auth.py
Normal file
42
files/views/auth.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from allauth.socialaccount.models import SocialApp
|
||||
from django.conf import settings
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import reverse
|
||||
|
||||
from identity_providers.models import LoginOption
|
||||
|
||||
|
||||
def saml_metadata(request):
|
||||
if not (hasattr(settings, "USE_SAML") and settings.USE_SAML):
|
||||
raise Http404
|
||||
|
||||
xml_parts = ['<?xml version="1.0"?>']
|
||||
saml_social_apps = SocialApp.objects.filter(provider='saml')
|
||||
entity_id = f"{settings.FRONTEND_HOST}/saml/metadata/"
|
||||
xml_parts.append(f'<md:EntitiesDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" Name="{entity_id}">') # noqa
|
||||
xml_parts.append(f' <md:EntityDescriptor entityID="{entity_id}">') # noqa
|
||||
xml_parts.append(' <md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">') # noqa
|
||||
|
||||
# Add multiple AssertionConsumerService elements with different indices
|
||||
for index, app in enumerate(saml_social_apps, start=1):
|
||||
xml_parts.append(
|
||||
f' <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" ' # noqa
|
||||
f'Location="{settings.FRONTEND_HOST}/accounts/saml/{app.client_id}/acs/" index="{index}"/>' # noqa
|
||||
)
|
||||
|
||||
xml_parts.append(' </md:SPSSODescriptor>') # noqa
|
||||
xml_parts.append(' </md:EntityDescriptor>') # noqa
|
||||
xml_parts.append('</md:EntitiesDescriptor>') # noqa
|
||||
metadata_xml = '\n'.join(xml_parts)
|
||||
return HttpResponse(metadata_xml, content_type='application/xml')
|
||||
|
||||
|
||||
def custom_login_view(request):
|
||||
if not (hasattr(settings, "USE_IDENTITY_PROVIDERS") and settings.USE_IDENTITY_PROVIDERS):
|
||||
return redirect(reverse('login_system'))
|
||||
|
||||
login_options = []
|
||||
for option in LoginOption.objects.filter(active=True):
|
||||
login_options.append({'url': option.url, 'title': option.title})
|
||||
return render(request, 'account/custom_login_selector.html', {'login_options': login_options})
|
66
files/views/categories.py
Normal file
66
files/views/categories.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from django.conf import settings
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from ..methods import is_mediacms_editor
|
||||
from ..models import Category, Tag
|
||||
from ..serializers import CategorySerializer, TagSerializer
|
||||
|
||||
|
||||
class CategoryList(APIView):
|
||||
"""List categories"""
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[],
|
||||
tags=['Categories'],
|
||||
operation_summary='Lists Categories',
|
||||
operation_description='Lists all categories',
|
||||
responses={
|
||||
200: openapi.Response('response description', CategorySerializer),
|
||||
},
|
||||
)
|
||||
def get(self, request, format=None):
|
||||
base_filters = {}
|
||||
|
||||
if not is_mediacms_editor(request.user):
|
||||
base_filters = {"is_rbac_category": False}
|
||||
|
||||
base_queryset = Category.objects.prefetch_related("user")
|
||||
categories = base_queryset.filter(**base_filters)
|
||||
|
||||
if not is_mediacms_editor(request.user):
|
||||
if getattr(settings, 'USE_RBAC', False) and request.user.is_authenticated:
|
||||
rbac_categories = request.user.get_rbac_categories_as_member()
|
||||
categories = categories.union(rbac_categories)
|
||||
|
||||
categories = categories.order_by("title")
|
||||
|
||||
serializer = CategorySerializer(categories, many=True, context={"request": request})
|
||||
ret = serializer.data
|
||||
return Response(ret)
|
||||
|
||||
|
||||
class TagList(APIView):
|
||||
"""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',
|
||||
responses={
|
||||
200: openapi.Response('response description', TagSerializer),
|
||||
},
|
||||
)
|
||||
def get(self, request, format=None):
|
||||
tags = Tag.objects.filter().order_by("-media_count")
|
||||
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
|
||||
paginator = pagination_class()
|
||||
page = paginator.paginate_queryset(tags, request)
|
||||
serializer = TagSerializer(page, many=True, context={"request": request})
|
||||
return paginator.get_paginated_response(serializer.data)
|
159
files/views/comments.py
Normal file
159
files/views/comments.py
Normal file
@@ -0,0 +1,159 @@
|
||||
from django.conf import settings
|
||||
from django.shortcuts import get_object_or_404
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.parsers import (
|
||||
FileUploadParser,
|
||||
FormParser,
|
||||
JSONParser,
|
||||
MultiPartParser,
|
||||
)
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from cms.permissions import IsAuthorizedToAdd, IsAuthorizedToAddComment
|
||||
from users.models import User
|
||||
|
||||
from ..methods import (
|
||||
check_comment_for_mention,
|
||||
is_mediacms_editor,
|
||||
notify_user_on_comment,
|
||||
)
|
||||
from ..models import Comment, Media
|
||||
from ..serializers import CommentSerializer
|
||||
|
||||
|
||||
class CommentList(APIView):
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsAuthorizedToAdd)
|
||||
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',
|
||||
responses={
|
||||
200: openapi.Response('response description', CommentSerializer(many=True)),
|
||||
},
|
||||
)
|
||||
def get(self, request, format=None):
|
||||
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
|
||||
paginator = pagination_class()
|
||||
comments = Comment.objects.filter(media__state="public").order_by("-add_date")
|
||||
comments = comments.prefetch_related("user")
|
||||
comments = comments.prefetch_related("media")
|
||||
params = self.request.query_params
|
||||
if "author" in params:
|
||||
author_param = params["author"].strip()
|
||||
user_queryset = User.objects.all()
|
||||
user = get_object_or_404(user_queryset, username=author_param)
|
||||
comments = comments.filter(user=user)
|
||||
|
||||
page = paginator.paginate_queryset(comments, request)
|
||||
|
||||
serializer = CommentSerializer(page, many=True, context={"request": request})
|
||||
return paginator.get_paginated_response(serializer.data)
|
||||
|
||||
|
||||
class CommentDetail(APIView):
|
||||
"""Comments related views
|
||||
Listings of comments for a media (GET)
|
||||
Create comment (POST)
|
||||
Delete comment (DELETE)
|
||||
"""
|
||||
|
||||
permission_classes = (IsAuthorizedToAddComment,)
|
||||
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)
|
||||
|
||||
def get_object(self, friendly_token):
|
||||
try:
|
||||
media = Media.objects.select_related("user").get(friendly_token=friendly_token)
|
||||
self.check_object_permissions(self.request, media)
|
||||
if media.state == "private" and self.request.user != media.user:
|
||||
return Response({"detail": "media is private"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return media
|
||||
except PermissionDenied:
|
||||
return Response({"detail": "bad permissions"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except BaseException:
|
||||
return Response(
|
||||
{"detail": "media file does not exist"},
|
||||
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):
|
||||
# list comments for a media
|
||||
media = self.get_object(friendly_token)
|
||||
if isinstance(media, Response):
|
||||
return media
|
||||
comments = media.comments.filter().prefetch_related("user")
|
||||
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
|
||||
paginator = pagination_class()
|
||||
page = paginator.paginate_queryset(comments, request)
|
||||
serializer = CommentSerializer(page, many=True, context={"request": request})
|
||||
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):
|
||||
"""Delete a comment
|
||||
Administrators, MediaCMS editors and managers,
|
||||
media owner, and comment owners, can delete a comment
|
||||
"""
|
||||
if uid:
|
||||
try:
|
||||
comment = Comment.objects.get(uid=uid)
|
||||
except BaseException:
|
||||
return Response(
|
||||
{"detail": "comment does not exist"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
if (comment.user == self.request.user) or comment.media.user == self.request.user or is_mediacms_editor(self.request.user):
|
||||
comment.delete()
|
||||
else:
|
||||
return Response({"detail": "bad permissions"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
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):
|
||||
"""Create a comment"""
|
||||
media = self.get_object(friendly_token)
|
||||
if isinstance(media, Response):
|
||||
return media
|
||||
|
||||
if not media.enable_comments:
|
||||
return Response(
|
||||
{"detail": "comments not allowed here"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
serializer = CommentSerializer(data=request.data, context={"request": request})
|
||||
if serializer.is_valid():
|
||||
serializer.save(user=request.user, media=media)
|
||||
if request.user != media.user:
|
||||
notify_user_on_comment(friendly_token=media.friendly_token)
|
||||
# here forward the comment to check if a user was mentioned
|
||||
if settings.ALLOW_MENTION_IN_COMMENTS:
|
||||
check_comment_for_mention(friendly_token=media.friendly_token, comment_text=serializer.data['text'])
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
179
files/views/encoding.py
Normal file
179
files/views/encoding.py
Normal file
@@ -0,0 +1,179 @@
|
||||
from django.conf import settings
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.parsers import (
|
||||
FileUploadParser,
|
||||
FormParser,
|
||||
JSONParser,
|
||||
MultiPartParser,
|
||||
)
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from ..helpers import produce_ffmpeg_commands
|
||||
from ..models import EncodeProfile, Encoding
|
||||
from ..serializers import EncodeProfileSerializer
|
||||
|
||||
|
||||
class EncodingDetail(APIView):
|
||||
"""Experimental. This View is used by remote workers
|
||||
Needs heavy testing and documentation.
|
||||
"""
|
||||
|
||||
permission_classes = (permissions.IsAdminUser,)
|
||||
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)
|
||||
|
||||
@swagger_auto_schema(auto_schema=None)
|
||||
def post(self, request, encoding_id):
|
||||
ret = {}
|
||||
force = request.data.get("force", False)
|
||||
task_id = request.data.get("task_id", False)
|
||||
action = request.data.get("action", "")
|
||||
chunk = request.data.get("chunk", False)
|
||||
chunk_file_path = request.data.get("chunk_file_path", "")
|
||||
|
||||
encoding_status = request.data.get("status", "")
|
||||
progress = request.data.get("progress", "")
|
||||
commands = request.data.get("commands", "")
|
||||
logs = request.data.get("logs", "")
|
||||
retries = request.data.get("retries", "")
|
||||
worker = request.data.get("worker", "")
|
||||
temp_file = request.data.get("temp_file", "")
|
||||
total_run_time = request.data.get("total_run_time", "")
|
||||
if action == "start":
|
||||
try:
|
||||
encoding = Encoding.objects.get(id=encoding_id)
|
||||
media = encoding.media
|
||||
profile = encoding.profile
|
||||
except BaseException:
|
||||
Encoding.objects.filter(id=encoding_id).delete()
|
||||
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
# TODO: break chunk True/False logic here
|
||||
if (
|
||||
Encoding.objects.filter(
|
||||
media=media,
|
||||
profile=profile,
|
||||
chunk=chunk,
|
||||
chunk_file_path=chunk_file_path,
|
||||
).count()
|
||||
> 1 # noqa
|
||||
and force is False # noqa
|
||||
):
|
||||
Encoding.objects.filter(id=encoding_id).delete()
|
||||
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
Encoding.objects.filter(
|
||||
media=media,
|
||||
profile=profile,
|
||||
chunk=chunk,
|
||||
chunk_file_path=chunk_file_path,
|
||||
).exclude(id=encoding.id).delete()
|
||||
|
||||
encoding.status = "running"
|
||||
if task_id:
|
||||
encoding.task_id = task_id
|
||||
|
||||
encoding.save()
|
||||
if chunk:
|
||||
original_media_path = chunk_file_path
|
||||
original_media_md5sum = encoding.md5sum
|
||||
original_media_url = settings.SSL_FRONTEND_HOST + encoding.media_chunk_url
|
||||
else:
|
||||
original_media_path = media.media_file.path
|
||||
original_media_md5sum = media.md5sum
|
||||
original_media_url = settings.SSL_FRONTEND_HOST + media.original_media_url
|
||||
|
||||
ret["original_media_url"] = original_media_url
|
||||
ret["original_media_path"] = original_media_path
|
||||
ret["original_media_md5sum"] = original_media_md5sum
|
||||
|
||||
# generating the commands here, and will replace these with temporary
|
||||
# files created on the remote server
|
||||
tf = "TEMP_FILE_REPLACE"
|
||||
tfpass = "TEMP_FPASS_FILE_REPLACE"
|
||||
ffmpeg_commands = produce_ffmpeg_commands(
|
||||
original_media_path,
|
||||
media.media_info,
|
||||
resolution=profile.resolution,
|
||||
codec=profile.codec,
|
||||
output_filename=tf,
|
||||
pass_file=tfpass,
|
||||
chunk=chunk,
|
||||
)
|
||||
if not ffmpeg_commands:
|
||||
encoding.delete()
|
||||
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
ret["duration"] = media.duration
|
||||
ret["ffmpeg_commands"] = ffmpeg_commands
|
||||
ret["profile_extension"] = profile.extension
|
||||
return Response(ret, status=status.HTTP_201_CREATED)
|
||||
elif action == "update_fields":
|
||||
try:
|
||||
encoding = Encoding.objects.get(id=encoding_id)
|
||||
except BaseException:
|
||||
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
to_update = ["size", "update_date"]
|
||||
if encoding_status:
|
||||
encoding.status = encoding_status
|
||||
to_update.append("status")
|
||||
if progress:
|
||||
encoding.progress = progress
|
||||
to_update.append("progress")
|
||||
if logs:
|
||||
encoding.logs = logs
|
||||
to_update.append("logs")
|
||||
if commands:
|
||||
encoding.commands = commands
|
||||
to_update.append("commands")
|
||||
if task_id:
|
||||
encoding.task_id = task_id
|
||||
to_update.append("task_id")
|
||||
if total_run_time:
|
||||
encoding.total_run_time = total_run_time
|
||||
to_update.append("total_run_time")
|
||||
if worker:
|
||||
encoding.worker = worker
|
||||
to_update.append("worker")
|
||||
if temp_file:
|
||||
encoding.temp_file = temp_file
|
||||
to_update.append("temp_file")
|
||||
|
||||
if retries:
|
||||
encoding.retries = retries
|
||||
to_update.append("retries")
|
||||
|
||||
try:
|
||||
encoding.save(update_fields=to_update)
|
||||
except BaseException:
|
||||
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response({"status": "success"}, status=status.HTTP_201_CREATED)
|
||||
|
||||
@swagger_auto_schema(auto_schema=None)
|
||||
def put(self, request, encoding_id, format=None):
|
||||
encoding_file = request.data["file"]
|
||||
encoding = Encoding.objects.filter(id=encoding_id).first()
|
||||
if not encoding:
|
||||
return Response(
|
||||
{"detail": "encoding does not exist"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
encoding.media_file = encoding_file
|
||||
encoding.save()
|
||||
return Response({"detail": "ok"}, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class EncodeProfileList(APIView):
|
||||
"""List encode profiles"""
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[],
|
||||
tags=['Encoding Profiles'],
|
||||
operation_summary='List Encoding Profiles',
|
||||
operation_description='Lists all encoding profiles for videos',
|
||||
responses={200: EncodeProfileSerializer(many=True)},
|
||||
)
|
||||
def get(self, request, format=None):
|
||||
profiles = EncodeProfile.objects.all()
|
||||
serializer = EncodeProfileSerializer(profiles, many=True, context={"request": request})
|
||||
return Response(serializer.data)
|
763
files/views/media.py
Normal file
763
files/views/media.py
Normal file
@@ -0,0 +1,763 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.postgres.search import SearchQuery
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.parsers import (
|
||||
FileUploadParser,
|
||||
FormParser,
|
||||
JSONParser,
|
||||
MultiPartParser,
|
||||
)
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from actions.models import MediaAction
|
||||
from cms.custom_pagination import FastPaginationWithoutCount
|
||||
from cms.permissions import IsAuthorizedToAdd, IsUserOrEditor
|
||||
from users.models import User
|
||||
|
||||
from .. import helpers
|
||||
from ..methods import (
|
||||
change_media_owner,
|
||||
copy_media,
|
||||
get_user_or_session,
|
||||
is_mediacms_editor,
|
||||
show_recommended_media,
|
||||
show_related_media,
|
||||
update_user_ratings,
|
||||
)
|
||||
from ..models import EncodeProfile, Media, MediaPermission, Playlist, PlaylistMedia
|
||||
from ..serializers import MediaSearchSerializer, MediaSerializer, SingleMediaSerializer
|
||||
from ..stop_words import STOP_WORDS
|
||||
from ..tasks import save_user_action
|
||||
|
||||
|
||||
class MediaList(APIView):
|
||||
"""Media listings views"""
|
||||
|
||||
permission_classes = (IsAuthorizedToAdd,)
|
||||
parser_classes = (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'),
|
||||
openapi.Parameter(name='show', type=openapi.TYPE_STRING, in_=openapi.IN_QUERY, description='show', enum=['recommended', 'featured', 'latest']),
|
||||
],
|
||||
tags=['Media'],
|
||||
operation_summary='List Media',
|
||||
operation_description='Lists all media',
|
||||
responses={200: MediaSerializer(many=True)},
|
||||
)
|
||||
def _get_media_queryset(self, request, user=None):
|
||||
base_filters = Q(listable=True)
|
||||
if user:
|
||||
base_filters &= Q(user=user)
|
||||
|
||||
base_queryset = Media.objects.prefetch_related("user")
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
return base_queryset.filter(base_filters).order_by("-add_date")
|
||||
|
||||
# Build OR conditions for authenticated users
|
||||
conditions = base_filters # Start with listable media
|
||||
|
||||
# Add user permissions
|
||||
permission_filter = {'user': request.user}
|
||||
if user:
|
||||
permission_filter['owner_user'] = user
|
||||
|
||||
if MediaPermission.objects.filter(**permission_filter).exists():
|
||||
perm_conditions = Q(permissions__user=request.user)
|
||||
if user:
|
||||
perm_conditions &= Q(user=user)
|
||||
conditions |= perm_conditions
|
||||
|
||||
# Add RBAC conditions
|
||||
if getattr(settings, 'USE_RBAC', False):
|
||||
rbac_categories = request.user.get_rbac_categories_as_member()
|
||||
rbac_conditions = Q(category__in=rbac_categories)
|
||||
if user:
|
||||
rbac_conditions &= Q(user=user)
|
||||
conditions |= rbac_conditions
|
||||
|
||||
return base_queryset.filter(conditions).distinct().order_by("-add_date")[:1000]
|
||||
|
||||
def get(self, request, format=None):
|
||||
# Show media
|
||||
# authenticated users can see:
|
||||
|
||||
# All listable media (public access)
|
||||
# Non-listable media they have RBAC access to
|
||||
# Non-listable media they have direct permissions for
|
||||
|
||||
params = self.request.query_params
|
||||
show_param = params.get("show", "")
|
||||
|
||||
author_param = params.get("author", "").strip()
|
||||
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
|
||||
|
||||
if show_param == "recommended":
|
||||
pagination_class = FastPaginationWithoutCount
|
||||
media = show_recommended_media(request, limit=50)
|
||||
elif show_param == "featured":
|
||||
media = Media.objects.filter(listable=True, featured=True).prefetch_related("user").order_by("-add_date")
|
||||
elif show_param == "shared_by_me":
|
||||
if not self.request.user.is_authenticated:
|
||||
media = Media.objects.none()
|
||||
else:
|
||||
media = Media.objects.filter(permissions__owner_user=self.request.user).prefetch_related("user")
|
||||
elif show_param == "shared_with_me":
|
||||
if not self.request.user.is_authenticated:
|
||||
media = Media.objects.none()
|
||||
else:
|
||||
base_queryset = Media.objects.prefetch_related("user")
|
||||
user_media_filters = {'permissions__user': request.user}
|
||||
media = base_queryset.filter(**user_media_filters)
|
||||
|
||||
if getattr(settings, 'USE_RBAC', False):
|
||||
rbac_categories = request.user.get_rbac_categories_as_member()
|
||||
rbac_filters = {'category__in': rbac_categories}
|
||||
|
||||
rbac_media = base_queryset.filter(**rbac_filters)
|
||||
media = media.union(rbac_media)
|
||||
media = media.order_by("-add_date")[:1000] # limit to 1000 results
|
||||
elif author_param:
|
||||
user_queryset = User.objects.all()
|
||||
user = get_object_or_404(user_queryset, username=author_param)
|
||||
if self.request.user == user:
|
||||
media = Media.objects.filter(user=user).prefetch_related("user").order_by("-add_date")
|
||||
else:
|
||||
media = self._get_media_queryset(request, user)
|
||||
else:
|
||||
media = self._get_media_queryset(request)
|
||||
|
||||
paginator = pagination_class()
|
||||
|
||||
page = paginator.paginate_queryset(media, request)
|
||||
|
||||
serializer = MediaSerializer(page, many=True, context={"request": request})
|
||||
return paginator.get_paginated_response(serializer.data)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[
|
||||
openapi.Parameter(name="media_file", in_=openapi.IN_FORM, type=openapi.TYPE_FILE, required=True, description="media_file"),
|
||||
openapi.Parameter(name="description", in_=openapi.IN_FORM, type=openapi.TYPE_STRING, required=False, description="description"),
|
||||
openapi.Parameter(name="title", in_=openapi.IN_FORM, type=openapi.TYPE_STRING, required=False, description="title"),
|
||||
],
|
||||
tags=['Media'],
|
||||
operation_summary='Add new Media',
|
||||
operation_description='Adds a new media, for authenticated users',
|
||||
responses={201: openapi.Response('response description', MediaSerializer), 401: 'bad request'},
|
||||
)
|
||||
def post(self, request, format=None):
|
||||
# Add new media
|
||||
serializer = MediaSerializer(data=request.data, context={"request": request})
|
||||
if serializer.is_valid():
|
||||
media_file = request.data["media_file"]
|
||||
serializer.save(user=request.user, media_file=media_file)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class MediaBulkUserActions(APIView):
|
||||
"""Bulk actions on media items"""
|
||||
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
parser_classes = (JSONParser,)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[
|
||||
openapi.Parameter(name='media_ids', in_=openapi.IN_FORM, type=openapi.TYPE_ARRAY, items=openapi.Items(type=openapi.TYPE_STRING), required=True, description="List of media IDs"),
|
||||
openapi.Parameter(
|
||||
name='action',
|
||||
in_=openapi.IN_FORM,
|
||||
type=openapi.TYPE_STRING,
|
||||
required=True,
|
||||
description="Action to perform",
|
||||
enum=[
|
||||
"enable_comments",
|
||||
"disable_comments",
|
||||
"delete_media",
|
||||
"enable_download",
|
||||
"disable_download",
|
||||
"add_to_playlist",
|
||||
"remove_from_playlist",
|
||||
"set_state",
|
||||
"change_owner",
|
||||
"copy_media",
|
||||
],
|
||||
),
|
||||
openapi.Parameter(
|
||||
name='playlist_ids',
|
||||
in_=openapi.IN_FORM,
|
||||
type=openapi.TYPE_ARRAY,
|
||||
items=openapi.Items(type=openapi.TYPE_INTEGER),
|
||||
required=False,
|
||||
description="List of playlist IDs (required for add_to_playlist and remove_from_playlist actions)",
|
||||
),
|
||||
openapi.Parameter(
|
||||
name='state', in_=openapi.IN_FORM, type=openapi.TYPE_STRING, required=False, description="State to set (required for set_state action)", enum=["private", "public", "unlisted"]
|
||||
),
|
||||
openapi.Parameter(name='owner', in_=openapi.IN_FORM, type=openapi.TYPE_STRING, required=False, description="New owner username (required for change_owner action)"),
|
||||
],
|
||||
tags=['Media'],
|
||||
operation_summary='Perform bulk actions on media',
|
||||
operation_description='Perform various bulk actions on multiple media items at once',
|
||||
responses={
|
||||
200: openapi.Response('Action performed successfully'),
|
||||
400: 'Bad request',
|
||||
401: 'Not authenticated',
|
||||
},
|
||||
)
|
||||
def post(self, request, format=None):
|
||||
# Check if user is authenticated
|
||||
if not request.user.is_authenticated:
|
||||
return Response({"detail": "Authentication required"}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# Get required parameters
|
||||
media_ids = request.data.get('media_ids', [])
|
||||
action = request.data.get('action')
|
||||
|
||||
# Validate required parameters
|
||||
if not media_ids:
|
||||
return Response({"detail": "media_ids is required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if not action:
|
||||
return Response({"detail": "action is required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Get media objects owned by the user
|
||||
media = Media.objects.filter(user=request.user, friendly_token__in=media_ids)
|
||||
|
||||
if not media:
|
||||
return Response({"detail": "No matching media found"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Process based on action
|
||||
if action == "enable_comments":
|
||||
media.update(enable_comments=True)
|
||||
return Response({"detail": f"Comments enabled for {media.count()} media items"})
|
||||
|
||||
elif action == "disable_comments":
|
||||
media.update(enable_comments=False)
|
||||
return Response({"detail": f"Comments disabled for {media.count()} media items"})
|
||||
|
||||
elif action == "delete_media":
|
||||
count = media.count()
|
||||
media.delete()
|
||||
return Response({"detail": f"{count} media items deleted"})
|
||||
|
||||
elif action == "enable_download":
|
||||
media.update(allow_download=True)
|
||||
return Response({"detail": f"Download enabled for {media.count()} media items"})
|
||||
|
||||
elif action == "disable_download":
|
||||
media.update(allow_download=False)
|
||||
return Response({"detail": f"Download disabled for {media.count()} media items"})
|
||||
|
||||
elif action == "add_to_playlist":
|
||||
playlist_ids = request.data.get('playlist_ids', [])
|
||||
if not playlist_ids:
|
||||
return Response({"detail": "playlist_ids is required for add_to_playlist action"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
playlists = Playlist.objects.filter(user=request.user, id__in=playlist_ids)
|
||||
if not playlists:
|
||||
return Response({"detail": "No matching playlists found"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
added_count = 0
|
||||
for playlist in playlists:
|
||||
for m in media:
|
||||
media_in_playlist = PlaylistMedia.objects.filter(playlist=playlist).count()
|
||||
if media_in_playlist < settings.MAX_MEDIA_PER_PLAYLIST:
|
||||
obj, created = PlaylistMedia.objects.get_or_create(
|
||||
playlist=playlist,
|
||||
media=m,
|
||||
ordering=media_in_playlist + 1,
|
||||
)
|
||||
if created:
|
||||
added_count += 1
|
||||
|
||||
return Response({"detail": f"Added {added_count} media items to {playlists.count()} playlists"})
|
||||
|
||||
elif action == "remove_from_playlist":
|
||||
playlist_ids = request.data.get('playlist_ids', [])
|
||||
if not playlist_ids:
|
||||
return Response({"detail": "playlist_ids is required for remove_from_playlist action"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
playlists = Playlist.objects.filter(user=request.user, id__in=playlist_ids)
|
||||
if not playlists:
|
||||
return Response({"detail": "No matching playlists found"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
removed_count = 0
|
||||
for playlist in playlists:
|
||||
removed = PlaylistMedia.objects.filter(playlist=playlist, media__in=media).delete()[0]
|
||||
removed_count += removed
|
||||
|
||||
return Response({"detail": f"Removed {removed_count} media items from {playlists.count()} playlists"})
|
||||
|
||||
elif action == "set_state":
|
||||
state = request.data.get('state')
|
||||
if not state:
|
||||
return Response({"detail": "state is required for set_state action"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
valid_states = ["private", "public", "unlisted"]
|
||||
if state not in valid_states:
|
||||
return Response({"detail": f"state must be one of {valid_states}"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Check if user can set public state
|
||||
if not is_mediacms_editor(request.user) and settings.PORTAL_WORKFLOW != "public":
|
||||
if state == "public":
|
||||
return Response({"detail": "You are not allowed to set media to public state"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Update media state
|
||||
for m in media:
|
||||
m.state = state
|
||||
if m.state == "public" and m.encoding_status == "success" and m.is_reviewed is True:
|
||||
m.listable = True
|
||||
else:
|
||||
m.listable = False
|
||||
|
||||
m.save(update_fields=["state", "listable"])
|
||||
|
||||
return Response({"detail": f"State updated to {state} for {media.count()} media items"})
|
||||
|
||||
elif action == "change_owner":
|
||||
owner = request.data.get('owner')
|
||||
if not owner:
|
||||
return Response({"detail": "owner is required for change_owner action"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
new_user = User.objects.filter(username=owner).first()
|
||||
if not new_user:
|
||||
return Response({"detail": "User not found"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
changed_count = 0
|
||||
for m in media:
|
||||
result = change_media_owner(m.id, new_user)
|
||||
if result:
|
||||
changed_count += 1
|
||||
|
||||
return Response({"detail": f"Owner changed for {changed_count} media items"})
|
||||
|
||||
elif action == "copy_media":
|
||||
for m in media:
|
||||
copy_media(m.id)
|
||||
|
||||
return Response({"detail": f"{media.count()} media items copied"})
|
||||
|
||||
else:
|
||||
return Response({"detail": f"Unknown action: {action}"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class MediaDetail(APIView):
|
||||
"""
|
||||
Retrieve, update or delete a media instance.
|
||||
"""
|
||||
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsUserOrEditor)
|
||||
parser_classes = (MultiPartParser, FormParser, FileUploadParser)
|
||||
|
||||
def get_object(self, friendly_token):
|
||||
try:
|
||||
media = Media.objects.select_related("user").prefetch_related("encodings__profile").get(friendly_token=friendly_token)
|
||||
|
||||
# this need be explicitly called, and will call
|
||||
# has_object_permission() after has_permission has succeeded
|
||||
self.check_object_permissions(self.request, media)
|
||||
if media.state == "private":
|
||||
if self.request.user.has_member_access_to_media(media) or is_mediacms_editor(self.request.user):
|
||||
pass
|
||||
else:
|
||||
return Response(
|
||||
{"detail": "media is private"},
|
||||
status=status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
return media
|
||||
except PermissionDenied:
|
||||
return Response({"detail": "bad permissions"}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
except BaseException:
|
||||
return Response(
|
||||
{"detail": "media file does not exist"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[
|
||||
openapi.Parameter(name='friendly_token', type=openapi.TYPE_STRING, in_=openapi.IN_PATH, description='unique identifier', required=True),
|
||||
],
|
||||
tags=['Media'],
|
||||
operation_summary='Get information for Media',
|
||||
operation_description='Get information for a media',
|
||||
responses={200: SingleMediaSerializer(), 400: 'bad request'},
|
||||
)
|
||||
def get(self, request, friendly_token, format=None):
|
||||
# Get media details
|
||||
# password = request.GET.get("password")
|
||||
media = self.get_object(friendly_token)
|
||||
if isinstance(media, Response):
|
||||
return media
|
||||
|
||||
serializer = SingleMediaSerializer(media, context={"request": request})
|
||||
if media.state == "private":
|
||||
related_media = []
|
||||
else:
|
||||
related_media = show_related_media(media, request=request, limit=100)
|
||||
related_media_serializer = MediaSerializer(related_media, many=True, context={"request": request})
|
||||
related_media = related_media_serializer.data
|
||||
ret = serializer.data
|
||||
|
||||
# update rattings info with user specific ratings
|
||||
# eg user has already rated for this media
|
||||
# this only affects user rating and only if enabled
|
||||
if settings.ALLOW_RATINGS 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
|
||||
return Response(ret)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[
|
||||
openapi.Parameter(name='friendly_token', type=openapi.TYPE_STRING, in_=openapi.IN_PATH, description='unique identifier', required=True),
|
||||
openapi.Parameter(name='type', type=openapi.TYPE_STRING, in_=openapi.IN_FORM, description='action to perform', enum=['encode', 'review']),
|
||||
openapi.Parameter(
|
||||
name='encoding_profiles',
|
||||
type=openapi.TYPE_ARRAY,
|
||||
items=openapi.Items(type=openapi.TYPE_STRING),
|
||||
in_=openapi.IN_FORM,
|
||||
description='if action to perform is encode, need to specify list of ids of encoding profiles',
|
||||
),
|
||||
openapi.Parameter(name='result', type=openapi.TYPE_BOOLEAN, in_=openapi.IN_FORM, description='if action is review, this is the result (True for reviewed, False for not reviewed)'),
|
||||
],
|
||||
tags=['Media'],
|
||||
operation_summary='Run action on Media',
|
||||
operation_description='Actions for a media, for MediaCMS editors and managers',
|
||||
responses={201: 'action created', 400: 'bad request'},
|
||||
operation_id='media_manager_actions',
|
||||
)
|
||||
def post(self, request, friendly_token, format=None):
|
||||
"""superuser actions
|
||||
Available only to MediaCMS editors and managers
|
||||
|
||||
Action is a POST variable, review and encode are implemented
|
||||
"""
|
||||
|
||||
media = self.get_object(friendly_token)
|
||||
if isinstance(media, Response):
|
||||
return media
|
||||
|
||||
if not is_mediacms_editor(request.user):
|
||||
return Response({"detail": "not allowed"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
action = request.data.get("type")
|
||||
profiles_list = request.data.get("encoding_profiles")
|
||||
result = request.data.get("result", True)
|
||||
if action == "encode":
|
||||
# Create encoding tasks for specific profiles
|
||||
valid_profiles = []
|
||||
if profiles_list:
|
||||
if isinstance(profiles_list, list):
|
||||
for p in profiles_list:
|
||||
p = EncodeProfile.objects.filter(id=p).first()
|
||||
if p:
|
||||
valid_profiles.append(p)
|
||||
elif isinstance(profiles_list, str):
|
||||
try:
|
||||
p = EncodeProfile.objects.filter(id=int(profiles_list)).first()
|
||||
valid_profiles.append(p)
|
||||
except ValueError:
|
||||
return Response(
|
||||
{"detail": "encoding_profiles must be int or list of ints of valid encode profiles"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
media.encode(profiles=valid_profiles)
|
||||
return Response({"detail": "media will be encoded"}, status=status.HTTP_201_CREATED)
|
||||
elif action == "review":
|
||||
if result:
|
||||
media.is_reviewed = True
|
||||
elif result is False:
|
||||
media.is_reviewed = False
|
||||
media.save(update_fields=["is_reviewed"])
|
||||
return Response({"detail": "media reviewed set"}, status=status.HTTP_201_CREATED)
|
||||
return Response(
|
||||
{"detail": "not valid action or no action specified"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[
|
||||
openapi.Parameter(name="description", in_=openapi.IN_FORM, type=openapi.TYPE_STRING, required=False, description="description"),
|
||||
openapi.Parameter(name="title", in_=openapi.IN_FORM, type=openapi.TYPE_STRING, required=False, description="title"),
|
||||
openapi.Parameter(name="media_file", in_=openapi.IN_FORM, type=openapi.TYPE_FILE, required=False, description="media_file"),
|
||||
],
|
||||
tags=['Media'],
|
||||
operation_summary='Update Media',
|
||||
operation_description='Update a Media, for Media uploader',
|
||||
responses={201: openapi.Response('response description', MediaSerializer), 401: 'bad request'},
|
||||
)
|
||||
def put(self, request, friendly_token, format=None):
|
||||
# Update a media object
|
||||
media = self.get_object(friendly_token)
|
||||
if isinstance(media, Response):
|
||||
return media
|
||||
|
||||
if not (request.user.has_contributor_access_to_media(media) or is_mediacms_editor(request.user)):
|
||||
return Response({"detail": "not allowed"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
serializer = MediaSerializer(media, data=request.data, context={"request": request})
|
||||
if serializer.is_valid():
|
||||
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)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[
|
||||
openapi.Parameter(name='friendly_token', type=openapi.TYPE_STRING, in_=openapi.IN_PATH, description='unique identifier', required=True),
|
||||
],
|
||||
tags=['Media'],
|
||||
operation_summary='Delete Media',
|
||||
operation_description='Delete a Media, for MediaCMS editors and managers',
|
||||
responses={
|
||||
204: 'no content',
|
||||
},
|
||||
)
|
||||
def delete(self, request, friendly_token, format=None):
|
||||
# Delete a media object
|
||||
media = self.get_object(friendly_token)
|
||||
if isinstance(media, Response):
|
||||
return media
|
||||
media.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class MediaActions(APIView):
|
||||
"""
|
||||
Retrieve, update or delete a media action instance.
|
||||
"""
|
||||
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
parser_classes = (JSONParser,)
|
||||
|
||||
def get_object(self, friendly_token):
|
||||
try:
|
||||
media = Media.objects.select_related("user").prefetch_related("encodings__profile").get(friendly_token=friendly_token)
|
||||
if media.state == "private" and self.request.user != media.user:
|
||||
return Response({"detail": "media is private"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return media
|
||||
except PermissionDenied:
|
||||
return Response({"detail": "bad permissions"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except BaseException:
|
||||
return Response(
|
||||
{"detail": "media file does not exist"},
|
||||
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):
|
||||
# show date and reason for each time media was reported
|
||||
media = self.get_object(friendly_token)
|
||||
if not (request.user == media.user or is_mediacms_editor(request.user)):
|
||||
return Response({"detail": "not allowed"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if isinstance(media, Response):
|
||||
return media
|
||||
|
||||
ret = {}
|
||||
reported = MediaAction.objects.filter(media=media, action="report")
|
||||
ret["reported"] = []
|
||||
for rep in reported:
|
||||
item = {"reported_date": rep.action_date, "reason": rep.extra_info}
|
||||
ret["reported"].append(item)
|
||||
|
||||
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):
|
||||
# perform like/dislike/report actions
|
||||
media = self.get_object(friendly_token)
|
||||
if isinstance(media, Response):
|
||||
return media
|
||||
|
||||
action = request.data.get("type")
|
||||
extra = request.data.get("extra_info")
|
||||
if request.user.is_anonymous:
|
||||
# there is a list of allowed actions for
|
||||
# anonymous users, specified in settings
|
||||
if action not in settings.ALLOW_ANONYMOUS_ACTIONS:
|
||||
return Response(
|
||||
{"detail": "action allowed on logged in users only"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
if action:
|
||||
user_or_session = get_user_or_session(request)
|
||||
save_user_action.delay(
|
||||
user_or_session,
|
||||
friendly_token=media.friendly_token,
|
||||
action=action,
|
||||
extra_info=extra,
|
||||
)
|
||||
|
||||
return Response({"detail": "action received"}, status=status.HTTP_201_CREATED)
|
||||
else:
|
||||
return Response({"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):
|
||||
media = self.get_object(friendly_token)
|
||||
if isinstance(media, Response):
|
||||
return media
|
||||
|
||||
if not request.user.is_superuser:
|
||||
return Response({"detail": "not allowed"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
action = request.data.get("type")
|
||||
if action:
|
||||
if action == "report": # delete reported actions
|
||||
MediaAction.objects.filter(media=media, action="report").delete()
|
||||
media.reported_times = 0
|
||||
media.save(update_fields=["reported_times"])
|
||||
return Response(
|
||||
{"detail": "reset reported times counter"},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
else:
|
||||
return Response({"detail": "no action specified"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class MediaSearch(APIView):
|
||||
"""
|
||||
Retrieve results for search
|
||||
Only GET is implemented here
|
||||
"""
|
||||
|
||||
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):
|
||||
params = self.request.query_params
|
||||
query = params.get("q", "").strip().lower()
|
||||
category = params.get("c", "").strip()
|
||||
tag = params.get("t", "").strip()
|
||||
|
||||
ordering = params.get("ordering", "").strip()
|
||||
sort_by = params.get("sort_by", "").strip()
|
||||
media_type = params.get("media_type", "").strip()
|
||||
|
||||
author = params.get("author", "").strip()
|
||||
upload_date = params.get('upload_date', '').strip()
|
||||
|
||||
sort_by_options = ["title", "add_date", "edit_date", "views", "likes"]
|
||||
if sort_by not in sort_by_options:
|
||||
sort_by = "add_date"
|
||||
if ordering == "asc":
|
||||
ordering = ""
|
||||
else:
|
||||
ordering = "-"
|
||||
|
||||
if media_type not in ["video", "image", "audio", "pdf"]:
|
||||
media_type = None
|
||||
|
||||
if not (query or category or tag):
|
||||
ret = {}
|
||||
return Response(ret, status=status.HTTP_200_OK)
|
||||
|
||||
if request.user.is_authenticated:
|
||||
basic_query = Q(listable=True) | Q(permissions__user=request.user)
|
||||
|
||||
if getattr(settings, 'USE_RBAC', False):
|
||||
rbac_categories = request.user.get_rbac_categories_as_member()
|
||||
basic_query |= Q(category__in=rbac_categories)
|
||||
|
||||
else:
|
||||
basic_query = Q(listable=True)
|
||||
|
||||
media = Media.objects.filter(basic_query).distinct()
|
||||
|
||||
if query:
|
||||
# move this processing to a prepare_query function
|
||||
query = helpers.clean_query(query)
|
||||
q_parts = [q_part.rstrip("y") for q_part in query.split() if q_part not in STOP_WORDS]
|
||||
if q_parts:
|
||||
query = SearchQuery(q_parts[0] + ":*", search_type="raw")
|
||||
for part in q_parts[1:]:
|
||||
query &= SearchQuery(part + ":*", search_type="raw")
|
||||
else:
|
||||
query = None
|
||||
if query:
|
||||
media = media.filter(search=query)
|
||||
|
||||
if tag:
|
||||
media = media.filter(tags__title=tag)
|
||||
|
||||
if category:
|
||||
media = media.filter(category__title__contains=category)
|
||||
|
||||
if media_type:
|
||||
media = media.filter(media_type=media_type)
|
||||
|
||||
if author:
|
||||
media = media.filter(user__username=author)
|
||||
|
||||
if upload_date:
|
||||
gte = None
|
||||
if upload_date == 'today':
|
||||
gte = datetime.now().date()
|
||||
if upload_date == 'this_week':
|
||||
gte = datetime.now() - timedelta(days=7)
|
||||
if upload_date == 'this_month':
|
||||
year = datetime.now().date().year
|
||||
month = datetime.now().date().month
|
||||
gte = datetime(year, month, 1)
|
||||
if upload_date == 'this_year':
|
||||
year = datetime.now().date().year
|
||||
gte = datetime(year, 1, 1)
|
||||
if gte:
|
||||
media = media.filter(add_date__gte=gte)
|
||||
|
||||
media = media.order_by(f"{ordering}{sort_by}")
|
||||
|
||||
if self.request.query_params.get("show", "").strip() == "titles":
|
||||
media = media.values("title")[:40]
|
||||
return Response(media, status=status.HTTP_200_OK)
|
||||
else:
|
||||
media = media.prefetch_related("user")[:1000] # limit to 1000 results
|
||||
|
||||
if category or tag:
|
||||
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
|
||||
else:
|
||||
# pagination_class = FastPaginationWithoutCount
|
||||
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
|
||||
paginator = pagination_class()
|
||||
page = paginator.paginate_queryset(media, request)
|
||||
serializer = MediaSearchSerializer(page, many=True, context={"request": request})
|
||||
return paginator.get_paginated_response(serializer.data)
|
593
files/views/pages.py
Normal file
593
files/views/pages.py
Normal file
@@ -0,0 +1,593 @@
|
||||
import json
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.mail import EmailMessage
|
||||
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
|
||||
from django.shortcuts import render
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from cms.permissions import user_allowed_to_upload
|
||||
from cms.version import VERSION
|
||||
from users.models import User
|
||||
|
||||
from .. import helpers
|
||||
from ..forms import (
|
||||
ContactForm,
|
||||
EditSubtitleForm,
|
||||
MediaMetadataForm,
|
||||
MediaPublishForm,
|
||||
SubtitleForm,
|
||||
)
|
||||
from ..frontend_translations import translate_string
|
||||
from ..helpers import get_alphanumeric_only
|
||||
from ..methods import (
|
||||
create_video_trim_request,
|
||||
get_user_or_session,
|
||||
handle_video_chapters,
|
||||
is_mediacms_editor,
|
||||
)
|
||||
from ..models import Category, Media, Playlist, Subtitle, Tag, VideoTrimRequest
|
||||
from ..tasks import save_user_action, video_trim_task
|
||||
|
||||
|
||||
def about(request):
|
||||
"""About view"""
|
||||
|
||||
context = {"VERSION": VERSION}
|
||||
return render(request, "cms/about.html", context)
|
||||
|
||||
|
||||
def setlanguage(request):
|
||||
"""Set Language view"""
|
||||
|
||||
context = {}
|
||||
return render(request, "cms/set_language.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def add_subtitle(request):
|
||||
"""Add subtitle view"""
|
||||
|
||||
friendly_token = request.GET.get("m", "").strip()
|
||||
if not friendly_token:
|
||||
return HttpResponseRedirect("/")
|
||||
media = Media.objects.filter(friendly_token=friendly_token).first()
|
||||
if not media:
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
if not (request.user == media.user or is_mediacms_editor(request.user)):
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
if request.method == "POST":
|
||||
form = SubtitleForm(media, request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
subtitle = form.save()
|
||||
new_subtitle = Subtitle.objects.filter(id=subtitle.id).first()
|
||||
try:
|
||||
new_subtitle.convert_to_srt()
|
||||
messages.add_message(request, messages.INFO, "Subtitle was added!")
|
||||
return HttpResponseRedirect(subtitle.media.get_absolute_url())
|
||||
except: # noqa: E722
|
||||
new_subtitle.delete()
|
||||
error_msg = "Invalid subtitle format. Use SubRip (.srt) or WebVTT (.vtt) files."
|
||||
form.add_error("subtitle_file", error_msg)
|
||||
|
||||
else:
|
||||
form = SubtitleForm(media_item=media)
|
||||
subtitles = media.subtitles.all()
|
||||
context = {"media": media, "form": form, "subtitles": subtitles}
|
||||
return render(request, "cms/add_subtitle.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def edit_subtitle(request):
|
||||
subtitle_id = request.GET.get("id", "").strip()
|
||||
action = request.GET.get("action", "").strip()
|
||||
if not subtitle_id:
|
||||
return HttpResponseRedirect("/")
|
||||
subtitle = Subtitle.objects.filter(id=subtitle_id).first()
|
||||
|
||||
if not subtitle:
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
if not (request.user == subtitle.user or is_mediacms_editor(request.user)):
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
context = {"subtitle": subtitle, "action": action}
|
||||
|
||||
if action == "download":
|
||||
response = HttpResponse(subtitle.subtitle_file.read(), content_type="text/vtt")
|
||||
filename = subtitle.subtitle_file.name.split("/")[-1]
|
||||
|
||||
if not filename.endswith(".vtt"):
|
||||
filename = f"{filename}.vtt"
|
||||
|
||||
response["Content-Disposition"] = f"attachment; filename={filename}" # noqa
|
||||
|
||||
return response
|
||||
|
||||
if request.method == "GET":
|
||||
form = EditSubtitleForm(subtitle)
|
||||
context["form"] = form
|
||||
elif request.method == "POST":
|
||||
confirm = request.GET.get("confirm", "").strip()
|
||||
if confirm == "true":
|
||||
messages.add_message(request, messages.INFO, "Subtitle was deleted")
|
||||
redirect_url = subtitle.media.get_absolute_url()
|
||||
subtitle.delete()
|
||||
return HttpResponseRedirect(redirect_url)
|
||||
form = EditSubtitleForm(subtitle, request.POST)
|
||||
subtitle_text = form.data["subtitle"]
|
||||
with open(subtitle.subtitle_file.path, "w") as ff:
|
||||
ff.write(subtitle_text)
|
||||
|
||||
messages.add_message(request, messages.INFO, "Subtitle was edited")
|
||||
return HttpResponseRedirect(subtitle.media.get_absolute_url())
|
||||
return render(request, "cms/edit_subtitle.html", context)
|
||||
|
||||
|
||||
def categories(request):
|
||||
"""List categories view"""
|
||||
|
||||
context = {}
|
||||
return render(request, "cms/categories.html", context)
|
||||
|
||||
|
||||
def contact(request):
|
||||
"""Contact view"""
|
||||
|
||||
context = {}
|
||||
if request.method == "GET":
|
||||
form = ContactForm(request.user)
|
||||
context["form"] = form
|
||||
|
||||
else:
|
||||
form = ContactForm(request.user, request.POST)
|
||||
if form.is_valid():
|
||||
if request.user.is_authenticated:
|
||||
from_email = request.user.email
|
||||
name = request.user.name
|
||||
else:
|
||||
from_email = request.POST.get("from_email")
|
||||
name = request.POST.get("name")
|
||||
message = request.POST.get("message")
|
||||
|
||||
title = f"[{settings.PORTAL_NAME}] - Contact form message received"
|
||||
|
||||
msg = """
|
||||
You have received a message through the contact form\n
|
||||
Sender name: %s
|
||||
Sender email: %s\n
|
||||
\n %s
|
||||
""" % (
|
||||
name,
|
||||
from_email,
|
||||
message,
|
||||
)
|
||||
email = EmailMessage(
|
||||
title,
|
||||
msg,
|
||||
settings.DEFAULT_FROM_EMAIL,
|
||||
settings.ADMIN_EMAIL_LIST,
|
||||
reply_to=[from_email],
|
||||
)
|
||||
email.send(fail_silently=True)
|
||||
success_msg = "Message was sent! Thanks for contacting"
|
||||
context["success_msg"] = success_msg
|
||||
|
||||
return render(request, "cms/contact.html", context)
|
||||
|
||||
|
||||
def history(request):
|
||||
"""Show personal history view"""
|
||||
|
||||
context = {}
|
||||
return render(request, "cms/history.html", context)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@login_required
|
||||
def video_chapters(request, friendly_token):
|
||||
# this is not ready...
|
||||
return False
|
||||
if not request.method == "POST":
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
media = Media.objects.filter(friendly_token=friendly_token).first()
|
||||
|
||||
if not media:
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
if not (request.user == media.user or is_mediacms_editor(request.user)):
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
try:
|
||||
data = json.loads(request.body)["chapters"]
|
||||
chapters = []
|
||||
for _, chapter_data in enumerate(data):
|
||||
start_time = chapter_data.get('start')
|
||||
title = chapter_data.get('title')
|
||||
if start_time and title:
|
||||
chapters.append(
|
||||
{
|
||||
'start': start_time,
|
||||
'title': title,
|
||||
}
|
||||
)
|
||||
except Exception as e: # noqa
|
||||
return JsonResponse({'success': False, 'error': 'Request data must be a list of video chapters with start and title'}, status=400)
|
||||
|
||||
ret = handle_video_chapters(media, chapters)
|
||||
|
||||
return JsonResponse(ret, safe=False)
|
||||
|
||||
|
||||
@login_required
|
||||
def edit_media(request):
|
||||
"""Edit a media view"""
|
||||
|
||||
friendly_token = request.GET.get("m", "").strip()
|
||||
if not friendly_token:
|
||||
return HttpResponseRedirect("/")
|
||||
media = Media.objects.filter(friendly_token=friendly_token).first()
|
||||
|
||||
if not media:
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
if not (request.user.has_contributor_access_to_media(media) or is_mediacms_editor(request.user)):
|
||||
return HttpResponseRedirect("/")
|
||||
if request.method == "POST":
|
||||
form = MediaMetadataForm(request.user, request.POST, request.FILES, instance=media)
|
||||
if form.is_valid():
|
||||
media = form.save()
|
||||
for tag in media.tags.all():
|
||||
media.tags.remove(tag)
|
||||
if form.cleaned_data.get("new_tags"):
|
||||
for tag in form.cleaned_data.get("new_tags").split(","):
|
||||
tag = get_alphanumeric_only(tag)
|
||||
tag = tag[:99]
|
||||
if tag:
|
||||
try:
|
||||
tag = Tag.objects.get(title=tag)
|
||||
except Tag.DoesNotExist:
|
||||
tag = Tag.objects.create(title=tag, user=request.user)
|
||||
if tag not in media.tags.all():
|
||||
media.tags.add(tag)
|
||||
messages.add_message(request, messages.INFO, translate_string(request.LANGUAGE_CODE, "Media was edited"))
|
||||
return HttpResponseRedirect(media.get_absolute_url())
|
||||
else:
|
||||
form = MediaMetadataForm(request.user, instance=media)
|
||||
return render(
|
||||
request,
|
||||
"cms/edit_media.html",
|
||||
{"form": form, "media_object": media, "add_subtitle_url": media.add_subtitle_url},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def publish_media(request):
|
||||
"""Publish media"""
|
||||
|
||||
friendly_token = request.GET.get("m", "").strip()
|
||||
if not friendly_token:
|
||||
return HttpResponseRedirect("/")
|
||||
media = Media.objects.filter(friendly_token=friendly_token).first()
|
||||
|
||||
if not media:
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
if not (request.user.has_contributor_access_to_media(media) or is_mediacms_editor(request.user)):
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
if request.method == "POST":
|
||||
form = MediaPublishForm(request.user, request.POST, request.FILES, instance=media)
|
||||
if form.is_valid():
|
||||
media = form.save()
|
||||
messages.add_message(request, messages.INFO, translate_string(request.LANGUAGE_CODE, "Media was edited"))
|
||||
return HttpResponseRedirect(media.get_absolute_url())
|
||||
else:
|
||||
form = MediaPublishForm(request.user, instance=media)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"cms/publish_media.html",
|
||||
{"form": form, "media_object": media, "add_subtitle_url": media.add_subtitle_url},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def edit_chapters(request):
|
||||
"""Edit chapters"""
|
||||
# not implemented yet
|
||||
return False
|
||||
friendly_token = request.GET.get("m", "").strip()
|
||||
if not friendly_token:
|
||||
return HttpResponseRedirect("/")
|
||||
media = Media.objects.filter(friendly_token=friendly_token).first()
|
||||
|
||||
if not media:
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
if not (request.user == media.user or is_mediacms_editor(request.user)):
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
return render(
|
||||
request,
|
||||
"cms/edit_chapters.html",
|
||||
{"media_object": media, "add_subtitle_url": media.add_subtitle_url, "media_file_path": helpers.url_from_path(media.media_file.path), "media_id": media.friendly_token},
|
||||
)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@login_required
|
||||
def trim_video(request, friendly_token):
|
||||
if not settings.ALLOW_VIDEO_TRIMMER:
|
||||
return JsonResponse({"success": False, "error": "Video trimming is not allowed"}, status=400)
|
||||
|
||||
if not request.method == "POST":
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
media = Media.objects.filter(friendly_token=friendly_token).first()
|
||||
|
||||
if not media:
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
if not (request.user == media.user or is_mediacms_editor(request.user)):
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
existing_requests = VideoTrimRequest.objects.filter(media=media, status__in=["initial", "running"]).exists()
|
||||
|
||||
if existing_requests:
|
||||
return JsonResponse({"success": False, "error": "A trim request is already in progress for this video"}, status=400)
|
||||
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
video_trim_request = create_video_trim_request(media, data)
|
||||
video_trim_task.delay(video_trim_request.id)
|
||||
ret = {"success": True, "request_id": video_trim_request.id}
|
||||
return JsonResponse(ret, safe=False, status=200)
|
||||
except Exception as e: # noqa
|
||||
ret = {"success": False, "error": "Incorrect request data"}
|
||||
return JsonResponse(ret, safe=False, status=400)
|
||||
|
||||
|
||||
@login_required
|
||||
def edit_video(request):
|
||||
"""Edit video"""
|
||||
|
||||
friendly_token = request.GET.get("m", "").strip()
|
||||
if not friendly_token:
|
||||
return HttpResponseRedirect("/")
|
||||
media = Media.objects.filter(friendly_token=friendly_token).first()
|
||||
|
||||
if not media:
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
if not (request.user == media.user or is_mediacms_editor(request.user)):
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
if not media.media_type == "video":
|
||||
messages.add_message(request, messages.INFO, "Media is not video")
|
||||
return HttpResponseRedirect(media.get_absolute_url())
|
||||
|
||||
if not settings.ALLOW_VIDEO_TRIMMER:
|
||||
messages.add_message(request, messages.INFO, "Video Trimmer is not enabled")
|
||||
return HttpResponseRedirect(media.get_absolute_url())
|
||||
|
||||
# Check if there's a running trim request
|
||||
running_trim_request = VideoTrimRequest.objects.filter(media=media, status__in=["initial", "running"]).exists()
|
||||
|
||||
if running_trim_request:
|
||||
messages.add_message(request, messages.INFO, "Video trim request is already running")
|
||||
return HttpResponseRedirect(media.get_absolute_url())
|
||||
|
||||
media_file_path = media.trim_video_url
|
||||
|
||||
if not media_file_path:
|
||||
messages.add_message(request, messages.INFO, "Media processing has not finished yet")
|
||||
return HttpResponseRedirect(media.get_absolute_url())
|
||||
|
||||
if media.encoding_status in ["pending", "running"]:
|
||||
video_msg = "Media encoding hasn't finished yet. Attempting to show the original video file"
|
||||
messages.add_message(request, messages.INFO, video_msg)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"cms/edit_video.html",
|
||||
{"media_object": media, "add_subtitle_url": media.add_subtitle_url, "media_file_path": media_file_path},
|
||||
)
|
||||
|
||||
|
||||
def embed_media(request):
|
||||
"""Embed media view"""
|
||||
|
||||
friendly_token = request.GET.get("m", "").strip()
|
||||
if not friendly_token:
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
media = Media.objects.values("title").filter(friendly_token=friendly_token).first()
|
||||
|
||||
if not media:
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
context = {}
|
||||
context["media"] = friendly_token
|
||||
return render(request, "cms/embed.html", context)
|
||||
|
||||
|
||||
def featured_media(request):
|
||||
"""List featured media view"""
|
||||
|
||||
context = {}
|
||||
return render(request, "cms/featured-media.html", context)
|
||||
|
||||
|
||||
def index(request):
|
||||
"""Index view"""
|
||||
|
||||
context = {}
|
||||
return render(request, "cms/index.html", context)
|
||||
|
||||
|
||||
def latest_media(request):
|
||||
"""List latest media view"""
|
||||
|
||||
context = {}
|
||||
return render(request, "cms/latest-media.html", context)
|
||||
|
||||
|
||||
def liked_media(request):
|
||||
"""List user's liked media view"""
|
||||
|
||||
context = {}
|
||||
return render(request, "cms/liked_media.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def manage_users(request):
|
||||
"""List users management view"""
|
||||
|
||||
if not is_mediacms_editor(request.user):
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
context = {}
|
||||
return render(request, "cms/manage_users.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def manage_media(request):
|
||||
"""List media management view"""
|
||||
if not is_mediacms_editor(request.user):
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
categories = Category.objects.all().order_by('title').values_list('title', flat=True)
|
||||
context = {'categories': list(categories)}
|
||||
return render(request, "cms/manage_media.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def manage_comments(request):
|
||||
"""List comments management view"""
|
||||
if not is_mediacms_editor(request.user):
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
context = {}
|
||||
return render(request, "cms/manage_comments.html", context)
|
||||
|
||||
|
||||
def members(request):
|
||||
"""List members view"""
|
||||
|
||||
context = {}
|
||||
return render(request, "cms/members.html", context)
|
||||
|
||||
|
||||
def recommended_media(request):
|
||||
"""List recommended media view"""
|
||||
|
||||
context = {}
|
||||
return render(request, "cms/recommended-media.html", context)
|
||||
|
||||
|
||||
def search(request):
|
||||
"""Search view"""
|
||||
|
||||
context = {}
|
||||
RSS_URL = f"/rss{request.environ.get('REQUEST_URI')}"
|
||||
context["RSS_URL"] = RSS_URL
|
||||
return render(request, "cms/search.html", context)
|
||||
|
||||
|
||||
def sitemap(request):
|
||||
"""Sitemap"""
|
||||
|
||||
context = {}
|
||||
context["media"] = list(Media.objects.filter(listable=True).order_by("-add_date"))
|
||||
context["playlists"] = list(Playlist.objects.filter().order_by("-add_date"))
|
||||
context["users"] = list(User.objects.filter())
|
||||
return render(request, "sitemap.xml", context, content_type="application/xml")
|
||||
|
||||
|
||||
def tags(request):
|
||||
"""List tags view"""
|
||||
|
||||
context = {}
|
||||
return render(request, "cms/tags.html", context)
|
||||
|
||||
|
||||
def tos(request):
|
||||
"""Terms of service view"""
|
||||
|
||||
context = {}
|
||||
return render(request, "cms/tos.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def upload_media(request):
|
||||
"""Upload media view"""
|
||||
|
||||
from allauth.account.forms import LoginForm
|
||||
|
||||
form = LoginForm()
|
||||
context = {}
|
||||
context["form"] = form
|
||||
context["can_add"] = user_allowed_to_upload(request)
|
||||
can_upload_exp = settings.CANNOT_ADD_MEDIA_MESSAGE
|
||||
context["can_upload_exp"] = can_upload_exp
|
||||
|
||||
return render(request, "cms/add-media.html", context)
|
||||
|
||||
|
||||
def view_media(request):
|
||||
"""View media view"""
|
||||
|
||||
friendly_token = request.GET.get("m", "").strip()
|
||||
context = {}
|
||||
media = Media.objects.filter(friendly_token=friendly_token).first()
|
||||
if not media:
|
||||
context["media"] = None
|
||||
return render(request, "cms/media.html", context)
|
||||
|
||||
user_or_session = get_user_or_session(request)
|
||||
save_user_action.delay(user_or_session, friendly_token=friendly_token, action="watch")
|
||||
context = {}
|
||||
context["media"] = friendly_token
|
||||
context["media_object"] = media
|
||||
|
||||
context["CAN_DELETE_MEDIA"] = False
|
||||
context["CAN_EDIT_MEDIA"] = False
|
||||
context["CAN_DELETE_COMMENTS"] = False
|
||||
|
||||
if request.user.is_authenticated:
|
||||
if request.user.has_contributor_access_to_media(media) or is_mediacms_editor(request.user):
|
||||
context["CAN_DELETE_MEDIA"] = True
|
||||
context["CAN_EDIT_MEDIA"] = True
|
||||
context["CAN_DELETE_COMMENTS"] = True
|
||||
|
||||
# in case media is video and is processing (eg the case a video was just uploaded)
|
||||
# attempt to show it (rather than showing a blank video player)
|
||||
if media.media_type == 'video':
|
||||
video_msg = None
|
||||
if media.encoding_status == "pending":
|
||||
video_msg = "Media encoding hasn't started yet. Attempting to show the original video file"
|
||||
if media.encoding_status == "running":
|
||||
video_msg = "Media encoding is under processing. Attempting to show the original video file"
|
||||
if video_msg:
|
||||
messages.add_message(request, messages.INFO, video_msg)
|
||||
|
||||
return render(request, "cms/media.html", context)
|
||||
|
||||
|
||||
def view_playlist(request, friendly_token):
|
||||
"""View playlist view"""
|
||||
|
||||
try:
|
||||
playlist = Playlist.objects.get(friendly_token=friendly_token)
|
||||
except BaseException:
|
||||
playlist = None
|
||||
|
||||
context = {}
|
||||
context["playlist"] = playlist
|
||||
return render(request, "cms/playlist.html", context)
|
195
files/views/playlists.py
Normal file
195
files/views/playlists.py
Normal file
@@ -0,0 +1,195 @@
|
||||
from django.conf import settings
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.parsers import (
|
||||
FileUploadParser,
|
||||
FormParser,
|
||||
JSONParser,
|
||||
MultiPartParser,
|
||||
)
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from cms.permissions import IsAuthorizedToAdd, IsUserOrEditor
|
||||
|
||||
from ..models import Media, Playlist, PlaylistMedia
|
||||
from ..serializers import MediaSerializer, PlaylistDetailSerializer, PlaylistSerializer
|
||||
|
||||
|
||||
class PlaylistList(APIView):
|
||||
"""Playlists listings and creation views"""
|
||||
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsAuthorizedToAdd)
|
||||
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[],
|
||||
tags=['Playlists'],
|
||||
operation_summary='to_be_written',
|
||||
operation_description='to_be_written',
|
||||
responses={
|
||||
200: openapi.Response('response description', PlaylistSerializer(many=True)),
|
||||
},
|
||||
)
|
||||
def get(self, request, format=None):
|
||||
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
|
||||
paginator = pagination_class()
|
||||
playlists = Playlist.objects.filter().prefetch_related("user")
|
||||
|
||||
if "author" in self.request.query_params:
|
||||
author = self.request.query_params["author"].strip()
|
||||
playlists = playlists.filter(user__username=author)
|
||||
|
||||
page = paginator.paginate_queryset(playlists, request)
|
||||
|
||||
serializer = PlaylistSerializer(page, many=True, context={"request": request})
|
||||
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):
|
||||
serializer = PlaylistSerializer(data=request.data, context={"request": request})
|
||||
if serializer.is_valid():
|
||||
serializer.save(user=request.user)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class PlaylistDetail(APIView):
|
||||
"""Playlist related views"""
|
||||
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsUserOrEditor)
|
||||
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)
|
||||
|
||||
def get_playlist(self, friendly_token):
|
||||
try:
|
||||
playlist = Playlist.objects.get(friendly_token=friendly_token)
|
||||
self.check_object_permissions(self.request, playlist)
|
||||
return playlist
|
||||
except PermissionDenied:
|
||||
return Response({"detail": "not enough permissions"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except BaseException:
|
||||
return Response(
|
||||
{"detail": "Playlist does not exist"},
|
||||
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):
|
||||
playlist = self.get_playlist(friendly_token)
|
||||
if isinstance(playlist, Response):
|
||||
return playlist
|
||||
|
||||
serializer = PlaylistDetailSerializer(playlist, context={"request": request})
|
||||
|
||||
playlist_media = PlaylistMedia.objects.filter(playlist=playlist, media__state="public").prefetch_related("media__user")
|
||||
|
||||
playlist_media = [c.media for c in playlist_media]
|
||||
|
||||
playlist_media_serializer = MediaSerializer(playlist_media, many=True, context={"request": request})
|
||||
ret = serializer.data
|
||||
ret["playlist_media"] = playlist_media_serializer.data
|
||||
|
||||
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):
|
||||
playlist = self.get_playlist(friendly_token)
|
||||
if isinstance(playlist, Response):
|
||||
return playlist
|
||||
serializer = PlaylistDetailSerializer(playlist, data=request.data, context={"request": request})
|
||||
if serializer.is_valid():
|
||||
serializer.save(user=request.user)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
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):
|
||||
playlist = self.get_playlist(friendly_token)
|
||||
if isinstance(playlist, Response):
|
||||
return playlist
|
||||
action = request.data.get("type")
|
||||
media_friendly_token = request.data.get("media_friendly_token")
|
||||
ordering = 0
|
||||
if request.data.get("ordering"):
|
||||
try:
|
||||
ordering = int(request.data.get("ordering"))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if action in ["add", "remove", "ordering"]:
|
||||
media = Media.objects.filter(friendly_token=media_friendly_token).first()
|
||||
if media:
|
||||
if action == "add":
|
||||
media_in_playlist = PlaylistMedia.objects.filter(playlist=playlist).count()
|
||||
if media_in_playlist >= settings.MAX_MEDIA_PER_PLAYLIST:
|
||||
return Response(
|
||||
{"detail": "max number of media for a Playlist reached"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
else:
|
||||
obj, created = PlaylistMedia.objects.get_or_create(
|
||||
playlist=playlist,
|
||||
media=media,
|
||||
ordering=media_in_playlist + 1,
|
||||
)
|
||||
obj.save()
|
||||
return Response(
|
||||
{"detail": "media added to Playlist"},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
elif action == "remove":
|
||||
PlaylistMedia.objects.filter(playlist=playlist, media=media).delete()
|
||||
return Response(
|
||||
{"detail": "media removed from Playlist"},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
elif action == "ordering":
|
||||
if ordering:
|
||||
playlist.set_ordering(media, ordering)
|
||||
return Response(
|
||||
{"detail": "new ordering set"},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
else:
|
||||
return Response({"detail": "media is not valid"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(
|
||||
{"detail": "invalid or not specified action"},
|
||||
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):
|
||||
playlist = self.get_playlist(friendly_token)
|
||||
if isinstance(playlist, Response):
|
||||
return playlist
|
||||
|
||||
playlist.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
30
files/views/tasks.py
Normal file
30
files/views/tasks.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from ..methods import list_tasks
|
||||
|
||||
|
||||
class TasksList(APIView):
|
||||
"""List tasks"""
|
||||
|
||||
swagger_schema = None
|
||||
|
||||
permission_classes = (permissions.IsAdminUser,)
|
||||
|
||||
def get(self, request, format=None):
|
||||
ret = list_tasks()
|
||||
return Response(ret)
|
||||
|
||||
|
||||
class TaskDetail(APIView):
|
||||
"""Cancel a task"""
|
||||
|
||||
swagger_schema = None
|
||||
|
||||
permission_classes = (permissions.IsAdminUser,)
|
||||
|
||||
def delete(self, request, uid, format=None):
|
||||
# This is not imported!
|
||||
# revoke(uid, terminate=True)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
45
files/views/user.py
Normal file
45
files/views/user.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework.parsers import JSONParser
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from actions.models import USER_MEDIA_ACTIONS
|
||||
|
||||
from ..models import Media
|
||||
from ..serializers import MediaSerializer
|
||||
|
||||
VALID_USER_ACTIONS = [action for action, name in USER_MEDIA_ACTIONS]
|
||||
|
||||
|
||||
class UserActions(APIView):
|
||||
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):
|
||||
media = []
|
||||
if action in VALID_USER_ACTIONS:
|
||||
if request.user.is_authenticated:
|
||||
media = Media.objects.select_related("user").filter(mediaactions__user=request.user, mediaactions__action=action).order_by("-mediaactions__action_date")
|
||||
elif request.session.session_key:
|
||||
media = (
|
||||
Media.objects.select_related("user")
|
||||
.filter(
|
||||
mediaactions__session_key=request.session.session_key,
|
||||
mediaactions__action=action,
|
||||
)
|
||||
.order_by("-mediaactions__action_date")
|
||||
)
|
||||
|
||||
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
|
||||
paginator = pagination_class()
|
||||
page = paginator.paginate_queryset(media, request)
|
||||
serializer = MediaSerializer(page, many=True, context={"request": request})
|
||||
return paginator.get_paginated_response(serializer.data)
|
BIN
fixtures/test_image2.jpg
Normal file
BIN
fixtures/test_image2.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
@@ -19,4 +19,4 @@
|
||||
{% include "components/footer.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block bottomimports %}<script src="{% static "js/base.js" %}"></script>{% endblock bottomimports %}
|
||||
{% block bottomimports %}<script src="{% static "js/base.js" %}?v={{ VERSION }}"></script>{% endblock bottomimports %}
|
@@ -129,7 +129,7 @@
|
||||
{% endblock innercontent %}
|
||||
|
||||
{% block bottomimports %}
|
||||
<script src="{% static "js/add-media.js" %}"></script>
|
||||
<script src="{% static "js/add-media.js" %}?v={{ VERSION }}"></script>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function(event) {
|
||||
function getCSRFToken() {
|
||||
|
@@ -41,5 +41,5 @@
|
||||
{% block content %}<div id="page-categories"></div>{% endblock %}
|
||||
|
||||
{% block bottomimports %}
|
||||
<script src="{% static "js/categories.js" %}"></script>
|
||||
<script src="{% static "js/categories.js" %}?v={{ VERSION }}"></script>
|
||||
{% endblock bottomimports %}
|
||||
|
@@ -9,5 +9,5 @@
|
||||
{% block content %}<div id="page-embed"></div>{% endblock content %}
|
||||
|
||||
{% block bottomimports %}
|
||||
<script src="{% static "js/embed.js" %}"></script>
|
||||
<script src="{% static "js/embed.js" %}?v={{ VERSION }}"></script>
|
||||
{% endblock bottomimports %}
|
||||
|
@@ -43,5 +43,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block bottomimports %}
|
||||
<script src="{% static "js/featured.js" %}"></script>
|
||||
<script src="{% static "js/featured.js" %}?v={{ VERSION }}"></script>
|
||||
{% endblock bottomimports %}
|
||||
|
@@ -13,5 +13,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block bottomimports %}
|
||||
<script src="{% static "js/history.js" %}"></script>
|
||||
<script src="{% static "js/history.js" %}?v={{ VERSION }}"></script>
|
||||
{% endblock bottomimports %}
|
||||
|
@@ -48,5 +48,5 @@
|
||||
{% block content %}<div id="page-home"></div>{% endblock %}
|
||||
|
||||
{% block bottomimports %}
|
||||
<script src="{% static "js/index.js" %}"></script>
|
||||
<script src="{% static "js/index.js" %}?v={{ VERSION }}"></script>
|
||||
{% endblock bottomimports %}
|
||||
|
@@ -43,5 +43,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block bottomimports %}
|
||||
<script src="{% static "js/latest.js" %}"></script>
|
||||
<script src="{% static "js/latest.js" %}?v={{ VERSION }}"></script>
|
||||
{% endblock bottomimports %}
|
||||
|
@@ -13,5 +13,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block bottomimports %}
|
||||
<script src="{% static "js/liked.js" %}"></script>
|
||||
<script src="{% static "js/liked.js" %}?v={{ VERSION }}"></script>
|
||||
{% endblock bottomimports %}
|
||||
|
@@ -14,5 +14,5 @@
|
||||
{% block content %}<div id="page-manage-comments"></div>{% endblock %}
|
||||
|
||||
{% block bottomimports %}
|
||||
<script src="{% static "js/manage-comments.js" %}"></script>
|
||||
<script src="{% static "js/manage-comments.js" %}?v={{ VERSION }}"></script>
|
||||
{% endblock bottomimports %}
|
||||
|
@@ -19,6 +19,6 @@ window.CATEGORIES = {{ categories|safe }};
|
||||
{% endblock %}
|
||||
|
||||
{% block bottomimports %}
|
||||
<script src="{% static "js/manage-media.js" %}"></script>
|
||||
<script src="{% static "js/manage-media.js" %}?v={{ VERSION }}"></script>
|
||||
{% endblock bottomimports %}
|
||||
|
||||
|
@@ -14,5 +14,5 @@
|
||||
{% block content %}<div id="page-manage-users"></div>{% endblock %}
|
||||
|
||||
{% block bottomimports %}
|
||||
<script src="{% static "js/manage-users.js" %}"></script>
|
||||
<script src="{% static "js/manage-users.js" %}?v={{ VERSION }}"></script>
|
||||
{% endblock bottomimports %}
|
||||
|
@@ -129,5 +129,5 @@
|
||||
{% block content %}<div id="page-media"></div>{% endblock content %}
|
||||
|
||||
{% block bottomimports %}
|
||||
<script src="{% static "js/media.js" %}"></script>
|
||||
<script src="{% static "js/media.js" %}?v={{ VERSION }}"></script>
|
||||
{% endblock bottomimports %}
|
||||
|
@@ -41,5 +41,5 @@
|
||||
{% block content %}<div id="page-members"></div>{% endblock %}
|
||||
|
||||
{% block bottomimports %}
|
||||
<script src="{% static "js/members.js" %}"></script>
|
||||
<script src="{% static "js/members.js" %}?v={{ VERSION }}"></script>
|
||||
{% endblock bottomimports %}
|
||||
|
@@ -13,5 +13,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block bottomimports %}
|
||||
<script src="{% static "js/playlist.js" %}"></script>
|
||||
<script src="{% static "js/playlist.js" %}?v={{ VERSION }}"></script>
|
||||
{% endblock bottomimports %}
|
||||
|
@@ -43,5 +43,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block bottomimports %}
|
||||
<script src="{% static "js/recommended.js" %}"></script>
|
||||
<script src="{% static "js/recommended.js" %}?v={{ VERSION }}"></script>
|
||||
{% endblock bottomimports %}
|
||||
|
@@ -6,5 +6,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block bottomimports %}
|
||||
<script src="{% static "js/search.js" %}"></script>
|
||||
<script src="{% static "js/search.js" %}?v={{ VERSION }}"></script>
|
||||
{% endblock bottomimports %}
|
||||
|
@@ -41,5 +41,5 @@
|
||||
{% block content %}<div id="page-tags"></div>{% endblock %}
|
||||
|
||||
{% block bottomimports %}
|
||||
<script src="{% static "js/tags.js" %}"></script>
|
||||
<script src="{% static "js/tags.js" %}?v={{ VERSION }}"></script>
|
||||
{% endblock bottomimports %}
|
||||
|
@@ -29,5 +29,5 @@ No such user
|
||||
{% endblock %}
|
||||
|
||||
{% block bottomimports %}
|
||||
<script src="{% static "js/profile-media.js" %}"></script>
|
||||
<script src="{% static "js/profile-media.js" %}?v={{ VERSION }}"></script>
|
||||
{% endblock bottomimports %}
|
@@ -29,5 +29,5 @@ No such user
|
||||
{% endblock %}
|
||||
|
||||
{% block bottomimports %}
|
||||
<script src="{% static "js/profile-about.js" %}"></script>
|
||||
<script src="{% static "js/profile-about.js" %}?v={{ VERSION }}"></script>
|
||||
{% endblock bottomimports %}
|
||||
|
@@ -29,5 +29,5 @@ No such user
|
||||
{% endblock %}
|
||||
|
||||
{% block bottomimports %}
|
||||
<script src="{% static "js/profile-playlists.js" %}"></script>
|
||||
<script src="{% static "js/profile-playlists.js" %}?v={{ VERSION }}"></script>
|
||||
{% endblock bottomimports %}
|
||||
|
37
templates/cms/user_shared_by_me.html
Normal file
37
templates/cms/user_shared_by_me.html
Normal file
@@ -0,0 +1,37 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block headtitle %}{% if user.name %}{{user.name}} - {% endif %}{{PORTAL_NAME}}{% endblock headtitle %}
|
||||
|
||||
{% block headermeta %}
|
||||
|
||||
<meta property="og:title" content="{% if user.name %}{{user.name}} - {% endif %}{{PORTAL_NAME}}">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:description" content="">
|
||||
|
||||
<meta name="twitter:card" content="summary">
|
||||
|
||||
{% endblock headermeta %}
|
||||
|
||||
{% block topimports %}
|
||||
{% load static %}
|
||||
<link href="{% static "css/profile-media.css" %}?v={{ VERSION }}" rel="preload" as="style">
|
||||
<link href="{% static "css/profile-media.css" %}?v={{ VERSION }}" rel="stylesheet">
|
||||
{%endblock topimports %}
|
||||
|
||||
{% block innercontent %}
|
||||
{% if user %}{% else %}
|
||||
No such user
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if user %}
|
||||
<div id="page-profile-media"></div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block bottomimports %}
|
||||
<script src="{% static "js/profile-media.js" %}?v={{ VERSION }}"></script>
|
||||
{% endblock bottomimports %}
|
34
templates/cms/user_shared_with_me.html
Normal file
34
templates/cms/user_shared_with_me.html
Normal file
@@ -0,0 +1,34 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block headtitle %}{% if user.name %}{{user.name}} - {% endif %}{{PORTAL_NAME}}{% endblock headtitle %}
|
||||
|
||||
{% block headermeta %}
|
||||
|
||||
<meta property="og:title" content="{% if user.name %}{{user.name}} - {% endif %}{{PORTAL_NAME}}">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:description" content="">
|
||||
|
||||
<meta name="twitter:card" content="summary">
|
||||
|
||||
{% endblock headermeta %}
|
||||
|
||||
{% block topimports %}
|
||||
{% load static %}
|
||||
<link href="{% static "css/profile-media.css" %}?v={{ VERSION }}" rel="preload" as="style">
|
||||
<link href="{% static "css/profile-media.css" %}?v={{ VERSION }}" rel="stylesheet">
|
||||
{%endblock topimports %}
|
||||
|
||||
{% block innercontent %}
|
||||
{% if user %}{% else %}
|
||||
No such user
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if user %}<div id="page-profile-media"></div>{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block bottomimports %}
|
||||
<script src="{% static "js/profile-media.js" %}?v={{ VERSION }}"></script>
|
||||
{% endblock bottomimports %}
|
67
tests/api/test_media_listings.py
Normal file
67
tests/api/test_media_listings.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from django.core.files import File
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from files.models import Media
|
||||
from files.tests import create_account
|
||||
|
||||
|
||||
class TestMediaListings(TestCase):
|
||||
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
self.password = 'this_is_a_fake_password'
|
||||
self.user = create_account(password=self.password)
|
||||
|
||||
# Create a test media item
|
||||
with open('fixtures/test_image2.jpg', "rb") as f:
|
||||
myfile = File(f)
|
||||
self.media = Media.objects.create(
|
||||
title="Test Media", description="Test Description", user=self.user, state="public", encoding_status="success", is_reviewed=True, listable=True, media_file=myfile
|
||||
)
|
||||
|
||||
def test_media_list_endpoint(self):
|
||||
"""Test the media list API endpoint"""
|
||||
url = '/api/v1/media'
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, 200, "Media list endpoint should return 200")
|
||||
self.assertIn('results', response.data, "Response should contain results")
|
||||
self.assertIn('count', response.data, "Response should contain count")
|
||||
|
||||
# Check if our test media is in the results
|
||||
media_titles = [item['title'] for item in response.data['results']]
|
||||
self.assertIn(self.media.title, media_titles, "Test media should be in the results")
|
||||
|
||||
def test_featured_media_listing(self):
|
||||
"""Test the featured media listing"""
|
||||
# Mark our test media as featured
|
||||
self.media.featured = True
|
||||
self.media.save()
|
||||
|
||||
url = '/api/v1/media?show=featured'
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, 200, "Featured media endpoint should return 200")
|
||||
|
||||
# Check if our featured media is in the results
|
||||
media_titles = [item['title'] for item in response.data['results']]
|
||||
self.assertIn(self.media.title, media_titles, "Featured media should be in the results")
|
||||
|
||||
def test_user_media_listing(self):
|
||||
"""Test listing media for a specific user"""
|
||||
url = f'/api/v1/media?author={self.user.username}'
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, 200, "User media endpoint should return 200")
|
||||
|
||||
# Check if our user's media is in the results
|
||||
media_titles = [item['title'] for item in response.data['results']]
|
||||
self.assertIn(self.media.title, media_titles, "User's media should be in the results")
|
||||
|
||||
def test_recommended_media_listing(self):
|
||||
"""Test the recommended media listing"""
|
||||
url = '/api/v1/media?show=recommended'
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, 200, "Recommended media endpoint should return 200")
|
@@ -1,10 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestX(TestCase):
|
||||
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
|
||||
|
||||
def test_X(self):
|
||||
# test a number of listings works (index, featured, user etc)
|
||||
|
||||
pass
|
114
tests/api/test_search.py
Normal file
114
tests/api/test_search.py
Normal file
@@ -0,0 +1,114 @@
|
||||
from django.core.files import File
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from files.models import Category, Media, Tag
|
||||
from files.tests import create_account
|
||||
|
||||
|
||||
class TestSearch(TestCase):
|
||||
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
self.password = 'this_is_a_fake_password'
|
||||
self.user = create_account(password=self.password)
|
||||
|
||||
# Create test media items with different attributes for search testing
|
||||
with open('fixtures/test_image2.jpg', "rb") as f:
|
||||
myfile = File(f)
|
||||
self.media1 = Media.objects.create(title="Python Tutorial", description="Learn Python programming", user=self.user, media_file=myfile)
|
||||
|
||||
with open('fixtures/test_image2.jpg', "rb") as f:
|
||||
myfile = File(f)
|
||||
self.media2 = Media.objects.create(
|
||||
title="Django Framework",
|
||||
description="Web development with Django",
|
||||
user=self.user,
|
||||
media_file=myfile,
|
||||
)
|
||||
|
||||
with open('fixtures/test_image2.jpg', "rb") as f:
|
||||
myfile = File(f)
|
||||
self.media3 = Media.objects.create(
|
||||
title="JavaScript Basics",
|
||||
description="Introduction to JavaScript",
|
||||
user=self.user,
|
||||
media_file=myfile,
|
||||
)
|
||||
# Add categories and tags
|
||||
self.category = Category.objects.first()
|
||||
self.tag = Tag.objects.create(title="programming", user=self.user)
|
||||
|
||||
self.media1.category.add(self.category)
|
||||
self.media2.category.add(self.category)
|
||||
self.media1.tags.add(self.tag)
|
||||
self.media2.tags.add(self.tag)
|
||||
|
||||
# Update search vectors
|
||||
self.media1.update_search_vector()
|
||||
self.media2.update_search_vector()
|
||||
self.media3.update_search_vector()
|
||||
|
||||
def test_search_by_title(self):
|
||||
"""Test searching media by title"""
|
||||
url = '/api/v1/search?q=python'
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, 200, "Search endpoint should return 200")
|
||||
|
||||
# Check if our media with "Python" in the title is in the results
|
||||
|
||||
media_titles = [item['title'] for item in response.data['results']]
|
||||
self.assertIn(self.media1.title, media_titles, "Media with 'Python' in title should be in results")
|
||||
self.assertNotIn(self.media3.title, media_titles, "Media without 'Python' should not be in results")
|
||||
|
||||
def test_search_by_category(self):
|
||||
"""Test searching media by category"""
|
||||
url = f'/api/v1/search?c={self.category.title}'
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, 200, "Category search endpoint should return 200")
|
||||
|
||||
# Check if media in the category are in the results
|
||||
media_titles = [item['title'] for item in response.data['results']]
|
||||
self.assertIn(self.media1.title, media_titles, "Media in category should be in results")
|
||||
self.assertIn(self.media2.title, media_titles, "Media in category should be in results")
|
||||
self.assertNotIn(self.media3.title, media_titles, "Media not in category should not be in results")
|
||||
|
||||
def test_search_by_tag(self):
|
||||
"""Test searching media by tag"""
|
||||
url = f'/api/v1/search?t={self.tag.title}'
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, 200, "Tag search endpoint should return 200")
|
||||
|
||||
# Check if media with the tag are in the results
|
||||
media_titles = [item['title'] for item in response.data['results']]
|
||||
self.assertIn(self.media1.title, media_titles, "Media with tag should be in results")
|
||||
self.assertIn(self.media2.title, media_titles, "Media with tag should be in results")
|
||||
self.assertNotIn(self.media3.title, media_titles, "Media without tag should not be in results")
|
||||
|
||||
def test_search_with_media_type_filter(self):
|
||||
"""Test searching with media type filter"""
|
||||
url = '/api/v1/search?q=tutorial&media_type=video'
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, 200, "Media type filtered search should return 200")
|
||||
|
||||
# Create an image media with the same search term
|
||||
with open('fixtures/test_image2.jpg', "rb") as f:
|
||||
myfile = File(f)
|
||||
image_media = Media.objects.create(
|
||||
title="Tutorial Image",
|
||||
description="Tutorial image description",
|
||||
user=self.user,
|
||||
media_file=myfile,
|
||||
)
|
||||
image_media.update_search_vector()
|
||||
|
||||
# Search with media_type=video
|
||||
url = '/api/v1/search?q=tutorial&media_type=video'
|
||||
response = self.client.get(url)
|
||||
|
||||
media_titles = [item['title'] for item in response.data['results']]
|
||||
self.assertNotIn(image_media.title, media_titles, "Image media should not be in results")
|
@@ -1,10 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestX(TestCase):
|
||||
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
|
||||
|
||||
def test_X(self):
|
||||
# add a few files, check search different cases that work
|
||||
|
||||
pass
|
97
tests/settings/test_portal_workflow.py
Normal file
97
tests/settings/test_portal_workflow.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from django.conf import settings
|
||||
from django.core.files import File
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from files.helpers import get_default_state, get_portal_workflow
|
||||
from files.models import Media
|
||||
from files.tests import create_account
|
||||
|
||||
|
||||
class TestPortalWorkflow(TestCase):
|
||||
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
|
||||
|
||||
def setUp(self):
|
||||
self.user = create_account()
|
||||
self.advanced_user = create_account(username="advanced_user")
|
||||
self.advanced_user.advancedUser = True
|
||||
self.advanced_user.save()
|
||||
|
||||
def test_default_portal_workflow(self):
|
||||
"""Test the default portal workflow setting"""
|
||||
workflow = get_portal_workflow()
|
||||
self.assertEqual(workflow, settings.PORTAL_WORKFLOW, "get_portal_workflow should return the PORTAL_WORKFLOW setting")
|
||||
|
||||
@override_settings(PORTAL_WORKFLOW='public')
|
||||
def test_public_workflow(self):
|
||||
"""Test the public workflow setting"""
|
||||
# Check that get_portal_workflow returns the correct value
|
||||
self.assertEqual(get_portal_workflow(), 'public', "get_portal_workflow should return 'public'")
|
||||
|
||||
# Check that get_default_state returns the correct value
|
||||
self.assertEqual(get_default_state(), 'public', "get_default_state should return 'public'")
|
||||
|
||||
# Check that a new media gets the correct state
|
||||
# Mock the media_file requirement by patching the save method
|
||||
with open('fixtures/test_image2.jpg', "rb") as f:
|
||||
myfile = File(f)
|
||||
media = Media.objects.create(title="Test Media", description="Test Description", user=self.user, media_file=myfile)
|
||||
self.assertEqual(media.state, 'public', "Media state should be 'public' in public workflow")
|
||||
|
||||
@override_settings(PORTAL_WORKFLOW='unlisted')
|
||||
def test_unlisted_workflow(self):
|
||||
"""Test the unlisted workflow setting"""
|
||||
# Check that get_portal_workflow returns the correct value
|
||||
self.assertEqual(get_portal_workflow(), 'unlisted', "get_portal_workflow should return 'unlisted'")
|
||||
|
||||
# Check that get_default_state returns the correct value
|
||||
self.assertEqual(get_default_state(), 'unlisted', "get_default_state should return 'unlisted'")
|
||||
|
||||
# Check that a new media gets the correct state
|
||||
# Mock the media_file requirement by patching the save method
|
||||
with open('fixtures/test_image2.jpg', "rb") as f:
|
||||
myfile = File(f)
|
||||
media = Media.objects.create(title="Test Media", description="Test Description", user=self.user, media_file=myfile)
|
||||
self.assertEqual(media.state, 'unlisted', "Media state should be 'unlisted' in unlisted workflow")
|
||||
|
||||
@override_settings(PORTAL_WORKFLOW='private')
|
||||
def test_private_workflow(self):
|
||||
"""Test the private workflow setting"""
|
||||
# Check that get_portal_workflow returns the correct value
|
||||
self.assertEqual(get_portal_workflow(), 'private', "get_portal_workflow should return 'private'")
|
||||
|
||||
# Check that get_default_state returns the correct value
|
||||
self.assertEqual(get_default_state(), 'private', "get_default_state should return 'private'")
|
||||
|
||||
# Check that a new media gets the correct state
|
||||
# Mock the media_file requirement by patching the save method
|
||||
with open('fixtures/test_image2.jpg', "rb") as f:
|
||||
myfile = File(f)
|
||||
media = Media.objects.create(title="Test Media", description="Test Description", user=self.user, media_file=myfile)
|
||||
self.assertEqual(media.state, 'private', "Media state should be 'private' in private workflow")
|
||||
|
||||
@override_settings(PORTAL_WORKFLOW='private_verified')
|
||||
def test_private_verified_workflow(self):
|
||||
"""Test the private_verified workflow setting"""
|
||||
# Check that get_portal_workflow returns the correct value
|
||||
self.assertEqual(get_portal_workflow(), 'private_verified', "get_portal_workflow should return 'private_verified'")
|
||||
|
||||
# Check that get_default_state returns the correct value for regular user
|
||||
self.assertEqual(get_default_state(user=self.user), 'private', "get_default_state should return 'private' for regular user")
|
||||
|
||||
# Check that get_default_state returns the correct value for advanced user
|
||||
self.advanced_user.advancedUser = True
|
||||
self.advanced_user.save()
|
||||
self.assertEqual(get_default_state(user=self.advanced_user), 'unlisted', "get_default_state should return 'unlisted' for advanced user")
|
||||
|
||||
# Check that a new media gets the correct state for regular user
|
||||
# Mock the media_file requirement by patching the save method
|
||||
with open('fixtures/test_image2.jpg', "rb") as f:
|
||||
myfile = File(f)
|
||||
media = Media.objects.create(title="Test Media", description="Test Description", user=self.user, media_file=myfile)
|
||||
self.assertEqual(media.state, 'private', "Media state should be 'private' for regular user in private_verified workflow")
|
||||
|
||||
# Check that a new media gets the correct state for advanced user
|
||||
with open('fixtures/test_image2.jpg', "rb") as f:
|
||||
myfile = File(f)
|
||||
media = Media.objects.create(title="Advanced Test Media", description="Test Description", user=self.advanced_user, media_file=myfile)
|
||||
self.assertEqual(media.state, 'unlisted', "Media state should be 'unlisted' for advanced user in private_verified workflow")
|
@@ -1,11 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestX(TestCase):
|
||||
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
|
||||
|
||||
def test_X(self):
|
||||
# test what is the default portal workflow
|
||||
# change it and make sure nothing strange happens (public/unlisted/private)
|
||||
|
||||
pass
|
51
tests/test_imports.py
Normal file
51
tests/test_imports.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestImports(TestCase):
|
||||
"""Test that all models and views can be imported correctly"""
|
||||
|
||||
def test_model_imports(self):
|
||||
"""Test that all models can be imported"""
|
||||
# Import all models
|
||||
from files.models import Category # noqa: F401
|
||||
from files.models import Comment # noqa: F401
|
||||
from files.models import EncodeProfile # noqa: F401
|
||||
from files.models import Encoding # noqa: F401
|
||||
from files.models import Language # noqa: F401
|
||||
from files.models import License # noqa: F401
|
||||
from files.models import Media # noqa: F401
|
||||
from files.models import MediaPermission # noqa: F401
|
||||
from files.models import Playlist # noqa: F401
|
||||
from files.models import PlaylistMedia # noqa: F401
|
||||
from files.models import Rating # noqa: F401
|
||||
from files.models import RatingCategory # noqa: F401
|
||||
from files.models import Subtitle # noqa: F401
|
||||
from files.models import Tag # noqa: F401
|
||||
from files.models import VideoChapterData # noqa: F401
|
||||
from files.models import VideoTrimRequest # noqa: F401
|
||||
|
||||
# Simple assertion to verify imports worked
|
||||
self.assertTrue(True, "All model imports succeeded")
|
||||
|
||||
def test_view_imports(self):
|
||||
"""Test that all views can be imported"""
|
||||
# Import all views
|
||||
from files.views import CategoryList # noqa: F401
|
||||
from files.views import CommentDetail # noqa: F401
|
||||
from files.views import CommentList # noqa: F401
|
||||
from files.views import EncodeProfileList # noqa: F401
|
||||
from files.views import EncodingDetail # noqa: F401
|
||||
from files.views import MediaActions # noqa: F401
|
||||
from files.views import MediaBulkUserActions # noqa: F401
|
||||
from files.views import MediaDetail # noqa: F401
|
||||
from files.views import MediaList # noqa: F401
|
||||
from files.views import MediaSearch # noqa: F401
|
||||
from files.views import PlaylistDetail # noqa: F401
|
||||
from files.views import PlaylistList # noqa: F401
|
||||
from files.views import TagList # noqa: F401
|
||||
from files.views import TaskDetail # noqa: F401
|
||||
from files.views import TasksList # noqa: F401
|
||||
from files.views import UserActions # noqa: F401
|
||||
|
||||
# Simple assertion to verify imports worked
|
||||
self.assertTrue(True, "All view imports succeeded")
|
@@ -11,7 +11,7 @@ from imagekit.models import ProcessedImageField
|
||||
from imagekit.processors import ResizeToFill
|
||||
|
||||
import files.helpers as helpers
|
||||
from files.models import Category, Media, Tag
|
||||
from files.models import Category, Media, MediaPermission, Tag
|
||||
from rbac.models import RBACGroup
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ class User(AbstractUser):
|
||||
Get all categories related to RBAC groups the user belongs to
|
||||
"""
|
||||
rbac_groups = RBACGroup.objects.filter(memberships__user=self, memberships__role__in=["member", "contributor", "manager"])
|
||||
categories = Category.objects.filter(rbac_groups__in=rbac_groups).distinct()
|
||||
categories = Category.objects.prefetch_related("user").filter(rbac_groups__in=rbac_groups).distinct()
|
||||
return categories
|
||||
|
||||
def has_member_access_to_category(self, category):
|
||||
@@ -131,8 +131,63 @@ class User(AbstractUser):
|
||||
return rbac_groups.exists()
|
||||
|
||||
def has_member_access_to_media(self, media):
|
||||
# First check if user is the owner
|
||||
if media.user == self:
|
||||
return True
|
||||
|
||||
# Then check RBAC permissions
|
||||
if getattr(settings, 'USE_RBAC', False):
|
||||
rbac_groups = RBACGroup.objects.filter(memberships__user=self, memberships__role__in=["member", "contributor", "manager"], categories__in=media.category.all()).distinct()
|
||||
return rbac_groups.exists()
|
||||
if rbac_groups.exists():
|
||||
return True
|
||||
|
||||
# Then check MediaShare permissions for any access
|
||||
media_permission_exists = MediaPermission.objects.filter(
|
||||
user=self,
|
||||
media=media,
|
||||
).exists()
|
||||
|
||||
return media_permission_exists
|
||||
|
||||
def has_contributor_access_to_media(self, media):
|
||||
# First check if user is the owner
|
||||
if media.user == self:
|
||||
return True
|
||||
|
||||
# Then check RBAC permissions
|
||||
if getattr(settings, 'USE_RBAC', False):
|
||||
rbac_groups = RBACGroup.objects.filter(memberships__user=self, memberships__role__in=["contributor", "manager"], categories__in=media.category.all()).distinct()
|
||||
if rbac_groups.exists():
|
||||
return True
|
||||
|
||||
# Then check MediaShare permissions for editor or owner access
|
||||
media_permission_exists = MediaPermission.objects.filter(
|
||||
user=self,
|
||||
media=media,
|
||||
permission__in=["editor", "owner"],
|
||||
).exists()
|
||||
|
||||
return media_permission_exists
|
||||
|
||||
def has_owner_access_to_media(self, media):
|
||||
# First check if user is the owner
|
||||
if media.user == self:
|
||||
return True
|
||||
|
||||
# Then check RBAC permissions
|
||||
if getattr(settings, 'USE_RBAC', False):
|
||||
rbac_groups = RBACGroup.objects.filter(memberships__user=self, memberships__role__in=["manager"], categories__in=media.category.all()).distinct()
|
||||
if rbac_groups.exists():
|
||||
return True
|
||||
|
||||
# Then check MediaShare permissions for owner access
|
||||
media_permission_exists = MediaPermission.objects.filter(
|
||||
user=self,
|
||||
media=media,
|
||||
permission="owner",
|
||||
).exists()
|
||||
|
||||
return media_permission_exists
|
||||
|
||||
def get_rbac_categories_as_contributor(self):
|
||||
"""
|
||||
|
@@ -5,11 +5,8 @@ from . import views
|
||||
urlpatterns = [
|
||||
re_path(r"^user/(?P<username>[\w@._-]*)$", views.view_user, name="get_user"),
|
||||
re_path(r"^user/(?P<username>[\w@._-]*)/$", views.view_user, name="get_user"),
|
||||
re_path(
|
||||
r"^user/(?P<username>[\w@.]*)/media$",
|
||||
views.view_user_media,
|
||||
name="get_user_media",
|
||||
),
|
||||
re_path(r"^user/(?P<username>[\w@._-]*)/shared_with_me", views.shared_with_me, name="shared_with_me"),
|
||||
re_path(r"^user/(?P<username>[\w@._-]*)/shared_by_me", views.shared_by_me, name="shared_by_me"),
|
||||
re_path(
|
||||
r"^user/(?P<username>[\w@.]*)/playlists$",
|
||||
views.view_user_playlists,
|
||||
@@ -20,7 +17,7 @@ urlpatterns = [
|
||||
views.view_user_about,
|
||||
name="get_user_about",
|
||||
),
|
||||
re_path(r"^user/(?P<username>[\w@.]*)/edit$", views.edit_user, name="edit_user"),
|
||||
re_path(r"^user/(?P<username>[\w@._-]*)/edit$", views.edit_user, name="edit_user"),
|
||||
re_path(r"^channel/(?P<friendly_token>[\w]*)$", views.view_channel, name="view_channel"),
|
||||
re_path(
|
||||
r"^channel/(?P<friendly_token>[\w]*)/edit$",
|
||||
|
@@ -1,6 +1,7 @@
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.mail import EmailMessage
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from drf_yasg import openapi as openapi
|
||||
@@ -47,17 +48,28 @@ def view_user(request, username):
|
||||
return render(request, "cms/user.html", context)
|
||||
|
||||
|
||||
def view_user_media(request, username):
|
||||
def shared_with_me(request, username):
|
||||
context = {}
|
||||
user = get_user(username=username)
|
||||
if not user:
|
||||
return HttpResponseRedirect("/members")
|
||||
if not user or (user != request.user):
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
context["user"] = user
|
||||
context["CAN_EDIT"] = 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["SHOW_CONTACT_FORM"] = True if (user.allow_contact or is_mediacms_editor(request.user)) else False
|
||||
return render(request, "cms/user_media.html", context)
|
||||
context["CAN_EDIT"] = True
|
||||
context["CAN_DELETE"] = True
|
||||
return render(request, "cms/user_shared_with_me.html", context)
|
||||
|
||||
|
||||
def shared_by_me(request, username):
|
||||
context = {}
|
||||
user = get_user(username=username)
|
||||
if not user or (user != request.user):
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
context["user"] = user
|
||||
context["CAN_EDIT"] = True
|
||||
context["CAN_DELETE"] = True
|
||||
return render(request, "cms/user_shared_by_me.html", context)
|
||||
|
||||
|
||||
def view_user_playlists(request, username):
|
||||
@@ -176,12 +188,17 @@ Sender email: %s\n
|
||||
|
||||
|
||||
class UserList(APIView):
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
||||
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)
|
||||
|
||||
def get_permissions(self):
|
||||
if not settings.ALLOW_ANONYMOUS_USER_LISTING:
|
||||
return [permissions.IsAuthenticated()]
|
||||
return [permissions.IsAuthenticatedOrReadOnly()]
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[
|
||||
openapi.Parameter(name='page', type=openapi.TYPE_INTEGER, in_=openapi.IN_QUERY, description='Page number'),
|
||||
openapi.Parameter(name='name', type=openapi.TYPE_STRING, in_=openapi.IN_QUERY, description='Search by name or username'),
|
||||
],
|
||||
tags=['Users'],
|
||||
operation_summary='List users',
|
||||
@@ -191,9 +208,10 @@ class UserList(APIView):
|
||||
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
|
||||
paginator = pagination_class()
|
||||
users = User.objects.filter()
|
||||
location = request.GET.get("location", "").strip()
|
||||
if location:
|
||||
users = users.filter(location=location)
|
||||
|
||||
name = request.GET.get("name", "").strip()
|
||||
if name:
|
||||
users = users.filter(Q(name__icontains=name) | Q(username__icontains=name))
|
||||
|
||||
page = paginator.paginate_queryset(users, request)
|
||||
|
||||
|
Reference in New Issue
Block a user