Compare commits
12 Commits
develop-1.
...
02a8bb7a4e
Author | SHA1 | Date | |
---|---|---|---|
02a8bb7a4e | |||
7e6f0e377a | |||
3e6a86a813 | |||
4aea8b281f | |||
39a8fa637e | |||
8fcfdcc74f | |||
b2f4bce717 | |||
a289e1ef05 | |||
93e57d746c | |||
d15ca98447 | |||
25dcce742c | |||
b8ad0358c5 |
@ -13,7 +13,7 @@ class DateFieldFilter(filters.FieldListFilter):
|
||||
input_type = "date"
|
||||
|
||||
def __init__(self, field, request, params, model, model_admin, field_path):
|
||||
self.field_generic = "%s__" % field_path
|
||||
self.field_generic = f"{field_path}__"
|
||||
self.date_params = {
|
||||
k: v for k, v in params.items() if k.startswith(self.field_generic)
|
||||
}
|
||||
|
@ -1,41 +0,0 @@
|
||||
class UnrelatedInlineMixin:
|
||||
"""Inline class that can be included in an admin change view whose model is
|
||||
not directly related to inline's model."""
|
||||
|
||||
view_model = None
|
||||
parent_model = None
|
||||
parent_fk = ""
|
||||
|
||||
def __init__(self, parent_model, admin_site):
|
||||
self.view_model = parent_model
|
||||
super().__init__(self.parent_model, admin_site)
|
||||
|
||||
def get_parent(self, view_obj):
|
||||
"""Get formset's instance from `obj` of AdminSite's change form."""
|
||||
field = self.parent_model._meta.get_field(self.parent_fk).remote_field
|
||||
return getattr(view_obj, field.name, None)
|
||||
|
||||
def save_parent(self, parent, view_obj):
|
||||
"""Save formset's instance."""
|
||||
setattr(parent, self.parent_fk, view_obj)
|
||||
parent.save()
|
||||
return parent
|
||||
|
||||
def get_formset(self, request, obj):
|
||||
ParentFormSet = super().get_formset(request, obj)
|
||||
inline = self
|
||||
|
||||
class FormSet(ParentFormSet):
|
||||
view_obj = None
|
||||
|
||||
def __init__(self, *args, instance=None, **kwargs):
|
||||
self.view_obj = instance
|
||||
instance = inline.get_parent(instance)
|
||||
self.instance = instance
|
||||
super().__init__(*args, instance=instance, **kwargs)
|
||||
|
||||
def save(self):
|
||||
inline.save_parent(self.instance, self.view_obj)
|
||||
return super().save()
|
||||
|
||||
return FormSet
|
@ -53,8 +53,10 @@ def page_post_save(sender, instance, created, *args, **kwargs):
|
||||
def program_post_save(sender, instance, created, *args, **kwargs):
|
||||
"""Clean-up later diffusions when a program becomes inactive."""
|
||||
if not instance.active:
|
||||
Diffusion.object.program(instance).after(tz.now()).delete()
|
||||
Episode.object.parent(instance).filter(diffusion__isnull=True).delete()
|
||||
Diffusion.objects.program(instance).after(tz.now()).delete()
|
||||
Episode.objects.parent(instance).filter(
|
||||
diffusion__isnull=True
|
||||
).delete()
|
||||
|
||||
cover = getattr(instance, "__initial_cover", None)
|
||||
if cover is None and instance.cover is not None:
|
||||
|
@ -121,7 +121,7 @@ class SpoofMixin:
|
||||
if funcs is not None:
|
||||
self.funcs = funcs
|
||||
|
||||
def get_trace(self, name, args=False, kw=False):
|
||||
def get_trace(self, name="__call__", args=False, kw=False):
|
||||
"""Get a function call parameters.
|
||||
|
||||
:param str name: function name
|
||||
@ -137,7 +137,7 @@ class SpoofMixin:
|
||||
raise ValueError(f"{name} called multiple times.")
|
||||
return self._get_trace(trace, args=args, kw=kw)
|
||||
|
||||
def get_traces(self, name, args=False, kw=False):
|
||||
def get_traces(self, name="__call__", args=False, kw=False):
|
||||
"""Get a tuple of all call parameters.
|
||||
|
||||
Parameters are the same as `get()`.
|
||||
|
62
aircox/tests/admin/test_filters.py
Normal file
62
aircox/tests/admin/test_filters.py
Normal file
@ -0,0 +1,62 @@
|
||||
from datetime import date, timedelta
|
||||
|
||||
from django.contrib.admin import filters as d_filters
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import pytest
|
||||
|
||||
from ..conftest import req_factory
|
||||
from aircox import models
|
||||
from aircox.admin import filters
|
||||
|
||||
|
||||
class FakeFilter(d_filters.FieldListFilter):
|
||||
def __init__(self, field, request, params, model, model_admin, field_path):
|
||||
self.field = field
|
||||
self.request = request
|
||||
self.params = params
|
||||
self.model = model
|
||||
self.model_admin = model_admin
|
||||
self.field_path = field_path
|
||||
|
||||
|
||||
class DateFieldFilter(filters.DateFieldFilter, FakeFilter):
|
||||
pass
|
||||
|
||||
|
||||
today = date.today()
|
||||
tomorrow = date.today() + timedelta(days=1)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def req():
|
||||
return req_factory.get("/test", {"pub_date__gte": today})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def date_filter(req):
|
||||
return DateFieldFilter(
|
||||
models.Page._meta.get_field("pub_date"),
|
||||
req,
|
||||
{"pub_date__lte": tomorrow, "other_param": 13},
|
||||
models.Page,
|
||||
None,
|
||||
"pub_date",
|
||||
)
|
||||
|
||||
|
||||
class TestDateFieldFilter:
|
||||
def test___init__(self, date_filter):
|
||||
assert date_filter.date_params == {"pub_date__lte": tomorrow}
|
||||
|
||||
date_filter.links = [
|
||||
(str(link[0]), *list(link[1:])) for link in date_filter.links
|
||||
]
|
||||
assert date_filter.links == [
|
||||
(str(_("None")), "pub_date__isnull", None, "1"),
|
||||
(str(_("Exact")), "pub_date__date", date_filter.input_type),
|
||||
(str(_("Since")), "pub_date__gte", date_filter.input_type),
|
||||
(str(_("Until")), "pub_date__lte", date_filter.input_type),
|
||||
]
|
||||
assert date_filter.query_attrs == {
|
||||
"pub_date__gte": today.strftime("%Y-%m-%d")
|
||||
}
|
@ -2,6 +2,9 @@ from datetime import time, timedelta
|
||||
import itertools
|
||||
import logging
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import RequestFactory
|
||||
|
||||
import pytest
|
||||
from model_bakery import baker
|
||||
|
||||
@ -9,6 +12,15 @@ from aircox import models
|
||||
from aircox.test import Interface
|
||||
|
||||
|
||||
req_factory = RequestFactory()
|
||||
"""Request Factory used among different tests."""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def staff_user():
|
||||
return baker.make(User, is_active=True, is_staff=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def logger():
|
||||
logger = Interface(
|
||||
@ -18,8 +30,13 @@ def logger():
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def stations():
|
||||
return baker.make(models.Station, _quantity=2)
|
||||
def station():
|
||||
return baker.make(models.Station, audio_streams="stream 1\nstream 2")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def stations(station):
|
||||
return [station, baker.make(models.Station)]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -28,7 +45,11 @@ def programs(stations):
|
||||
itertools.chain(
|
||||
*(
|
||||
baker.make(
|
||||
models.Program, station=station, cover=None, _quantity=2
|
||||
models.Program,
|
||||
station=station,
|
||||
cover=None,
|
||||
status=models.Program.STATUS_PUBLISHED,
|
||||
_quantity=2,
|
||||
)
|
||||
for station in stations
|
||||
)
|
||||
|
45
aircox/tests/test_admin_site.py
Normal file
45
aircox/tests/test_admin_site.py
Normal file
@ -0,0 +1,45 @@
|
||||
from django.urls import path, reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import pytest
|
||||
|
||||
from aircox import admin_site, urls as _urls
|
||||
from .conftest import req_factory
|
||||
|
||||
|
||||
# Just for code quality: urls module is required because we need some
|
||||
# url resolvers to be registered in order to run tests.
|
||||
_urls
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def site():
|
||||
return admin_site.AdminSite()
|
||||
|
||||
|
||||
class TestAdminSite:
|
||||
@pytest.mark.django_db
|
||||
def test_each_context(self, site, staff_user):
|
||||
req = req_factory.get("admin/test")
|
||||
req.user = staff_user
|
||||
context = site.each_context(req)
|
||||
assert "programs" in context
|
||||
assert "diffusions" in context
|
||||
assert "comments" in context
|
||||
|
||||
def test_get_urls(self, site):
|
||||
extra_url = path("test/path", lambda *_, **kw: _)
|
||||
site.extra_urls.append(extra_url)
|
||||
urls = site.get_urls()
|
||||
assert extra_url in urls
|
||||
|
||||
def test_get_tools(self, site):
|
||||
tools = site.get_tools()
|
||||
tools = dict(tools)
|
||||
assert tools == {
|
||||
_("Statistics"): reverse("admin:tools-stats"),
|
||||
}
|
||||
|
||||
def test_route_view(self, site):
|
||||
# TODO
|
||||
pass
|
54
aircox/tests/test_converters.py
Normal file
54
aircox/tests/test_converters.py
Normal file
@ -0,0 +1,54 @@
|
||||
from datetime import date
|
||||
from django.utils.safestring import SafeString
|
||||
|
||||
import pytest
|
||||
|
||||
from aircox import converters
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def page_path_conv():
|
||||
return converters.PagePathConverter()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def week_conv():
|
||||
return converters.WeekConverter()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def date_conv():
|
||||
return converters.DateConverter()
|
||||
|
||||
|
||||
class TestPagePathConverter:
|
||||
def test_to_python(self, page_path_conv):
|
||||
val = "path_value"
|
||||
result = page_path_conv.to_python(val)
|
||||
assert result == "/" + val + "/"
|
||||
|
||||
def test_to_url(self, page_path_conv):
|
||||
val = "/val"
|
||||
result = page_path_conv.to_url(val)
|
||||
assert isinstance(result, SafeString)
|
||||
assert result == val[1:] + "/"
|
||||
|
||||
|
||||
class TestWeekConverter:
|
||||
def test_to_python(self, week_conv):
|
||||
val = "2023/02"
|
||||
assert week_conv.to_python(val) == date(2023, 1, 9)
|
||||
|
||||
def test_to_url(self, week_conv):
|
||||
val = date(2023, 1, 10)
|
||||
assert week_conv.to_url(val) == "2023/02"
|
||||
|
||||
|
||||
class TestDateConverter:
|
||||
def test_to_python(self, date_conv):
|
||||
val = "2023/02/05"
|
||||
assert date_conv.to_python(val) == date(2023, 2, 5)
|
||||
|
||||
def test_to_url(self, date_conv):
|
||||
val = date(2023, 5, 10)
|
||||
assert date_conv.to_url(val) == "2023/05/10"
|
@ -4,7 +4,6 @@ import django.utils.timezone as tz
|
||||
__all__ = (
|
||||
"Redirect",
|
||||
"redirect",
|
||||
"date_range",
|
||||
"cast_date",
|
||||
"date_or_default",
|
||||
"to_timedelta",
|
||||
@ -29,31 +28,17 @@ def redirect(url):
|
||||
|
||||
|
||||
def str_to_date(value, sep="/"):
|
||||
"""Return a date from the provided `value` string, formated as "yyyy/mm/dd"
|
||||
(or "dd/mm/yyyy" if `reverse` is True).
|
||||
"""Return a date from the provided `value` string, formated as
|
||||
"yyyy/mm/dd".
|
||||
|
||||
Raises ValueError for incorrect value format.
|
||||
:raises: ValueError for incorrect value format.
|
||||
"""
|
||||
value = value.split(sep)[:3]
|
||||
if len(value) < 3:
|
||||
return ValueError("incorrect date format")
|
||||
raise ValueError("incorrect date format")
|
||||
return datetime.date(int(value[0]), int(value[1]), int(value[2]))
|
||||
|
||||
|
||||
def date_range(date, delta=None, **delta_kwargs):
|
||||
"""Return a range of provided date such as `[date-delta, date+delta]`.
|
||||
|
||||
:param date: the reference date
|
||||
:param delta: timedelta
|
||||
:param **delta_kwargs: timedelta init arguments
|
||||
|
||||
Return a datetime range for a given day, as:
|
||||
```(date, 0:0:0:0; date, 23:59:59:999)```.
|
||||
"""
|
||||
delta = tz.timedelta(**delta_kwargs) if delta is None else delta
|
||||
return [date - delta, date + delta]
|
||||
|
||||
|
||||
def cast_date(date, into=datetime.date):
|
||||
"""Cast a given date into the provided class' instance.
|
||||
|
||||
|
@ -48,9 +48,7 @@ class BaseView(TemplateResponseMixin, ContextMixin):
|
||||
kwargs["sidebar_list_url"] = self.get_sidebar_url()
|
||||
|
||||
if "audio_streams" not in kwargs:
|
||||
streams = self.station.audio_streams
|
||||
streams = streams and streams.split("\n")
|
||||
kwargs["audio_streams"] = streams
|
||||
kwargs["audio_streams"] = self.station.streams
|
||||
|
||||
if "model" not in kwargs:
|
||||
model = (
|
||||
@ -63,7 +61,7 @@ class BaseView(TemplateResponseMixin, ContextMixin):
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
# FIXME: rename to sth like [Base]?StationAPIView
|
||||
# FIXME: rename to sth like [Base]?StationAPIView/Mixin
|
||||
class BaseAPIView:
|
||||
@property
|
||||
def station(self):
|
||||
|
@ -91,6 +91,8 @@ class AttachedToMixin:
|
||||
return super().get_page()
|
||||
|
||||
|
||||
# FIXME: django-filter provides filter mixin, but I don't remember why this is
|
||||
# used.
|
||||
class FiltersMixin:
|
||||
"""Mixin integrating Django filters' filter set."""
|
||||
|
||||
|
6
notes.md
6
notes.md
@ -76,6 +76,6 @@ cms:
|
||||
|
||||
# For the next version:
|
||||
## Refactorisation
|
||||
Move:
|
||||
- into `aircox_streamer`: `Log`, `Port`
|
||||
- into `aircox_cms`: `Page`, `NavItem`, `Category`, `StaticPage`, etc.
|
||||
- move into `aircox_streamer`: `Log`, `Port`
|
||||
- move into `aircox_cms`: `Page`, `NavItem`, `Category`, `StaticPage`, etc.
|
||||
- use TextChoice and IntegerChoices in models fields enums
|
||||
|
Reference in New Issue
Block a user