├── tests ├── testapp │ ├── __init__.py │ ├── media │ │ └── test.jpg │ ├── migrate │ │ ├── __init__.py │ │ ├── elephantblog │ │ │ ├── __init__.py │ │ │ └── 0001_initial.py │ │ └── medialibrary │ │ │ ├── __init__.py │ │ │ └── 0001_initial.py │ ├── templates │ │ ├── 404.html │ │ ├── 500.html │ │ ├── base.html │ │ └── test_templatetags.html │ ├── fixtures │ │ ├── page_application.json │ │ └── page_page.json │ ├── models.py │ ├── urls.py │ ├── test_sitemap.py │ ├── test_feed.py │ ├── test_templatetags.py │ ├── test_admin.py │ ├── settings.py │ ├── factories.py │ ├── test_contents.py │ ├── test_translations.py │ ├── test_timezones.py │ └── test_archive_views.py └── manage.py ├── elephantblog ├── extensions │ ├── __init__.py │ ├── tags.py │ └── sites.py ├── management │ ├── __init__.py │ └── commands │ │ └── __init__.py ├── templatetags │ ├── __init__.py │ ├── blog_widgets.py │ └── elephantblog_tags.py ├── __init__.py ├── tests.py ├── templates │ ├── elephantblog │ │ ├── entry_description.html │ │ ├── entry_detail.html │ │ └── entry_archive.html │ ├── content │ │ └── elephantblog │ │ │ ├── category_list.html │ │ │ └── entry_list.html │ └── admin │ │ └── feincms │ │ └── elephantblog │ │ └── entry │ │ └── item_editor.html ├── locale │ ├── cs │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── de │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fr │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── ru │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── admin.py ├── sitemap.py ├── navigation_extensions │ ├── __init__.py │ ├── common.py │ ├── treeinfo.py │ └── recursetree.py ├── utils.py ├── feeds.py ├── transforms.py ├── modeladmins.py ├── contents.py ├── urls.py ├── models.py └── views.py ├── setup.py ├── docs ├── _static │ ├── widget_teaser.jpg │ ├── widget_naviextension_admin.jpg │ └── widget_navigationextension.jpg ├── index.rst ├── contents.rst ├── widgets.rst ├── Makefile ├── make.bat ├── installation.rst └── conf.py ├── .mailmap ├── .gitignore ├── AUTHORS ├── tox.ini ├── MANIFEST.in ├── .github └── workflows │ └── tests.yml ├── CHANGELOG.rst ├── .pre-commit-config.yaml ├── biome.json ├── setup.cfg ├── LICENSE └── README.rst /tests/testapp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/testapp/media/test.jpg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/testapp/migrate/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /elephantblog/extensions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /elephantblog/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /elephantblog/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/testapp/templates/404.html: -------------------------------------------------------------------------------- 1 | 404! 2 | -------------------------------------------------------------------------------- /elephantblog/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/testapp/migrate/elephantblog/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/testapp/migrate/medialibrary/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/testapp/templates/500.html: -------------------------------------------------------------------------------- 1 |

Server error

2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from setuptools import setup 3 | 4 | 5 | setup() 6 | -------------------------------------------------------------------------------- /elephantblog/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = (22, 0, 0) 2 | __version__ = ".".join(map(str, VERSION)) 3 | -------------------------------------------------------------------------------- /elephantblog/tests.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | 4 | warnings.warn("Test elephantblog using target 'testapp'.") 5 | -------------------------------------------------------------------------------- /elephantblog/templates/elephantblog/entry_description.html: -------------------------------------------------------------------------------- 1 | {% load feincms_tags %}{% feincms_render_content content %} 2 | -------------------------------------------------------------------------------- /docs/_static/widget_teaser.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feincms/feincms-elephantblog/HEAD/docs/_static/widget_teaser.jpg -------------------------------------------------------------------------------- /docs/_static/widget_naviextension_admin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feincms/feincms-elephantblog/HEAD/docs/_static/widget_naviextension_admin.jpg -------------------------------------------------------------------------------- /docs/_static/widget_navigationextension.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feincms/feincms-elephantblog/HEAD/docs/_static/widget_navigationextension.jpg -------------------------------------------------------------------------------- /elephantblog/locale/cs/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feincms/feincms-elephantblog/HEAD/elephantblog/locale/cs/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /elephantblog/locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feincms/feincms-elephantblog/HEAD/elephantblog/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /elephantblog/locale/fr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feincms/feincms-elephantblog/HEAD/elephantblog/locale/fr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /elephantblog/locale/ru/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feincms/feincms-elephantblog/HEAD/elephantblog/locale/ru/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Simon Bächler 2 | Simon Bächler 3 | Tobias Haffner 4 | Vladimir Kuzma 5 | -------------------------------------------------------------------------------- /tests/testapp/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block title %}{% endblock %} 5 | 6 | 7 | {% block content %} 8 | {% endblock %} 9 | 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *~ 3 | .*.sw? 4 | \#*# 5 | /.idea 6 | /.settings 7 | /.project 8 | /.pydevproject 9 | /build 10 | /dist 11 | /docs/_build 12 | venv 13 | /feincms_elephantblog.egg-info 14 | .tox 15 | .coverage 16 | htmlcov 17 | -------------------------------------------------------------------------------- /elephantblog/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from elephantblog.modeladmins import CategoryAdmin, EntryAdmin 4 | from elephantblog.models import Category, Entry 5 | 6 | 7 | admin.site.register(Entry, EntryAdmin) 8 | admin.site.register(Category, CategoryAdmin) 9 | -------------------------------------------------------------------------------- /elephantblog/sitemap.py: -------------------------------------------------------------------------------- 1 | from django.contrib.sitemaps import Sitemap 2 | 3 | from elephantblog.models import Entry 4 | 5 | 6 | class EntrySitemap(Sitemap): 7 | def items(self): 8 | return Entry.objects.active() 9 | 10 | def lastmod(self, obj): 11 | return obj.last_changed 12 | -------------------------------------------------------------------------------- /tests/testapp/fixtures/page_application.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "page.applicationcontent", 5 | "fields": { 6 | "ordering": 0, 7 | "region": "main", 8 | "parent": 1, 9 | "parameters": "", 10 | "urlconf_path": "elephantblog.urls" 11 | } 12 | } 13 | ] 14 | -------------------------------------------------------------------------------- /elephantblog/navigation_extensions/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | For support of recursetree, the old navigation extensions have been moved 3 | to navigation_extensions.treeinfo. Please adjust the path in your models.py 4 | and your FeinCMS Pages. 5 | 6 | For performance reasons I could not support both types in the same extension. 7 | """ 8 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | The authors of Elephantblog are: 2 | 3 | * Simon Bächler 4 | * Simon Schmid 5 | * Matthias Kestenholz 6 | * Simon Schürpf 7 | * Stefan Reinhard 8 | * Vladimir Kuzma 9 | * Jamie Matthews 10 | * Sergio de la Cruz Rodriguez 11 | * Tobias Haffner 12 | * Joshua "jag" Ginsberg 13 | * Vaclav Klecanda 14 | * Alexey Boriskin 15 | * George Marshall 16 | * David Evans 17 | * Joshua Jonah 18 | -------------------------------------------------------------------------------- /tests/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | 6 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) 7 | 8 | if __name__ == "__main__": 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testapp.settings") 10 | 11 | from django.core.management import execute_from_command_line 12 | 13 | execute_from_command_line(sys.argv) 14 | -------------------------------------------------------------------------------- /elephantblog/templates/content/elephantblog/category_list.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | {% for category in categories %} 4 | {% if forloop.first %} 5 |
6 |

{% trans "Categories" %}

7 |
    8 | {% endif %} 9 |
  • 10 | {{ category }} 11 |
  • 12 | {% if forloop.last %} 13 |
14 |
15 | {% endif %} 16 | {% endfor %} 17 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{38,39,310}-dj{32,42} 4 | py{310,311}-dj{42} 5 | py{312,313}-dj{52,main} 6 | 7 | [testenv] 8 | usedevelop = true 9 | extras = all,tests 10 | commands = 11 | python -Wd {envbindir}/coverage run tests/manage.py test -v2 --keepdb {posargs:testapp} 12 | coverage report -m 13 | deps = 14 | dj32: Django>=3.2,<4.0 15 | dj42: Django>=4.2,<5.0 16 | dj52: Django>=5.2,<6.0 17 | djmain: https://github.com/django/django/archive/main.tar.gz 18 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include LICENSE 3 | include README.rst 4 | recursive-include elephantblog/static *.png 5 | recursive-include elephantblog/static *.jpg 6 | recursive-include elephantblog/static *.gif 7 | recursive-include elephantblog/static *.js 8 | recursive-include elephantblog/static *.css 9 | recursive-include elephantblog/locale *.po 10 | recursive-include elephantblog/locale *.mo 11 | recursive-include elephantblog/templates *.html 12 | recursive-include elephantblog/templates *.txt 13 | -------------------------------------------------------------------------------- /tests/testapp/models.py: -------------------------------------------------------------------------------- 1 | from feincms.contents import RichTextContent 2 | from feincms.module.medialibrary.contents import MediaFileContent 3 | 4 | from elephantblog.models import Entry 5 | 6 | 7 | Entry.register_regions( 8 | ("main", "Main content area"), 9 | ) 10 | Entry.register_extensions("feincms.extensions.translations") 11 | Entry.create_content_type(RichTextContent, cleanse=False, regions=("main",)) 12 | Entry.create_content_type(MediaFileContent, TYPE_CHOICES=(("default", "default"),)) 13 | -------------------------------------------------------------------------------- /elephantblog/extensions/tags.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import gettext_lazy as _ 2 | from feincms.extensions import Extension as FeincmsExtension 3 | from taggit.managers import TaggableManager 4 | 5 | 6 | class Extension(FeincmsExtension): 7 | def handle_model(self): 8 | self.model.add_to_class( 9 | "tags", 10 | TaggableManager(help_text=_("A comma-separated list of tags."), blank=True), 11 | ) 12 | 13 | def handle_modeladmin(self, modeladmin): 14 | if hasattr(modeladmin, "add_extension_options"): 15 | modeladmin.add_extension_options("tags") 16 | modeladmin.extend_list("list_filter", ["tags"]) 17 | -------------------------------------------------------------------------------- /elephantblog/templates/admin/feincms/elephantblog/entry/item_editor.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/feincms/item_editor.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block object-tools %} 6 | {{ block.super }} 7 |
    8 | {% comment %}{% if original.is_active and FEINCMS_FRONTEND_EDITING %} 9 |
  • {% trans "Edit on site" %}
  • 10 | {% endif %}{% endcomment %} 11 | {% if original and not original.is_active %} 12 |
  • {% trans "Preview" %}
  • 13 | {% endif %} 14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /tests/testapp/fixtures/page_page.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "page.page", 5 | "fields": { 6 | "meta_description": "", 7 | "rght": 2, 8 | "redirect_to": "", 9 | "navigation_extension": null, 10 | "language": "en", 11 | "parent": null, 12 | "level": 0, 13 | "translation_of": null, 14 | "_page_title": "", 15 | "title": "home", 16 | "override_url": "", 17 | "lft": 1, 18 | "_content_title": "", 19 | "meta_keywords": "", 20 | "template_key": "site_base.html", 21 | "tree_id": 1, 22 | "active": true, 23 | "in_navigation": true, 24 | "_cached_url": "/de/", 25 | "slug": "de", 26 | "_ct_inventory": "" 27 | } 28 | } 29 | ] 30 | -------------------------------------------------------------------------------- /elephantblog/utils.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.utils.module_loading import import_string 3 | 4 | from .models import Entry 5 | 6 | 7 | if hasattr(settings, "BLOG_LOOKUP_CLASS"): 8 | LookupClass = import_string(settings.BLOG_LOOKUP_CLASS) 9 | else: 10 | from .transforms import RichTextMediaFileAndCategoriesLookup as LookupClass 11 | 12 | 13 | def entry_list_lookup_related(entry_qs): 14 | LookupClass.lookup(entry_qs) 15 | 16 | 17 | def same_category_entries(entry): 18 | """@return: all entries that have at least one category in common""" 19 | return ( 20 | Entry.objects.active() 21 | .filter( 22 | categories__in=entry.categories.all(), 23 | ) 24 | .exclude(pk=entry.pk) 25 | .distinct() 26 | ) 27 | -------------------------------------------------------------------------------- /tests/testapp/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.sitemaps.views import sitemap 3 | from django.urls import include, path 4 | 5 | from elephantblog.sitemap import EntrySitemap 6 | from elephantblog.urls import elephantblog_patterns 7 | 8 | 9 | admin.autodiscover() 10 | 11 | 12 | sitemaps = { 13 | "elephantblog": EntrySitemap, 14 | } 15 | 16 | 17 | urlpatterns = [ 18 | path("admin/", admin.site.urls), 19 | path("blog/", include("elephantblog.urls")), 20 | path( 21 | "sitemap.xml", 22 | sitemap, 23 | {"sitemaps": sitemaps}, 24 | ), 25 | path( 26 | "multilang/", 27 | include( 28 | elephantblog_patterns( 29 | list_kwargs={"only_active_language": False}, 30 | ) 31 | ), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /tests/testapp/test_sitemap.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from django.test import TestCase 4 | 5 | from .factories import EntryFactory, create_entries 6 | 7 | 8 | class SitemapTestCase(TestCase): 9 | def test_sitemap(self): 10 | entries = create_entries(EntryFactory) 11 | entries[0].richtextcontent_set.create( 12 | region="main", ordering=1, text="Hello world" 13 | ) 14 | 15 | response = self.client.get("/sitemap.xml") 16 | 17 | today = date.today().strftime("%Y-%m-%d") 18 | 19 | self.assertContains( 20 | response, 21 | f"{today}", 22 | 2, 23 | ) 24 | 25 | self.assertContains( 26 | response, 27 | "http://testserver/multilang/", 28 | 2, 29 | ) 30 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ========================================================= 2 | ElephantBlog - An extensible Blog Module based on FeinCMS 3 | ========================================================= 4 | 5 | Every Django Developer has written its own Django-based blog. But most of them 6 | have a lot of features, that you'll never use and never wanted to, or they are 7 | just too simple for your needs, so you'll be quicker writing your own. 8 | 9 | Following the principles of FeinCMS, ElephantBlog tries to offer simple and 10 | basic blog functionality, but remains to be extensible so that you just pick 11 | what you need. And if you don't find an extension, you can quickly write your 12 | own and integrate it to ElephantBlog. 13 | 14 | 15 | Contents: 16 | ========= 17 | 18 | .. toctree:: 19 | :maxdepth: 1 20 | 21 | installation 22 | contents 23 | widgets 24 | 25 | Indices and tables 26 | ================== 27 | 28 | * :ref:`genindex` 29 | * :ref:`modindex` 30 | * :ref:`search` 31 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | tests: 11 | name: Python ${{ matrix.python-version }} 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | python-version: 17 | - "3.9" 18 | - "3.10" 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v2 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip wheel setuptools tox 29 | - name: Run tox targets for ${{ matrix.python-version }} 30 | run: | 31 | ENV_PREFIX=$(tr -C -d "0-9" <<< "${{ matrix.python-version }}") 32 | TOXENV=$(tox --listenvs | grep "^py$ENV_PREFIX" | tr '\n' ',') python -m tox 33 | -------------------------------------------------------------------------------- /elephantblog/extensions/sites.py: -------------------------------------------------------------------------------- 1 | """ 2 | Allows the blog to use the sites framework. 3 | """ 4 | 5 | from django.contrib.sites.models import Site 6 | from django.db.models import ManyToManyField, Q 7 | from django.utils.translation import gettext_lazy as _ 8 | 9 | 10 | def register(cls, admin_cls): 11 | cls.add_to_class( 12 | "sites", 13 | ManyToManyField( 14 | Site, 15 | blank=True, 16 | help_text=_("The sites where the blogpost should appear."), 17 | default=Site.objects.get_current, 18 | ), 19 | ) 20 | 21 | cls.objects.add_to_active_filters(Q(sites=Site.objects.get_current), key="sites") 22 | 23 | def sites_admin(self, obj): 24 | available_sites = self.obj.all() 25 | return ", ".join("%s" % site.name for site in available_sites) 26 | 27 | sites_admin.allow_tags = True 28 | sites_admin.short_description = _("Sites") 29 | 30 | admin_cls.sites_admin = sites_admin 31 | admin_cls.list_filter.append("sites") 32 | admin_cls.list_display.append("sites_admin") 33 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | .. _changelog: 2 | 3 | Change log 4 | ========== 5 | 6 | `Next version`_ 7 | ~~~~~~~~~~~~~~~ 8 | 9 | `v22.0.0`_ (2022-01-07) 10 | ~~~~~~~~~~~~~~~~~~~~~~~ 11 | 12 | - Added pre-commit. 13 | - Dropped compatibility with Python < 3.9, Django < 3.2. 14 | - Made the tags extension add tags to the ``list_filter``. 15 | 16 | 17 | `v1.3.0`_ (2020-01-29) 18 | ~~~~~~~~~~~~~~~~~~~~~~ 19 | 20 | - Changed contents to return a ``(template, context)`` tuple from their 21 | render methods. The minimum FeinCMS version supporting this is 1.15. 22 | 23 | 24 | `v1.2.0`_ (2020-01-21) 25 | ~~~~~~~~~~~~~~~~~~~~~~ 26 | 27 | - Reformatted the code using black. 28 | - Updated the code to work with Django 2 and 3. 29 | 30 | 31 | 32 | .. _v1.2.0: https://github.com/feincms/feincms-elephantblog/compare/v1.1.0...v1.2.0 33 | .. _v1.3.0: https://github.com/feincms/feincms-elephantblog/compare/v1.2.0...v1.3.0 34 | .. _v22.0.0: https://github.com/feincms/feincms-elephantblog/compare/v1.3.0...v22.0.0 35 | .. _Next version: https://github.com/feincms/feincms-elephantblog/compare/v22.0.0...master 36 | -------------------------------------------------------------------------------- /tests/testapp/test_feed.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from .factories import EntryFactory, create_entries 4 | 5 | 6 | class FeedTestCase(TestCase): 7 | def test_feed(self): 8 | entries = create_entries(EntryFactory) 9 | entries[0].richtextcontent_set.create( 10 | region="main", ordering=1, text="Hello world" 11 | ) 12 | 13 | response = self.client.get("/blog/feed/") 14 | self.assertContains(response, "rss") 15 | self.assertContains(response, "xmlns:atom") 16 | self.assertContains( 17 | response, 18 | "Blog of the usual elephant", 19 | 1, 20 | ) 21 | self.assertContains( 22 | response, 23 | "http://testserver/multilang/2012/10/12/eintrag-1/", 24 | 1, 25 | ) 26 | self.assertContains( 27 | response, 28 | "http://testserver/multilang/2012/08/12/entry-1/", 29 | 1, 30 | ) 31 | self.assertContains( 32 | response, 33 | "Hello world", 34 | 1, 35 | ) 36 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: ".yarn/|yarn.lock|\\.min\\.(css|js)$" 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v6.0.0 5 | hooks: 6 | - id: check-added-large-files 7 | - id: check-builtin-literals 8 | - id: check-executables-have-shebangs 9 | - id: check-merge-conflict 10 | - id: check-toml 11 | - id: check-yaml 12 | - id: detect-private-key 13 | - id: end-of-file-fixer 14 | - id: mixed-line-ending 15 | - id: trailing-whitespace 16 | - repo: https://github.com/adamchainz/django-upgrade 17 | rev: 1.28.0 18 | hooks: 19 | - id: django-upgrade 20 | args: [--target-version, "3.2"] 21 | - repo: https://github.com/astral-sh/ruff-pre-commit 22 | rev: "v0.13.0" 23 | hooks: 24 | - id: ruff 25 | args: [--unsafe-fixes] 26 | - id: ruff-format 27 | - repo: https://github.com/biomejs/pre-commit 28 | rev: "v2.2.4" 29 | hooks: 30 | - id: biome-check 31 | args: [--unsafe] 32 | verbose: true 33 | - repo: https://github.com/tox-dev/pyproject-fmt 34 | rev: v2.6.0 35 | hooks: 36 | - id: pyproject-fmt 37 | - repo: https://github.com/abravalheri/validate-pyproject 38 | rev: v0.24.1 39 | hooks: 40 | - id: validate-pyproject 41 | -------------------------------------------------------------------------------- /elephantblog/templates/elephantblog/entry_detail.html: -------------------------------------------------------------------------------- 1 | {% extends feincms_page.template.path|default:"base.html" %} 2 | 3 | {% load feincms_tags i18n %} 4 | 5 | {% block title %}{% trans "News" %} - {{ block.super }}{% endblock %} 6 | 7 | {% block content %} 8 | 9 |
10 |
11 |

{{ entry }}

12 | 24 |
25 |
26 | {% feincms_render_region entry "main" request %} 27 |
28 |
29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /docs/contents.rst: -------------------------------------------------------------------------------- 1 | .. _contents: 2 | 3 | ====================== 4 | Supplied Content Types 5 | ====================== 6 | 7 | While you can use all FeinCMS content types within elephantblog, the following 8 | content types are specific to elephantblog and can be used within FeinCMS. 9 | 10 | If you whish to show a list of top entries on the entry page of your website or 11 | promote articles within your blog, have a look at the widgets_ section. 12 | 13 | 14 | ``BlogEntryListContent`` 15 | ======================== 16 | 17 | This content type shows a list view of all blog entries. It can be used to show 18 | blog entries in FeinCMS pages. If the 'featured only' flag is set, only posts 19 | which are marked as featured are shown. 20 | 21 | The templates used by this content are ``content/elephantblog/entry_list.html`` 22 | and ``content/elephantblog/entry_list_featured.html``. The latter is optional. 23 | If it does not exist, the former is used. 24 | 25 | If you want to limit the entries shown to a certain number, just set that 26 | number for pagination and remove the pagination part from the template. 27 | 28 | 29 | ``BlogCategoryListContent`` 30 | =========================== 31 | 32 | This content type may be used to display a list of blog categories. It is most 33 | useful in a sidebar region. Categories which are not related to any blog 34 | entries are not shown by default, but this can be changed when adding the 35 | content type. 36 | -------------------------------------------------------------------------------- /tests/testapp/test_templatetags.py: -------------------------------------------------------------------------------- 1 | from django.template.loader import render_to_string 2 | from django.test import TransactionTestCase 3 | from django.test.utils import override_settings 4 | 5 | from .factories import EntryFactory, create_category, create_entries 6 | 7 | 8 | class TemplateTagsTest(TransactionTestCase): 9 | def setUp(self): 10 | entries = create_entries(EntryFactory) 11 | category = create_category(title="Category 1") 12 | create_category(title="Category 2") 13 | 14 | entries[0].categories.add(category) 15 | entries[1].is_featured = True 16 | entries[1].save() 17 | 18 | def test_templatetags(self): 19 | html = render_to_string("test_templatetags.html", {}) 20 | 21 | self.assertIn("

categories:Category 1,

", html) 22 | self.assertIn("

categories+empty:Category 1,Category 2,

", html) 23 | self.assertIn("

years:2012,

", html) 24 | self.assertIn("

months:10.2012,08.2012,

", html) 25 | self.assertIn("

entries:Eintrag 1,Entry 1,

", html) 26 | self.assertIn("

entries+featured:Eintrag 1,

", html) 27 | self.assertIn("

entries+category0:Entry 1,

", html) 28 | self.assertIn("

entries+category1:

", html) 29 | self.assertIn("

entries+limit:Eintrag 1,

", html) 30 | 31 | 32 | @override_settings(USE_TZ=True, TIME_ZONE="America/Chicago") 33 | class TimezoneTemplateTagsTest(TemplateTagsTest): 34 | pass 35 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/2.1.4/schema.json", 3 | "assist": { "actions": { "source": { "organizeImports": "off" } } }, 4 | "formatter": { 5 | "enabled": true, 6 | "indentStyle": "space", 7 | "indentWidth": 2 8 | }, 9 | "linter": { 10 | "enabled": true, 11 | "rules": { 12 | "recommended": true, 13 | "a11y": { 14 | "noSvgWithoutTitle": "off" 15 | }, 16 | "correctness": { 17 | "noInnerDeclarations": "off", 18 | "noUndeclaredVariables": "warn", 19 | "noUnknownTypeSelector": "warn", 20 | "noUnusedImports": "error", 21 | "noUnusedVariables": "error", 22 | "useHookAtTopLevel": "error" 23 | }, 24 | "security": { 25 | "noDangerouslySetInnerHtml": "warn" 26 | }, 27 | "style": { 28 | "noDescendingSpecificity": "warn", 29 | "noParameterAssign": "off", 30 | "useForOf": "warn", 31 | "useArrayLiterals": "error" 32 | }, 33 | "suspicious": { 34 | "noArrayIndexKey": "warn", 35 | "noAssignInExpressions": "off" 36 | } 37 | } 38 | }, 39 | "javascript": { 40 | "formatter": { 41 | "semicolons": "asNeeded" 42 | }, 43 | "globals": ["django", "CKEDITOR"] 44 | }, 45 | "css": { 46 | "formatter": { 47 | "enabled": true 48 | }, 49 | "linter": { 50 | "enabled": true 51 | } 52 | }, 53 | "json": { 54 | "formatter": { 55 | "enabled": false 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/testapp/templates/test_templatetags.html: -------------------------------------------------------------------------------- 1 | {% load elephantblog_tags %} 2 | 3 | {% elephantblog_categories as categories %} 4 |

categories:{% for c in categories %}{{ c }},{% endfor %}

5 | 6 | {# Django 1.4 does not support True yet, use "1" instead #} 7 | {% elephantblog_categories show_empty_categories=1 as categories %} 8 |

categories+empty:{% for c in categories %}{{ c }},{% endfor %}

9 | 10 | {% elephantblog_archive_years as years %} 11 |

years:{% for y in years %}{{ y.year }},{% endfor %}

12 | 13 | {% elephantblog_archive_months as months %} 14 | {# We are not using |date here because Django before 1.6 returns #} 15 | {# naive datetimes from dates(), which in turn are not handled correctly #} 16 | {# by the date filter. #} 17 |

months:{% for m in months %}{{ m.month|stringformat:"02d" }}.{{ m.year }},{% endfor %}

18 | 19 | {% elephantblog_entries as entries %} 20 |

entries:{% for e in entries %}{{ e }},{% endfor %}

21 | 22 | {# Django 1.4 does not support True yet, use "1" instead #} 23 | {% elephantblog_entries featured_only=1 as entries %} 24 |

entries+featured:{% for e in entries %}{{ e }},{% endfor %}

25 | 26 | {% elephantblog_entries category=categories.0 as entries %} 27 |

entries+category0:{% for e in entries %}{{ e }},{% endfor %}

28 | 29 | {% elephantblog_entries category=categories.1 as entries %} 30 |

entries+category1:{% for e in entries %}{{ e }},{% endfor %}

31 | 32 | {% elephantblog_entries limit=1 as entries %} 33 |

entries+limit:{% for e in entries %}{{ e }},{% endfor %}

34 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = feincms_elephantblog 3 | version = attr: elephantblog.__version__ 4 | description = A blog for FeinCMS 5 | long_description = file: README.rst 6 | long_description_content_type = text/x-rst 7 | url = https://github.com/feincms/feincms-elephantblog/ 8 | author = Simon Baechler 9 | author_email = sb@feinheit.ch 10 | license = BSD-3-Clause 11 | license_file = LICENSE 12 | platforms = OS Independent 13 | classifiers = 14 | Development Status :: 5 - Production/Stable 15 | Environment :: Web Environment 16 | Framework :: Django 17 | Intended Audience :: Developers 18 | License :: OSI Approved :: BSD License 19 | Operating System :: OS Independent 20 | Programming Language :: Python 21 | Programming Language :: Python :: 3 22 | Programming Language :: Python :: 3 :: Only 23 | Programming Language :: Python :: 3.9 24 | Programming Language :: Python :: 3.10 25 | Topic :: Internet :: WWW/HTTP :: Dynamic Content 26 | Topic :: Software Development 27 | Topic :: Software Development :: Libraries :: Application Frameworks 28 | 29 | [options] 30 | packages = find: 31 | install_requires = 32 | Django>=3.2 33 | FeinCMS>=22.0 34 | python_requires = >=3.9 35 | include_package_data = True 36 | zip_safe = False 37 | 38 | [options.extras_require] 39 | tests = 40 | coverage 41 | factory-boy 42 | 43 | [options.packages.find] 44 | exclude = 45 | tests 46 | tests.* 47 | 48 | [coverage:run] 49 | branch = True 50 | include = 51 | *elephantblog* 52 | omit = 53 | *migrations* 54 | *migrate* 55 | *tests* 56 | *.tox* 57 | -------------------------------------------------------------------------------- /elephantblog/feeds.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.syndication.views import Feed 3 | from django.template.loader import render_to_string 4 | from django.utils.translation import get_language 5 | 6 | from elephantblog.models import Entry 7 | 8 | 9 | if not (hasattr(settings, "BLOG_TITLE") and hasattr(settings, "BLOG_DESCRIPTION")): 10 | import warnings 11 | 12 | warnings.warn( 13 | "BLOG_TITLE and/or BLOG_DESCRIPTION not defined in" 14 | " settings.py. Standard values used for the Feed" 15 | ) 16 | 17 | 18 | def tryrender(content): 19 | try: 20 | return render_to_string( 21 | "elephantblog/entry_description.html", {"content": content} 22 | ) 23 | except Exception: # Required request argument or something else? 24 | return "" 25 | 26 | 27 | class EntryFeed(Feed): 28 | title = getattr(settings, "BLOG_TITLE", "Unnamed") 29 | link = "/blog/" 30 | description = getattr(settings, "BLOG_DESCRIPTION", "") 31 | 32 | def items(self): 33 | if hasattr(Entry, "translation_of"): 34 | return ( 35 | Entry.objects.active() 36 | .filter(language=get_language()) 37 | .order_by("-published_on")[:20] 38 | ) 39 | else: 40 | return Entry.objects.active().order_by("-published_on")[:20] 41 | 42 | def item_title(self, item): 43 | return item.title 44 | 45 | def item_description(self, item): 46 | return "".join(tryrender(c) for c in item.content.main) 47 | 48 | def item_pubdate(self, item): 49 | return item.published_on 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2011, FEINHEIT GmbH and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of FEINHEIT GmbH nor the names of its contributors 15 | may be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /elephantblog/templatetags/blog_widgets.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.core.exceptions import FieldError 3 | from django.utils.translation import get_language 4 | 5 | from elephantblog.models import Entry 6 | from elephantblog.utils import entry_list_lookup_related, same_category_entries 7 | 8 | 9 | register = template.Library() 10 | 11 | 12 | @register.simple_tag(takes_context=True) 13 | def get_entries(context, limit): 14 | try: 15 | queryset = Entry.objects.active().filter(language=get_language()) 16 | except (AttributeError, FieldError): 17 | queryset = Entry.objects.active() 18 | 19 | if limit: 20 | queryset = queryset[:limit] 21 | 22 | context["entries"] = queryset 23 | return "" 24 | 25 | 26 | @register.simple_tag(takes_context=True) 27 | def get_frontpage(context, category=None): 28 | queryset = Entry.objects.featured() 29 | 30 | try: 31 | queryset = queryset.filter(language=get_language()) 32 | except (AttributeError, FieldError): 33 | pass 34 | 35 | if category: 36 | queryset = queryset.filter(categories__translations__title=category).distinct() 37 | 38 | context["entries"] = queryset 39 | return "" 40 | 41 | 42 | @register.simple_tag(takes_context=True) 43 | def get_others(context, number=3, same_category=True, featured_only=False): 44 | """This tag can be used on an entry detail page to tease 45 | other related entries 46 | """ 47 | if same_category: 48 | entries = same_category_entries(context["object"]) 49 | else: 50 | entries = Entry.objects.exclude(pk=context["object"].pk) 51 | if featured_only: 52 | entries.filter(is_featured=True) 53 | 54 | entries = entries[:number] 55 | entries = entries.transform(entry_list_lookup_related) 56 | context["others"] = entries 57 | return "" 58 | -------------------------------------------------------------------------------- /elephantblog/transforms.py: -------------------------------------------------------------------------------- 1 | from feincms.content.richtext.models import RichTextContent 2 | from feincms.module.medialibrary.contents import MediaFileContent 3 | 4 | from elephantblog.models import Category, Entry 5 | 6 | 7 | class BaseLookup: 8 | """The base class for the transformation instructions""" 9 | 10 | @staticmethod 11 | def lookup(entry_qs): 12 | """ 13 | The main lookup function. 14 | Edit the models in place. 15 | Overwrite this. 16 | :param entry_qs: The Entry query set 17 | """ 18 | pass 19 | 20 | 21 | class RichTextMediaFileAndCategoriesLookup(BaseLookup): 22 | @staticmethod 23 | def lookup(entry_qs): 24 | entry_dict = {e.pk: e for e in entry_qs} 25 | 26 | model = Entry.content_type_for(RichTextContent) 27 | if model: 28 | for content in model.objects.filter( 29 | parent__in=entry_dict.keys(), 30 | ).reverse(): 31 | entry_dict[content.parent_id].first_richtext = content 32 | 33 | model = Entry.content_type_for(MediaFileContent) 34 | if model: 35 | for content in ( 36 | model.objects.filter( 37 | parent__in=entry_dict.keys(), 38 | mediafile__type="image", 39 | ) 40 | .reverse() 41 | .select_related("mediafile") 42 | ): 43 | entry_dict[content.parent_id].first_image = content 44 | 45 | m2mfield = Entry._meta.get_field("categories") 46 | categories = Category.objects.filter( 47 | blogentries__in=entry_dict.keys(), 48 | ).extra( 49 | select={ 50 | "entry_id": "%s.%s" 51 | % (m2mfield.m2m_db_table(), m2mfield.m2m_column_name()), 52 | }, 53 | ) 54 | 55 | for category in categories: 56 | entry = entry_dict[category.entry_id] 57 | if not hasattr(entry, "fetched_categories"): 58 | entry.fetched_categories = [] 59 | entry.fetched_categories.append(category) 60 | -------------------------------------------------------------------------------- /elephantblog/templates/content/elephantblog/entry_list.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 |
3 | {% for entry in content.entries %} 4 |
5 |
6 |

{{ entry }}

7 | 18 |
19 |
20 | {% if entry.first_image %}{{ entry.first_image.render }}{% endif %} 21 | {% if entry.first_richtext %}{{ entry.first_richtext.render }}{% endif %} 22 |
23 |
24 | {% endfor %} 25 |
26 | 27 | {# if not content.featured_only #} 28 | {% if content.paginate_by %} 29 | {% with content.entries as page_obj %} 30 | 46 | {% endwith %} 47 | {% endif %} 48 | {# endif #} 49 | -------------------------------------------------------------------------------- /elephantblog/modeladmins.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.utils.encoding import force_str 3 | from django.utils.translation import gettext_lazy as _ 4 | from feincms.admin import item_editor 5 | from feincms.translations import admin_translationinline 6 | 7 | from elephantblog.models import CategoryTranslation, Entry 8 | 9 | 10 | CategoryTranslationInline = admin_translationinline( 11 | CategoryTranslation, prepopulated_fields={"slug": ("title",)} 12 | ) 13 | 14 | 15 | class CategoryAdmin(admin.ModelAdmin): 16 | inlines = [CategoryTranslationInline] 17 | list_display = ["__str__", "ordering", "entries"] 18 | list_editable = ["ordering"] 19 | search_fields = ["translations__title"] 20 | 21 | @admin.display(description=_("Blog entries in category")) 22 | def entries(self, obj): 23 | return ( 24 | ", ".join( 25 | force_str(entry) for entry in Entry.objects.filter(categories=obj) 26 | ) 27 | or "-" 28 | ) 29 | 30 | 31 | class EntryAdmin(item_editor.ItemEditor): 32 | actions = [] 33 | 34 | date_hierarchy = "published_on" 35 | filter_horizontal = ["categories"] 36 | list_display = ["title", "is_active", "is_featured", "published_on", "author"] 37 | list_editable = ["is_active", "is_featured"] 38 | list_filter = ["is_active", "is_featured", "categories", "author"] 39 | raw_id_fields = ["author"] 40 | search_fields = ["title", "slug"] 41 | prepopulated_fields = { 42 | "slug": ("title",), 43 | } 44 | 45 | fieldset_insertion_index = 1 46 | fieldsets = [ 47 | [None, {"fields": [("is_active", "is_featured", "author"), "title"]}], 48 | [ 49 | _("Other options"), 50 | { 51 | "fields": ["categories", "published_on", "slug"], 52 | "classes": ("collapse",), 53 | }, 54 | ], 55 | item_editor.FEINCMS_CONTENT_FIELDSET, 56 | ] 57 | 58 | def formfield_for_foreignkey(self, db_field, request, **kwargs): 59 | if db_field.name == "author": 60 | kwargs["initial"] = request.user.id 61 | return super().formfield_for_foreignkey(db_field, request, **kwargs) 62 | -------------------------------------------------------------------------------- /docs/widgets.rst: -------------------------------------------------------------------------------- 1 | .. _widgets: 2 | 3 | ============ 4 | Blog Widgets 5 | ============ 6 | 7 | Widgets are a way tho show blog content outside of the main app. There are two ways of doing this: 8 | Using content types or template tags. 9 | 10 | Here we are going to describe some use-cases and how to implement them: 11 | 12 | Teaser blog entries and FeinCMS pages on the landingpage 13 | ======================================================== 14 | 15 | .. image:: _static/widget_teaser.jpg 16 | 17 | For a landing page that uses its own temlate anyway it makes sense to use a template tag. 18 | Elephantblog comes with a template library called 'blog_widgets' that provides the function 19 | get_frontpage which you could use like this:: 20 | 21 | {% load i18n blog_widgets %} 22 | 23 |
24 | {% get_frontpage %} 25 |

{% trans 'Neues von Helsi' %}

26 |
27 | {% for item in entries %} 28 |
29 | {% feincms_render_region item "teaser" request %} 30 |
31 | {% endfor %} 32 |
33 |
34 | 35 | In this particular case we used the pageteaser content form the FeinCMS Contenttype Box Vol.1. 36 | That one allows to combine FeinCMS pages and blog entries. 37 | We also added a 'Teaser' region to both pages so the user can define manually what content 38 | is displayed. 39 | 40 | 41 | Adding categories or date breakdown to FeinCMS page navigation 42 | ============================================================== 43 | 44 | .. image:: _static/widget_navigationextension.jpg 45 | 46 | It is possible to add blog entries directly into the FeinCMS navigation path when using the blog 47 | as ApplicationContent. You need to activate the navigation extensions for FeinCMS:: 48 | 49 | Page.register_extensions('navigation', 'seo', 'titles', 'translations') 50 | 51 | On the page where you add the 'Blog' ApplicationContent, also select a navigation extension 52 | 53 | .. image:: _static/widget_naviextension_admin.jpg 54 | 55 | In this example we used the CategoryAndDateNavigationExtension. 56 | -------------------------------------------------------------------------------- /elephantblog/navigation_extensions/common.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from collections import OrderedDict 3 | 4 | from django.conf import settings 5 | from django.utils.translation import gettext_lazy as _ 6 | from feincms.module.page.extensions.navigation import NavigationExtension, PagePretender 7 | 8 | from elephantblog.models import Category, Entry 9 | 10 | 11 | all_months = [datetime.date(2008, i, 1) for i in range(1, 13)] 12 | 13 | 14 | def date_of_first_entry(): 15 | entry = Entry.objects.filter(is_active=True).order_by("published_on")[0] 16 | return entry.published_on.date() 17 | 18 | 19 | def date_tree(): 20 | """returns a dict in the form {2012: [1,2,3,4,5,6], 2011: [10,11,12]}""" 21 | today = datetime.date.today() 22 | first_day = date_of_first_entry() 23 | years = range(today.year, first_day.year - 1, -1) 24 | date_tree = OrderedDict((year, range(1, 13)) for year in years) 25 | for year, months in date_tree.items(): 26 | if year == first_day.year: 27 | date_tree[year] = months[first_day.month - 1 :] 28 | if year == today.year: 29 | # list might be missing some elements because it is also the 30 | # first year. 31 | months_this_year = range(1, today.month + 1) 32 | date_tree[year] = [m for m in date_tree[year] if m in months_this_year] 33 | return date_tree.items() 34 | 35 | 36 | class BlogCategoriesNavigationExtension(NavigationExtension): 37 | """ 38 | Navigation extension for FeinCMS which lists all available categories 39 | """ 40 | 41 | name = _("blog categories") 42 | 43 | def children(self, page, **kwargs): 44 | categories = Category.objects.all() 45 | for category in categories: 46 | yield PagePretender( 47 | title=category.translation.title, 48 | url="%scategory/%s/" 49 | % (page.get_absolute_url(), category.translation.slug), 50 | tree_id=page.tree_id, 51 | level=page.level + 1, 52 | language=getattr(page, "language", settings.LANGUAGE_CODE), 53 | slug=category.translation.slug, 54 | parent=page, 55 | parent_id=page.id, 56 | lft=page.lft, 57 | rght=page.rght, 58 | _mptt_meta=getattr(page, "_mptt_meta", None), 59 | ) 60 | -------------------------------------------------------------------------------- /tests/testapp/test_admin.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.urls import reverse 3 | 4 | from elephantblog.models import Entry 5 | 6 | from .factories import UserFactory 7 | 8 | 9 | class AdminTestCase(TestCase): 10 | def test_admin(self): 11 | author = UserFactory.create(is_staff=True, is_superuser=True) 12 | author.set_password("elephant") 13 | author.save() 14 | 15 | self.client.login(username=author.username, password="elephant") 16 | 17 | self.assertContains( 18 | self.client.get("/admin/elephantblog/entry/"), 19 | "0 entries", 20 | ) 21 | 22 | response = self.client.post( 23 | "/admin/elephantblog/entry/add/", 24 | { 25 | "title": "First entry", 26 | "slug": "first-entry", 27 | "author": author.id, 28 | "language": "en", 29 | "richtextcontent_set-TOTAL_FORMS": 0, 30 | "richtextcontent_set-INITIAL_FORMS": 0, 31 | "richtextcontent_set-MAX_NUM_FORMS": 1000, 32 | "mediafilecontent_set-TOTAL_FORMS": 0, 33 | "mediafilecontent_set-INITIAL_FORMS": 0, 34 | "mediafilecontent_set-MAX_NUM_FORMS": 1000, 35 | }, 36 | ) 37 | 38 | self.assertRedirects( 39 | response, 40 | "/admin/elephantblog/entry/", 41 | ) 42 | 43 | entry = Entry.objects.get() 44 | 45 | response = self.client.post( 46 | reverse("admin:elephantblog_entry_change", args=(entry.id,)), 47 | { 48 | "title": "First entry", 49 | "slug": "first-entry", 50 | "author": author.id, 51 | "language": "en", 52 | "is_active": True, 53 | "richtextcontent_set-TOTAL_FORMS": 0, 54 | "richtextcontent_set-INITIAL_FORMS": 0, 55 | "richtextcontent_set-MAX_NUM_FORMS": 1000, 56 | "mediafilecontent_set-TOTAL_FORMS": 0, 57 | "mediafilecontent_set-INITIAL_FORMS": 0, 58 | "mediafilecontent_set-MAX_NUM_FORMS": 1000, 59 | }, 60 | ) 61 | 62 | entry = Entry.objects.get() 63 | # entry.published_on has been set automatically 64 | self.assertNotEqual(entry.published_on, None) 65 | -------------------------------------------------------------------------------- /tests/testapp/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | INSTALLED_APPS = ( 5 | "django.contrib.auth", 6 | "django.contrib.contenttypes", 7 | "django.contrib.sessions", 8 | "django.contrib.sitemaps", 9 | "django.contrib.admin", 10 | "django.contrib.staticfiles", 11 | "django.contrib.messages", 12 | "feincms", 13 | "feincms.module.medialibrary", 14 | "testapp", 15 | "elephantblog", 16 | # 'django_nose', 17 | ) 18 | 19 | # TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' 20 | 21 | SECRET_KEY = "elephant" 22 | DATABASES = { 23 | "default": { 24 | "ENGINE": "django.db.backends.sqlite3", 25 | "NAME": "runserver.sqlite", 26 | # 'TEST_NAME': 'blog_test.sqlite', 27 | "USER": "", 28 | "PASSWORD": "", 29 | "HOST": "", 30 | "PORT": "", 31 | } 32 | } 33 | DEFAULT_AUTO_FIELD = "django.db.models.AutoField" 34 | 35 | MIDDLEWARE_CLASSES = MIDDLEWARE = ( 36 | "django.middleware.common.CommonMiddleware", 37 | "django.contrib.sessions.middleware.SessionMiddleware", 38 | "django.middleware.csrf.CsrfViewMiddleware", 39 | "django.contrib.auth.middleware.AuthenticationMiddleware", 40 | "django.middleware.locale.LocaleMiddleware", 41 | "django.contrib.messages.middleware.MessageMiddleware", 42 | ) 43 | SILENCED_SYSTEM_CHECKS = ["1_10.W001"] 44 | 45 | ROOT_URLCONF = "testapp.urls" 46 | BLOG_TITLE = "Blog of the usual elephant" 47 | BLOG_DESCRIPTION = "" 48 | TIME_ZONE = "America/Chicago" 49 | USE_TZ = True 50 | DEFAULT_CHARSET = "utf-8" 51 | LANGUAGES = ( 52 | ("en", "English"), 53 | ("de", "German"), 54 | ("zh-hans", "Chinese simplified"), 55 | ("zh-hant", "Chinese traditional"), 56 | ) 57 | LANGUAGE_CODE = "en" 58 | USE_I18N = True 59 | 60 | DEBUG = True # tests run with DEBUG=False 61 | SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies" 62 | 63 | TEMPLATES = [ 64 | { 65 | "BACKEND": "django.template.backends.django.DjangoTemplates", 66 | "DIRS": [], 67 | "APP_DIRS": True, 68 | "OPTIONS": { 69 | "context_processors": [ 70 | "django.template.context_processors.debug", 71 | "django.template.context_processors.request", 72 | "django.contrib.auth.context_processors.auth", 73 | "django.contrib.messages.context_processors.messages", 74 | ], 75 | }, 76 | }, 77 | ] 78 | 79 | STATIC_URL = "/static/" 80 | MEDIA_ROOT = os.path.join(os.path.dirname(__file__), "media") 81 | MEDIA_URL = "/media/" 82 | 83 | MIGRATION_MODULES = { 84 | # "page": "testapp.migrate.page", 85 | "medialibrary": "testapp.migrate.medialibrary", 86 | "elephantblog": "testapp.migrate.elephantblog", 87 | } 88 | -------------------------------------------------------------------------------- /elephantblog/templates/elephantblog/entry_archive.html: -------------------------------------------------------------------------------- 1 | {% extends feincms_page.template.path|default:"base.html" %} 2 | 3 | {% load feincms_tags i18n %} 4 | 5 | {% block title %}{% trans "News" %} - {{ block.super }}{% endblock %} 6 | 7 | {% block content %} 8 | {% block content_title %} 9 |

{% trans 'News' %} {% if year %}{% trans "for" context "date" %} {{ year|date:"Y"|default:year }}{% endif %} 10 | {% if month %}{% trans "for" context "date" %} {{ month|date:"F Y"|default:month }}{% endif %} 11 | {% if day %}{% trans "for" context "date" %} {{ day|date:"DATE_FORMAT"|default:day }}{% endif %} 12 | {% if category %}{% trans "for" context "category" %} {{ category }}{% endif %} 13 |

14 | {% endblock %} 15 | 16 | {% block object_list %} 17 |
18 | {% for entry in object_list %} 19 |
20 |
21 |

{{ entry }}

22 | 33 |
34 |
35 | {% if entry.first_image %}{{ entry.first_image.render }}{% endif %} 36 | {% if entry.first_richtext %}{{ entry.first_richtext.render }}{% endif %} 37 |
38 |
39 | {% endfor %} 40 |
41 | {% endblock %} 42 | 43 | {% block pagination %} 44 | 60 | {% endblock %} 61 | {% endblock %} 62 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========================================================= 2 | ElephantBlog - An extensible Blog Module based on FeinCMS 3 | ========================================================= 4 | 5 | Every Django Developer has written its own Django-based blog. But most of them have a lot 6 | of features, that you'll never use and never wanted to, or they are just too simple for your 7 | needs, so you'll be quicker writing your own. 8 | 9 | Following the principles of FeinCMS, ElephantBlog tries to offer simple and basic blog 10 | functionality, but remains extensible so that you just pick what you need. And if 11 | you don't find an extension, you can quickly write your own and use it with 12 | ElephantBlog. 13 | 14 | 15 | How it works 16 | ============ 17 | 18 | Basically, ElephantBlog just uses what FeinCMS already has in a blog way. A blog way means: 19 | Multiple entries in a timeline. One blogentry is similar to a FeinCMS page: It can have 20 | multiple content types and some meta informations like title, slug, publishing date, ... 21 | 22 | If you need more, like comments, tagging, categories, translations, you name it, 23 | then you can use the bundled extensions or write your own. (and please don't forget 24 | to publish your extensions back to the community). 25 | 26 | And obviously, ElephantBlog can also be integrated as application content in your existing 27 | FeinCMS site. But if you want to run a blog only, then you don't have to activate FeinCMS 28 | page module. 29 | 30 | 31 | Features 32 | ======== 33 | 34 | The biggest feature may be that there are only a few features: 35 | 36 | * Pretty URLs 37 | * Feed creation 38 | * Time-based publishing 39 | * Based on FeinCMS item editor with the ability to integrate all of those FeinCMS 40 | content types 41 | 42 | 43 | Bundled extensions 44 | ------------------ 45 | 46 | You can, if you want, activate those extensions: 47 | 48 | * Disqus comments 49 | * Categories 50 | * Multilingual support 51 | * ... (more to come, you name it!) 52 | 53 | 54 | Getting started 55 | =============== 56 | 57 | If you are not familiar with FeinCMS then you probably want to learn more about FeinCMS: 58 | http://feincms.org 59 | 60 | Read the docs: http://feincms-elephantblog.readthedocs.org/en/latest/ 61 | 62 | Read the source: https://github.com/feincms/feincms-elephantblog 63 | 64 | .. image:: https://pypip.in/wheel/feincms-elephantblog/badge.svg 65 | :target: https://pypi.python.org/pypi/feincms-elephantblog/ 66 | :alt: Wheel Status 67 | 68 | 69 | 70 | Changelog 71 | ========= 72 | 73 | - 1.1: Support for Django 1.8 and FeinCMS 1.11 to Django 1.10 and 74 | FeinCMS 1.13 (not all combinations). 75 | - 1.0.2: Support for Django 1.8 and FeinCMS 1.11 76 | Experimental support for FeinCMS 2 77 | - 1.0.1: Support for Django 1.7 78 | - 1.0.0: First official release 79 | -------------------------------------------------------------------------------- /tests/testapp/factories.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import zoneinfo 3 | 4 | from django.conf import settings 5 | from django.contrib.auth.models import User 6 | from django.template.defaultfilters import slugify 7 | from factory.django import DjangoModelFactory 8 | 9 | from elephantblog.models import Category, CategoryTranslation, Entry 10 | 11 | 12 | class UserFactory(DjangoModelFactory): 13 | class Meta: 14 | model = User 15 | 16 | username = "author" 17 | password = "elephant" 18 | email = "admin@elephantblog.ch" 19 | 20 | 21 | class EntryFactory(DjangoModelFactory): 22 | class Meta: 23 | model = Entry 24 | 25 | is_active = True 26 | is_featured = False 27 | 28 | 29 | def create_entries(factory): 30 | tz = zoneinfo.ZoneInfo(settings.TIME_ZONE) 31 | author = UserFactory() 32 | entries = [] 33 | date1 = datetime.datetime(2012, 8, 12, 11, 0, 0, tzinfo=tz) 34 | delta = datetime.timedelta(hours=4) 35 | date2 = datetime.datetime(2012, 10, 12, 11, 1, 0, tzinfo=tz) 36 | 37 | entries.append( 38 | factory.create( 39 | pk=1, 40 | author=author, 41 | title="Entry 1", 42 | published_on=date1, 43 | last_changed=date1 + delta, 44 | slug="entry-1", 45 | language="en", 46 | ) 47 | ) 48 | entries.append( 49 | factory.create( 50 | pk=2, 51 | author=author, 52 | title="Eintrag 1", 53 | published_on=date2, 54 | last_changed=date2 + delta, 55 | slug="eintrag-1", 56 | language="en", 57 | ) 58 | ) 59 | return entries 60 | 61 | 62 | def create_chinese_entries(factory): 63 | entries = create_entries(factory) 64 | author = entries[0].author 65 | factory.create( 66 | pk=3, 67 | author=author, 68 | title="Entry 2 chinese traditional", 69 | language="zh-hans", 70 | translation_of=entries[0], 71 | published_on=datetime.datetime(2012, 10, 12, 12, 0, 0), 72 | last_changed=datetime.datetime(2012, 10, 12, 16, 0, 0), 73 | slug="entry-2-cn", 74 | ) 75 | factory.create( 76 | pk=4, 77 | author=author, 78 | title="Entry 2 chinese simplified", 79 | language="zh-hant", 80 | translation_of=entries[0], 81 | published_on=datetime.datetime(2012, 10, 12, 12, 0, 0), 82 | last_changed=datetime.datetime(2012, 10, 12, 16, 0, 0), 83 | slug="entry-2-tw", 84 | ) 85 | 86 | 87 | class CategoryTranslationFactory(DjangoModelFactory): 88 | class Meta: 89 | model = CategoryTranslation 90 | 91 | 92 | class CategoryFactory(DjangoModelFactory): 93 | class Meta: 94 | model = Category 95 | 96 | 97 | def create_category(title): 98 | category = CategoryFactory.create() 99 | CategoryTranslationFactory.create(parent=category, title=title, slug=slugify(title)) 100 | return category 101 | -------------------------------------------------------------------------------- /elephantblog/contents.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.translation import get_language, gettext_lazy as _ 3 | 4 | from elephantblog.models import Category, Entry 5 | from elephantblog.utils import entry_list_lookup_related 6 | 7 | 8 | try: 9 | # Load paginator with additional goodies form towel if possible 10 | from towel.paginator import EmptyPage, PageNotAnInteger, Paginator 11 | except ImportError: 12 | from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator 13 | 14 | 15 | class BlogEntryListContent(models.Model): 16 | category = models.ForeignKey( 17 | Category, 18 | blank=True, 19 | null=True, 20 | related_name="+", 21 | on_delete=models.CASCADE, 22 | verbose_name=_("category"), 23 | help_text=_("Only show entries from this category."), 24 | ) 25 | paginate_by = models.PositiveIntegerField( 26 | _("entries per page"), default=0, help_text=_("Set to 0 to disable pagination.") 27 | ) 28 | featured_only = models.BooleanField( 29 | _("featured only"), 30 | blank=True, 31 | default=False, 32 | help_text=_("Only show articles marked as featured"), 33 | ) 34 | 35 | only_active_language = False 36 | 37 | class Meta: 38 | abstract = True 39 | verbose_name = _("Blog entry list") 40 | verbose_name_plural = _("Blog entry lists") 41 | 42 | def process(self, request, **kwargs): 43 | if self.featured_only: 44 | entries = Entry.objects.featured() 45 | else: 46 | entries = Entry.objects.active() 47 | 48 | if self.category: 49 | entries = entries.filter(categories=self.category) 50 | 51 | if self.only_active_language: 52 | entries = entries.filter(language=get_language()) 53 | 54 | entries = entries.transform(entry_list_lookup_related) 55 | 56 | if self.paginate_by: 57 | paginator = Paginator(entries, self.paginate_by) 58 | page = request.GET.get("page", 1) 59 | try: 60 | self.entries = paginator.page(page) 61 | except PageNotAnInteger: 62 | self.entries = paginator.page(1) 63 | except EmptyPage: 64 | self.entries = paginator.page(paginator.num_pages) 65 | 66 | else: 67 | self.entries = entries 68 | 69 | def render(self, **kwargs): 70 | template_names = ["content/elephantblog/entry_list.html"] 71 | if self.featured_only: 72 | template_names.insert(0, "entry_list_featured.html") 73 | return template_names, {"content": self} 74 | 75 | 76 | class BlogCategoryListContent(models.Model): 77 | show_empty_categories = models.BooleanField(_("show empty categories?")) 78 | 79 | class Meta: 80 | abstract = True 81 | verbose_name = _("Blog category list") 82 | verbose_name_plural = _("Blog category lists") 83 | 84 | def render(self, **kwargs): 85 | if self.show_empty_categories: 86 | categories = Category.objects.all() 87 | else: 88 | categories = Category.objects.exclude(blogentries__isnull=True) 89 | 90 | return ( 91 | "content/elephantblog/category_list.html", 92 | {"content": self, "categories": categories}, 93 | ) 94 | -------------------------------------------------------------------------------- /tests/testapp/test_contents.py: -------------------------------------------------------------------------------- 1 | from django.template.loader import render_to_string 2 | from django.test import TestCase 3 | 4 | from elephantblog.contents import BlogCategoryListContent, BlogEntryListContent 5 | 6 | from .factories import EntryFactory, create_category, create_entries 7 | 8 | 9 | class Request: 10 | GET = {"page": 1} 11 | 12 | 13 | class ContentsTestCase(TestCase): 14 | def test_contents(self): 15 | entries = create_entries(EntryFactory) 16 | entries[0].richtextcontent_set.create( 17 | region="main", ordering=1, text="Hello world" 18 | ) 19 | 20 | entries[0].is_featured = True 21 | entries[0].save() 22 | category = create_category(title="Category 1") 23 | entries[1].categories.add(category) 24 | create_category(title="Empty category") 25 | 26 | BlogEntryListContent._meta.abstract = False # Hack to allow instantiation 27 | content = BlogEntryListContent() 28 | 29 | content.process(Request) 30 | html = render_to_string(*content.render()) 31 | self.assertIn( 32 | 'h2 class="entry-title">"), 102 | 1, 103 | ) 104 | 105 | content.show_empty_categories = True 106 | html = render_to_string(*content.render()) 107 | self.assertEqual( 108 | html.count("
  • "), 109 | 2, 110 | ) 111 | -------------------------------------------------------------------------------- /elephantblog/navigation_extensions/treeinfo.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import gettext as _, gettext_lazy 2 | 3 | from .common import Category, NavigationExtension, PagePretender, all_months, date_tree 4 | 5 | 6 | class BlogDateNavigationExtension(NavigationExtension): 7 | """ 8 | Navigation extension for FeinCMS which shows a year and month Breakdown: 9 | 2012 10 | April 11 | March 12 | February 13 | January 14 | 2011 15 | 2010 16 | """ 17 | 18 | name = gettext_lazy("Blog date") 19 | 20 | def children(self, page, **kwargs): 21 | for year, months in date_tree(): 22 | yield PagePretender( 23 | title="%s" % year, 24 | url=f"{page.get_absolute_url()}{year}/", 25 | tree_id=page.tree_id, 26 | lft=0, 27 | rght=len(months) + 1, 28 | level=page.level + 1, 29 | slug="%s" % year, 30 | ) 31 | for month in months: 32 | yield PagePretender( 33 | title="%s" % _(all_months[month - 1].strftime("%B")), 34 | url="%s%04d/%02d/" % (page.get_absolute_url(), year, month), 35 | tree_id=page.tree_id, 36 | lft=0, 37 | rght=0, 38 | level=page.level + 2, 39 | slug="%04d/%02d" % (year, month), 40 | ) 41 | 42 | 43 | class CategoryAndDateNavigationExtension(NavigationExtension): 44 | name = gettext_lazy("Blog category and date") 45 | 46 | def children(self, page, **kwargs): 47 | all_categories = Category.objects.all() 48 | yield PagePretender( 49 | title=_("Categories"), 50 | url="#", 51 | tree_id=page.tree_id, 52 | lft=0, 53 | rght=len(all_categories) + 1, 54 | level=page.level, 55 | slug="#", 56 | ) 57 | 58 | for category in all_categories: 59 | yield PagePretender( 60 | title=category.translation.title, 61 | url="%scategory/%s/" 62 | % (page.get_absolute_url(), category.translation.slug), 63 | tree_id=page.tree_id, 64 | lft=0, 65 | rght=0, 66 | level=page.level + 1, 67 | slug=category.translation.slug, 68 | ) 69 | 70 | yield PagePretender( 71 | title=_("Archive"), 72 | url="#", 73 | tree_id=page.tree_id, 74 | lft=0, 75 | rght=500, # does it really matter? 76 | level=page.level, 77 | slug="#", 78 | ) 79 | 80 | for year, months in date_tree(): 81 | yield PagePretender( 82 | title="%s" % year, 83 | url=f"{page.get_absolute_url()}{year}/", 84 | tree_id=page.tree_id, 85 | lft=0, 86 | rght=len(months) + 1, 87 | level=page.level + 1, 88 | slug="%s" % year, 89 | ) 90 | 91 | for month in months: 92 | yield PagePretender( 93 | title="%s" % _(all_months[month - 1].strftime("%B")), 94 | url="%s%04d/%02d/" % (page.get_absolute_url(), year, month), 95 | tree_id=page.tree_id, 96 | lft=0, 97 | rght=0, 98 | level=page.level + 2, 99 | slug="%04d/%02d" % (year, month), 100 | ) 101 | -------------------------------------------------------------------------------- /tests/testapp/test_translations.py: -------------------------------------------------------------------------------- 1 | from django.test import Client 2 | from django.test.testcases import TestCase 3 | from django.utils import translation 4 | from feincms.translations import short_language_code 5 | 6 | from elephantblog.models import Entry 7 | 8 | from .factories import EntryFactory, create_chinese_entries 9 | 10 | 11 | class TranslationsTest(TestCase): 12 | def testTranslation(self): 13 | create_chinese_entries(EntryFactory) 14 | 15 | # Make sure the Entry has a translation extension 16 | entry = Entry() 17 | self.assertTrue(hasattr(entry, "language")) 18 | self.assertTrue(hasattr(entry, "translation_of")) 19 | 20 | # define the language of entry 2 21 | entries = Entry.objects.order_by("pk") 22 | entry1 = entries[0] 23 | self.assertEqual(entry1.pk, 1) 24 | self.assertEqual(entry1.title, "Entry 1") 25 | entry1.language = "en" 26 | entry1.save() 27 | entry2 = entries[1] 28 | self.assertEqual(entry2.pk, 2) 29 | self.assertEqual(entry2.title, "Eintrag 1") 30 | entry2.language = "de" 31 | entry2.translation_of = entry1 32 | entry2.save() 33 | entry3 = entries[2] 34 | entry4 = entries[3] 35 | self.assertEqual(entry3.language, "zh-hans") 36 | self.assertEqual(entry4.language, "zh-hant") 37 | 38 | entry = Entry.objects.get(language="de") 39 | self.assertEqual(entry.title, "Eintrag 1") 40 | 41 | with translation.override("de"): 42 | c = Client() 43 | self.assertEqual(short_language_code(), "de") 44 | # Test Archive URL 45 | response = c.get("/blog/", HTTP_ACCEPT_LANGUAGE="de") 46 | self.assertEqual(len(response.context["object_list"]), 1) 47 | self.assertEqual(response.status_code, 200) 48 | self.assertNotContains(response, "Entry 1") 49 | self.assertContains(response, "Eintrag 1") 50 | # test all languages override 51 | response = c.get("/multilang/", HTTP_ACCEPT_LANGUAGE="de") 52 | self.assertEqual(len(response.context["object_list"]), 4) 53 | self.assertEqual(response.status_code, 200) 54 | 55 | with translation.override("en"): 56 | c = Client() 57 | response = c.get("/blog/", HTTP_ACCEPT_LANGUAGE="en") 58 | self.assertEqual(short_language_code(), "en") 59 | self.assertEqual(len(response.context["object_list"]), 1) 60 | self.assertEqual(response.status_code, 200) 61 | self.assertContains(response, "Entry 1") 62 | self.assertNotContains(response, "Eintrag 1") 63 | 64 | with translation.override("zh-hans"): 65 | c = Client() 66 | self.assertEqual(translation.get_language(), "zh-hans") 67 | self.assertEqual(short_language_code(), "zh") 68 | response = c.get("/blog/", HTTP_ACCEPT_LANGUAGE="zh-hans") 69 | self.assertEqual(len(response.context["object_list"]), 1) 70 | self.assertEqual(response.status_code, 200) 71 | self.assertContains(response, "Entry 2 chinese traditional") 72 | self.assertNotContains(response, "Eintrag 1") 73 | 74 | with translation.override("zh-hant"): 75 | c = Client() 76 | self.assertEqual(translation.get_language(), "zh-hant") 77 | self.assertEqual(short_language_code(), "zh") 78 | response = c.get("/blog/", HTTP_ACCEPT_LANGUAGE="zh-hant") 79 | self.assertEqual(len(response.context["object_list"]), 1) 80 | self.assertEqual(response.status_code, 200) 81 | self.assertContains(response, "Entry 2 chinese simplified") 82 | self.assertNotContains(response, "Eintrag 1") 83 | -------------------------------------------------------------------------------- /elephantblog/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | Class-based, modern views for elephantblog 3 | ========================================== 4 | 5 | Add the following code to ``settings.py`` if you want to integrate Elephantblog 6 | through ApplicationContent:: 7 | 8 | def elephantblog_entry_url_app(self): 9 | from feincms.apps import app_reverse 10 | return app_reverse( 11 | 'elephantblog_entry_detail', 12 | 'elephantblog', 13 | kwargs={ 14 | 'year': self.published_on.strftime('%Y'), 15 | 'month': self.published_on.strftime('%m'), 16 | 'day': self.published_on.strftime('%d'), 17 | 'slug': self.slug, 18 | }) 19 | 20 | def elephantblog_categorytranslation_url_app(self): 21 | from feincms.apps import app_reverse 22 | return app_reverse( 23 | 'elephantblog_category_detail', 24 | 'elephantblog', 25 | kwargs={ 26 | 'slug': self.slug, 27 | }) 28 | 29 | ABSOLUTE_URL_OVERRIDES = { 30 | 'elephantblog.entry': elephantblog_entry_url_app, 31 | 'elephantblog.categorytranslation':\ 32 | elephantblog_categorytranslation_url_app, 33 | } 34 | 35 | 36 | NOTE! You need to register the app as follows for the application content 37 | snippet:: 38 | 39 | Page.create_content_type(ApplicationContent, APPLICATIONS=( 40 | ('elephantblog', _('Blog'), {'urls': 'elephantblog.urls'}), 41 | )) 42 | 43 | """ 44 | 45 | from django.urls import path, re_path 46 | 47 | from elephantblog import views 48 | from elephantblog.feeds import EntryFeed 49 | 50 | 51 | def elephantblog_patterns(list_kwargs={}, detail_kwargs={}): 52 | """ 53 | Returns an instance of ready-to-use URL patterns for the blog. 54 | 55 | In the future, we will have a few configuration parameters here: 56 | 57 | - A parameter to specify a custom mixin for all view classes (or for 58 | list / detail view classes?) 59 | - Parameters to specify the language handling (probably some initialization 60 | arguments for the ``as_view`` methods) 61 | - The format of the month (three chars or two digits) 62 | - etc. 63 | """ 64 | return [ 65 | path("feed/", EntryFeed(), name="elephantblog_feed"), 66 | path( 67 | "", 68 | views.ArchiveIndexView.as_view(**list_kwargs), 69 | name="elephantblog_entry_archive", 70 | ), 71 | re_path( 72 | r"^(?P\d{4})/$", 73 | views.YearArchiveView.as_view(**list_kwargs), 74 | name="elephantblog_entry_archive_year", 75 | ), 76 | re_path( 77 | r"^(?P\d{4})/(?P\d{2})/$", 78 | views.MonthArchiveView.as_view(**list_kwargs), 79 | name="elephantblog_entry_archive_month", 80 | ), 81 | re_path( 82 | r"^(?P\d{4})/(?P\d{2})/(?P\d{2})/$", 83 | views.DayArchiveView.as_view(**list_kwargs), 84 | name="elephantblog_entry_archive_day", 85 | ), 86 | re_path( 87 | r"^(?P\d{4})/(?P\d{2})/(?P\d{2})/" r"(?P[-\w]+)/$", 88 | views.DateDetailView.as_view(**detail_kwargs), 89 | name="elephantblog_entry_detail", 90 | ), 91 | re_path( 92 | r"^category/(?P[-\w]+)/$", 93 | views.CategoryArchiveIndexView.as_view(**list_kwargs), 94 | name="elephantblog_category_detail", 95 | ), 96 | re_path( 97 | r"^author/(?P[-\w]+)/$", 98 | views.AuthorArchiveIndexView.as_view(**list_kwargs), 99 | name="elephantblog_author_detail", 100 | ), 101 | ] 102 | 103 | 104 | # Backwards compatibility: Create a URL patterns object with the default 105 | # configuration 106 | urlpatterns = elephantblog_patterns() 107 | -------------------------------------------------------------------------------- /tests/testapp/test_timezones.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import zoneinfo 3 | 4 | from django.contrib.auth.models import User 5 | from django.test.testcases import TestCase 6 | 7 | from elephantblog.models import Entry 8 | 9 | 10 | class TimezoneTest(TestCase): 11 | def setUp(self): 12 | self.author = User.objects.create(username="author", password="elephant") 13 | 14 | def test_chicago_night(self): 15 | chicago_tz = zoneinfo.ZoneInfo("America/Chicago") 16 | published_date = datetime.datetime( 17 | year=2012, month=3, day=3, hour=1, minute=30, tzinfo=chicago_tz 18 | ) 19 | entry = Entry.objects.create( 20 | is_active=True, 21 | author=self.author, 22 | slug="test-entry", 23 | published_on=published_date, 24 | ) 25 | 26 | # print entry 27 | # print entry.get_absolute_url() 28 | self.assertEqual(entry.published_on, published_date) 29 | response = self.client.get(entry.get_absolute_url()) 30 | self.assertEqual(response.status_code, 200) 31 | 32 | def test_chicago_evening(self): 33 | chicago_tz = zoneinfo.ZoneInfo("America/Chicago") 34 | published_date = datetime.datetime( 35 | year=2012, month=3, day=3, hour=22, minute=30, tzinfo=chicago_tz 36 | ) 37 | 38 | entry = Entry.objects.create( 39 | is_active=True, 40 | author=self.author, 41 | slug="test-entry", 42 | published_on=published_date, 43 | ) 44 | 45 | # print entry.published_on 46 | # print entry.get_absolute_url() 47 | self.assertEqual(entry.published_on, published_date) 48 | response = self.client.get(entry.get_absolute_url()) 49 | self.assertEqual(response.status_code, 200) 50 | 51 | def test_moscow_night(self): 52 | moscow_tz = zoneinfo.ZoneInfo("Europe/Moscow") 53 | published_date = datetime.datetime( 54 | year=2012, month=3, day=3, hour=1, minute=30, tzinfo=moscow_tz 55 | ) 56 | entry = Entry.objects.create( 57 | is_active=True, 58 | author=self.author, 59 | slug="test-entry", 60 | published_on=published_date, 61 | ) 62 | 63 | response = self.client.get(entry.get_absolute_url()) 64 | self.assertEqual(response.status_code, 200) 65 | 66 | def test_permalink_equality(self): 67 | urls = [] 68 | for tzinfo in ( 69 | zoneinfo.ZoneInfo("America/Chicago"), 70 | zoneinfo.ZoneInfo("Europe/Moscow"), 71 | ): 72 | published_date = datetime.datetime( 73 | year=2012, month=3, day=3, hour=1, minute=30, tzinfo=tzinfo 74 | ) 75 | 76 | entry = Entry.objects.create( 77 | is_active=True, 78 | author=self.author, 79 | slug="test-entry", 80 | published_on=published_date, 81 | ) 82 | 83 | urls.append(entry.get_absolute_url()) 84 | entry.delete() 85 | 86 | url_chicago, url_moscow = urls 87 | self.assertNotEqual(url_chicago, url_moscow) 88 | urls = [] 89 | for tzinfo, day, hour in [ 90 | (zoneinfo.ZoneInfo("America/Chicago"), 2, 15), 91 | (zoneinfo.ZoneInfo("Europe/Moscow"), 3, 1), 92 | ]: 93 | published_date = datetime.datetime( 94 | year=2012, month=3, day=day, hour=hour, minute=30, tzinfo=tzinfo 95 | ) 96 | 97 | entry = Entry.objects.create( 98 | is_active=True, 99 | author=self.author, 100 | slug="test-entry", 101 | published_on=published_date, 102 | ) 103 | urls.append(entry.get_absolute_url()) 104 | entry.delete() 105 | 106 | url_chicago, url_moscow = urls 107 | self.assertEqual(url_chicago, url_moscow) 108 | -------------------------------------------------------------------------------- /elephantblog/templatetags/elephantblog_tags.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django import template 3 | from django.contrib.auth import get_user_model 4 | from django.core.exceptions import FieldDoesNotExist 5 | from django.utils.translation import get_language 6 | 7 | from elephantblog.models import Category, Entry 8 | from elephantblog.utils import entry_list_lookup_related 9 | 10 | 11 | register = template.Library() 12 | assignment_tag = ( 13 | register.simple_tag if django.VERSION >= (1, 9) else register.assignment_tag 14 | ) 15 | 16 | 17 | @assignment_tag 18 | def elephantblog_categories(show_empty_categories=False): 19 | """ 20 | Assigns the list of categories to a template variable of your choice. The 21 | default is to only return categories which have at least one blog entry:: 22 | 23 | {% elephantblog_categories as categories %} 24 | 25 | It's also possible to return all categories:: 26 | 27 | {% elephantblog_categories show_empty_categories=True as categories %} 28 | """ 29 | if show_empty_categories: 30 | return Category.objects.all() 31 | return Category.objects.exclude(blogentries__isnull=True) 32 | 33 | 34 | @assignment_tag 35 | def elephantblog_archive_years(): 36 | """ 37 | Assigns a list of years with active entries to a template variable of 38 | your choice. Especially useful to generate archive links:: 39 | 40 | {% elephantblog_archive_years as years %} 41 | 51 | 52 | (Wrapped for legibility, the ``{% url %}`` template tag must be on a 53 | single line.) 54 | """ 55 | return Entry.objects.active().datetimes("published_on", "year", "DESC") 56 | 57 | 58 | @assignment_tag 59 | def elephantblog_archive_months(): 60 | """ 61 | Assigns a list of months with active entries to a template variable of 62 | your choice. Especially useful to generate archive links:: 63 | 64 | {% elephantblog_archive_months as months %} 65 | 76 | 77 | (Wrapped for legibility, the ``{% url %}`` template tag must be on a 78 | single line.) 79 | """ 80 | return Entry.objects.active().datetimes("published_on", "month", "DESC") 81 | 82 | 83 | @assignment_tag 84 | def elephantblog_entries( 85 | limit=10, featured_only=False, active_language_only=True, category=None 86 | ): 87 | """ 88 | Usage:: 89 | 90 | {% elephantblog_entries limit=2 featured_only=True as entries %} 91 | 92 | or:: 93 | 94 | {% elephantblog_entries limit=10 as entries %} 95 | 96 | or:: 97 | 98 | {% elephantblog_entries active_language_only=False as entries %} 99 | 100 | or:: 101 | 102 | {% elephantblog_entries category=some_category as entries %} 103 | """ 104 | 105 | queryset = Entry.objects.active() 106 | if featured_only: 107 | queryset = queryset.filter(is_featured=True) 108 | 109 | try: 110 | queryset.model._meta.get_field("language") 111 | except FieldDoesNotExist: 112 | pass 113 | else: 114 | if active_language_only: 115 | queryset = queryset.filter(language=get_language()) 116 | 117 | if category is not None: 118 | queryset = queryset.filter(categories=category) 119 | 120 | return queryset.transform(entry_list_lookup_related)[:limit] 121 | 122 | 123 | @assignment_tag 124 | def elephantblog_authors(): 125 | return get_user_model().objects.filter( 126 | id__in=Entry.objects.active().order_by().values("author"), 127 | ) 128 | -------------------------------------------------------------------------------- /elephantblog/navigation_extensions/recursetree.py: -------------------------------------------------------------------------------- 1 | """optimized for use with the feincms_nav and recursetree template tag.""" 2 | 3 | from django.conf import settings 4 | from django.utils.translation import gettext as _, gettext_lazy 5 | 6 | from .common import Category, NavigationExtension, PagePretender, all_months, date_tree 7 | 8 | 9 | class RBlogDateNavigationExtension(NavigationExtension): 10 | """ 11 | Special version optimized for recursetree template tag 12 | """ 13 | 14 | name = gettext_lazy("Blog date") 15 | 16 | def children(self, page, **kwargs): 17 | for year, months in date_tree(): 18 | 19 | def return_months(): 20 | for month in months: 21 | yield PagePretender( 22 | title="%s" % _(all_months[month - 1].strftime("%B")), 23 | url="%s%04d/%02d/" % (page.get_absolute_url(), year, month), 24 | tree_id=page.tree_id, 25 | level=page.level + 2, 26 | language=getattr(page, "language", settings.LANGUAGE_CODE), 27 | slug="%04d/%02d" % (year, month), 28 | lft=0, 29 | rght=0, 30 | _mptt_meta=page._mptt_meta, 31 | ) 32 | 33 | yield PagePretender( 34 | title="%s" % year, 35 | url=f"{page.get_absolute_url()}{year}/", 36 | tree_id=page.tree_id, 37 | language=getattr(page, "language", settings.LANGUAGE_CODE), 38 | level=page.level + 1, 39 | slug="%s" % year, 40 | parent=page, 41 | parent_id=page.id, 42 | get_children=return_months, 43 | lft=page.lft + 1, 44 | rght=len(months) + 1, 45 | _mptt_meta=page._mptt_meta, 46 | ) 47 | 48 | 49 | class RCategoryAndDateNavigationExtension(NavigationExtension): 50 | name = gettext_lazy("Blog category and date") 51 | 52 | def children(self, page, **kwargs): 53 | all_categories = Category.objects.all() 54 | 55 | def return_children(): 56 | for category in all_categories: 57 | yield PagePretender( 58 | title=category.translation.title, 59 | url="%scategory/%s/" 60 | % (page.get_absolute_url(), category.translation.slug), 61 | tree_id=page.tree_id, 62 | level=page.level + 2, 63 | language=getattr(page, "language", settings.LANGUAGE_CODE), 64 | slug=category.translation.slug, 65 | lft=0, 66 | rght=0, 67 | _mptt_meta=page._mptt_meta, 68 | ) 69 | 70 | yield PagePretender( 71 | title=_("Categories"), 72 | url="./", 73 | tree_id=page.tree_id, 74 | level=page.level + 1, 75 | parent=page, 76 | parent_id=page.id, 77 | slug="", 78 | language=getattr(page, "language", settings.LANGUAGE_CODE), 79 | get_children=return_children, 80 | lft=page.lft + 1, 81 | rght=len(all_categories) + 1, 82 | _mptt_meta=page._mptt_meta, 83 | ) 84 | 85 | def return_dates(): 86 | for year, months in date_tree(): 87 | 88 | def return_months(): 89 | for month in months: 90 | yield PagePretender( 91 | title="%s" % _(all_months[month - 1].strftime("%B")), 92 | url="%s%04d/%02d/" % (page.get_absolute_url(), year, month), 93 | tree_id=page.tree_id, 94 | level=page.level + 3, 95 | language=getattr(page, "language", settings.LANGUAGE_CODE), 96 | slug="%04d/%02d" % (year, month), 97 | ) 98 | 99 | yield PagePretender( 100 | title="%s" % year, 101 | url=f"{page.get_absolute_url()}{year}/", 102 | tree_id=page.tree_id, 103 | level=page.level + 2, 104 | slug="%s" % year, 105 | language=getattr(page, "language", settings.LANGUAGE_CODE), 106 | get_children=return_months, 107 | ) 108 | 109 | yield PagePretender( 110 | title=_("Archive"), 111 | url="./", 112 | tree_id=page.tree_id, 113 | level=page.level + 1, 114 | slug="", 115 | parent=page, 116 | parent_id=page.id, 117 | language=getattr(page, "language", settings.LANGUAGE_CODE), 118 | get_children=return_dates, 119 | lft=0, 120 | rght=0, 121 | _mptt_meta=page._mptt_meta, 122 | ) 123 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Elephantblog.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Elephantblog.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Elephantblog" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Elephantblog" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | if NOT "%PAPER%" == "" ( 11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 12 | ) 13 | 14 | if "%1" == "" goto help 15 | 16 | if "%1" == "help" ( 17 | :help 18 | echo.Please use `make ^` where ^ is one of 19 | echo. html to make standalone HTML files 20 | echo. dirhtml to make HTML files named index.html in directories 21 | echo. singlehtml to make a single large HTML file 22 | echo. pickle to make pickle files 23 | echo. json to make JSON files 24 | echo. htmlhelp to make HTML files and a HTML help project 25 | echo. qthelp to make HTML files and a qthelp project 26 | echo. devhelp to make HTML files and a Devhelp project 27 | echo. epub to make an epub 28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 29 | echo. text to make text files 30 | echo. man to make manual pages 31 | echo. changes to make an overview over all changed/added/deprecated items 32 | echo. linkcheck to check all external links for integrity 33 | echo. doctest to run all doctests embedded in the documentation if enabled 34 | goto end 35 | ) 36 | 37 | if "%1" == "clean" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | goto end 41 | ) 42 | 43 | if "%1" == "html" ( 44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 45 | if errorlevel 1 exit /b 1 46 | echo. 47 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 48 | goto end 49 | ) 50 | 51 | if "%1" == "dirhtml" ( 52 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 53 | if errorlevel 1 exit /b 1 54 | echo. 55 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 56 | goto end 57 | ) 58 | 59 | if "%1" == "singlehtml" ( 60 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 61 | if errorlevel 1 exit /b 1 62 | echo. 63 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 64 | goto end 65 | ) 66 | 67 | if "%1" == "pickle" ( 68 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 69 | if errorlevel 1 exit /b 1 70 | echo. 71 | echo.Build finished; now you can process the pickle files. 72 | goto end 73 | ) 74 | 75 | if "%1" == "json" ( 76 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished; now you can process the JSON files. 80 | goto end 81 | ) 82 | 83 | if "%1" == "htmlhelp" ( 84 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished; now you can run HTML Help Workshop with the ^ 88 | .hhp project file in %BUILDDIR%/htmlhelp. 89 | goto end 90 | ) 91 | 92 | if "%1" == "qthelp" ( 93 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 94 | if errorlevel 1 exit /b 1 95 | echo. 96 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 97 | .qhcp project file in %BUILDDIR%/qthelp, like this: 98 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Elephantblog.qhcp 99 | echo.To view the help file: 100 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Elephantblog.ghc 101 | goto end 102 | ) 103 | 104 | if "%1" == "devhelp" ( 105 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 106 | if errorlevel 1 exit /b 1 107 | echo. 108 | echo.Build finished. 109 | goto end 110 | ) 111 | 112 | if "%1" == "epub" ( 113 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 117 | goto end 118 | ) 119 | 120 | if "%1" == "latex" ( 121 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 122 | if errorlevel 1 exit /b 1 123 | echo. 124 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 125 | goto end 126 | ) 127 | 128 | if "%1" == "text" ( 129 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 130 | if errorlevel 1 exit /b 1 131 | echo. 132 | echo.Build finished. The text files are in %BUILDDIR%/text. 133 | goto end 134 | ) 135 | 136 | if "%1" == "man" ( 137 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 141 | goto end 142 | ) 143 | 144 | if "%1" == "changes" ( 145 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.The overview file is in %BUILDDIR%/changes. 149 | goto end 150 | ) 151 | 152 | if "%1" == "linkcheck" ( 153 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Link check complete; look for any errors in the above output ^ 157 | or in %BUILDDIR%/linkcheck/output.txt. 158 | goto end 159 | ) 160 | 161 | if "%1" == "doctest" ( 162 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 163 | if errorlevel 1 exit /b 1 164 | echo. 165 | echo.Testing of doctests in the sources finished, look at the ^ 166 | results in %BUILDDIR%/doctest/output.txt. 167 | goto end 168 | ) 169 | 170 | :end 171 | -------------------------------------------------------------------------------- /elephantblog/locale/fr/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2013-11-25 11:20-0600\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 19 | 20 | #: admin.py:25 21 | msgid "Blog entries in category" 22 | msgstr "" 23 | 24 | #: contents.py:21 models.py:32 25 | msgid "category" 26 | msgstr "catégorie" 27 | 28 | #: contents.py:21 29 | msgid "Only show entries from this category." 30 | msgstr "" 31 | 32 | #: contents.py:22 33 | msgid "entries per page" 34 | msgstr "" 35 | 36 | #: contents.py:23 37 | msgid "Set to 0 to disable pagination." 38 | msgstr "" 39 | 40 | #: contents.py:24 41 | msgid "featured only" 42 | msgstr "" 43 | 44 | #: contents.py:25 45 | msgid "Only show articles marked as featured" 46 | msgstr "" 47 | 48 | #: contents.py:31 49 | msgid "Blog entry list" 50 | msgstr "" 51 | 52 | #: contents.py:32 53 | msgid "Blog entry lists" 54 | msgstr "" 55 | 56 | #: contents.py:67 57 | msgid "show empty categories?" 58 | msgstr "" 59 | 60 | #: contents.py:71 61 | msgid "Blog category list" 62 | msgstr "" 63 | 64 | #: contents.py:72 65 | msgid "Blog category lists" 66 | msgstr "" 67 | 68 | #: models.py:29 69 | msgid "ordering" 70 | msgstr "" 71 | 72 | #: models.py:33 models.py:102 73 | msgid "categories" 74 | msgstr "catégories" 75 | 76 | #: models.py:40 77 | msgid "Unnamed category" 78 | msgstr "" 79 | 80 | #: models.py:44 81 | msgid "category title" 82 | msgstr "" 83 | 84 | #: models.py:45 models.py:90 85 | msgid "slug" 86 | msgstr "" 87 | 88 | #: models.py:46 89 | msgid "description" 90 | msgstr "" 91 | 92 | #: models.py:49 93 | msgid "category translation" 94 | msgstr "" 95 | 96 | #: models.py:50 97 | msgid "category translations" 98 | msgstr "" 99 | 100 | #: models.py:86 101 | msgid "is active" 102 | msgstr "" 103 | 104 | #: models.py:87 105 | msgid "is featured" 106 | msgstr "" 107 | 108 | #: models.py:89 109 | msgid "title" 110 | msgstr "" 111 | 112 | #: models.py:95 113 | msgid "author" 114 | msgstr "" 115 | 116 | #: models.py:96 117 | msgid "published on" 118 | msgstr "" 119 | 120 | #: models.py:98 121 | msgid "Will be filled in automatically when entry gets published." 122 | msgstr "" 123 | 124 | #: models.py:99 125 | msgid "last change" 126 | msgstr "" 127 | 128 | #: models.py:108 129 | msgid "entry" 130 | msgstr "" 131 | 132 | #: models.py:109 133 | msgid "entries" 134 | msgstr "" 135 | 136 | #: models.py:153 137 | #, python-format 138 | msgid "One entry was successfully marked as %(state)s" 139 | msgid_plural "%(count)s entries were successfully marked as %(state)s" 140 | msgstr[0] "" 141 | msgstr[1] "" 142 | 143 | #: views.py:172 144 | #, python-format 145 | msgid "" 146 | "Future %(verbose_name_plural)s not available because %(class_name)s." 147 | "allow_future is False." 148 | msgstr "" 149 | 150 | #: extensions/blogping.py:20 151 | msgid "sleeping" 152 | msgstr "" 153 | 154 | #: extensions/blogping.py:21 extensions/blogping.py:34 155 | msgid "queued" 156 | msgstr "" 157 | 158 | #: extensions/blogping.py:22 159 | msgid "sent" 160 | msgstr "" 161 | 162 | #: extensions/blogping.py:23 163 | msgid "unknown" 164 | msgstr "" 165 | 166 | #: extensions/blogping.py:26 167 | msgid "ping" 168 | msgstr "" 169 | 170 | #: extensions/blogping.py:35 171 | msgid "Ping Again" 172 | msgstr "" 173 | 174 | #: extensions/sites.py:11 175 | msgid "The sites where the blogpost should appear." 176 | msgstr "" 177 | 178 | #: extensions/sites.py:23 179 | msgid "Sites" 180 | msgstr "" 181 | 182 | #: extensions/tags.py:8 183 | msgid "A comma-separated list of tags." 184 | msgstr "" 185 | 186 | #: navigation_extensions/common.py:41 187 | msgid "blog categories" 188 | msgstr "" 189 | 190 | #: navigation_extensions/recursetree.py:13 191 | #: navigation_extensions/treeinfo.py:18 192 | msgid "Blog date" 193 | msgstr "" 194 | 195 | #: navigation_extensions/recursetree.py:47 196 | #: navigation_extensions/treeinfo.py:44 197 | msgid "Blog category and date" 198 | msgstr "" 199 | 200 | #: navigation_extensions/recursetree.py:68 201 | #: navigation_extensions/treeinfo.py:49 202 | #: templates/content/elephantblog/category_list.html:6 203 | msgid "Categories" 204 | msgstr "Catégories" 205 | 206 | #: navigation_extensions/recursetree.py:108 207 | #: navigation_extensions/treeinfo.py:69 208 | msgid "Archive" 209 | msgstr "Archives" 210 | 211 | #: templates/admin/feincms/elephantblog/entry/item_editor.html:13 212 | msgid "Preview" 213 | msgstr "" 214 | 215 | #: templates/content/elephantblog/entry_list.html:13 216 | #: templates/elephantblog/entry_archive.html:28 217 | #: templates/elephantblog/entry_detail.html:18 218 | msgid "by" 219 | msgstr "" 220 | 221 | #: templates/elephantblog/entry_archive.html:5 222 | #: templates/elephantblog/entry_detail.html:5 223 | msgid "News" 224 | msgstr "" 225 | 226 | #: templates/elephantblog/entry_archive.html:9 227 | #: templates/elephantblog/entry_archive.html:10 228 | #: templates/elephantblog/entry_archive.html:11 229 | #: templates/elephantblog/entry_archive.html:12 230 | msgid "for" 231 | msgstr "" 232 | -------------------------------------------------------------------------------- /elephantblog/models.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import models 3 | from django.db.models import Q 4 | from django.template.defaultfilters import slugify 5 | from django.urls import reverse 6 | from django.utils import timezone 7 | from django.utils.translation import gettext, gettext_lazy as _ 8 | from feincms import translations 9 | from feincms.models import Base 10 | from feincms.module.mixins import ContentModelMixin 11 | from feincms.utils.managers import ActiveAwareContentManagerMixin 12 | from feincms.utils.queryset_transform import TransformManager 13 | 14 | 15 | class Category(models.Model, translations.TranslatedObjectMixin): 16 | """ 17 | Category is language-aware and connected to the Entry model via 18 | a many to many relationship. 19 | """ 20 | 21 | ordering = models.SmallIntegerField(_("ordering"), default=0) 22 | 23 | objects = translations.TranslatedObjectManager() 24 | 25 | class Meta: 26 | verbose_name = _("category") 27 | verbose_name_plural = _("categories") 28 | ordering = ["ordering"] 29 | 30 | def __str__(self): 31 | try: 32 | translation = self.translation 33 | except models.ObjectDoesNotExist: 34 | return gettext("Unnamed category") 35 | 36 | if translation: 37 | return "%s" % translation 38 | 39 | return gettext("Unnamed category") 40 | 41 | 42 | class CategoryTranslation(translations.Translation(Category)): 43 | title = models.CharField(_("category title"), max_length=100) 44 | slug = models.SlugField(_("slug"), unique=True) 45 | description = models.CharField(_("description"), max_length=250, blank=True) 46 | 47 | class Meta: 48 | verbose_name = _("category translation") 49 | verbose_name_plural = _("category translations") 50 | ordering = ["title"] 51 | 52 | def __str__(self): 53 | return self.title 54 | 55 | def get_absolute_url(self): 56 | return reverse("elephantblog_category_detail", kwargs={"slug": self.slug}) 57 | 58 | def save(self, *args, **kwargs): 59 | if not self.slug: 60 | self.slug = slugify(self.title) 61 | 62 | super().save(*args, **kwargs) 63 | 64 | 65 | class EntryManager(ActiveAwareContentManagerMixin, TransformManager): 66 | def featured(self): 67 | return self.active().filter(is_featured=True) 68 | 69 | 70 | EntryManager.add_to_active_filters(Q(is_active=True), key="cleared") 71 | 72 | EntryManager.add_to_active_filters( 73 | lambda queryset: queryset.filter(published_on__lte=timezone.now()), 74 | key="published_on_past", 75 | ) 76 | 77 | 78 | class Entry(Base, ContentModelMixin): 79 | is_active = models.BooleanField(_("is active"), default=True, db_index=True) 80 | is_featured = models.BooleanField(_("is featured"), default=False, db_index=True) 81 | 82 | title = models.CharField(_("title"), max_length=100) 83 | slug = models.SlugField(_("slug"), max_length=100, unique_for_date="published_on") 84 | author = models.ForeignKey( 85 | getattr(settings, "AUTH_USER_MODEL", "auth.User"), 86 | on_delete=models.CASCADE, 87 | related_name="blogentries", 88 | limit_choices_to={"is_staff": True}, 89 | verbose_name=_("author"), 90 | ) 91 | published_on = models.DateTimeField( 92 | _("published on"), 93 | blank=True, 94 | null=True, 95 | default=timezone.now, 96 | db_index=True, 97 | help_text=_("Will be filled in automatically when entry gets published."), 98 | ) 99 | last_changed = models.DateTimeField(_("last change"), auto_now=True, editable=False) 100 | 101 | categories = models.ManyToManyField( 102 | Category, verbose_name=_("categories"), related_name="blogentries", blank=True 103 | ) 104 | 105 | objects = EntryManager() 106 | 107 | class Meta: 108 | get_latest_by = "published_on" 109 | ordering = ["-published_on"] 110 | verbose_name = _("entry") 111 | verbose_name_plural = _("entries") 112 | 113 | def __str__(self): 114 | return self.title 115 | 116 | def __init__(self, *args, **kwargs): 117 | super().__init__(*args, **kwargs) 118 | self._old_is_active = self.is_active 119 | 120 | def save(self, *args, **kwargs): 121 | if self.is_active and not self.published_on: 122 | self.published_on = timezone.now() 123 | 124 | super().save(*args, **kwargs) 125 | 126 | save.alters_data = True 127 | 128 | def get_absolute_url(self): 129 | # The view/template layer always works with visitors' local time. 130 | # Therefore, also generate localtime URLs, otherwise visitors will 131 | # hit 404s on blog entry URLs generated for them. Unfortunately, this 132 | # also means that you cannot send a permalink around half the globe 133 | # and expect it to work... 134 | # https://code.djangoproject.com/ticket/18794 135 | if settings.USE_TZ: 136 | pub_date = timezone.localtime(self.published_on) 137 | else: 138 | pub_date = self.published_on 139 | 140 | return reverse( 141 | "elephantblog_entry_detail", 142 | kwargs={ 143 | "year": pub_date.strftime("%Y"), 144 | "month": pub_date.strftime("%m"), 145 | "day": pub_date.strftime("%d"), 146 | "slug": self.slug, 147 | }, 148 | ) 149 | 150 | @classmethod 151 | def register_extension(cls, register_fn): 152 | from .modeladmins import EntryAdmin 153 | 154 | register_fn(cls, EntryAdmin) 155 | -------------------------------------------------------------------------------- /tests/testapp/test_archive_views.py: -------------------------------------------------------------------------------- 1 | from django.test import Client 2 | from django.test.testcases import TestCase 3 | from django.test.utils import override_settings 4 | from feincms.module.medialibrary.models import MediaFile 5 | 6 | from elephantblog import views as blogviews 7 | from elephantblog.models import Entry 8 | 9 | from .factories import EntryFactory, create_category, create_entries 10 | 11 | 12 | @override_settings(SITE_ID=1) 13 | class GenericViewsTest(TestCase): 14 | def setUp(self): 15 | create_entries(EntryFactory) 16 | 17 | def testURLs(self): 18 | c = Client() 19 | # Test Archive URL 20 | response = c.get("/blog/") 21 | self.assertTrue( 22 | isinstance(response.context["view"], blogviews.ArchiveIndexView) 23 | ) 24 | self.assertEqual(len(response.context["object_list"]), 2) 25 | self.assertEqual(response.status_code, 200) 26 | self.assertContains(response, "Entry 1") 27 | self.assertContains(response, "Eintrag 1") 28 | 29 | # Test year archive 30 | response = c.get("/blog/2012/") 31 | self.assertTrue(isinstance(response.context["view"], blogviews.YearArchiveView)) 32 | self.assertEqual(len(response.context["object_list"]), 2) 33 | self.assertEqual( 34 | response.context["view"].get_template_names(), 35 | ["elephantblog/entry_archive.html"], 36 | ) 37 | self.assertEqual(response.status_code, 200) 38 | self.assertContains(response, "News for 2012") 39 | self.assertContains(response, "Entry 1") 40 | self.assertContains(response, "Eintrag 1") 41 | # No entries in 2011: 42 | response = c.get("/blog/2011/") 43 | self.assertEqual(response.status_code, 404) 44 | 45 | # Test month archive 46 | response = c.get("/blog/2012/10/") 47 | self.assertTrue( 48 | isinstance(response.context["view"], blogviews.MonthArchiveView) 49 | ) 50 | self.assertEqual(len(response.context["object_list"]), 1) 51 | self.assertEqual(response.status_code, 200) 52 | self.assertRegex(response.content.decode("utf-8"), r"News\s+for October 2012") 53 | self.assertContains(response, "Eintrag 1") 54 | response = c.get("/blog/2012/08/") 55 | self.assertEqual(len(response.context["object_list"]), 1) 56 | self.assertRegex(response.content.decode("utf-8"), r"News\s+for August 2012") 57 | self.assertContains(response, "Entry 1") 58 | response = c.get("/blog/2012/06/") 59 | self.assertEqual(response.status_code, 404) 60 | 61 | # Test day archive 62 | response = c.get("/blog/2012/10/12/") 63 | self.assertTrue(isinstance(response.context["view"], blogviews.DayArchiveView)) 64 | self.assertEqual(len(response.context["object_list"]), 1) 65 | self.assertEqual(response.status_code, 200) 66 | self.assertRegex(response.content.decode("utf-8"), r"News\s+for Oct. 12, 2012") 67 | self.assertContains(response, "Eintrag 1") 68 | response = c.get("/blog/2012/08/12/") 69 | self.assertEqual(len(response.context["object_list"]), 1) 70 | self.assertRegex(response.content.decode("utf-8"), r"News\s+for Aug. 12, 2012") 71 | self.assertContains(response, "Entry 1") 72 | # No entries in 2011: 73 | response = c.get("/blog/2012/10/13/") 74 | self.assertEqual(response.status_code, 404) 75 | 76 | # Test category archive 77 | # assign a category to the entry 78 | category1 = create_category("Category 1") 79 | category2 = create_category("Category 2") 80 | entry = Entry.objects.get(slug="entry-1") 81 | entry.categories.add(category1) 82 | entry.categories.add(category2) 83 | entry = Entry.objects.get(slug="eintrag-1") 84 | entry.categories.add(category2) 85 | 86 | response = c.get("/blog/category/category-1/") 87 | self.assertEqual(response.status_code, 200) 88 | self.assertTrue( 89 | isinstance(response.context["view"], blogviews.CategoryArchiveIndexView) 90 | ) 91 | self.assertEqual(len(response.context["object_list"]), 1) 92 | self.assertContains(response, "Entry 1") 93 | self.assertNotContains(response, "Eintrag 1") 94 | 95 | response = c.get("/blog/category/category-2/") 96 | self.assertEqual(response.status_code, 200) 97 | self.assertTrue( 98 | isinstance(response.context["view"], blogviews.CategoryArchiveIndexView) 99 | ) 100 | self.assertEqual(len(response.context["object_list"]), 2) 101 | self.assertContains(response, "Entry 1") 102 | self.assertContains(response, "Eintrag 1") 103 | 104 | def test_lookup_related(self): 105 | entry = Entry.objects.get(slug="entry-1") 106 | richtext = entry.richtextcontent_set.create( 107 | ordering=1, region="main", text="Hello" 108 | ) 109 | 110 | image = entry.mediafilecontent_set.create( 111 | ordering=2, 112 | region="main", 113 | mediafile=MediaFile.objects.create( 114 | type="image", 115 | file="test.jpg", 116 | ), 117 | ) 118 | 119 | c = Client() 120 | 121 | response = c.get("/blog/2012/08/12/entry-1/") 122 | self.assertEqual(response.status_code, 200) 123 | self.assertTrue(isinstance(response.context["view"], blogviews.DateDetailView)) 124 | self.assertContains(response, "Entry 1") 125 | self.assertContains(response, "Hello") 126 | self.assertContains(response, 'src="/media/test.jpg"') 127 | 128 | self.assertEqual(response.context["entry"].first_image, image) 129 | self.assertEqual(response.context["entry"].first_richtext, richtext) 130 | 131 | response = c.get("/blog/") 132 | self.assertEqual(response.status_code, 200) 133 | self.assertEqual(response.context["object_list"][1].first_image, image) 134 | self.assertEqual(response.context["object_list"][1].first_richtext, richtext) 135 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. _installation: 2 | 3 | ========================= 4 | Installation instructions 5 | ========================= 6 | 7 | 8 | Requirements 9 | ============ 10 | 11 | ElephantBlog needs at least: 12 | 13 | * Django v1.4 (get it here: https://github.com/django/django) 14 | * FeinCMS v1.7 (get it here: https://github.com/feincms/feincms) 15 | * TinyMCE_ or any other Richtext editor. TinyMCE goes in /media/js/tiny_mce. 16 | 17 | .. _TinyMCE: http://www.tinymce.com/download/download.php 18 | 19 | Installation 20 | ============ 21 | 22 | You can install elephantblog using ``pip install feincms-elephantblog``. 23 | It is also recommended to use a HTML sanitizer for rich text content, 24 | e.g. ``pip install html-sanitizer`` 25 | 26 | * Add ``elephantblog`` to your ``INSTALLED_APPS`` in your ``settings.py`` 27 | 28 | The first step is to create a new app. As an example, let's name it ``blog``. You can use ``manage.py`` to create it:: 29 | 30 | python manage.py startapp blog 31 | 32 | Then, add ``elephantblog`` and ``blog`` to your ``INSTALLED_APPS`` in your ``settings.py``:: 33 | 34 | INSTALLED_APPS = [ 35 | 'blog.apps.BlogConfig', 36 | 'elephantblog', 37 | # Your other apps, e.g. 'feincms.module.medialibrary' etc. 38 | ] 39 | 40 | In the ``models.py`` file of your ``blog`` app, register the elephantblog module, extensions and 41 | content types:: 42 | 43 | from django.utils.translation import gettext_lazy as _ 44 | from feincms.contents import RichTextContent 45 | from html_sanitizer.django import get_sanitizer 46 | 47 | from elephantblog.models import Entry 48 | 49 | Entry.register_extensions( 50 | 'feincms.module.extensions.datepublisher', 51 | 'feincms.module.extensions.translations', 52 | ) 53 | Entry.register_regions( 54 | ('main', _('Main content area')), 55 | ) 56 | Entry.create_content_type( 57 | RichTextContent, 58 | cleanse=get_sanitizer().sanitize, 59 | regions=('main',), 60 | ) 61 | 62 | 63 | .. note:: 64 | 65 | Of course, you can create all of the content types that you have for your 66 | FeinCMS Page. 67 | 68 | Migrations 69 | ---------- 70 | 71 | Specify the migration modules of your ``blog`` app to your ``MIGRATION_MODULES`` in your ``settings.py`` file:: 72 | 73 | MIGRATION_MODULES = { 74 | 'category': 'blog.category_migrations', 75 | 'categorytranslation': 'blog.categorytranslation_migrations', 76 | 'entry': 'blog.entry_migrations', 77 | } 78 | 79 | Create an empty migration file for ``elephantblog``:: 80 | 81 | python manage.py makemigrations --empty elephantblog 82 | 83 | Perform the migration for ``elephantblog``:: 84 | 85 | python manage.py migrate elephantblog 86 | 87 | Once you will be finished with the integration (see below) perform the migration of your ``blog`` app by running again:: 88 | 89 | python manage.py makemigrations 90 | 91 | and:: 92 | 93 | python manage.py migrate 94 | 95 | Integrating Standalone: 96 | ----------------------- 97 | 98 | Add the following lines to your urls.py:: 99 | 100 | from django.urls import include, url 101 | 102 | # Elephantblog urls 103 | urlpatterns += [ 104 | url(r'^blog/', include('elephantblog.urls')), 105 | ] 106 | 107 | If you're using the ``translations`` extension, and don't want to have your 108 | entries filtered by language use this snippet instead:: 109 | 110 | from django.urls import include, url 111 | from elephantblog.urls import elephantblog_patterns 112 | 113 | urlpatterns += [ 114 | url( 115 | r'^blog/', 116 | include(elephantblog_patterns( 117 | list_kwargs={'only_active_language': False }, 118 | )), 119 | ), 120 | ] 121 | 122 | 123 | FeinCMS Integration as ApplicationContent 124 | ----------------------------------------- 125 | 126 | You can easily add the blog to your FeinCMS Page based app. 127 | 128 | Just import and add the ApplicationContent to your Page object:: 129 | 130 | from feincms.content.application.models import ApplicationContent 131 | 132 | # init your Page object here 133 | 134 | Page.create_content_type(ApplicationContent, APPLICATIONS=( 135 | ('elephantblog.urls', 'Blog'), 136 | )) 137 | 138 | Use Django's ``ABSOLUTE_URL_OVERRIDES`` mechanism to override the 139 | ``get_absolute_url`` method of blog entries and categories. Add the 140 | following methods and settings to your ``settings.py`` file:: 141 | 142 | def elephantblog_entry_url_app(self): 143 | from feincms.content.application.models import app_reverse 144 | return app_reverse('elephantblog_entry_detail', 'elephantblog.urls', kwargs={ 145 | 'year': self.published_on.strftime('%Y'), 146 | 'month': self.published_on.strftime('%m'), 147 | 'day': self.published_on.strftime('%d'), 148 | 'slug': self.slug, 149 | }) 150 | 151 | def elephantblog_categorytranslation_url_app(self): 152 | from feincms.content.application.models import app_reverse 153 | return app_reverse('elephantblog_category_detail', 'elephantblog.urls', kwargs={ 154 | 'slug': self.slug, 155 | }) 156 | 157 | ABSOLUTE_URL_OVERRIDES = { 158 | 'elephantblog.entry': elephantblog_entry_url_app, 159 | 'elephantblog.categorytranslation': elephantblog_categorytranslation_url_app, 160 | } 161 | 162 | 163 | Elephantblog also provides a navigation extension for FeinCMS. 164 | Just make sure you have registered the ``navigation`` extension on your Page object. 165 | You have to import the correct module depending on the mptt tags you are using 166 | to build your navigation. Available are ``treeinfo`` and ``recursetree``. 167 | 168 | Add those lines to the ``models.py`` of your app:: 169 | 170 | from elephantblog.navigation_extensions import treeinfo # so the extensions can be found. 171 | 172 | Page.register_extensions('feincms.module.page.extensions.navigation',) 173 | 174 | 175 | Settings 176 | -------- 177 | 178 | You can set the number of entries per page with the following setting:: 179 | 180 | BLOG_PAGINATE_BY = 10 181 | -------------------------------------------------------------------------------- /elephantblog/locale/cs/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR vencax77@gmail.com, 2012. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PACKAGE VERSION\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2013-11-14 15:35+0100\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language-Team: LANGUAGE \n" 14 | "Language: \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2\n" 19 | 20 | #: admin.py:25 21 | msgid "Blog entries in category" 22 | msgstr "Články v kategorii" 23 | 24 | #: contents.py:21 models.py:33 25 | msgid "category" 26 | msgstr "kategorie" 27 | 28 | #: contents.py:21 29 | msgid "Only show entries from this category." 30 | msgstr "Zobrazit pouze články v této kategorii" 31 | 32 | #: contents.py:22 33 | #, fuzzy 34 | msgid "entries per page" 35 | msgstr "Články" 36 | 37 | #: contents.py:23 38 | msgid "Set to 0 to disable pagination." 39 | msgstr "Zadej 0 pro vypnutí stránkování" 40 | 41 | #: contents.py:24 42 | #, fuzzy 43 | msgid "featured only" 44 | msgstr "Je zvýrazněný" 45 | 46 | #: contents.py:25 47 | msgid "Only show articles marked as featured" 48 | msgstr "" 49 | 50 | #: contents.py:31 51 | msgid "Blog entry list" 52 | msgstr "Seznam článků" 53 | 54 | #: contents.py:32 55 | msgid "Blog entry lists" 56 | msgstr "Seznamy článků" 57 | 58 | #: contents.py:67 59 | #, fuzzy 60 | msgid "show empty categories?" 61 | msgstr "Kategorie článku" 62 | 63 | #: contents.py:71 64 | #, fuzzy 65 | msgid "Blog category list" 66 | msgstr "Seznam článků" 67 | 68 | #: contents.py:72 69 | #, fuzzy 70 | msgid "Blog category lists" 71 | msgstr "Seznamy článků" 72 | 73 | #: models.py:30 74 | msgid "ordering" 75 | msgstr "Pořadí" 76 | 77 | #: models.py:34 models.py:99 78 | msgid "categories" 79 | msgstr "Kategorie" 80 | 81 | #: models.py:41 82 | msgid "Unnamed category" 83 | msgstr "Bezejmenná kategorie" 84 | 85 | #: models.py:45 86 | msgid "category title" 87 | msgstr "Název kategorie" 88 | 89 | #: models.py:46 models.py:92 90 | msgid "slug" 91 | msgstr "slug" 92 | 93 | #: models.py:47 94 | msgid "description" 95 | msgstr "Popis" 96 | 97 | #: models.py:50 98 | msgid "category translation" 99 | msgstr "Překlad kategorie" 100 | 101 | #: models.py:51 102 | msgid "category translations" 103 | msgstr "Překlady kategorie" 104 | 105 | #: models.py:88 106 | msgid "is active" 107 | msgstr "Je aktivní" 108 | 109 | #: models.py:89 110 | msgid "is featured" 111 | msgstr "Je zvýrazněný" 112 | 113 | #: models.py:91 114 | msgid "title" 115 | msgstr "Název" 116 | 117 | #: models.py:94 118 | msgid "author" 119 | msgstr "Autor" 120 | 121 | #: models.py:95 122 | msgid "published on" 123 | msgstr "publikován dne" 124 | 125 | #: models.py:96 126 | msgid "Will be filled in automatically when entry gets published." 127 | msgstr "Pude automaticky vyplněno jakmile bude článek publikován" 128 | 129 | #: models.py:97 130 | msgid "last change" 131 | msgstr "poslední změna" 132 | 133 | #: models.py:105 134 | msgid "entry" 135 | msgstr "Článek" 136 | 137 | #: models.py:106 138 | msgid "entries" 139 | msgstr "Články" 140 | 141 | #: models.py:150 142 | #, python-format 143 | msgid "One entry was successfully marked as %(state)s" 144 | msgid_plural "%(count)s entries were successfully marked as %(state)s" 145 | msgstr[0] "Článek úspěšně označen jako %(state)s" 146 | msgstr[1] "%(count)s článků úspěšně označeno jako %(state)s" 147 | 148 | #: views.py:175 149 | #, python-format 150 | msgid "" 151 | "Future %(verbose_name_plural)s not available because %(class_name)s." 152 | "allow_future is False." 153 | msgstr "" 154 | 155 | #: extensions/blogping.py:20 156 | msgid "sleeping" 157 | msgstr "Čekající" 158 | 159 | #: extensions/blogping.py:21 extensions/blogping.py:33 160 | msgid "queued" 161 | msgstr "Zařazené" 162 | 163 | #: extensions/blogping.py:22 164 | msgid "sent" 165 | msgstr "Poslané" 166 | 167 | #: extensions/blogping.py:23 168 | msgid "unknown" 169 | msgstr "Neznámé" 170 | 171 | #: extensions/blogping.py:26 172 | msgid "ping" 173 | msgstr "Pingnout" 174 | 175 | #: extensions/blogping.py:34 176 | #, fuzzy 177 | msgid "Ping Again" 178 | msgstr "pingnout znovu" 179 | 180 | #: extensions/sites.py:11 181 | msgid "The sites where the blogpost should appear." 182 | msgstr "weby, na kterých má článek být" 183 | 184 | #: extensions/sites.py:23 185 | msgid "Sites" 186 | msgstr "Sajty" 187 | 188 | #: extensions/tags.py:8 189 | msgid "A comma-separated list of tags." 190 | msgstr "Čárkami oddělený seznam tagů" 191 | 192 | #: navigation_extensions/common.py:38 193 | msgid "blog categories" 194 | msgstr "Kategorie článku" 195 | 196 | #: navigation_extensions/recursetree.py:11 197 | #: navigation_extensions/treeinfo.py:16 198 | msgid "Blog date" 199 | msgstr "" 200 | 201 | #: navigation_extensions/recursetree.py:45 202 | #: navigation_extensions/treeinfo.py:42 203 | #, fuzzy 204 | msgid "Blog category and date" 205 | msgstr "Kategorie článku" 206 | 207 | #: navigation_extensions/recursetree.py:65 208 | #: navigation_extensions/treeinfo.py:47 209 | #: templates/content/elephantblog/category_list.html:6 210 | #, fuzzy 211 | msgid "Categories" 212 | msgstr "Kategorie" 213 | 214 | #: navigation_extensions/recursetree.py:102 215 | #: navigation_extensions/treeinfo.py:66 216 | msgid "Archive" 217 | msgstr "" 218 | 219 | #: templates/admin/feincms/elephantblog/entry/item_editor.html:13 220 | msgid "Preview" 221 | msgstr "Náhled" 222 | 223 | #: templates/content/elephantblog/entry_list.html:13 224 | #: templates/elephantblog/entry_archive.html:28 225 | #: templates/elephantblog/entry_detail.html:18 226 | msgid "by" 227 | msgstr "" 228 | 229 | #: templates/elephantblog/entry_archive.html:5 230 | #: templates/elephantblog/entry_detail.html:5 231 | msgid "News" 232 | msgstr "Články" 233 | 234 | #: templates/elephantblog/entry_archive.html:9 235 | #: templates/elephantblog/entry_archive.html:10 236 | #: templates/elephantblog/entry_archive.html:11 237 | #: templates/elephantblog/entry_archive.html:12 238 | msgid "for" 239 | msgstr "" 240 | 241 | #~ msgid "paginate by" 242 | #~ msgstr "stránkováno od" 243 | -------------------------------------------------------------------------------- /elephantblog/locale/ru/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # This file is distributed under the same license as the feincms-elephantblog package. 2 | # Translators: 3 | # Alexander Paramonov , 2016. 4 | msgid "" 5 | msgstr "" 6 | "Project-Id-Version: v1.0.2\n" 7 | "Report-Msgid-Bugs-To: \n" 8 | "POT-Creation-Date: 2016-09-05 16:42+0300\n" 9 | "PO-Revision-Date: 2016-09-05 15:48+0300\n" 10 | "Last-Translator: Alexander Paramonov \n" 11 | "Language: ru\n" 12 | "MIME-Version: 1.0\n" 13 | "Content-Type: text/plain; charset=UTF-8\n" 14 | "Content-Transfer-Encoding: 8bit\n" 15 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 16 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" 17 | 18 | #: contents.py:22 models.py:31 19 | msgid "category" 20 | msgstr "Категория" 21 | 22 | #: contents.py:23 23 | msgid "Only show entries from this category." 24 | msgstr "Показывать записи только из этой категории" 25 | 26 | #: contents.py:25 27 | msgid "entries per page" 28 | msgstr "Кол-во записей на страницу" 29 | 30 | #: contents.py:26 31 | msgid "Set to 0 to disable pagination." 32 | msgstr "Введите 0 чтобы отключить нумерацию страниц" 33 | 34 | #: contents.py:28 35 | msgid "featured only" 36 | msgstr "только избранные" 37 | 38 | #: contents.py:29 39 | msgid "Only show articles marked as featured" 40 | msgstr "Показывать только избранные записи" 41 | 42 | #: contents.py:35 43 | msgid "Blog entry list" 44 | msgstr "Список блогозаписей" 45 | 46 | #: contents.py:36 47 | msgid "Blog entry lists" 48 | msgstr "Списки блогозаписей" 49 | 50 | #: contents.py:74 51 | msgid "show empty categories?" 52 | msgstr "показывать пустые категории?" 53 | 54 | #: contents.py:78 55 | msgid "Blog category list" 56 | msgstr "Список категорий блога" 57 | 58 | #: contents.py:79 59 | msgid "Blog category lists" 60 | msgstr "Списки категорий блога" 61 | 62 | #: extensions/blogping.py:26 63 | #, python-format 64 | msgid "One entry was successfully marked as %(state)s" 65 | msgid_plural "%(count)s entries were successfully marked as %(state)s" 66 | msgstr[0] "" 67 | msgstr[1] "" 68 | 69 | #: extensions/blogping.py:41 70 | msgid "sleeping" 71 | msgstr "ожидание" 72 | 73 | #: extensions/blogping.py:42 extensions/blogping.py:62 74 | msgid "queued" 75 | msgstr "в очереди" 76 | 77 | #: extensions/blogping.py:43 78 | msgid "sent" 79 | msgstr "отправлено" 80 | 81 | #: extensions/blogping.py:44 82 | msgid "unknown" 83 | msgstr "неизвестно" 84 | 85 | #: extensions/blogping.py:50 86 | msgid "ping" 87 | msgstr "пинг" 88 | 89 | #: extensions/blogping.py:64 90 | msgid "Ping Again" 91 | msgstr "Пропинговать ещё раз" 92 | 93 | #: extensions/sites.py:15 94 | msgid "The sites where the blogpost should appear." 95 | msgstr "Сайты на которых блогозапись должна появиться" 96 | 97 | #: extensions/sites.py:27 98 | msgid "Sites" 99 | msgstr "Сайты" 100 | 101 | #: extensions/tags.py:13 102 | msgid "A comma-separated list of tags." 103 | msgstr "Список тегов, разделённый запятыми" 104 | 105 | #: modeladmins.py:31 106 | msgid "Blog entries in category" 107 | msgstr "Блогозаписи в категории" 108 | 109 | #: modeladmins.py:57 110 | msgid "Other options" 111 | msgstr "Другие опции" 112 | 113 | #: models.py:26 114 | msgid "ordering" 115 | msgstr "очерёдность" 116 | 117 | #: models.py:32 models.py:112 118 | msgid "categories" 119 | msgstr "категории" 120 | 121 | #: models.py:39 models.py:44 122 | msgid "Unnamed category" 123 | msgstr "Безымянная категория" 124 | 125 | #: models.py:49 126 | msgid "category title" 127 | msgstr "название категории" 128 | 129 | #: models.py:50 models.py:97 130 | msgid "slug" 131 | msgstr "слаг" 132 | 133 | #: models.py:52 134 | msgid "description" 135 | msgstr "описание" 136 | 137 | #: models.py:55 138 | msgid "category translation" 139 | msgstr "перевод категории" 140 | 141 | #: models.py:56 142 | msgid "category translations" 143 | msgstr "переводы категории" 144 | 145 | #: models.py:91 146 | msgid "is active" 147 | msgstr "Активная?" 148 | 149 | #: models.py:93 150 | msgid "is featured" 151 | msgstr "Избранная?" 152 | 153 | #: models.py:95 154 | msgid "title" 155 | msgstr "Заголовок" 156 | 157 | #: models.py:102 158 | msgid "author" 159 | msgstr "автор" 160 | 161 | #: models.py:104 162 | msgid "published on" 163 | msgstr "опубликовано" 164 | 165 | #: models.py:107 166 | msgid "Will be filled in automatically when entry gets published." 167 | msgstr "Заполнится автоматически, когда запись будет опубликована" 168 | 169 | #: models.py:109 170 | msgid "last change" 171 | msgstr "последнее изменение" 172 | 173 | #: models.py:120 174 | msgid "entry" 175 | msgstr "запись" 176 | 177 | #: models.py:121 178 | msgid "entries" 179 | msgstr "записи" 180 | 181 | #: navigation_extensions/common.py:46 182 | msgid "blog categories" 183 | msgstr "категории блога" 184 | 185 | #: navigation_extensions/recursetree.py:16 navigation_extensions/treeinfo.py:20 186 | msgid "Blog date" 187 | msgstr "Дата блога" 188 | 189 | #: navigation_extensions/recursetree.py:53 navigation_extensions/treeinfo.py:47 190 | msgid "Blog category and date" 191 | msgstr "Категория и дата блога" 192 | 193 | #: navigation_extensions/recursetree.py:74 navigation_extensions/treeinfo.py:52 194 | #: templates/content/elephantblog/category_list.html:6 195 | msgid "Categories" 196 | msgstr "Категории" 197 | 198 | #: navigation_extensions/recursetree.py:114 199 | #: navigation_extensions/treeinfo.py:74 200 | msgid "Archive" 201 | msgstr "Архив" 202 | 203 | #: templates/admin/feincms/elephantblog/entry/item_editor.html:13 204 | msgid "Preview" 205 | msgstr "Предпросмотр" 206 | 207 | # "by" is rarely translated in this context, thus I am leaving an empty string 208 | #: templates/content/elephantblog/entry_list.html:13 209 | #: templates/elephantblog/entry_archive.html:28 210 | #: templates/elephantblog/entry_detail.html:18 211 | msgid "by" 212 | msgstr "" 213 | 214 | #: templates/elephantblog/entry_archive.html:5 215 | #: templates/elephantblog/entry_archive.html:9 216 | #: templates/elephantblog/entry_detail.html:5 217 | msgid "News" 218 | msgstr "Новости" 219 | 220 | # I am splitting "for" translations in two contexts ("date" and category") 221 | # in order to make Russian translation grammatically correct 222 | # examples for date context: 223 | # "Новости за 2016", "Новости за сентябрь 2016", etc 224 | #: templates/elephantblog/entry_archive.html:9 225 | #: templates/elephantblog/entry_archive.html:10 226 | #: templates/elephantblog/entry_archive.html:11 227 | msgctxt "date" 228 | msgid "for" 229 | msgstr "за" 230 | 231 | # example for category: Новости категории "{{ category }}" 232 | # it is recommended to wrap {{category}} variable in quotes, since this is 233 | # the right way for Russian language; yet I am leaving this to user discretion 234 | #: templates/elephantblog/entry_archive.html:12 235 | msgctxt "category" 236 | msgid "for" 237 | msgstr "категории" 238 | -------------------------------------------------------------------------------- /tests/testapp/migrate/medialibrary/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.2 on 2020-01-21 09:28 2 | 3 | import django.db.models.deletion 4 | import django.utils.timezone 5 | import feincms.extensions.base 6 | import feincms.translations 7 | from django.db import migrations, models 8 | 9 | 10 | class Migration(migrations.Migration): 11 | initial = True 12 | 13 | dependencies = [] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name="Category", 18 | fields=[ 19 | ( 20 | "id", 21 | models.AutoField( 22 | auto_created=True, 23 | primary_key=True, 24 | serialize=False, 25 | verbose_name="ID", 26 | ), 27 | ), 28 | ("title", models.CharField(max_length=200, verbose_name="title")), 29 | ("slug", models.SlugField(max_length=150, verbose_name="slug")), 30 | ( 31 | "parent", 32 | models.ForeignKey( 33 | blank=True, 34 | limit_choices_to={"parent__isnull": True}, 35 | null=True, 36 | on_delete=django.db.models.deletion.CASCADE, 37 | related_name="children", 38 | to="medialibrary.Category", 39 | verbose_name="parent", 40 | ), 41 | ), 42 | ], 43 | options={ 44 | "verbose_name": "category", 45 | "verbose_name_plural": "categories", 46 | "ordering": ["parent__title", "title"], 47 | }, 48 | ), 49 | migrations.CreateModel( 50 | name="MediaFile", 51 | fields=[ 52 | ( 53 | "id", 54 | models.AutoField( 55 | auto_created=True, 56 | primary_key=True, 57 | serialize=False, 58 | verbose_name="ID", 59 | ), 60 | ), 61 | ( 62 | "file", 63 | models.FileField( 64 | max_length=255, 65 | upload_to="medialibrary/%Y/%m/", 66 | verbose_name="file", 67 | ), 68 | ), 69 | ( 70 | "type", 71 | models.CharField( 72 | choices=[ 73 | ("image", "Image"), 74 | ("video", "Video"), 75 | ("audio", "Audio"), 76 | ("pdf", "PDF document"), 77 | ("swf", "Flash"), 78 | ("txt", "Text"), 79 | ("rtf", "Rich Text"), 80 | ("zip", "Zip archive"), 81 | ("doc", "Microsoft Word"), 82 | ("xls", "Microsoft Excel"), 83 | ("ppt", "Microsoft PowerPoint"), 84 | ("other", "Binary"), 85 | ], 86 | editable=False, 87 | max_length=12, 88 | verbose_name="file type", 89 | ), 90 | ), 91 | ( 92 | "created", 93 | models.DateTimeField( 94 | default=django.utils.timezone.now, 95 | editable=False, 96 | verbose_name="created", 97 | ), 98 | ), 99 | ( 100 | "copyright", 101 | models.CharField( 102 | blank=True, max_length=200, verbose_name="copyright" 103 | ), 104 | ), 105 | ( 106 | "file_size", 107 | models.IntegerField( 108 | blank=True, editable=False, null=True, verbose_name="file size" 109 | ), 110 | ), 111 | ( 112 | "categories", 113 | models.ManyToManyField( 114 | blank=True, 115 | to="medialibrary.Category", 116 | verbose_name="categories", 117 | ), 118 | ), 119 | ], 120 | bases=( 121 | models.Model, 122 | feincms.extensions.base.ExtensionsMixin, 123 | feincms.translations.TranslatedObjectMixin, 124 | ), 125 | ), 126 | migrations.CreateModel( 127 | name="MediaFileTranslation", 128 | fields=[ 129 | ( 130 | "id", 131 | models.AutoField( 132 | auto_created=True, 133 | primary_key=True, 134 | serialize=False, 135 | verbose_name="ID", 136 | ), 137 | ), 138 | ( 139 | "language_code", 140 | models.CharField( 141 | choices=[ 142 | ("en", "English"), 143 | ("de", "German"), 144 | ("zh-hans", "Chinese simplified"), 145 | ("zh-hant", "Chinese traditional"), 146 | ], 147 | default="en", 148 | max_length=10, 149 | verbose_name="language", 150 | ), 151 | ), 152 | ("caption", models.CharField(max_length=1000, verbose_name="caption")), 153 | ( 154 | "description", 155 | models.TextField(blank=True, verbose_name="description"), 156 | ), 157 | ( 158 | "parent", 159 | models.ForeignKey( 160 | on_delete=django.db.models.deletion.CASCADE, 161 | related_name="translations", 162 | to="medialibrary.MediaFile", 163 | ), 164 | ), 165 | ], 166 | options={ 167 | "verbose_name": "media file translation", 168 | "verbose_name_plural": "media file translations", 169 | "unique_together": {("parent", "language_code")}, 170 | }, 171 | ), 172 | ] 173 | -------------------------------------------------------------------------------- /elephantblog/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.auth import get_user_model 3 | from django.core import paginator 4 | from django.core.exceptions import FieldDoesNotExist 5 | from django.http import Http404 6 | from django.shortcuts import get_object_or_404 7 | from django.utils.translation import get_language 8 | from django.views.generic import dates 9 | from feincms.module.mixins import ContentObjectMixin 10 | 11 | from elephantblog.models import Category, Entry 12 | from elephantblog.utils import entry_list_lookup_related 13 | 14 | 15 | __all__ = ( 16 | "ArchiveIndexView", 17 | "YearArchiveView", 18 | "MonthArchiveView", 19 | "DayArchiveView", 20 | "DateDetailView", 21 | "CategoryArchiveIndexView", 22 | ) 23 | 24 | 25 | PAGINATE_BY = getattr(settings, "BLOG_PAGINATE_BY", 10) 26 | 27 | 28 | class ElephantblogMixin: 29 | """ 30 | This mixin autodetects whether the blog is integrated through an 31 | ApplicationContent and automatically switches to inheritance2.0 32 | if that's the case. 33 | 34 | Additionally, it adds the view instance to the template context 35 | as ``view``. 36 | 37 | This requires at least FeinCMS v1.5. 38 | """ 39 | 40 | entry_class = Entry 41 | 42 | def get_context_data(self, **kwargs): 43 | kwargs.update({"view": self}) 44 | return super().get_context_data(**kwargs) 45 | 46 | def get_queryset(self): 47 | return self.entry_class.objects.active().transform(entry_list_lookup_related) 48 | 49 | def render_to_response(self, context, **response_kwargs): 50 | if "app_config" in getattr(self.request, "_feincms_extra_context", {}): 51 | return self.get_template_names(), context 52 | 53 | return super().render_to_response(context, **response_kwargs) 54 | 55 | 56 | class TranslationMixin: 57 | """ 58 | #: Determines, whether list views should only display entries from 59 | #: the active language at a time. Requires the translations extension. 60 | """ 61 | 62 | only_active_language = True 63 | 64 | def get_queryset(self): 65 | queryset = super().get_queryset() 66 | try: 67 | queryset.model._meta.get_field("language") 68 | except FieldDoesNotExist: 69 | return queryset 70 | else: 71 | if self.only_active_language: 72 | return queryset.filter(language=get_language()) 73 | else: 74 | return queryset 75 | 76 | 77 | class ArchiveIndexView(TranslationMixin, ElephantblogMixin, dates.ArchiveIndexView): 78 | paginator_class = paginator.Paginator 79 | paginate_by = PAGINATE_BY 80 | date_field = "published_on" 81 | template_name_suffix = "_archive" 82 | allow_empty = True 83 | 84 | 85 | class YearArchiveView(TranslationMixin, ElephantblogMixin, dates.YearArchiveView): 86 | paginator_class = paginator.Paginator 87 | paginate_by = PAGINATE_BY 88 | date_field = "published_on" 89 | make_object_list = True 90 | template_name_suffix = "_archive" 91 | 92 | 93 | class MonthArchiveView(TranslationMixin, ElephantblogMixin, dates.MonthArchiveView): 94 | paginator_class = paginator.Paginator 95 | paginate_by = PAGINATE_BY 96 | month_format = "%m" 97 | date_field = "published_on" 98 | template_name_suffix = "_archive" 99 | 100 | 101 | class DayArchiveView(TranslationMixin, ElephantblogMixin, dates.DayArchiveView): 102 | paginator_class = paginator.Paginator 103 | paginate_by = PAGINATE_BY 104 | month_format = "%m" 105 | date_field = "published_on" 106 | template_name_suffix = "_archive" 107 | 108 | 109 | class DateDetailView( 110 | TranslationMixin, ContentObjectMixin, ElephantblogMixin, dates.DateDetailView 111 | ): 112 | paginator_class = paginator.Paginator 113 | paginate_by = PAGINATE_BY 114 | month_format = "%m" 115 | date_field = "published_on" 116 | context_object_name = "entry" 117 | 118 | def get_queryset(self): 119 | if self.request.user.is_staff and self.request.GET.get("eb_preview"): 120 | return self.entry_class.objects.all() 121 | return super().get_queryset() 122 | 123 | def dispatch(self, request, *args, **kwargs): 124 | if request.method.lower() not in self.http_method_names: 125 | return self.http_method_not_allowed(request, *args, **kwargs) 126 | self.request = request 127 | self.args = args 128 | self.kwargs = kwargs 129 | self.object = self.get_object() 130 | self.lookup_related() 131 | return self.handler(request, *args, **kwargs) 132 | 133 | def lookup_related(self): 134 | """ 135 | This method mirrors ``entry_list_lookup_related``, and assigns 136 | ``first_richtext`` and ``first_image`` to the elephantblog entry 137 | if suitable contents can be found. 138 | """ 139 | from feincms.contents import RichTextContent 140 | from feincms.module.medialibrary.contents import MediaFileContent 141 | 142 | try: 143 | self.object.first_image = [ 144 | mediafile 145 | for mediafile in self.object.content.all_of_type(MediaFileContent) 146 | if mediafile.mediafile.type == "image" 147 | ][0] 148 | except IndexError: 149 | pass 150 | 151 | try: 152 | self.object.first_richtext = self.object.content.all_of_type( 153 | RichTextContent 154 | )[0] 155 | except IndexError: 156 | pass 157 | 158 | def get_next_or_none(self): 159 | try: 160 | return ( 161 | self.get_queryset() 162 | .filter( 163 | published_on__gte=self.object.published_on, 164 | ) 165 | .exclude(id=self.object.id) 166 | .order_by("published_on")[0] 167 | ) 168 | except IndexError: 169 | return None 170 | 171 | def get_previous_or_none(self): 172 | try: 173 | return ( 174 | self.get_queryset() 175 | .filter( 176 | published_on__lte=self.object.published_on, 177 | ) 178 | .exclude(id=self.object.id) 179 | .order_by("-published_on")[0] 180 | ) 181 | except IndexError: 182 | return None 183 | 184 | 185 | class CategoryArchiveIndexView(ArchiveIndexView): 186 | template_name_suffix = "_archive" 187 | 188 | def get_queryset(self): 189 | slug = self.kwargs["slug"] 190 | 191 | try: 192 | self.category = Category.objects.get( 193 | translations__slug=slug, 194 | ) 195 | except Category.DoesNotExist: 196 | raise Http404("Category with slug %s does not exist" % slug) 197 | 198 | except Category.MultipleObjectsReturned: 199 | self.category = get_object_or_404( 200 | Category, 201 | translations__slug=slug, 202 | translations__language_code=get_language(), 203 | ) 204 | 205 | queryset = super().get_queryset() 206 | return queryset.filter(categories=self.category) 207 | 208 | def get_context_data(self, **kwargs): 209 | return super().get_context_data(category=self.category, **kwargs) 210 | 211 | 212 | class AuthorArchiveIndexView(ArchiveIndexView): 213 | template_name_suffix = "_archive" 214 | 215 | def get_queryset(self): 216 | self.author = get_object_or_404( 217 | get_user_model(), 218 | is_staff=True, 219 | pk=self.kwargs["pk"], 220 | ) 221 | return ( 222 | super() 223 | .get_queryset() 224 | .filter( 225 | author=self.author, 226 | ) 227 | ) 228 | 229 | def get_context_data(self, **kwargs): 230 | return super().get_context_data(author=self.author, **kwargs) 231 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # 2 | # Elephantblog documentation build configuration file, created by 3 | # sphinx-quickstart on Sun Feb 13 16:41:49 2011. 4 | # 5 | # This file is execfile()d with the current directory set to its containing dir. 6 | # 7 | # Note that not all possible configuration values are present in this 8 | # autogenerated file. 9 | # 10 | # All configuration values have a default; values that are commented out 11 | # serve to show the default. 12 | 13 | # If extensions (or modules to document with autodoc) are in another directory, 14 | # add these directories to sys.path here. If the directory is relative to the 15 | # documentation root, use os.path.abspath to make it absolute, like shown here. 16 | # sys.path.insert(0, os.path.abspath('.')) 17 | 18 | # -- General configuration ----------------------------------------------------- 19 | 20 | # If your documentation needs a minimal Sphinx version, state it here. 21 | # needs_sphinx = '1.0' 22 | 23 | # Add any Sphinx extension module names here, as strings. They can be extensions 24 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 25 | extensions = ["sphinx.ext.autodoc", "sphinx.ext.doctest"] 26 | 27 | # Add any paths that contain templates here, relative to this directory. 28 | templates_path = ["_templates"] 29 | 30 | # The suffix of source filenames. 31 | source_suffix = ".rst" 32 | 33 | # The encoding of source files. 34 | # source_encoding = 'utf-8-sig' 35 | 36 | # The master toctree document. 37 | master_doc = "index" 38 | 39 | # General information about the project. 40 | project = "Elephantblog" 41 | copyright = "2011, Simon Bächler" 42 | 43 | # The version info for the project you're documenting, acts as replacement for 44 | # |version| and |release|, also used in various other places throughout the 45 | # built documents. 46 | # 47 | # The short X.Y version. 48 | version = "1.0" 49 | # The full version, including alpha/beta/rc tags. 50 | release = "1.0" 51 | 52 | # The language for content autogenerated by Sphinx. Refer to documentation 53 | # for a list of supported languages. 54 | # language = None 55 | 56 | # There are two options for replacing |today|: either, you set today to some 57 | # non-false value, then it is used: 58 | # today = '' 59 | # Else, today_fmt is used as the format for a strftime call. 60 | # today_fmt = '%B %d, %Y' 61 | 62 | # List of patterns, relative to source directory, that match files and 63 | # directories to ignore when looking for source files. 64 | exclude_patterns = ["_build"] 65 | 66 | # The reST default role (used for this markup: `text`) to use for all documents. 67 | # default_role = None 68 | 69 | # If true, '()' will be appended to :func: etc. cross-reference text. 70 | # add_function_parentheses = True 71 | 72 | # If true, the current module name will be prepended to all description 73 | # unit titles (such as .. function::). 74 | # add_module_names = True 75 | 76 | # If true, sectionauthor and moduleauthor directives will be shown in the 77 | # output. They are ignored by default. 78 | # show_authors = False 79 | 80 | # The name of the Pygments (syntax highlighting) style to use. 81 | pygments_style = "sphinx" 82 | 83 | # A list of ignored prefixes for module index sorting. 84 | # modindex_common_prefix = [] 85 | 86 | 87 | # -- Options for HTML output --------------------------------------------------- 88 | 89 | # The theme to use for HTML and HTML Help pages. See the documentation for 90 | # a list of builtin themes. 91 | html_theme = "default" 92 | 93 | # Theme options are theme-specific and customize the look and feel of a theme 94 | # further. For a list of options available for each theme, see the 95 | # documentation. 96 | # html_theme_options = {} 97 | 98 | # Add any paths that contain custom themes here, relative to this directory. 99 | # html_theme_path = [] 100 | 101 | # The name for this set of Sphinx documents. If None, it defaults to 102 | # " v documentation". 103 | # html_title = None 104 | 105 | # A shorter title for the navigation bar. Default is the same as html_title. 106 | # html_short_title = None 107 | 108 | # The name of an image file (relative to this directory) to place at the top 109 | # of the sidebar. 110 | # html_logo = None 111 | 112 | # The name of an image file (within the static path) to use as favicon of the 113 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 114 | # pixels large. 115 | # html_favicon = None 116 | 117 | # Add any paths that contain custom static files (such as style sheets) here, 118 | # relative to this directory. They are copied after the builtin static files, 119 | # so a file named "default.css" will overwrite the builtin "default.css". 120 | html_static_path = ["_static"] 121 | 122 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 123 | # using the given strftime format. 124 | # html_last_updated_fmt = '%b %d, %Y' 125 | 126 | # If true, SmartyPants will be used to convert quotes and dashes to 127 | # typographically correct entities. 128 | # html_use_smartypants = True 129 | 130 | # Custom sidebar templates, maps document names to template names. 131 | # html_sidebars = {} 132 | 133 | # Additional templates that should be rendered to pages, maps page names to 134 | # template names. 135 | # html_additional_pages = {} 136 | 137 | # If false, no module index is generated. 138 | # html_domain_indices = True 139 | 140 | # If false, no index is generated. 141 | # html_use_index = True 142 | 143 | # If true, the index is split into individual pages for each letter. 144 | # html_split_index = False 145 | 146 | # If true, links to the reST sources are added to the pages. 147 | # html_show_sourcelink = True 148 | 149 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 150 | # html_show_sphinx = True 151 | 152 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 153 | # html_show_copyright = True 154 | 155 | # If true, an OpenSearch description file will be output, and all pages will 156 | # contain a tag referring to it. The value of this option must be the 157 | # base URL from which the finished HTML is served. 158 | # html_use_opensearch = '' 159 | 160 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 161 | # html_file_suffix = None 162 | 163 | # Output file base name for HTML help builder. 164 | htmlhelp_basename = "Elephantblogdoc" 165 | 166 | 167 | # -- Options for LaTeX output -------------------------------------------------- 168 | 169 | # The paper size ('letter' or 'a4'). 170 | # latex_paper_size = 'letter' 171 | 172 | # The font size ('10pt', '11pt' or '12pt'). 173 | # latex_font_size = '10pt' 174 | 175 | # Grouping the document tree into LaTeX files. List of tuples 176 | # (source start file, target name, title, author, documentclass [howto/manual]). 177 | latex_documents = [ 178 | ( 179 | "index", 180 | "Elephantblog.tex", 181 | "Elephantblog Documentation", 182 | "Simon Bächler", 183 | "manual", 184 | ), 185 | ] 186 | 187 | # The name of an image file (relative to this directory) to place at the top of 188 | # the title page. 189 | # latex_logo = None 190 | 191 | # For "manual" documents, if this is true, then toplevel headings are parts, 192 | # not chapters. 193 | # latex_use_parts = False 194 | 195 | # If true, show page references after internal links. 196 | # latex_show_pagerefs = False 197 | 198 | # If true, show URL addresses after external links. 199 | # latex_show_urls = False 200 | 201 | # Additional stuff for the LaTeX preamble. 202 | # latex_preamble = '' 203 | 204 | # Documents to append as an appendix to all manuals. 205 | # latex_appendices = [] 206 | 207 | # If false, no module index is generated. 208 | # latex_domain_indices = True 209 | 210 | 211 | # -- Options for manual page output -------------------------------------------- 212 | 213 | # One entry per manual page. List of tuples 214 | # (source start file, name, description, authors, manual section). 215 | man_pages = [ 216 | ("index", "elephantblog", "Elephantblog Documentation", ["Simon Bächler"], 1) 217 | ] 218 | -------------------------------------------------------------------------------- /elephantblog/locale/de/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2013-11-14 15:35+0100\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 20 | 21 | #: admin.py:25 22 | msgid "Blog entries in category" 23 | msgstr "Blogeinträge in Kategorie" 24 | 25 | #: contents.py:21 models.py:33 26 | msgid "category" 27 | msgstr "Kategorie" 28 | 29 | #: contents.py:21 30 | msgid "Only show entries from this category." 31 | msgstr "Nur Einträge aus dieser Kategorie anzeigen." 32 | 33 | #: contents.py:22 34 | msgid "entries per page" 35 | msgstr "Einträge pro Seite" 36 | 37 | #: contents.py:23 38 | msgid "Set to 0 to disable pagination." 39 | msgstr "Auf 0 setzen um alle anzuzeigen." 40 | 41 | #: contents.py:24 42 | msgid "featured only" 43 | msgstr "Nur Frontseiteneinträge" 44 | 45 | #: contents.py:25 46 | msgid "Only show articles marked as featured" 47 | msgstr "Zeigt nur Hauptartikel an." 48 | 49 | #: contents.py:31 50 | msgid "Blog entry list" 51 | msgstr "Blogeinträge" 52 | 53 | #: contents.py:32 54 | msgid "Blog entry lists" 55 | msgstr "Blogeinträge" 56 | 57 | #: contents.py:67 58 | msgid "show empty categories?" 59 | msgstr "Leere Kategorien anzeigen?" 60 | 61 | #: contents.py:71 62 | msgid "Blog category list" 63 | msgstr "Blog-Kategorien" 64 | 65 | #: contents.py:72 66 | msgid "Blog category lists" 67 | msgstr "Blog-Kategorien" 68 | 69 | #: models.py:30 70 | msgid "ordering" 71 | msgstr "Sortierung" 72 | 73 | #: models.py:34 models.py:99 74 | msgid "categories" 75 | msgstr "Kategorien" 76 | 77 | #: models.py:41 78 | msgid "Unnamed category" 79 | msgstr "Unbenannte Kategorie" 80 | 81 | #: models.py:45 82 | msgid "category title" 83 | msgstr "Kategorietitel" 84 | 85 | #: models.py:46 models.py:92 86 | msgid "slug" 87 | msgstr "Slug" 88 | 89 | #: models.py:47 90 | msgid "description" 91 | msgstr "Beschreibung" 92 | 93 | #: models.py:50 94 | msgid "category translation" 95 | msgstr "Kategorie-Übersetzungen" 96 | 97 | #: models.py:51 98 | msgid "category translations" 99 | msgstr "Kategorie-Übersetzungen" 100 | 101 | #: models.py:88 102 | msgid "is active" 103 | msgstr "aktiv" 104 | 105 | #: models.py:89 106 | msgid "is featured" 107 | msgstr "auf Frontseite" 108 | 109 | #: models.py:91 110 | msgid "title" 111 | msgstr "Titel" 112 | 113 | #: models.py:94 114 | msgid "author" 115 | msgstr "Author" 116 | 117 | #: models.py:95 118 | msgid "published on" 119 | msgstr "Veröffentlicht am" 120 | 121 | #: models.py:96 122 | msgid "Will be filled in automatically when entry gets published." 123 | msgstr "Wird automatisch eingefüllt sobald dieser Eintrag veröffentlicht wird." 124 | 125 | #: models.py:97 126 | msgid "last change" 127 | msgstr "letzte Änderung" 128 | 129 | #: models.py:105 130 | msgid "entry" 131 | msgstr "Eintrag" 132 | 133 | #: models.py:106 134 | msgid "entries" 135 | msgstr "Einträge" 136 | 137 | #: models.py:150 138 | msgid "One entry was successfully marked as %(state)s" 139 | msgid_plural "%(count)s entries were successfully marked as %(state)s" 140 | msgstr[0] "Ein Eintrag wurde erfolgreich als %(state)s markiert" 141 | msgstr[1] "%(count)s Einträge wurden erfolgreich als %(state)s markiert" 142 | 143 | #: views.py:175 144 | #, python-format 145 | msgid "" 146 | "Future %(verbose_name_plural)s not available because %(class_name)s." 147 | "allow_future is False." 148 | msgstr "" 149 | 150 | #: extensions/blogping.py:20 151 | msgid "sleeping" 152 | msgstr "schlafend" 153 | 154 | #: extensions/blogping.py:21 extensions/blogping.py:33 155 | msgid "queued" 156 | msgstr "wartend" 157 | 158 | #: extensions/blogping.py:22 159 | msgid "sent" 160 | msgstr "gesendet" 161 | 162 | #: extensions/blogping.py:23 163 | msgid "unknown" 164 | msgstr "unbekannt" 165 | 166 | #: extensions/blogping.py:26 167 | msgid "ping" 168 | msgstr "Ping" 169 | 170 | #: extensions/blogping.py:34 171 | msgid "Ping Again" 172 | msgstr "nochmals pingen" 173 | 174 | #: extensions/sites.py:11 175 | msgid "The sites where the blogpost should appear." 176 | msgstr "Die Webseiten (Sites), auf denen der Eintrag erscheint." 177 | 178 | #: extensions/sites.py:23 179 | msgid "Sites" 180 | msgstr "Sites" 181 | 182 | #: extensions/tags.py:8 183 | msgid "A comma-separated list of tags." 184 | msgstr "Eine kommaseparierte Liste von Tags." 185 | 186 | #: navigation_extensions/common.py:38 187 | msgid "blog categories" 188 | msgstr "Blog-Kategorien" 189 | 190 | #: navigation_extensions/recursetree.py:11 191 | #: navigation_extensions/treeinfo.py:16 192 | msgid "Blog date" 193 | msgstr "Blog Datum" 194 | 195 | #: navigation_extensions/recursetree.py:45 196 | #: navigation_extensions/treeinfo.py:42 197 | msgid "Blog category and date" 198 | msgstr "Blog Kategorien und Datum" 199 | 200 | #: navigation_extensions/recursetree.py:65 201 | #: navigation_extensions/treeinfo.py:47 202 | #: templates/content/elephantblog/category_list.html:6 203 | msgid "Categories" 204 | msgstr "Kategorien" 205 | 206 | #: navigation_extensions/recursetree.py:102 207 | #: navigation_extensions/treeinfo.py:66 208 | msgid "Archive" 209 | msgstr "Archiv" 210 | 211 | #: templates/admin/feincms/elephantblog/entry/item_editor.html:13 212 | msgid "Preview" 213 | msgstr "Vorschau" 214 | 215 | #: templates/content/elephantblog/entry_list.html:13 216 | #: templates/elephantblog/entry_archive.html:28 217 | #: templates/elephantblog/entry_detail.html:18 218 | msgid "by" 219 | msgstr "von" 220 | 221 | #: templates/elephantblog/entry_archive.html:5 222 | #: templates/elephantblog/entry_detail.html:5 223 | msgid "News" 224 | msgstr "News" 225 | 226 | #: templates/elephantblog/entry_archive.html:9 227 | #: templates/elephantblog/entry_archive.html:10 228 | #: templates/elephantblog/entry_archive.html:11 229 | #: templates/elephantblog/entry_archive.html:12 230 | msgid "for" 231 | msgstr "für" 232 | 233 | #~ msgid "paginate by" 234 | #~ msgstr "Einträge pro Seite" 235 | 236 | #~ msgid "cleared" 237 | #~ msgstr "freigegeben" 238 | 239 | #~ msgid "front page" 240 | #~ msgstr "Titelseite" 241 | 242 | #~ msgid "needs re-editing" 243 | #~ msgstr "muss überarbeitet werden" 244 | 245 | #~ msgid "deleted" 246 | #~ msgstr "gelöscht" 247 | 248 | #~ msgid "published" 249 | #~ msgstr "veröffentlicht" 250 | 251 | #~ msgid "Shows the status of the entry for the pinging management command." 252 | #~ msgstr "Zeigt den Status des Eintrags für das Pinging Management Tool." 253 | 254 | #~ msgid " again." 255 | #~ msgstr " nochmals." 256 | 257 | #~ msgid "expired" 258 | #~ msgstr "verfallen" 259 | 260 | #~ msgid "on hold" 261 | #~ msgstr "auf Eis gelegt" 262 | 263 | #~ msgid "Status" 264 | #~ msgstr "Status" 265 | 266 | #~ msgid "mark publish" 267 | #~ msgstr "als veröffentlicht markieren" 268 | 269 | #~ msgid "front-page" 270 | #~ msgstr "Titelseite" 271 | 272 | #~ msgid "mark frontpage" 273 | #~ msgstr "als Titelseite markieren" 274 | 275 | #~ msgid "need re-editing" 276 | #~ msgstr "muss überarbeitet werden" 277 | 278 | #~ msgid "mark re-edit" 279 | #~ msgstr "zur Überarbeitung markieren" 280 | 281 | #~ msgid "mark inactive" 282 | #~ msgstr "als inaktiv markieren" 283 | 284 | #~ msgid "title undefined" 285 | #~ msgstr "Titel nicht definiert" 286 | 287 | #~ msgid "description undefined" 288 | #~ msgstr "Beschreibung nicht definiert" 289 | 290 | #~ msgid "Main content area" 291 | #~ msgstr "Hauptinhaltsbereich" 292 | 293 | #~ msgid "publication end date" 294 | #~ msgstr "veröffentlichen bis" 295 | 296 | #~ msgid "Leave empty if the entry should stay active forever." 297 | #~ msgstr "Leer lassen, wenn kein Enddatum." 298 | 299 | #~ msgid "visible from - to" 300 | #~ msgstr "Sichtbar von, bis" 301 | 302 | #~ msgid "language" 303 | #~ msgstr "Sprache" 304 | 305 | #~ msgid "translation of" 306 | #~ msgstr "Übersetzung von" 307 | 308 | #~ msgid "Leave this empty for entries in the primary language." 309 | #~ msgstr "Leer lassen für Einträge in der Hauptsprache" 310 | 311 | #~ msgid "available translations" 312 | #~ msgstr "Übersetzungen" 313 | 314 | #~ msgid "By" 315 | #~ msgstr "von" 316 | 317 | #~ msgid "Posted on" 318 | #~ msgstr "Veröffentlicht am" 319 | 320 | #~ msgid "at" 321 | #~ msgstr "um" 322 | 323 | #~ msgid "in" 324 | #~ msgstr "in" 325 | 326 | #~ msgid "more..." 327 | #~ msgstr "mehr..." 328 | 329 | #~ msgid "Page" 330 | #~ msgstr "Seite" 331 | 332 | #~ msgid "of" 333 | #~ msgstr "von" 334 | 335 | #~ msgid "previous" 336 | #~ msgstr "vorherige" 337 | 338 | #~ msgid "next" 339 | #~ msgstr "nächste" 340 | 341 | #~ msgid "No entries found." 342 | #~ msgstr "Keine Einträge gefunden." 343 | 344 | #~ msgid "tag" 345 | #~ msgstr "Tag" 346 | 347 | #~ msgid "Related Tags" 348 | #~ msgstr "Verwandte Tags" 349 | 350 | #~ msgid "News archive for " 351 | #~ msgstr "Newsarchiv für " 352 | 353 | #~ msgid "All" 354 | #~ msgstr "Alle" 355 | -------------------------------------------------------------------------------- /tests/testapp/migrate/elephantblog/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.2 on 2020-01-21 09:28 2 | 3 | import django.db.models.deletion 4 | import django.utils.timezone 5 | import feincms.contrib.richtext 6 | import feincms.extensions.base 7 | import feincms.module.medialibrary.fields 8 | import feincms.module.mixins 9 | import feincms.translations 10 | from django.conf import settings 11 | from django.db import migrations, models 12 | 13 | 14 | class Migration(migrations.Migration): 15 | initial = True 16 | 17 | dependencies = [ 18 | ("medialibrary", "0001_initial"), 19 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 20 | ] 21 | 22 | operations = [ 23 | migrations.CreateModel( 24 | name="Category", 25 | fields=[ 26 | ( 27 | "id", 28 | models.AutoField( 29 | auto_created=True, 30 | primary_key=True, 31 | serialize=False, 32 | verbose_name="ID", 33 | ), 34 | ), 35 | ( 36 | "ordering", 37 | models.SmallIntegerField(default=0, verbose_name="ordering"), 38 | ), 39 | ], 40 | options={ 41 | "verbose_name": "category", 42 | "verbose_name_plural": "categories", 43 | "ordering": ["ordering"], 44 | }, 45 | bases=(models.Model, feincms.translations.TranslatedObjectMixin), 46 | ), 47 | migrations.CreateModel( 48 | name="Entry", 49 | fields=[ 50 | ( 51 | "id", 52 | models.AutoField( 53 | auto_created=True, 54 | primary_key=True, 55 | serialize=False, 56 | verbose_name="ID", 57 | ), 58 | ), 59 | ( 60 | "is_active", 61 | models.BooleanField( 62 | db_index=True, default=True, verbose_name="is active" 63 | ), 64 | ), 65 | ( 66 | "is_featured", 67 | models.BooleanField( 68 | db_index=True, default=False, verbose_name="is featured" 69 | ), 70 | ), 71 | ("title", models.CharField(max_length=100, verbose_name="title")), 72 | ( 73 | "slug", 74 | models.SlugField( 75 | max_length=100, 76 | unique_for_date="published_on", 77 | verbose_name="slug", 78 | ), 79 | ), 80 | ( 81 | "published_on", 82 | models.DateTimeField( 83 | blank=True, 84 | db_index=True, 85 | default=django.utils.timezone.now, 86 | help_text="Will be filled in automatically when entry gets published.", 87 | null=True, 88 | verbose_name="published on", 89 | ), 90 | ), 91 | ( 92 | "last_changed", 93 | models.DateTimeField(auto_now=True, verbose_name="last change"), 94 | ), 95 | ( 96 | "language", 97 | models.CharField( 98 | choices=[ 99 | ("en", "English"), 100 | ("de", "German"), 101 | ("zh-hans", "Chinese simplified"), 102 | ("zh-hant", "Chinese traditional"), 103 | ], 104 | default="en", 105 | max_length=10, 106 | verbose_name="language", 107 | ), 108 | ), 109 | ( 110 | "author", 111 | models.ForeignKey( 112 | limit_choices_to={"is_staff": True}, 113 | on_delete=django.db.models.deletion.CASCADE, 114 | related_name="blogentries", 115 | to=settings.AUTH_USER_MODEL, 116 | verbose_name="author", 117 | ), 118 | ), 119 | ( 120 | "categories", 121 | models.ManyToManyField( 122 | blank=True, 123 | related_name="blogentries", 124 | to="elephantblog.Category", 125 | verbose_name="categories", 126 | ), 127 | ), 128 | ( 129 | "translation_of", 130 | models.ForeignKey( 131 | blank=True, 132 | help_text="Leave this empty for entries in the primary language.", 133 | limit_choices_to={"language": "en"}, 134 | null=True, 135 | on_delete=django.db.models.deletion.CASCADE, 136 | related_name="translations", 137 | to="elephantblog.Entry", 138 | verbose_name="translation of", 139 | ), 140 | ), 141 | ], 142 | options={ 143 | "verbose_name": "entry", 144 | "verbose_name_plural": "entries", 145 | "ordering": ["-published_on"], 146 | "get_latest_by": "published_on", 147 | }, 148 | bases=( 149 | models.Model, 150 | feincms.extensions.base.ExtensionsMixin, 151 | feincms.module.mixins.ContentModelMixin, 152 | ), 153 | ), 154 | migrations.CreateModel( 155 | name="RichTextContent", 156 | fields=[ 157 | ( 158 | "id", 159 | models.AutoField( 160 | auto_created=True, 161 | primary_key=True, 162 | serialize=False, 163 | verbose_name="ID", 164 | ), 165 | ), 166 | ("region", models.CharField(max_length=255)), 167 | ("ordering", models.IntegerField(default=0, verbose_name="ordering")), 168 | ( 169 | "text", 170 | feincms.contrib.richtext.RichTextField( 171 | blank=True, verbose_name="text" 172 | ), 173 | ), 174 | ( 175 | "parent", 176 | models.ForeignKey( 177 | on_delete=django.db.models.deletion.CASCADE, 178 | related_name="richtextcontent_set", 179 | to="elephantblog.Entry", 180 | ), 181 | ), 182 | ], 183 | options={ 184 | "verbose_name": "rich text", 185 | "verbose_name_plural": "rich texts", 186 | "db_table": "elephantblog_entry_richtextcontent", 187 | "ordering": ["ordering"], 188 | "permissions": [], 189 | "abstract": False, 190 | }, 191 | ), 192 | migrations.CreateModel( 193 | name="MediaFileContent", 194 | fields=[ 195 | ( 196 | "id", 197 | models.AutoField( 198 | auto_created=True, 199 | primary_key=True, 200 | serialize=False, 201 | verbose_name="ID", 202 | ), 203 | ), 204 | ("region", models.CharField(max_length=255)), 205 | ("ordering", models.IntegerField(default=0, verbose_name="ordering")), 206 | ( 207 | "type", 208 | models.CharField( 209 | choices=[("default", "default")], 210 | default="default", 211 | max_length=20, 212 | verbose_name="type", 213 | ), 214 | ), 215 | ( 216 | "mediafile", 217 | feincms.module.medialibrary.fields.MediaFileForeignKey( 218 | on_delete=django.db.models.deletion.PROTECT, 219 | related_name="+", 220 | to="medialibrary.MediaFile", 221 | verbose_name="media file", 222 | ), 223 | ), 224 | ( 225 | "parent", 226 | models.ForeignKey( 227 | on_delete=django.db.models.deletion.CASCADE, 228 | related_name="mediafilecontent_set", 229 | to="elephantblog.Entry", 230 | ), 231 | ), 232 | ], 233 | options={ 234 | "verbose_name": "media file", 235 | "verbose_name_plural": "media files", 236 | "db_table": "elephantblog_entry_mediafilecontent", 237 | "ordering": ["ordering"], 238 | "permissions": [], 239 | "abstract": False, 240 | }, 241 | ), 242 | migrations.CreateModel( 243 | name="CategoryTranslation", 244 | fields=[ 245 | ( 246 | "id", 247 | models.AutoField( 248 | auto_created=True, 249 | primary_key=True, 250 | serialize=False, 251 | verbose_name="ID", 252 | ), 253 | ), 254 | ( 255 | "language_code", 256 | models.CharField( 257 | choices=[ 258 | ("en", "English"), 259 | ("de", "German"), 260 | ("zh-hans", "Chinese simplified"), 261 | ("zh-hant", "Chinese traditional"), 262 | ], 263 | default="en", 264 | max_length=10, 265 | verbose_name="language", 266 | ), 267 | ), 268 | ( 269 | "title", 270 | models.CharField(max_length=100, verbose_name="category title"), 271 | ), 272 | ("slug", models.SlugField(unique=True, verbose_name="slug")), 273 | ( 274 | "description", 275 | models.CharField( 276 | blank=True, max_length=250, verbose_name="description" 277 | ), 278 | ), 279 | ( 280 | "parent", 281 | models.ForeignKey( 282 | on_delete=django.db.models.deletion.CASCADE, 283 | related_name="translations", 284 | to="elephantblog.Category", 285 | ), 286 | ), 287 | ], 288 | options={ 289 | "verbose_name": "category translation", 290 | "verbose_name_plural": "category translations", 291 | "ordering": ["title"], 292 | }, 293 | ), 294 | ] 295 | --------------------------------------------------------------------------------