Compare commits

...

10 Commits

11 changed files with 177 additions and 33 deletions

View File

@ -1,10 +1,9 @@
![](/logo.png)
Platform to manage a radio, schedules, website, and so on. We use the power of great tools like Django or Liquidsoap.
A platform to manage radio schedules, website content, and more. It uses the power of great tools like Django or Liquidsoap.
This project is distributed under GPL version 3. More information in the LICENSE file, except for some files whose license is indicated inside source code.
## Features
* **streams**: multiple random music streams when no program is played. We also can specify a time range and frequency for each;
* **diffusions**: generate diffusions time slot for programs that have schedule informations. Check for conflicts and rerun.
@ -15,7 +14,51 @@ This project is distributed under GPL version 3. More information in the LICENSE
* **cms**: content management system.
## Scripts
## Architecture and concepts
Aircox is divided in two main modules:
* `aircox`: basics of Aircox (programs, diffusions, sounds, etc. management); interface for managing a website with Aircox elements (playlists, timetable, players on the website);
* `aircox_streamer`: interact with application to generate audio stream (LiquidSoap);
## Development setup
Start installing a virtual environment :
```
virtualenv venv
source venv/bin/activate
pip install -r requirements.txt
pip install -r requirements_tests.txt
```
Then copy the default settings and initiate the database :
```
cp instance/settings/sample.py instance/settings/settings.py
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())" >> instance/settings/settings.py
DJANGO_SETTINGS_MODULE=instance.settings.dev ./manage.py migrate
```
Finally test and run the instance using development settings, and point your browser to http://localhost:8000 :
```
DJANGO_SETTINGS_MODULE=instance.settings.dev pytest
DJANGO_SETTINGS_MODULE=instance.settings.dev ./manage.py runserver
```
Before requesting a merge, enable pre-commit :
```
pip install pre-commit
pre-commit install
```
## Installation
Running Aircox on production involves:
* Aircox modules and a running Django project;
* a supervisor for common tasks (sounds monitoring, stream control, etc.) -- `supervisord`;
* a wsgi and an HTTP server -- `gunicorn`, `nginx`;
* a database supported by Django (MySQL, SQLite, PostGresSQL);
### Scripts
Are included various configuration scripts that can be used to ease setup. They
assume that the project is present in `/srv/apps/aircox`:
@ -27,7 +70,6 @@ The scripts are written with a combination of `cron`, `supervisord`, `nginx`
and `gunicorn` in mind.
## Installation
### Dependencies
For python dependencies take a peek at the `requirements.txt` file, plus
dependencies specific to Django (e.g. for database: `mysqlclient` for MySql

View File

@ -86,8 +86,8 @@ class Settings(BaseSettings):
# TODO include content_type in order to avoid clash with potential
# extra applications
# aircox
"change_program",
"change_episode",
"view_program",
"view_episode",
"change_diffusion",
"add_comment",
"change_comment",

View File

@ -0,0 +1,25 @@
# Generated by Django 4.2.5 on 2023-10-18 13:50
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
("aircox", "0014_alter_schedule_timezone"),
]
operations = [
migrations.AddField(
model_name="program",
name="editors",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="auth.group",
verbose_name="editors",
),
),
]

View File

@ -3,6 +3,8 @@ import os
import shutil
from django.conf import settings as conf
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models import F
from django.db.models.functions import Concat, Substr
@ -58,6 +60,7 @@ class Program(Page):
default=True,
help_text=_("update later diffusions according to schedule changes"),
)
editors = models.ForeignKey(Group, models.CASCADE, blank=True, null=True, verbose_name=_("editors"))
objects = ProgramQuerySet.as_manager()
detail_url_name = "program-detail"
@ -80,6 +83,14 @@ class Program(Page):
def excerpts_path(self):
return os.path.join(self.path, settings.SOUND_ARCHIVES_SUBDIR)
@property
def editors_group_name(self):
return "{self.title} editors"
@property
def change_permission_codename(self):
return f"change_program_{self.slug}"
def __init__(self, *kargs, **kwargs):
super().__init__(*kargs, **kwargs)
if self.slug:
@ -109,6 +120,18 @@ class Program(Page):
os.makedirs(path, exist_ok=True)
return os.path.exists(path)
def set_group_ownership(self):
editors, created = Group.objects.get_or_create(name=self.editors_group_name)
if created:
self.editors = editors
permission, _ = Permission.objects.get_or_create(
name=f"change program {self.title}",
codename=self.change_permission_codename,
content_type=ContentType.objects.get_for_model(self),
)
if permission not in editors.permissions.all():
editors.permissions.add(permission)
class Meta:
verbose_name = _("Program")
verbose_name_plural = _("Programs")
@ -134,6 +157,9 @@ class Program(Page):
shutil.move(abspath, self.abspath)
Sound.objects.filter(path__startswith=path_).update(file=Concat("file", Substr(F("file"), len(path_))))
self.set_group_ownership()
super().save(*kargs, **kwargs)
class ProgramChildQuerySet(PageQuerySet):
def station(self, station=None, id=None):

View File

@ -157,3 +157,8 @@ def tracks(episode, sound):
items += [baker.prepare(models.Track, sound=sound, position=i, timestamp=i * 60) for i in range(0, 3)]
models.Track.objects.bulk_create(items)
return items
@pytest.fixture
def user():
return User.objects.create_user(username="user1", password="bar")

View File

@ -0,0 +1,37 @@
import pytest
from django.contrib.auth.models import User, Group
from django.urls import reverse
@pytest.mark.django_db()
def test_no_admin(user, client):
client.force_login(user)
response = client.get("/admin/")
assert response.status_code != 200
@pytest.mark.django_db()
def test_user_cannot_change_program_or_episode(user, client, program):
assert not user.has_perm("aircox.change_program")
assert not user.has_perm("aircox.change_episode")
@pytest.mark.django_db()
def test_group_can_change_program(user, client, program):
program_editors = program.editors
assert program_editors in Group.objects.all()
assert not user.has_perm("aircox.%s" % program.change_permission_codename)
user.groups.add(program_editors)
user = User.objects.get(pk=user.pk) # reload user in order to have permissions set
assert program_editors in user.groups.all()
assert user.has_perm("aircox.%s" % program.change_permission_codename)
@pytest.mark.django_db()
def test_group_change_program(user, client, program):
client.force_login(user)
response = client.get(reverse("program-edit", kwargs={"slug": program.slug}))
assert response.status_code == 403
user.groups.add(program.editors)
response = client.get(reverse("program-edit", kwargs={"slug": program.slug}))
assert response.status_code == 200

View File

@ -92,6 +92,11 @@ urls = [
views.ProgramDetailView.as_view(),
name="program-detail",
),
path(
_("program/<slug:slug>/edit/"),
views.ProgramUpdateView.as_view(),
name="program-edit",
),
path(
_("programs/<slug:parent_slug>/episodes/"),
views.EpisodeListView.as_view(),

View File

@ -138,3 +138,7 @@ class PageDetailView(BasePageDetailView):
comment.page = self.object
comment.save()
return self.get(request, *args, **kwargs)
class PageUpdateView(PageDetailView):
pass

View File

@ -1,8 +1,10 @@
from django.urls import reverse
from django.contrib.auth.mixins import UserPassesTestMixin
from ..models import Page, Program, StaticPage
from .mixins import ParentMixin
from .page import PageDetailView, PageListView
from .page import PageDetailView, PageListView, PageUpdateView
__all__ = ["ProgramPageDetailView", "ProgramDetailView", "ProgramPageListView"]
@ -27,6 +29,18 @@ class ProgramDetailView(BaseProgramMixin, PageDetailView):
return super().get_sidebar_queryset().filter(parent=self.program)
class ProgramUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
model = Program
def get_sidebar_queryset(self):
return super().get_sidebar_queryset().filter(parent=self.program)
def test_func(self):
program = self.get_object()
print("XXX, aircox.%s" % program.change_permission_codename)
return self.request.user.has_perm("aircox.%s" % program.change_permission_codename)
class ProgramListView(PageListView):
model = Program
attach_to_value = StaticPage.ATTACH_TO_PROGRAMS

View File

@ -1,25 +0,0 @@
# General information
Aircox is a set of Django applications that aims to provide a radio management solution, and is
written in Python 3.5.
Running Aircox on production involves:
* Aircox modules and a running Django project;
* a supervisor for common tasks (sounds monitoring, stream control, etc.) -- `supervisord`;
* a wsgi and an HTTP server -- `gunicorn`, `nginx`;
* a database supported by Django (MySQL, SQLite, PostGresSQL);
# Architecture and concepts
Aircox is divided in three main modules:
* `programs`: basics of Aircox (programs, diffusions, sounds, etc. management);
* `controllers`: interact with application to generate audio stream (LiquidSoap);
* `cms`: create a website with Aircox elements (playlists, timetable, players on the website);
# Installation
# Configuration

View File

@ -7,6 +7,7 @@ try:
except ImportError:
pass
DEBUG = True
LOCALE_PATHS = ["aircox/locale", "aircox_streamer/locale"]
@ -15,7 +16,7 @@ LOGGING = {
"disable_existing_loggers": False,
"formatters": {
"timestamp": {
"format": "{asctime} {levelname} {message}",
"format": "{asctime} {module} {levelname} {message}",
"style": "{",
},
},
@ -26,6 +27,10 @@ LOGGING = {
},
},
"loggers": {
"root": {
"handlers": ["console"],
"level": os.getenv("DJANGO_LOG_LEVEL", "DEBUG"),
},
"aircox": {
"handlers": ["console"],
"level": os.getenv("DJANGO_LOG_LEVEL", "DEBUG"),
@ -40,3 +45,9 @@ LOGGING = {
},
},
}
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
}
}