path('', include('bookmarks.api.urls')),
path('', include('search.api.urls')),
path('', include('push.api.urls')),
+
+ path('partners/', include('partners.api.urls')),
]
--- /dev/null
+from django.contrib import admin
+from . import models
+
+
+class PriceLevelInline(admin.TabularInline):
+ model = models.PriceLevel
+ extra = 0
+
+
+@admin.register(models.Partner)
+class PartnerAdmin(admin.ModelAdmin):
+ inlines = [
+ PriceLevelInline,
+ ]
+
--- /dev/null
+# This file is part of Wolne Lektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
+#
+from django.urls import path
+from . import views
+
+
+urlpatterns = [
+ path('<slug:key>/books/',
+ views.PartnerBooksView.as_view()),
+]
--- /dev/null
+from django.utils.decorators import method_decorator
+from django.views.decorators.cache import never_cache
+from rest_framework.generics import (ListAPIView, get_object_or_404)
+from rest_framework import serializers
+from api.fields import AbsoluteURLField
+from catalogue.models import Book
+from partners import models
+
+
+
+
+class PartnerBookSerializer(serializers.ModelSerializer):
+ url = AbsoluteURLField(view_name='catalogue_api_book', view_args=['slug'])
+ price = serializers.SerializerMethodField()
+
+ class Meta:
+ model = Book
+ fields = ['url', 'epub_url', 'price']
+
+ def get_price(self, obj):
+ if obj.pages is None:
+ return None
+ return self.context['partner'].get_price(obj.pages)
+
+
+@method_decorator(never_cache, name='dispatch')
+class PartnerBooksView(ListAPIView):
+ serializer_class = PartnerBookSerializer
+
+ def get_serializer_context(self):
+ ctx = super().get_serializer_context()
+ ctx['partner'] = get_object_or_404(models.Partner, key=self.kwargs['key'])
+ return ctx
+
+ def get_queryset(self):
+ return Book.objects.filter(parent=None).filter(can_sell=True).exclude(pages=None)
--- /dev/null
+from django.apps import AppConfig
+
+
+class PartnersConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'partners'
--- /dev/null
+# Generated by Django 4.0.8 on 2026-03-06 13:32
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Partner',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=255)),
+ ('key', models.CharField(max_length=255)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='PriceLevel',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('min_pages', models.IntegerField(blank=True, null=True)),
+ ('price', models.IntegerField()),
+ ('partner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='partners.partner')),
+ ],
+ options={
+ 'ordering': ('price',),
+ },
+ ),
+ ]
--- /dev/null
+from django.db import models
+
+
+class Partner(models.Model):
+ name = models.CharField(max_length=255)
+ key = models.CharField(max_length=255)
+
+ def __str__(self):
+ return self.name
+
+ def get_price(self, pages):
+ price_obj = self.pricelevel_set.exclude(
+ min_pages__gt=pages
+ ).order_by('-price').first()
+ if price_obj is None:
+ return None
+ return price_obj.price
+
+
+class PriceLevel(models.Model):
+ partner = models.ForeignKey(Partner, models.CASCADE)
+ min_pages = models.IntegerField(null=True, blank=True)
+ price = models.IntegerField()
+
+ class Meta:
+ ordering = ('price',)
--- /dev/null
+from django.test import TestCase
+
+# Create your tests here.
--- /dev/null
+from django.shortcuts import render
+
+# Create your views here.
'push',
'club',
'redirects',
+ 'partners',
]
INSTALLED_APPS_CONTRIB = [