{% trans "Categories" %}
7 |-
8 | {% endif %}
9 |
- 10 | {{ category }} 11 | 12 | {% if forloop.last %} 13 |
├── 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 |
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 |