Compare commits
10 Commits
64615089fe
...
99413ccfd8
Author | SHA1 | Date | |
---|---|---|---|
99413ccfd8 | |||
608725df99 | |||
0e5a5e8cc5 | |||
0716b8b063 | |||
8a361924e9 | |||
b36d20a99d | |||
d9645865c8 | |||
0221fc1ac2 | |||
89ae1d666b | |||
22784efe25 |
25
README.md
25
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,6 +14,11 @@ This project is distributed under GPL version 3. More information in the LICENSE
|
||||
* **cms**: content management system.
|
||||
|
||||
|
||||
## 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 :
|
||||
|
||||
@ -40,7 +44,21 @@ DJANGO_SETTINGS_MODULE=instance.settings.dev pytest
|
||||
DJANGO_SETTINGS_MODULE=instance.settings.dev ./manage.py runserver
|
||||
```
|
||||
|
||||
## Scripts
|
||||
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`:
|
||||
|
||||
@ -52,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
|
||||
|
@ -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):
|
||||
|
@ -1,67 +1,90 @@
|
||||
{% extends "aircox/page_detail.html" %}
|
||||
{% comment %}Detail page of a show{% endcomment %}
|
||||
{% load i18n %}
|
||||
{% extends "aircox/basepage_detail.html" %}
|
||||
{% load static i18n humanize honeypot aircox %}
|
||||
{% comment %}
|
||||
Base template used to display a Page
|
||||
|
||||
{% include "aircox/program_sidebar.html" %}
|
||||
Context:
|
||||
- page: page
|
||||
- parent: parent page
|
||||
{% endcomment %}
|
||||
|
||||
|
||||
{% block header_nav %}
|
||||
{% block header_crumbs %}
|
||||
{{ block.super }}
|
||||
{% if page.category %}
|
||||
{% if parent %} / {% endif %} {{ page.category.title }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block top-nav-tools %}
|
||||
{% has_perm page page.change_permission_codename simple=True as can_edit %}
|
||||
{% if can_edit %}
|
||||
<a class="navbar-item" href="{% url 'program-edit' page.pk %}"
|
||||
target="new">
|
||||
<span class="icon is-small">
|
||||
<i class="fa fa-pen"></i>
|
||||
</span>
|
||||
<span>{% translate "Edit" %}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block main %}
|
||||
{{ block.super }}
|
||||
<br>
|
||||
{% with has_headline=False %}
|
||||
{% if articles %}
|
||||
<section>
|
||||
<h4 class="title is-4">{% translate "Articles" %}</h4>
|
||||
|
||||
{% for object in articles %}
|
||||
{% include "aircox/widgets/page_item.html" %}
|
||||
{% block comments %}
|
||||
{% if comments or comment_form %}
|
||||
<section class="mt-6">
|
||||
<h4 class="title is-4">{% translate "Comments" %}</h4>
|
||||
|
||||
{% for comment in comments %}
|
||||
<div class="media box">
|
||||
<div class="media-content">
|
||||
<p>
|
||||
<strong class="mr-2">{{ comment.nickname }}</strong>
|
||||
<time datetime="{{ comment.date }}" title="{{ comment.date }}">
|
||||
<small>{{ comment.date|naturaltime }}</small>
|
||||
</time>
|
||||
<br>
|
||||
{{ comment.content }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<br>
|
||||
<nav class="pagination is-centered">
|
||||
<ul class="pagination-list">
|
||||
<li>
|
||||
<a href="{% url "article-list" parent_slug=program.slug %}"
|
||||
class="pagination-link"
|
||||
aria-label="{% translate "Show all program's articles" %}">
|
||||
{% translate "More articles" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{% if comment_form %}
|
||||
<form method="POST">
|
||||
<h5 class="title is-5">{% translate "Post a comment" %}</h5>
|
||||
{% csrf_token %}
|
||||
{% render_honeypot_field "website" %}
|
||||
|
||||
{% for field in comment_form %}
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">
|
||||
{{ field.label_tag }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<p class="control is-expanded">{{ field }}</p>
|
||||
{% if field.errors %}
|
||||
<p class="help is-danger">{{ field.errors }}</p>
|
||||
{% endif %}
|
||||
{% if field.help_text %}
|
||||
<p class="help">{{ field.help_text|safe }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="has-text-right">
|
||||
<button type="reset" class="button is-danger">{% translate "Reset" %}</button>
|
||||
<button type="submit" class="button is-success">{% translate "Post comment" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
<section>
|
||||
<h4 class="title is-4">{% translate "Diffusions" %}</h4>
|
||||
{% for schedule in program.schedule_set.all %}
|
||||
{{ schedule.get_frequency_display }}
|
||||
{% with schedule.start|date:"H:i" as start %}
|
||||
{% with schedule.end|date:"H:i" as end %}
|
||||
<time datetime="{{ start }}">{{ start }}</time>
|
||||
—
|
||||
<time datetime="{{ end }}">{{ end }}</time>
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
<small>
|
||||
{% if schedule.initial %}
|
||||
{% with schedule.initial.date as date %}
|
||||
<span title="{% blocktranslate %}Rerun of {{ date }}{% endblocktranslate %}">
|
||||
({% translate "Rerun" %})
|
||||
</span>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</small>
|
||||
<br>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
|
91
aircox/templates/aircox/program_form.html
Normal file
91
aircox/templates/aircox/program_form.html
Normal file
@ -0,0 +1,91 @@
|
||||
{% extends "aircox/basepage_detail.html" %}
|
||||
{% load static i18n humanize honeypot aircox %}
|
||||
{% comment %}
|
||||
Base template used to display a Page
|
||||
|
||||
Context:
|
||||
- page: page
|
||||
- parent: parent page
|
||||
{% endcomment %}
|
||||
|
||||
{% block header_crumbs %}
|
||||
{{ block.super }}
|
||||
{% if page.category %}
|
||||
{% if parent %} / {% endif %} {{ page.category.title }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block top-nav-tools %}
|
||||
<a class="navbar-item" href="{% url 'program-detail' object.slug %}"
|
||||
target="new">
|
||||
<span class="icon is-small">
|
||||
<i class="fa fa-eye"></i>
|
||||
</span>
|
||||
<span>{% translate "View" %}</span>
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<form method="post">{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" value="Update" class="button is-success">
|
||||
</form>
|
||||
{{ block.super }}
|
||||
|
||||
{% block comments %}
|
||||
{% if comments or comment_form %}
|
||||
<section class="mt-6">
|
||||
<h4 class="title is-4">{% translate "Comments" %}</h4>
|
||||
|
||||
{% for comment in comments %}
|
||||
<div class="media box">
|
||||
<div class="media-content">
|
||||
<p>
|
||||
<strong class="mr-2">{{ comment.nickname }}</strong>
|
||||
<time datetime="{{ comment.date }}" title="{{ comment.date }}">
|
||||
<small>{{ comment.date|naturaltime }}</small>
|
||||
</time>
|
||||
<br>
|
||||
{{ comment.content }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if comment_form %}
|
||||
<form method="POST">
|
||||
<h5 class="title is-5">{% translate "Post a comment" %}</h5>
|
||||
{% csrf_token %}
|
||||
{% render_honeypot_field "website" %}
|
||||
|
||||
{% for field in comment_form %}
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">
|
||||
{{ field.label_tag }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<p class="control is-expanded">{{ field }}</p>
|
||||
{% if field.errors %}
|
||||
<p class="help is-danger">{{ field.errors }}</p>
|
||||
{% endif %}
|
||||
{% if field.help_text %}
|
||||
<p class="help">{{ field.help_text|safe }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="has-text-right">
|
||||
<button type="reset" class="button is-danger">{% translate "Reset" %}</button>
|
||||
<button type="submit" class="button is-success">{% translate "Post comment" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
@ -30,11 +30,14 @@ def do_get_tracks(obj):
|
||||
|
||||
|
||||
@register.simple_tag(name="has_perm", takes_context=True)
|
||||
def do_has_perm(context, obj, perm, user=None):
|
||||
def do_has_perm(context, obj, perm, user=None, simple=False):
|
||||
"""Return True if ``user.has_perm('[APP].[perm]_[MODEL]')``"""
|
||||
if user is None:
|
||||
user = context["request"].user
|
||||
return user.has_perm("{}.{}_{}".format(obj._meta.app_label, perm, obj._meta.model_name))
|
||||
if simple:
|
||||
return user.has_perm("aircox.{}".format(perm))
|
||||
else:
|
||||
return user.has_perm("{}.{}_{}".format(obj._meta.app_label, perm, obj._meta.model_name))
|
||||
|
||||
|
||||
@register.filter(name="is_diffusion")
|
||||
|
@ -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")
|
||||
|
36
aircox/tests/test_permissions.py
Normal file
36
aircox/tests/test_permissions.py
Normal file
@ -0,0 +1,36 @@
|
||||
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):
|
||||
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={"pk": program.pk}))
|
||||
assert response.status_code == 403
|
||||
user.groups.add(program.editors)
|
||||
response = client.get(reverse("program-edit", kwargs={"pk": program.pk}))
|
||||
assert response.status_code == 200
|
17
aircox/tests/test_program.py
Normal file
17
aircox/tests/test_program.py
Normal file
@ -0,0 +1,17 @@
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
@pytest.mark.django_db()
|
||||
def test_edit_program(user, client, program):
|
||||
client.force_login(user)
|
||||
response = client.get(reverse("program-detail", kwargs={"slug": program.slug}))
|
||||
assert response.status_code == 200
|
||||
assert b"fa-pen" not in response.content
|
||||
user.groups.add(program.editors)
|
||||
response = client.get(reverse("program-detail", kwargs={"slug": program.slug}))
|
||||
assert b"fa-pen" in response.content
|
||||
assert b"foobar" not in response.content
|
||||
response = client.post(reverse("program-edit", kwargs={"pk": program.pk}), {"content": "foobar"})
|
||||
response = client.get(reverse("program-detail", kwargs={"slug": program.slug}))
|
||||
assert b"foobar" in response.content
|
@ -92,6 +92,11 @@ urls = [
|
||||
views.ProgramDetailView.as_view(),
|
||||
name="program-detail",
|
||||
),
|
||||
path(
|
||||
_("program/<pk>/edit/"),
|
||||
views.ProgramUpdateView.as_view(),
|
||||
name="program-edit",
|
||||
),
|
||||
path(
|
||||
_("programs/<slug:parent_slug>/episodes/"),
|
||||
views.EpisodeListView.as_view(),
|
||||
|
@ -16,6 +16,7 @@ from .program import (
|
||||
ProgramListView,
|
||||
ProgramPageDetailView,
|
||||
ProgramPageListView,
|
||||
ProgramUpdateView,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
@ -39,4 +40,5 @@ __all__ = (
|
||||
"ProgramListView",
|
||||
"ProgramPageDetailView",
|
||||
"ProgramPageListView",
|
||||
"ProgramUpdateView",
|
||||
)
|
||||
|
@ -1,6 +1,7 @@
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import DetailView, ListView
|
||||
from django.views.generic.edit import UpdateView
|
||||
from honeypot.decorators import check_honeypot
|
||||
|
||||
from ..filters import PageFilters
|
||||
@ -138,3 +139,7 @@ class PageDetailView(BasePageDetailView):
|
||||
comment.page = self.object
|
||||
comment.save()
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
|
||||
class PageUpdateView(BaseView, UpdateView):
|
||||
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"]
|
||||
|
||||
@ -23,10 +25,25 @@ class BaseProgramMixin:
|
||||
class ProgramDetailView(BaseProgramMixin, PageDetailView):
|
||||
model = Program
|
||||
|
||||
def get_template_names(self):
|
||||
return super().get_template_names() + ["aircox/program_detail.html"]
|
||||
|
||||
def get_sidebar_queryset(self):
|
||||
return super().get_sidebar_queryset().filter(parent=self.program)
|
||||
|
||||
|
||||
class ProgramUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
|
||||
model = Program
|
||||
fields = ["content"]
|
||||
|
||||
def get_sidebar_queryset(self):
|
||||
return super().get_sidebar_queryset().filter(parent=self.program)
|
||||
|
||||
def test_func(self):
|
||||
program = self.get_object()
|
||||
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"),
|
||||
|
Reference in New Issue
Block a user