Compare commits
	
		
			12 Commits
		
	
	
		
			0f1ca3017e
			...
			1d9dc4628a
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 1d9dc4628a | |||
| 9e952735b8 | |||
| 291949e6e8 | |||
| d63d949096 | |||
| a89117f69d | |||
| 0eeeb3bc09 | |||
| a1d6f0ef4a | |||
| b0afa0fd86 | |||
| 92f9a08856 | |||
| 9097bced4a | |||
| 61e6732b19 | |||
| fd4c765dc4 | 
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -5,3 +5,6 @@ venv/
 | 
				
			|||||||
node_modules/
 | 
					node_modules/
 | 
				
			||||||
*.egg-info/
 | 
					*.egg-info/
 | 
				
			||||||
*.egg
 | 
					*.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.
 | 
					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
 | 
					## Features
 | 
				
			||||||
* **streams**: multiple random music streams when no program is played. We also can specify a time range and frequency for each;
 | 
					* **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.
 | 
					* **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.
 | 
					* **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('SECRET_KEY = \"%s\"' % 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
 | 
					Are included various configuration scripts that can be used to ease setup. They
 | 
				
			||||||
assume that the project is present in `/srv/apps/aircox`:
 | 
					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.
 | 
					and `gunicorn` in mind.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Installation
 | 
					 | 
				
			||||||
### Dependencies
 | 
					### Dependencies
 | 
				
			||||||
For python dependencies take a peek at the `requirements.txt` file, plus
 | 
					For python dependencies take a peek at the `requirements.txt` file, plus
 | 
				
			||||||
dependencies specific to Django (e.g. for database: `mysqlclient` for MySql
 | 
					dependencies specific to Django (e.g. for database: `mysqlclient` for MySql
 | 
				
			||||||
@ -62,8 +104,8 @@ pip install -r requirements.txt
 | 
				
			|||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Configuration
 | 
					### Configuration
 | 
				
			||||||
You must write a settings.py file in the `instance` directory (you can just
 | 
					You must write a settings.py file in the `instance/settings` directory (you can just
 | 
				
			||||||
copy and paste `instance/sample_settings.py`. There still is configuration
 | 
					copy and paste `instance/settings/sample.py`. There still is configuration
 | 
				
			||||||
required in this file, check it in for more info.
 | 
					required in this file, check it in for more info.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										3
									
								
								aircox/context_processors/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								aircox/context_processors/__init__.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					def station(request):
 | 
				
			||||||
 | 
					    station = request.station
 | 
				
			||||||
 | 
					    return {"station": station, "audio_streams": station.streams}
 | 
				
			||||||
@ -1,5 +1,4 @@
 | 
				
			|||||||
#! /usr/bin/env python3
 | 
					#! /usr/bin/env python3
 | 
				
			||||||
# TODO: SoundMonitor class
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""Monitor sound files; For each program, check for:
 | 
					"""Monitor sound files; For each program, check for:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -60,10 +59,8 @@ class Command(BaseCommand):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def handle(self, *args, **options):
 | 
					    def handle(self, *args, **options):
 | 
				
			||||||
        SoundMonitor()
 | 
					        monitor = SoundMonitor()
 | 
				
			||||||
        if options.get("scan"):
 | 
					        if options.get("scan"):
 | 
				
			||||||
            self.scan()
 | 
					            monitor.scan()
 | 
				
			||||||
        # if options.get('quality_check'):
 | 
					 | 
				
			||||||
        #    self.check_quality(check=(not options.get('scan')))
 | 
					 | 
				
			||||||
        if options.get("monitor"):
 | 
					        if options.get("monitor"):
 | 
				
			||||||
            self.monitor()
 | 
					            monitor.monitor()
 | 
				
			||||||
 | 
				
			|||||||
@ -85,7 +85,7 @@ class Program(Page):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def editors_group_name(self):
 | 
					    def editors_group_name(self):
 | 
				
			||||||
        return "{self.title} editors"
 | 
					        return f"{self.title} editors"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def change_permission_codename(self):
 | 
					    def change_permission_codename(self):
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										15
									
								
								aircox/templates/accounts/profile.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								aircox/templates/accounts/profile.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					{% extends "aircox/base.html" %}
 | 
				
			||||||
 | 
					{% load i18n aircox %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block head_title %}
 | 
				
			||||||
 | 
					    {% block title %}{{ user.username }}{% endblock %}
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block main %}
 | 
				
			||||||
 | 
					  <h2 class="subtitle is-3">Mes émissions</h2>
 | 
				
			||||||
 | 
					  <ul>
 | 
				
			||||||
 | 
					  {% for p in programs %}
 | 
				
			||||||
 | 
					    <li>• <a href="{% url 'program-detail' slug=p.slug %}">{{ p.title }}</a></li>
 | 
				
			||||||
 | 
					  {% endfor %}
 | 
				
			||||||
 | 
					  </ul>
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
@ -68,6 +68,7 @@ Usefull context:
 | 
				
			|||||||
                        <div class="navbar-end">
 | 
					                        <div class="navbar-end">
 | 
				
			||||||
                            {% block top-nav-tools %}
 | 
					                            {% block top-nav-tools %}
 | 
				
			||||||
                            {% endblock %}
 | 
					                            {% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                            {% block top-nav-end %}
 | 
					                            {% block top-nav-end %}
 | 
				
			||||||
                            <div class="navbar-item">
 | 
					                            <div class="navbar-item">
 | 
				
			||||||
                                <form action="{% url 'page-list' %}" method="GET">
 | 
					                                <form action="{% url 'page-list' %}" method="GET">
 | 
				
			||||||
@ -81,6 +82,12 @@ Usefull context:
 | 
				
			|||||||
                                </form>
 | 
					                                </form>
 | 
				
			||||||
                            </div>
 | 
					                            </div>
 | 
				
			||||||
                            {% endblock %}
 | 
					                            {% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            {% if user.is_authenticated %}
 | 
				
			||||||
 | 
					                                <div class="navbar-item">
 | 
				
			||||||
 | 
					                                    <a href="{% url 'profile' %}">{{ user.username }}</a>   <a href="{% url 'logout' %}"> <i class="fa fa-power-off"></i></a>
 | 
				
			||||||
 | 
					                                </div>
 | 
				
			||||||
 | 
					                            {% endif %}
 | 
				
			||||||
                        </div>
 | 
					                        </div>
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,67 +1,90 @@
 | 
				
			|||||||
{% extends "aircox/page_detail.html" %}
 | 
					{% extends "aircox/basepage_detail.html" %}
 | 
				
			||||||
{% comment %}Detail page of a show{% endcomment %}
 | 
					{% load static i18n humanize honeypot aircox %}
 | 
				
			||||||
{% load i18n %}
 | 
					{% comment %}
 | 
				
			||||||
 | 
					Base template used to display a Page
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% include "aircox/program_sidebar.html" %}
 | 
					Context:
 | 
				
			||||||
 | 
					- page: page
 | 
				
			||||||
 | 
					- parent: parent page
 | 
				
			||||||
 | 
					{% endcomment %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block header_crumbs %}
 | 
				
			||||||
{% block header_nav %}
 | 
					{{ block.super }}
 | 
				
			||||||
 | 
					{% if page.category %}
 | 
				
			||||||
 | 
					{% if parent %} / {% endif %} {{ page.category.title }}
 | 
				
			||||||
 | 
					{% endif %}
 | 
				
			||||||
{% endblock %}
 | 
					{% 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 }}
 | 
					{{ block.super }}
 | 
				
			||||||
<br>
 | 
					 | 
				
			||||||
{% with has_headline=False %}
 | 
					 | 
				
			||||||
{% if articles %}
 | 
					 | 
				
			||||||
<section>
 | 
					 | 
				
			||||||
    <h4 class="title is-4">{% translate "Articles" %}</h4>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {% for object in articles %}
 | 
					{% block comments %}
 | 
				
			||||||
    {% include "aircox/widgets/page_item.html" %}
 | 
					{% 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 %}
 | 
					    {% endfor %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <br>
 | 
					    {% if comment_form %}
 | 
				
			||||||
    <nav class="pagination is-centered">
 | 
					    <form method="POST">
 | 
				
			||||||
        <ul class="pagination-list">
 | 
					        <h5 class="title is-5">{% translate "Post a comment" %}</h5>
 | 
				
			||||||
            <li>
 | 
					        {% csrf_token %}
 | 
				
			||||||
                <a href="{% url "article-list" parent_slug=program.slug %}"
 | 
					        {% render_honeypot_field "website" %}
 | 
				
			||||||
                    class="pagination-link"
 | 
					
 | 
				
			||||||
                    aria-label="{% translate "Show all program's articles" %}">
 | 
					        {% for field in comment_form %}
 | 
				
			||||||
                    {% translate "More articles" %}
 | 
					        <div class="field is-horizontal">
 | 
				
			||||||
                </a>
 | 
					            <div class="field-label is-normal">
 | 
				
			||||||
            </li>
 | 
					                <label class="label">
 | 
				
			||||||
        </ul>
 | 
					                    {{ field.label_tag }}
 | 
				
			||||||
    </nav>
 | 
					                </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>
 | 
					</section>
 | 
				
			||||||
{% endif %}
 | 
					{% endif %}
 | 
				
			||||||
{% endwith %}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% endblock %}
 | 
					{% 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 %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										24
									
								
								aircox/templates/aircox/program_form.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								aircox/templates/aircox/program_form.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					{% extends "aircox/basepage_detail.html" %}
 | 
				
			||||||
 | 
					{% load static i18n humanize honeypot aircox %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% 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 %}
 | 
				
			||||||
 | 
					    <table>
 | 
				
			||||||
 | 
					        {{ form.as_table }}
 | 
				
			||||||
 | 
					        {% render_honeypot_field "website" %}
 | 
				
			||||||
 | 
					    </table>
 | 
				
			||||||
 | 
					    <br/>
 | 
				
			||||||
 | 
					    <input type="submit" value="Update" class="button is-success">
 | 
				
			||||||
 | 
					</form>
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
							
								
								
									
										20
									
								
								aircox/templates/registration/login.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								aircox/templates/registration/login.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					{% extends "aircox/base.html" %}
 | 
				
			||||||
 | 
					{% load i18n aircox %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block main %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<h2>{% trans "Log in" %}</h2>
 | 
				
			||||||
 | 
					<br/>
 | 
				
			||||||
 | 
					<form method="post" action="{% url 'login' %}">
 | 
				
			||||||
 | 
					  {% csrf_token %}
 | 
				
			||||||
 | 
					  <table>
 | 
				
			||||||
 | 
					  {{ form.as_table }}
 | 
				
			||||||
 | 
					  </table>
 | 
				
			||||||
 | 
					  <br/>
 | 
				
			||||||
 | 
					  <button type="submit">{% trans "Log in" %}</button>
 | 
				
			||||||
 | 
					  <input type="hidden" name="next" value="{{ next }}">
 | 
				
			||||||
 | 
					</form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{{ block.super }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
@ -30,11 +30,14 @@ def do_get_tracks(obj):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@register.simple_tag(name="has_perm", takes_context=True)
 | 
					@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]')``"""
 | 
					    """Return True if ``user.has_perm('[APP].[perm]_[MODEL]')``"""
 | 
				
			||||||
    if user is None:
 | 
					    if user is None:
 | 
				
			||||||
        user = context["request"].user
 | 
					        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")
 | 
					@register.filter(name="is_diffusion")
 | 
				
			||||||
 | 
				
			|||||||
@ -206,29 +206,48 @@ def monitor():
 | 
				
			|||||||
    yield sound_monitor.SoundMonitor()
 | 
					    yield sound_monitor.SoundMonitor()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SoundMonitor:
 | 
					class TestSoundMonitor:
 | 
				
			||||||
 | 
					    @pytest.mark.django_db
 | 
				
			||||||
    def test_report(self, monitor, program, logger):
 | 
					    def test_report(self, monitor, program, logger):
 | 
				
			||||||
        monitor.report(program, "component", "content", logger=logger)
 | 
					        monitor.report(program, "component", "content", logger=logger)
 | 
				
			||||||
        msg = f"{program}, component: content"
 | 
					        msg = f"{program}, component: content"
 | 
				
			||||||
        assert logger._trace("info", args=True) == (msg,)
 | 
					        assert logger._trace("info", args=True) == (msg,)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_scan(self, monitor, program, logger):
 | 
					    @pytest.mark.django_db
 | 
				
			||||||
 | 
					    def test_scan(self, monitor, programs, logger):
 | 
				
			||||||
        interface = Interface(None, {"scan_for_program": None})
 | 
					        interface = Interface(None, {"scan_for_program": None})
 | 
				
			||||||
        monitor.scan_for_program = interface.scan_for_program
 | 
					        monitor.scan_for_program = interface.scan_for_program
 | 
				
			||||||
        dirs = monitor.scan(logger)
 | 
					        dirs = monitor.scan(logger)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        assert logger._traces("info") == (
 | 
					        assert logger._traces("info") == tuple(
 | 
				
			||||||
            "scan all programs...",
 | 
					            [
 | 
				
			||||||
            f"#{program.id} {program.title}",
 | 
					                (("scan all programs...",), {}),
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					            + [
 | 
				
			||||||
 | 
					                ((f"#{program.id} {program.title}",), {})
 | 
				
			||||||
 | 
					                for program in programs
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        assert dirs == [program.abspath]
 | 
					        assert dirs == [program.abspath for program in programs]
 | 
				
			||||||
        assert interface._traces("scan_for_program") == (
 | 
					        traces = tuple(
 | 
				
			||||||
            ((program, settings.SOUND_ARCHIVES_SUBDIR), {"logger": logger})(
 | 
					            [
 | 
				
			||||||
                (program, settings.SOUND_EXCERPTS_SUBDIR), {"logger": logger}
 | 
					                [
 | 
				
			||||||
            )
 | 
					                    (
 | 
				
			||||||
 | 
					                        (program, settings.SOUND_ARCHIVES_SUBDIR),
 | 
				
			||||||
 | 
					                        {"logger": logger, "type": Sound.TYPE_ARCHIVE},
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    (
 | 
				
			||||||
 | 
					                        (program, settings.SOUND_EXCERPTS_SUBDIR),
 | 
				
			||||||
 | 
					                        {"logger": logger, "type": Sound.TYPE_EXCERPT},
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					                for program in programs
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					        traces_flat = tuple([item for sublist in traces for item in sublist])
 | 
				
			||||||
 | 
					        assert interface._traces("scan_for_program") == traces_flat
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_monitor(self, monitor, monitor_interfaces, logger):
 | 
					    def broken_test_monitor(self, monitor, monitor_interfaces, logger):
 | 
				
			||||||
        def sleep(*args, **kwargs):
 | 
					        def sleep(*args, **kwargs):
 | 
				
			||||||
            monitor.stop()
 | 
					            monitor.stop()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										23
									
								
								aircox/tests/management/test_sounds_monitor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								aircox/tests/management/test_sounds_monitor.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					import pytest
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.core.management import call_command
 | 
				
			||||||
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					wav = (
 | 
				
			||||||
 | 
					    b"RIFF$\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01\x00\x02\x00D\xac\x00\x00"
 | 
				
			||||||
 | 
					    b"\x10\xb1\x02\x00\x04\x00\x10\x00data\x00\x00\x00\x00"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.django_db
 | 
				
			||||||
 | 
					def test_adding_a_sound(programs, fs):
 | 
				
			||||||
 | 
					    p0 = programs[0]
 | 
				
			||||||
 | 
					    assert len(p0.sound_set.all()) == 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    s0 = os.path.join(
 | 
				
			||||||
 | 
					        settings.PROJECT_ROOT, "static/media/%s/archives/sound.wav" % p0.path
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    fs.create_file(s0, contents=wav)
 | 
				
			||||||
 | 
					    call_command("sounds_monitor", "-s")
 | 
				
			||||||
 | 
					    assert len(p0.sound_set.all()) == 1
 | 
				
			||||||
@ -18,20 +18,19 @@ def test_user_cannot_change_program_or_episode(user, client, program):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@pytest.mark.django_db()
 | 
					@pytest.mark.django_db()
 | 
				
			||||||
def test_group_can_change_program(user, client, program):
 | 
					def test_group_can_change_program(user, client, program):
 | 
				
			||||||
    program_editors = program.editors
 | 
					    assert program.editors in Group.objects.all()
 | 
				
			||||||
    assert program_editors in Group.objects.all()
 | 
					 | 
				
			||||||
    assert not user.has_perm("aircox.%s" % program.change_permission_codename)
 | 
					    assert not user.has_perm("aircox.%s" % program.change_permission_codename)
 | 
				
			||||||
    user.groups.add(program_editors)
 | 
					    user.groups.add(program.editors)
 | 
				
			||||||
    user = User.objects.get(pk=user.pk)  # reload user in order to have permissions set
 | 
					    user = User.objects.get(pk=user.pk)  # reload user in order to have permissions set
 | 
				
			||||||
    assert program_editors in user.groups.all()
 | 
					    assert program.editors in user.groups.all()
 | 
				
			||||||
    assert user.has_perm("aircox.%s" % program.change_permission_codename)
 | 
					    assert user.has_perm("aircox.%s" % program.change_permission_codename)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.mark.django_db()
 | 
					@pytest.mark.django_db()
 | 
				
			||||||
def test_group_change_program(user, client, program):
 | 
					def test_group_change_program(user, client, program):
 | 
				
			||||||
    client.force_login(user)
 | 
					    client.force_login(user)
 | 
				
			||||||
    response = client.get(reverse("program-edit", kwargs={"slug": program.slug}))
 | 
					    response = client.get(reverse("program-edit", kwargs={"pk": program.pk}))
 | 
				
			||||||
    assert response.status_code == 403
 | 
					    assert response.status_code == 403
 | 
				
			||||||
    user.groups.add(program.editors)
 | 
					    user.groups.add(program.editors)
 | 
				
			||||||
    response = client.get(reverse("program-edit", kwargs={"slug": program.slug}))
 | 
					    response = client.get(reverse("program-edit", kwargs={"pk": program.pk}))
 | 
				
			||||||
    assert response.status_code == 200
 | 
					    assert response.status_code == 200
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										22
									
								
								aircox/tests/test_profile.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								aircox/tests/test_profile.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					import pytest
 | 
				
			||||||
 | 
					from django.urls import reverse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.django_db()
 | 
				
			||||||
 | 
					def test_authenticate(user, client, program):
 | 
				
			||||||
 | 
					    r = client.get(reverse("login"))
 | 
				
			||||||
 | 
					    assert r.status_code == 200
 | 
				
			||||||
 | 
					    assert b"id_username" in r.content
 | 
				
			||||||
 | 
					    r = client.post(reverse("login"), kwargs={"username": "foo", "password": "bar"})
 | 
				
			||||||
 | 
					    assert b"errorlist" in r.content
 | 
				
			||||||
 | 
					    assert client.login(username="user1", password="bar")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.django_db()
 | 
				
			||||||
 | 
					def test_profile_programs(user, client, program):
 | 
				
			||||||
 | 
					    client.force_login(user)
 | 
				
			||||||
 | 
					    r = client.get(reverse("profile"))
 | 
				
			||||||
 | 
					    assert program.title not in r.content.decode("utf-8")
 | 
				
			||||||
 | 
					    user.groups.add(program.editors)
 | 
				
			||||||
 | 
					    r = client.get(reverse("profile"))
 | 
				
			||||||
 | 
					    assert program.title in r.content.decode("utf-8")
 | 
				
			||||||
							
								
								
									
										40
									
								
								aircox/tests/test_program.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								aircox/tests/test_program.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,40 @@
 | 
				
			|||||||
 | 
					import pytest
 | 
				
			||||||
 | 
					from django.urls import reverse
 | 
				
			||||||
 | 
					from django.core.files.uploadedfile import SimpleUploadedFile
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from aircox.models import Program
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					png_content = (
 | 
				
			||||||
 | 
					    b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde"
 | 
				
			||||||
 | 
					    + b"\x00\x00\x00\x0cIDATx\x9cc`\xf8\xcf\x00\x00\x02\x02\x01\x00{\t\x81x\x00\x00\x00\x00IEND\xaeB`\x82"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.django_db()
 | 
				
			||||||
 | 
					def test_add_cover(user, client, program):
 | 
				
			||||||
 | 
					    assert program.cover is None
 | 
				
			||||||
 | 
					    user.groups.add(program.editors)
 | 
				
			||||||
 | 
					    client.force_login(user)
 | 
				
			||||||
 | 
					    cover = SimpleUploadedFile("cover1.png", png_content, content_type="image/png")
 | 
				
			||||||
 | 
					    r = client.post(
 | 
				
			||||||
 | 
					        reverse("program-edit", kwargs={"pk": program.pk}), {"content": "foobar", "new_cover": cover}, follow=True
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    assert r.status_code == 200
 | 
				
			||||||
 | 
					    p = Program.objects.get(pk=program.pk)
 | 
				
			||||||
 | 
					    assert "cover1.png" in p.cover.url
 | 
				
			||||||
@ -54,13 +54,11 @@ class TestBaseView:
 | 
				
			|||||||
        context = base_view.get_context_data()
 | 
					        context = base_view.get_context_data()
 | 
				
			||||||
        assert context == {
 | 
					        assert context == {
 | 
				
			||||||
            "view": base_view,
 | 
					            "view": base_view,
 | 
				
			||||||
            "station": station,
 | 
					 | 
				
			||||||
            "page": None,  # get_page() returns None
 | 
					            "page": None,  # get_page() returns None
 | 
				
			||||||
            "has_sidebar": base_view.has_sidebar,
 | 
					            "has_sidebar": base_view.has_sidebar,
 | 
				
			||||||
            "has_filters": False,
 | 
					            "has_filters": False,
 | 
				
			||||||
            "sidebar_object_list": published_pages[: base_view.list_count],
 | 
					            "sidebar_object_list": published_pages[: base_view.list_count],
 | 
				
			||||||
            "sidebar_list_url": base_view.get_sidebar_url(),
 | 
					            "sidebar_list_url": base_view.get_sidebar_url(),
 | 
				
			||||||
            "audio_streams": station.streams,
 | 
					 | 
				
			||||||
            "model": base_view.model,
 | 
					            "model": base_view.model,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -93,7 +93,7 @@ urls = [
 | 
				
			|||||||
        name="program-detail",
 | 
					        name="program-detail",
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    path(
 | 
					    path(
 | 
				
			||||||
        _("program/<slug:slug>/edit/"),
 | 
					        _("program/<pk>/edit/"),
 | 
				
			||||||
        views.ProgramUpdateView.as_view(),
 | 
					        views.ProgramUpdateView.as_view(),
 | 
				
			||||||
        name="program-edit",
 | 
					        name="program-edit",
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
@ -117,4 +117,6 @@ urls = [
 | 
				
			|||||||
        views.errors.NoStationErrorView.as_view(),
 | 
					        views.errors.NoStationErrorView.as_view(),
 | 
				
			||||||
        name="errors-no-station",
 | 
					        name="errors-no-station",
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
 | 
					    path("gestion/", views.profile, name="profile"),
 | 
				
			||||||
 | 
					    path("accounts/profile/", views.profile, name="profile"),
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
				
			|||||||
@ -11,6 +11,7 @@ from .page import (
 | 
				
			|||||||
    PageDetailView,
 | 
					    PageDetailView,
 | 
				
			||||||
    PageListView,
 | 
					    PageListView,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					from .profile import profile
 | 
				
			||||||
from .program import (
 | 
					from .program import (
 | 
				
			||||||
    ProgramDetailView,
 | 
					    ProgramDetailView,
 | 
				
			||||||
    ProgramListView,
 | 
					    ProgramListView,
 | 
				
			||||||
@ -36,6 +37,7 @@ __all__ = (
 | 
				
			|||||||
    "BasePageListView",
 | 
					    "BasePageListView",
 | 
				
			||||||
    "PageDetailView",
 | 
					    "PageDetailView",
 | 
				
			||||||
    "PageListView",
 | 
					    "PageListView",
 | 
				
			||||||
 | 
					    "profile",
 | 
				
			||||||
    "ProgramDetailView",
 | 
					    "ProgramDetailView",
 | 
				
			||||||
    "ProgramListView",
 | 
					    "ProgramListView",
 | 
				
			||||||
    "ProgramPageDetailView",
 | 
					    "ProgramPageDetailView",
 | 
				
			||||||
 | 
				
			|||||||
@ -33,7 +33,6 @@ class BaseView(TemplateResponseMixin, ContextMixin):
 | 
				
			|||||||
        return None
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        kwargs.setdefault("station", self.station)
 | 
					 | 
				
			||||||
        kwargs.setdefault("page", self.get_page())
 | 
					        kwargs.setdefault("page", self.get_page())
 | 
				
			||||||
        kwargs.setdefault("has_filters", self.has_filters)
 | 
					        kwargs.setdefault("has_filters", self.has_filters)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -44,9 +43,6 @@ class BaseView(TemplateResponseMixin, ContextMixin):
 | 
				
			|||||||
                kwargs["sidebar_object_list"] = sidebar_object_list[: self.list_count]
 | 
					                kwargs["sidebar_object_list"] = sidebar_object_list[: self.list_count]
 | 
				
			||||||
                kwargs["sidebar_list_url"] = self.get_sidebar_url()
 | 
					                kwargs["sidebar_list_url"] = self.get_sidebar_url()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if "audio_streams" not in kwargs:
 | 
					 | 
				
			||||||
            kwargs["audio_streams"] = self.station.streams
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if "model" not in kwargs:
 | 
					        if "model" not in kwargs:
 | 
				
			||||||
            model = getattr(self, "model", None) or hasattr(self, "object") and type(self.object)
 | 
					            model = getattr(self, "model", None) or hasattr(self, "object") and type(self.object)
 | 
				
			||||||
            kwargs["model"] = model
 | 
					            kwargs["model"] = model
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
from django.http import Http404, HttpResponse
 | 
					from django.http import Http404, HttpResponse
 | 
				
			||||||
from django.utils.translation import gettext_lazy as _
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
from django.views.generic import DetailView, ListView
 | 
					from django.views.generic import DetailView, ListView
 | 
				
			||||||
 | 
					from django.views.generic.edit import UpdateView
 | 
				
			||||||
from honeypot.decorators import check_honeypot
 | 
					from honeypot.decorators import check_honeypot
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from ..filters import PageFilters
 | 
					from ..filters import PageFilters
 | 
				
			||||||
@ -140,5 +141,8 @@ class PageDetailView(BasePageDetailView):
 | 
				
			|||||||
        return self.get(request, *args, **kwargs)
 | 
					        return self.get(request, *args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PageUpdateView(PageDetailView):
 | 
					class PageUpdateView(BaseView, UpdateView):
 | 
				
			||||||
    pass
 | 
					    context_object_name = "page"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_page(self):
 | 
				
			||||||
 | 
					        return self.object
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										15
									
								
								aircox/views/profile.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								aircox/views/profile.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					from django.contrib.auth.decorators import login_required
 | 
				
			||||||
 | 
					from django.template.response import TemplateResponse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from aircox.models import Program
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@login_required
 | 
				
			||||||
 | 
					def profile(request):
 | 
				
			||||||
 | 
					    programs = []
 | 
				
			||||||
 | 
					    ugroups = request.user.groups.all()
 | 
				
			||||||
 | 
					    for p in Program.objects.all():
 | 
				
			||||||
 | 
					        if p.editors in ugroups:
 | 
				
			||||||
 | 
					            programs.append(p)
 | 
				
			||||||
 | 
					    context = {"user": request.user, "programs": programs}
 | 
				
			||||||
 | 
					    return TemplateResponse(request, "accounts/profile.html", context)
 | 
				
			||||||
@ -1,6 +1,8 @@
 | 
				
			|||||||
 | 
					from django.contrib.auth.mixins import UserPassesTestMixin
 | 
				
			||||||
 | 
					from django.forms import ModelForm, ImageField
 | 
				
			||||||
from django.urls import reverse
 | 
					from django.urls import reverse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.contrib.auth.mixins import UserPassesTestMixin
 | 
					from filer.models.imagemodels import Image
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from ..models import Page, Program, StaticPage
 | 
					from ..models import Page, Program, StaticPage
 | 
				
			||||||
from .mixins import ParentMixin
 | 
					from .mixins import ParentMixin
 | 
				
			||||||
@ -25,12 +27,31 @@ class BaseProgramMixin:
 | 
				
			|||||||
class ProgramDetailView(BaseProgramMixin, PageDetailView):
 | 
					class ProgramDetailView(BaseProgramMixin, PageDetailView):
 | 
				
			||||||
    model = Program
 | 
					    model = Program
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_template_names(self):
 | 
				
			||||||
 | 
					        return super().get_template_names() + ["aircox/program_detail.html"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_sidebar_queryset(self):
 | 
					    def get_sidebar_queryset(self):
 | 
				
			||||||
        return super().get_sidebar_queryset().filter(parent=self.program)
 | 
					        return super().get_sidebar_queryset().filter(parent=self.program)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ProgramForm(ModelForm):
 | 
				
			||||||
 | 
					    new_cover = ImageField(required=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = Program
 | 
				
			||||||
 | 
					        fields = ["content"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def save(self, commit=True):
 | 
				
			||||||
 | 
					        file_obj = self.cleaned_data["new_cover"]
 | 
				
			||||||
 | 
					        if file_obj:
 | 
				
			||||||
 | 
					            obj, _ = Image.objects.get_or_create(original_filename=file_obj.name, file=file_obj)
 | 
				
			||||||
 | 
					            self.instance.cover = obj
 | 
				
			||||||
 | 
					        super().save(commit=commit)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ProgramUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
 | 
					class ProgramUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
 | 
				
			||||||
    model = Program
 | 
					    model = Program
 | 
				
			||||||
 | 
					    form_class = ProgramForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_sidebar_queryset(self):
 | 
					    def get_sidebar_queryset(self):
 | 
				
			||||||
        return super().get_sidebar_queryset().filter(parent=self.program)
 | 
					        return super().get_sidebar_queryset().filter(parent=self.program)
 | 
				
			||||||
@ -39,6 +60,9 @@ class ProgramUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
 | 
				
			|||||||
        program = self.get_object()
 | 
					        program = self.get_object()
 | 
				
			||||||
        return self.request.user.has_perm("aircox.%s" % program.change_permission_codename)
 | 
					        return self.request.user.has_perm("aircox.%s" % program.change_permission_codename)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_success_url(self):
 | 
				
			||||||
 | 
					        return reverse("program-detail", kwargs={"slug": self.get_object().slug})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ProgramListView(PageListView):
 | 
					class ProgramListView(PageListView):
 | 
				
			||||||
    model = Program
 | 
					    model = Program
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
					 | 
				
			||||||
@ -237,6 +237,7 @@ TEMPLATES = [
 | 
				
			|||||||
                "django.template.context_processors.static",
 | 
					                "django.template.context_processors.static",
 | 
				
			||||||
                "django.template.context_processors.tz",
 | 
					                "django.template.context_processors.tz",
 | 
				
			||||||
                "django.contrib.messages.context_processors.messages",
 | 
					                "django.contrib.messages.context_processors.messages",
 | 
				
			||||||
 | 
					                "aircox.context_processors.station",
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            "loaders": (
 | 
					            "loaders": (
 | 
				
			||||||
                "django.template.loaders.filesystem.Loader",
 | 
					                "django.template.loaders.filesystem.Loader",
 | 
				
			||||||
@ -248,3 +249,5 @@ TEMPLATES = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
WSGI_APPLICATION = "instance.wsgi.application"
 | 
					WSGI_APPLICATION = "instance.wsgi.application"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					LOGOUT_REDIRECT_URL = "/"
 | 
				
			||||||
 | 
				
			|||||||
@ -7,6 +7,7 @@ try:
 | 
				
			|||||||
except ImportError:
 | 
					except ImportError:
 | 
				
			||||||
    pass
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DEBUG = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOCALE_PATHS = ["aircox/locale", "aircox_streamer/locale"]
 | 
					LOCALE_PATHS = ["aircox/locale", "aircox_streamer/locale"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -15,7 +16,7 @@ LOGGING = {
 | 
				
			|||||||
    "disable_existing_loggers": False,
 | 
					    "disable_existing_loggers": False,
 | 
				
			||||||
    "formatters": {
 | 
					    "formatters": {
 | 
				
			||||||
        "timestamp": {
 | 
					        "timestamp": {
 | 
				
			||||||
            "format": "{asctime} {levelname} {message}",
 | 
					            "format": "{asctime} {module} {levelname} {message}",
 | 
				
			||||||
            "style": "{",
 | 
					            "style": "{",
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -26,6 +27,10 @@ LOGGING = {
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "loggers": {
 | 
					    "loggers": {
 | 
				
			||||||
 | 
					        "root": {
 | 
				
			||||||
 | 
					            "handlers": ["console"],
 | 
				
			||||||
 | 
					            "level": os.getenv("DJANGO_LOG_LEVEL", "DEBUG"),
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        "aircox": {
 | 
					        "aircox": {
 | 
				
			||||||
            "handlers": ["console"],
 | 
					            "handlers": ["console"],
 | 
				
			||||||
            "level": os.getenv("DJANGO_LOG_LEVEL", "DEBUG"),
 | 
					            "level": os.getenv("DJANGO_LOG_LEVEL", "DEBUG"),
 | 
				
			||||||
 | 
				
			|||||||
@ -23,6 +23,7 @@ import aircox.urls
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
urlpatterns = aircox.urls.urls + [
 | 
					urlpatterns = aircox.urls.urls + [
 | 
				
			||||||
    path("admin/", admin.site.urls),
 | 
					    path("admin/", admin.site.urls),
 | 
				
			||||||
 | 
					    path("accounts/", include("django.contrib.auth.urls")),
 | 
				
			||||||
    path("ckeditor/", include("ckeditor_uploader.urls")),
 | 
					    path("ckeditor/", include("ckeditor_uploader.urls")),
 | 
				
			||||||
    path("filer/", include("filer.urls")),
 | 
					    path("filer/", include("filer.urls")),
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
				
			|||||||
@ -17,5 +17,5 @@ dateutils~=0.6
 | 
				
			|||||||
mutagen~=1.45
 | 
					mutagen~=1.45
 | 
				
			||||||
Pillow~=9.0
 | 
					Pillow~=9.0
 | 
				
			||||||
psutil~=5.9
 | 
					psutil~=5.9
 | 
				
			||||||
PyYAML==5.4
 | 
					PyYAML==6.0.1
 | 
				
			||||||
watchdog~=2.1
 | 
					watchdog~=2.1
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,4 @@
 | 
				
			|||||||
pytest~=7.2
 | 
					pytest~=7.2
 | 
				
			||||||
pytest-django~=4.5
 | 
					pytest-django~=4.5
 | 
				
			||||||
model_bakery~=1.10
 | 
					model_bakery~=1.10
 | 
				
			||||||
 | 
					pyfakefs~=5.2
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user