Compare commits
	
		
			14 Commits
		
	
	
		
			7cdba90bed
			...
			4b8acf5fbd
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4b8acf5fbd | |||
| 3a5eeba273 | |||
| 8a361924e9 | |||
| b36d20a99d | |||
| d9645865c8 | |||
| 0221fc1ac2 | |||
| 89ae1d666b | |||
| 22784efe25 | |||
| 64615089fe | |||
| f7210de088 | |||
| 2a8aa80d8b | |||
| ad60d9fce4 | |||
| 8c05712110 | |||
| b5eadf7f6c | 
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -5,3 +5,6 @@ venv/
 | 
			
		||||
node_modules/
 | 
			
		||||
*.egg-info/
 | 
			
		||||
*.egg
 | 
			
		||||
 | 
			
		||||
db.sqlite3
 | 
			
		||||
instance/settings/settings.py
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										54
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										54
									
								
								README.md
									
									
									
									
									
								
							@ -1,10 +1,9 @@
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
@ -62,8 +104,8 @@ pip install -r requirements.txt
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Configuration
 | 
			
		||||
You must write a settings.py file in the `instance` directory (you can just
 | 
			
		||||
copy and paste `instance/sample_settings.py`. There still is configuration
 | 
			
		||||
You must write a settings.py file in the `instance/settings` directory (you can just
 | 
			
		||||
copy and paste `instance/settings/sample.py`. There still is configuration
 | 
			
		||||
required in this file, check it in for more info.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										25
									
								
								aircox/migrations/0015_program_editors.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								aircox/migrations/0015_program_editors.py
									
									
									
									
									
										Normal 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",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -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):
 | 
			
		||||
 | 
			
		||||
@ -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")
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										37
									
								
								aircox/tests/test_permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								aircox/tests/test_permissions.py
									
									
									
									
									
										Normal 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
 | 
			
		||||
@ -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(),
 | 
			
		||||
 | 
			
		||||
@ -138,3 +138,7 @@ class PageDetailView(BasePageDetailView):
 | 
			
		||||
        comment.page = self.object
 | 
			
		||||
        comment.save()
 | 
			
		||||
        return self.get(request, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PageUpdateView(PageDetailView):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
@ -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",
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,7 @@ For Django settings see:
 | 
			
		||||
    https://docs.djangoproject.com/en/3.1/topics/settings/
 | 
			
		||||
    https://docs.djangoproject.com/en/3.1/ref/settings/
 | 
			
		||||
"""
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
from zoneinfo import ZoneInfo
 | 
			
		||||
from .prod import *
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -17,5 +17,5 @@ dateutils~=0.6
 | 
			
		||||
mutagen~=1.45
 | 
			
		||||
Pillow~=9.0
 | 
			
		||||
psutil~=5.9
 | 
			
		||||
PyYAML==5.4
 | 
			
		||||
PyYAML==6.0.1
 | 
			
		||||
watchdog~=2.1
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user