├── .editorconfig ├── .gitignore ├── .travis.yml ├── AUTHORS.rst ├── CONTRIBUTING.rst ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── andablog ├── __init__.py ├── admin.py ├── feeds.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20141204_1659.py │ ├── 0003_auto_20150826_2353.py │ ├── 0004_shorten_entry_title.py │ ├── 0005_auto_20151017_1747.py │ ├── 0006_auto_20170609_1759.py │ ├── 0007_auto_20190315_1540.py │ └── __init__.py ├── models.py ├── sitemaps.py ├── static │ ├── css │ │ └── andablog.css │ ├── img │ │ └── .gitignore │ └── js │ │ └── djangoandablog.js ├── templates │ ├── andablog │ │ ├── base.html │ │ ├── comments_count_snippet.html │ │ ├── comments_snippet.html │ │ ├── entry_detail.html │ │ ├── entry_list.html │ │ └── pagination_snippet.html │ └── base.html ├── templatetags │ ├── __init__.py │ └── andablog_tags.py ├── tests │ ├── __init__.py │ ├── test_entry_detail.py │ ├── test_entry_forms.py │ ├── test_entry_listing.py │ ├── test_feeds.py │ ├── test_models.py │ ├── test_sitemaps.py │ └── test_templatetags.py ├── urls.py └── views.py ├── build.py ├── demo ├── blog │ ├── __init__.py │ ├── admin.py │ ├── feeds.py │ ├── fixtures │ │ ├── tags_for_three_entries.json │ │ └── three_published_entries.json │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── common │ ├── __init__.py │ ├── admin.py │ ├── fixtures │ │ ├── admin_user.json │ │ ├── one_user.json │ │ └── three_users.json │ ├── forms.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── check_missing_migrations.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20150507_1708.py │ │ ├── 0003_auto_20150831_2043.py │ │ ├── 0004_auto_20151208_0047.py │ │ ├── 0005_auto_20160917_0035.py │ │ └── __init__.py │ ├── models.py │ └── tests.py ├── demo │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── democomments │ ├── __init__.py │ ├── admin.py │ ├── fixtures │ │ └── four_comments.json │ ├── forms.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ └── views.py ├── manage.py ├── media │ └── andablog │ │ └── images │ │ ├── SendOff-300px.png │ │ ├── crown.png │ │ ├── inchworm.png │ │ ├── rich_strut.png │ │ └── welcome.png ├── profiles │ ├── __init__.py │ ├── admin.py │ ├── fixtures │ │ ├── admin_profile.json │ │ ├── one_profile.json │ │ └── three_profiles.json │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── sitemaps.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── static │ ├── css │ │ ├── bootstrap.css │ │ └── demo.css │ └── js │ │ ├── bootstrap.js │ │ ├── ie10-viewport-bug-workaround.js │ │ └── jquery.js └── templates │ ├── 404.html │ ├── andablog │ ├── base.html │ ├── comments_count_snippet.html │ └── comments_snippet.html │ ├── base.html │ ├── comments │ └── list.html │ ├── home.html │ └── profiles │ └── userprofile_detail.html ├── docs ├── Makefile ├── conf.py ├── demo-site.rst ├── index.rst ├── install-usage.rst └── make.bat ├── local_requirements.txt ├── setup.cfg ├── setup.py ├── test_requirements.txt └── tox.ini /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | charset = utf-8 9 | end_of_line = lf 10 | 11 | [*.py] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.js] 16 | indent_style = tab 17 | 18 | [lib/**.js] 19 | indent_style = space 20 | indent_size = 2 21 | 22 | [LICENSE] 23 | insert_final_newline = false 24 | 25 | [Makefile] 26 | indent_style = tab 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | 56 | # Virtualenv 57 | venv 58 | 59 | # Pycharm 60 | .idea 61 | 62 | # Demo project 63 | db.sqlite3 64 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | python: 4 | - "3.6" 5 | install: pip install tox-travis 6 | script: tox 7 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * Ivan Ven Osdel 9 | 10 | Contributors 11 | ------------ 12 | 13 | * Mikko Ohtamaa 14 | * Brad Montgomery 15 | * Samuel Mendes 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every 6 | little bit helps, and credit will always be given. 7 | 8 | Types of Contributions 9 | ---------------------- 10 | 11 | Report Bugs 12 | ~~~~~~~~~~~ 13 | 14 | Report bugs at https://github.com/WimpyAnalytics/django-andablog/issues. 15 | 16 | If you are reporting a bug, please include: 17 | 18 | * Your operating system name and version. 19 | * Any details about your local setup that might be helpful in troubleshooting. 20 | * Detailed steps to reproduce the bug. 21 | 22 | Fix Bugs 23 | ~~~~~~~~ 24 | 25 | Look through the GitHub issues for bugs. Anything tagged with "bug" 26 | is open to whoever wants to implement it. 27 | 28 | Implement Features 29 | ~~~~~~~~~~~~~~~~~~ 30 | 31 | Look through the GitHub issues for features. Anything tagged with "feature" 32 | is open to whoever wants to implement it. 33 | 34 | Write Documentation 35 | ~~~~~~~~~~~~~~~~~~~ 36 | 37 | Andablog could always use more documentation, whether as part of the 38 | official andablog docs, in docstrings, or even on the web in blog posts, 39 | articles, and such. 40 | 41 | Submit Feedback 42 | ~~~~~~~~~~~~~~~ 43 | 44 | The best way to send feedback is to file an issue at https://github.com/WimpyAnalytics/django-andablog/issues. 45 | 46 | If you are proposing a feature: 47 | 48 | * Explain in detail how it would work. 49 | * Keep the scope as narrow as possible, to make it easier to implement. 50 | * Remember that this is a volunteer-driven project, and that contributions 51 | are welcome :) 52 | 53 | Get Started! 54 | ------------ 55 | 56 | Ready to let the code flow from your fingertips? Here's how to set up `django-andablog` for local development. 57 | 58 | Get the code 59 | ~~~~~~~~~~~~ 60 | 61 | 1. Fork the `django-andablog` repo on GitHub. 62 | 2. Clone your fork locally:: 63 | 64 | $ git clone git@github.com:your_name_here/django-andablog.git 65 | 66 | Install Build Tool (optional) 67 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 68 | 69 | You can install and use our make-style tool of choice if you don't want to worry about the virtualenv or navigating the project structure. 70 | 71 | On Linux (one time) from the cloned dir:: 72 | 73 | $ sudo pip install pynt pynt-of-django 74 | 75 | Or (one time) on windows:: 76 | 77 | $ pip install pynt pynt-of-django 78 | 79 | Install Dependencies 80 | ~~~~~~~~~~~~~~~~~~~~ 81 | 82 | System Packages 83 | ^^^^^^^^^^^^^^^ 84 | These are necessary for running tox. Which is required if you intend to make changes. 85 | 86 | * Python dev package (python-dev on apt) 87 | * Python 3 dev packages (python3-dev on apt) 88 | 89 | Python Packages 90 | ^^^^^^^^^^^^^^^ 91 | 92 | Using build script:: 93 | 94 | $ pynt create_venv 95 | 96 | Or manually: 97 | 98 | 1. Create and activate a virtualenv (somewhere) 99 | 2. Change directory to the cloned dir 100 | 3. Install the dev and test dependencies:: 101 | 102 | $ pip install -r local_requirements.txt 103 | 104 | Making changes 105 | ~~~~~~~~~~~~~~ 106 | 107 | 1. Create a branch for local development:: 108 | 109 | $ git checkout -b name-of-your-bugfix-or-feature 110 | 111 | Now you can make your changes locally and add yourself to AUTHORS.rst. Make sure to periodically run the tests for the active Python and Django version:: 112 | 113 | $ pynt test_venv 114 | 115 | Or run them manually, with the virtualenv activated:: 116 | 117 | $ cd demo 118 | $ python manage.py test 119 | $ python manage.py test andablog 120 | 121 | 2. When you're done making changes, check that your changes work with all supported Python and Django versions:: 122 | 123 | $ pynt test 124 | 125 | Or manually, with the virtualenv activated:: 126 | 127 | $ tox 128 | 129 | 3. Commit your changes and push your branch to GitHub:: 130 | 131 | $ git add . 132 | $ git commit -m "Your detailed description of your changes." 133 | $ git push origin name-of-your-bugfix-or-feature 134 | 135 | 4. Submit a pull request through the GitHub website. 136 | 137 | Pull Request Guidelines 138 | ----------------------- 139 | 140 | Before you submit a pull request, check that it meets these guidelines: 141 | 142 | 1. The pull request should include tests. 143 | 2. If the pull request adds functionality, the docs should be updated. Public functions should have docstrings, and add the feature to the list in docs/index.rst. 144 | 3. The pull request should work for all supported Python and Django versions, and for PyPy. Check 145 | https://travis-ci.org/WimpyAnalytics/django-andablog/pull_requests 146 | and make sure that the tests pass for all configurations. 147 | 148 | Tips 149 | ---- 150 | 151 | If you are using our make-style commands you really should never have to activate a virtualenv. Some more common commands. 152 | 153 | Command listing:: 154 | 155 | $ pynt -l 156 | 157 | Running the development server:: 158 | 159 | $ pynt runserver 160 | 161 | Interacting with demo's manage.py:: 162 | 163 | $ pynt manage["help"] 164 | 165 | Load all fixtures in the entire project:: 166 | 167 | $ pynt loadalldatas 168 | 169 | You are also free to add any new tasks to build.py. 170 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | History 4 | ------- 5 | 6 | 3.2.0 (2020-02-21) 7 | ------------------ 8 | Django 2.2 support, maintaining Django 2.0 support 9 | 10 | 3.1.0 (2019-04-27) 11 | ------------------ 12 | Django 2.1 support, drops Django 1.11 (along with Python2.7) support 13 | 14 | 3.0.0 (2019-03-15) 15 | ------------------ 16 | Django 2.0 support, drops Django 1.10 support. 17 | * Drops use of the, no longer maintained, django-markitup dependency in favor of django-markupfield. 18 | * Database migrations support the conversion of all entry content and previews. 19 | * Removes live preview in admin. See the django-markupfield project for additional usage. 20 | * Maintains markdown support. Removes Textile support in favor of RST. 21 | **If you previously used Textile you will have to write your own migration.** See the django-markupfield docs for assistance in this. 22 | 23 | 2.4.0 (2017-06-09) 24 | ------------------ 25 | New feature: Optional preview_content (markdown) and preview_image fields for direct control of appearance of item in listing. 26 | 27 | 2.3.0 (2017-06-09) 28 | ------------------ 29 | Django 1.11 support, drops Django 1.9 support 30 | 31 | 2.2.0 (2016-09-17) 32 | ------------------ 33 | Django 1.10 support, drops Django 1.8 support 34 | 35 | 2.1.1 (2016-01-17) 36 | ------------------ 37 | Fixes an issue with saving entries in Django 1.9 caused by a previously faulty version of django-markitup. 38 | 39 | 2.1.0 (2015-12-07) 40 | ------------------ 41 | Django 1.9 support, drops Django 1.7 support 42 | 43 | 2.0.0 (2015-10-18) 44 | ------------------ 45 | Adds support for titles and slugs up to 255 characters in length. **Major: Migration will auto-truncate existing titles that are > 255 characters** 46 | * Thanks Federico (fedejaure) for the fork that inspired the change. 47 | * Thanks Brad Montgomery for design input, fix and feature change. 48 | 49 | 1.4.2 (2015-09-17) 50 | ------------------ 51 | Fixed unicode support for models 52 | * Thanks Samuel Mendes for the report and fix. 53 | 54 | 1.4.1 (2015-09-11) 55 | ------------------ 56 | Fixed a missing migration bug 57 | * Thanks bradmontgomery for the report and fix. 58 | * CI tests now include a missing migration check. 59 | 60 | 1.4.0 (2015-05-07) 61 | ------------------ 62 | Support for Django 1.7.x - Django 1.8.x 63 | * Adds support for Django 1.8 64 | * Drops support for Django 1.6 and therefore south_migrations 65 | 66 | 1.3.0 (2015-03-10) 67 | ------------------ 68 | Authors are now able to see 'draft' (unpublished) versions of their blog entries. 69 | Upgraded taggit to address an issue that was locking us to an older Django 1.7 version. 70 | 71 | 1.2.2 (2014-12-04) 72 | ------------------ 73 | Fixed a bug where the Django 1.7.x migration for recent DB changes was somehow missed. 74 | 75 | 1.2.1 (2014-12-02) 76 | ------------------ 77 | The author is now selectable when editing entries in the admin. 78 | * The list is limited to superusers and anyone with an andablog Entry permission. 79 | * The initial value is the current user. 80 | 81 | 1.1.1 (2014-12-02) 82 | ------------------ 83 | Fixed a bug where the tags field was required in the admin. 84 | 85 | 1.1.0 (2014-12-01) 86 | ------------------ 87 | Blog entries can now have tags 88 | * The entry model now supports tags by way of the django-taggit package. 89 | * This affects the model only, there are no template examples or tags. 90 | 91 | 1.0.0 (2014-11-20) 92 | ------------------ 93 | **Backwards Incompatible with 0.1.0.** 94 | This release includes a rename of the django app package from djangoandablog to andablog to better follow 95 | community conventions. This of course is a very large breaking change, which is why the version is 1.0. 96 | As this is the second version and we have been out such a short time. My hope is that few if any people 97 | are using this app yet. If you are, please submit an issue on GitHub and I will try to help you migrate away. 98 | 99 | 0.1.0 (2014-11-16) 100 | ------------------ 101 | 102 | * First release on PyPI. 103 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Ivan VenOsdel 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY 14 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY 17 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 20 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 21 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 22 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 23 | DAMAGE. 24 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include CONTRIBUTING.rst 3 | include HISTORY.rst 4 | include AUTHORS.rst 5 | 6 | recursive-include andablog * 7 | recursive-include demo * 8 | recursive-include docs *.rst conf.py Makefile make.bat 9 | 10 | recursive-exclude * __pycache__ 11 | recursive-exclude * *.py[co] 12 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-andablog 2 | =============== 3 | 4 | |Build Status| 5 | 6 | A minimal blog app for Django 7 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 8 | 9 | Andablog is a blogging application for the `Django 10 | framework `__. Andablog comes with minimal 11 | dependencies, making it effortless to integrate to existing Django 12 | sites. It supports the latest Django and Python versions. 13 | 14 | - `Full 15 | documentation `__ 16 | - `Features `__ 17 | - `Installation and 18 | usage `__ 19 | - `Demo 20 | site `__ 21 | 22 | History 23 | ~~~~~~~ 24 | 25 | What is the point of all this? When this project got started blogging 26 | apps for Django generally fell into one of these categories: 27 | 28 | 1. A full CMS framework. 29 | 2. A Django App but intended for blog-only sites. 30 | 3. A Django App but intended for either a blog-only site (the default) 31 | or a site with a blog attached. 32 | 33 | Though all three of these categories had great projects to choose from 34 | any one of them could be frustrating to implement into (and maintain 35 | within) an existing site. Simply because the app was not directly and 36 | exclusively focused on the use case of a django site, and a blog (get 37 | it?). 38 | 39 | Andablog has a focus on integration-ease first and features second. So 40 | if the focus of your Django site is something else and you want to add a 41 | blog section to it you have come to the right place. 42 | 43 | .. |Build Status| image:: https://travis-ci.org/WimpyAnalytics/django-andablog.svg?branch=master 44 | :target: https://travis-ci.org/WimpyAnalytics/django-andablog 45 | -------------------------------------------------------------------------------- /andablog/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '3.1.0' 2 | -------------------------------------------------------------------------------- /andablog/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.db.models import Q 3 | from django.contrib.auth import get_user_model 4 | 5 | from .models import Entry, EntryImage 6 | 7 | 8 | class EntryImageInline(admin.TabularInline): 9 | model = EntryImage 10 | extra = 1 11 | readonly_fields = ['image_url'] 12 | 13 | 14 | class EntryAdmin(admin.ModelAdmin): 15 | 16 | list_display = ( 17 | 'title', 18 | 'is_published', 19 | 'published_timestamp', 20 | ) 21 | search_fields = ( 22 | 'title', 23 | 'content', 24 | ) 25 | readonly_fields = ( 26 | 'slug', 27 | 'published_timestamp', 28 | ) 29 | 30 | inlines = [ 31 | EntryImageInline, 32 | ] 33 | 34 | def formfield_for_foreignkey(self, db_field, request, **kwargs): 35 | if db_field.name == "author": 36 | kwargs["queryset"] = get_user_model().objects.filter( 37 | Q(is_superuser=True) | Q(user_permissions__content_type__app_label='andablog', 38 | user_permissions__content_type__model='entry')).distinct() 39 | kwargs['initial'] = request.user.id 40 | return super(EntryAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs) 41 | 42 | 43 | class EntryImageAdmin(admin.ModelAdmin): 44 | pass 45 | 46 | admin.site.register(Entry, EntryAdmin) 47 | admin.site.register(EntryImage, EntryImageAdmin) 48 | -------------------------------------------------------------------------------- /andablog/feeds.py: -------------------------------------------------------------------------------- 1 | from django.contrib.syndication.views import Feed 2 | from django.urls import reverse 3 | from django.template.defaultfilters import truncatewords_html 4 | 5 | from .models import Entry 6 | 7 | 8 | class LatestEntriesFeed(Feed): 9 | """ 10 | This is just a base class for a blog entry field. 11 | It should be sub-classed and further refined before use. 12 | """ 13 | MAX_ENTRIES = 10 14 | 15 | title = 'Latest blog entries' 16 | description = 'Latest blog entries sorted by newest to oldest.' 17 | 18 | def item_pubdate(self, item): 19 | return item.published_timestamp 20 | 21 | def item_author_name(self, item): 22 | if item.author is None: 23 | return None 24 | return item.author.get_short_name() 25 | 26 | def item_author_email(self, item): 27 | if item.author is None: 28 | return None 29 | return item.author.email 30 | 31 | def item_author_link(self, item): 32 | if item.author is None: 33 | return None 34 | return item.author.get_absolute_url() 35 | 36 | def link(self): 37 | return reverse('andablog:entrylist') 38 | 39 | def item_description(self, item): 40 | # TODO: "Better support for truncating markup" #2 41 | return truncatewords_html(item.content, 26) 42 | 43 | def item_title(self, item): 44 | return item.title 45 | 46 | def items(self): 47 | return Entry.objects.filter(is_published=True).order_by('-published_timestamp')[:self.MAX_ENTRIES] 48 | -------------------------------------------------------------------------------- /andablog/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | from markupfield.fields import MarkupField 6 | import model_utils.fields 7 | import django.utils.timezone 8 | from django.conf import settings 9 | 10 | 11 | class Migration(migrations.Migration): 12 | 13 | dependencies = [ 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Entry', 20 | fields=[ 21 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 22 | ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), 23 | ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), 24 | ('title', models.CharField(max_length=500)), 25 | ('slug', models.SlugField(unique=True, editable=False)), 26 | ('content', MarkupField(default_markup_type='markdown')), 27 | ('is_published', models.BooleanField(default=False)), 28 | ('published_timestamp', models.DateTimeField(null=True, editable=False, blank=True)), 29 | ('_content_rendered', models.TextField(editable=False, blank=True)), 30 | ('author', models.ForeignKey(editable=False, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE)), 31 | ], 32 | options={ 33 | 'verbose_name_plural': 'entries', 34 | }, 35 | bases=(models.Model,), 36 | ), 37 | migrations.CreateModel( 38 | name='EntryImage', 39 | fields=[ 40 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 41 | ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), 42 | ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), 43 | ('image', models.ImageField(upload_to=b'andablog/images', blank=True)), 44 | ('entry', models.ForeignKey(to='andablog.Entry', on_delete=models.CASCADE)), 45 | ], 46 | options={ 47 | 'abstract': False, 48 | }, 49 | bases=(models.Model,), 50 | ), 51 | ] 52 | -------------------------------------------------------------------------------- /andablog/migrations/0002_auto_20141204_1659.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | from django.conf import settings 6 | import taggit.managers 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('taggit', '0001_initial'), 13 | ('andablog', '0001_initial'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AddField( 18 | model_name='entry', 19 | name='tags', 20 | field=taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', blank=True, help_text='A comma-separated list of tags.', verbose_name='Tags'), 21 | preserve_default=True, 22 | ), 23 | migrations.AlterField( 24 | model_name='entry', 25 | name='author', 26 | field=models.ForeignKey(to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /andablog/migrations/0003_auto_20150826_2353.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('andablog', '0002_auto_20141204_1659'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='entryimage', 16 | name='image', 17 | field=models.ImageField(blank=True, upload_to='andablog/images'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /andablog/migrations/0004_shorten_entry_title.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import time 5 | from django.db import migrations, models 6 | 7 | 8 | # The new, maximum length of titles. 9 | TITLE_LENGTH = 255 10 | 11 | 12 | def truncate_entry_titles(apps, schema_editor): 13 | """This function will truncate the values of Entry.title so they are 14 | 255 characters or less. 15 | """ 16 | Entry = apps.get_model("andablog", "Entry") 17 | 18 | for entry in Entry.objects.all(): 19 | # Truncate to 255 characters (or less) but keep whole words intact. 20 | while len(entry.title) > TITLE_LENGTH: 21 | entry.title = ' '.join(entry.title.split()[:-1]) 22 | entry.save() 23 | 24 | 25 | class Migration(migrations.Migration): 26 | 27 | dependencies = [ 28 | ('andablog', '0003_auto_20150826_2353'), 29 | ] 30 | 31 | operations = [ 32 | migrations.RunPython(truncate_entry_titles) 33 | ] 34 | -------------------------------------------------------------------------------- /andablog/migrations/0005_auto_20151017_1747.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('andablog', '0004_shorten_entry_title'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='entry', 16 | name='slug', 17 | field=models.SlugField(unique=True, editable=False, max_length=255), 18 | ), 19 | migrations.AlterField( 20 | model_name='entry', 21 | name='title', 22 | field=models.CharField(max_length=255), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /andablog/migrations/0006_auto_20170609_1759.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.2 on 2017-06-09 17:59 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | from markupfield.fields import MarkupField 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('andablog', '0005_auto_20151017_1747'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='entry', 18 | name='_preview_content_rendered', 19 | field=models.TextField(blank=True, editable=False), 20 | ), 21 | migrations.AddField( 22 | model_name='entry', 23 | name='preview_content', 24 | field=MarkupField(blank=True, default_markup_type='markdown'), 25 | ), 26 | migrations.AddField( 27 | model_name='entry', 28 | name='preview_image', 29 | field=models.ImageField(blank=True, upload_to='andablog/images'), 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /andablog/migrations/0007_auto_20190315_1540.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.13 on 2019-03-15 15:40 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('andablog', '0006_auto_20170609_1759'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='entry', 15 | name='content_markup_type', 16 | field=models.CharField(choices=[('', '--'), ('html', 'HTML'), ('plain', 'Plain'), ('markdown', 'Markdown'), ('restructuredtext', 'Restructured Text')], default='markdown', max_length=30), 17 | ), 18 | migrations.AddField( 19 | model_name='entry', 20 | name='preview_content_markup_type', 21 | field=models.CharField(choices=[('', '--'), ('html', 'HTML'), ('plain', 'Plain'), ('markdown', 'Markdown'), ('restructuredtext', 'Restructured Text')], default='markdown', max_length=30), 22 | ), 23 | migrations.AlterField( 24 | model_name='entry', 25 | name='_content_rendered', 26 | field=models.TextField(editable=False), 27 | ), 28 | migrations.AlterField( 29 | model_name='entry', 30 | name='_preview_content_rendered', 31 | field=models.TextField(editable=False), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /andablog/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramblin-dev/django-andablog/e15b9af397d05ab3fd1649020a798df3b7df5041/andablog/migrations/__init__.py -------------------------------------------------------------------------------- /andablog/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | 4 | from django.urls import reverse 5 | from django.db import models 6 | from django.template.defaultfilters import truncatechars 7 | from django.utils.text import slugify 8 | from django.utils import timezone 9 | from django.conf import settings 10 | 11 | from markupfield.fields import MarkupField 12 | from model_utils.models import TimeStampedModel 13 | from taggit.managers import TaggableManager 14 | 15 | 16 | class Entry(TimeStampedModel): 17 | """ 18 | Represents a blog Entry. 19 | Uses TimeStampModel to provide created and modified fields 20 | """ 21 | title = models.CharField(max_length=255) 22 | slug = models.SlugField(max_length=255, unique=True, editable=False) 23 | content = MarkupField(default_markup_type='markdown') 24 | is_published = models.BooleanField(default=False) 25 | published_timestamp = models.DateTimeField(blank=True, null=True, editable=False) 26 | author = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, editable=True, on_delete=models.CASCADE) 27 | tags = TaggableManager(blank=True) 28 | preview_content = MarkupField(blank=True, default_markup_type='markdown') 29 | preview_image = models.ImageField(blank=True, upload_to='andablog/images') 30 | 31 | def __unicode__(self): 32 | return self.title 33 | 34 | class Meta: 35 | verbose_name_plural = "entries" 36 | 37 | def get_absolute_url(self): 38 | return reverse('andablog:entrydetail', args=[self.slug]) 39 | 40 | def _insert_timestamp(self, slug, max_length=255): 41 | """Appends a timestamp integer to the given slug, yet ensuring the 42 | result is less than the specified max_length. 43 | """ 44 | timestamp = str(int(time.time())) 45 | ts_len = len(timestamp) + 1 46 | while len(slug) + ts_len > max_length: 47 | slug = '-'.join(slug.split('-')[:-1]) 48 | slug = '-'.join([slug, timestamp]) 49 | return slug 50 | 51 | def _slugify_title(self): 52 | """Slugify the Entry title, but ensure it's less than the maximum 53 | number of characters. This method also ensures that a slug is unique by 54 | appending a timestamp to any duplicate slugs. 55 | """ 56 | # Restrict slugs to their maximum number of chars, but don't split mid-word 57 | self.slug = slugify(self.title) 58 | while len(self.slug) > 255: 59 | self.slug = '-'.join(self.slug.split('-')[:-1]) 60 | 61 | # Is the same slug as another entry? 62 | if Entry.objects.filter(slug=self.slug).exclude(id=self.id).exists(): 63 | # Append time to differentiate. 64 | self.slug = self._insert_timestamp(self.slug) 65 | 66 | def save(self, *args, **kwargs): 67 | self._slugify_title() 68 | 69 | # Time to publish? 70 | if not self.published_timestamp and self.is_published: 71 | self.published_timestamp = timezone.now() 72 | elif not self.is_published: 73 | self.published_timestamp = None 74 | 75 | super(Entry, self).save(*args, **kwargs) 76 | 77 | 78 | class EntryImage(TimeStampedModel): 79 | entry = models.ForeignKey(Entry, on_delete=models.CASCADE) 80 | image = models.ImageField(blank=True, upload_to='andablog/images') 81 | 82 | @property 83 | def image_url(self): 84 | return self.get_absolute_url() 85 | 86 | def get_absolute_url(self): 87 | return self.image.url 88 | 89 | def __unicode__(self): 90 | return u"{entry} - {image}".format( 91 | entry=truncatechars(self.entry, 10), 92 | image=truncatechars(self.image.name, 10), 93 | ) 94 | -------------------------------------------------------------------------------- /andablog/sitemaps.py: -------------------------------------------------------------------------------- 1 | from django.contrib.sitemaps import Sitemap 2 | 3 | from .models import Entry 4 | 5 | 6 | class EntrySitemap(Sitemap): 7 | """ 8 | Sitemap for entries. 9 | """ 10 | priority = 0.5 11 | changefreq = 'weekly' 12 | 13 | def items(self): 14 | """ 15 | Return published entries. 16 | """ 17 | return Entry.objects.filter(is_published=True).order_by('-published_timestamp') 18 | 19 | def lastmod(self, obj): 20 | """ 21 | Return last modification of an entry. 22 | """ 23 | return obj.modified 24 | -------------------------------------------------------------------------------- /andablog/static/css/andablog.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Blog name and description 3 | */ 4 | 5 | .andablog-header { 6 | padding-top: 20px; 7 | padding-bottom: 20px; 8 | } 9 | .andablog-title { 10 | margin-top: 30px; 11 | margin-bottom: 0; 12 | font-size: 60px; 13 | font-weight: normal; 14 | } 15 | .andablog-description { 16 | font-size: 20px; 17 | color: #999; 18 | } 19 | 20 | 21 | /* 22 | * Main column and sidebar layout 23 | */ 24 | 25 | .andablog-main { 26 | font-size: 18px; 27 | line-height: 1.5; 28 | } 29 | 30 | /* Sidebar modules for boxing content */ 31 | .sidebar-module { 32 | padding: 15px; 33 | margin: 0 -15px 15px; 34 | } 35 | .sidebar-module-inset { 36 | padding: 15px; 37 | background-color: #f5f5f5; 38 | border-radius: 4px; 39 | } 40 | .sidebar-module-inset p:last-child, 41 | .sidebar-module-inset ul:last-child, 42 | .sidebar-module-inset ol:last-child { 43 | margin-bottom: 0; 44 | } 45 | 46 | 47 | 48 | /* Pagination */ 49 | .pager { 50 | margin-bottom: 60px; 51 | text-align: left; 52 | } 53 | .pager > li > a { 54 | width: 140px; 55 | padding: 10px 20px; 56 | text-align: center; 57 | border-radius: 30px; 58 | } 59 | 60 | 61 | /* 62 | * Blog posts 63 | */ 64 | 65 | .andablog-post { 66 | margin-bottom: 60px; 67 | } 68 | .andablog-post-title { 69 | margin-bottom: 5px; 70 | font-size: 40px; 71 | } 72 | .andablog-post-meta { 73 | margin-bottom: 20px; 74 | color: #999; 75 | } 76 | -------------------------------------------------------------------------------- /andablog/static/img/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramblin-dev/django-andablog/e15b9af397d05ab3fd1649020a798df3b7df5041/andablog/static/img/.gitignore -------------------------------------------------------------------------------- /andablog/static/js/djangoandablog.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramblin-dev/django-andablog/e15b9af397d05ab3fd1649020a798df3b7df5041/andablog/static/js/djangoandablog.js -------------------------------------------------------------------------------- /andablog/templates/andablog/base.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | 4 | {% block subtitle %} 5 | {% block andablog_page_title %} 6 | {% endblock %} 7 | {% endblock %} 8 | 9 | {% block head_styles %} 10 | {% block andablog_head_extra %} 11 | 12 | {% endblock %} 13 | {% endblock %} 14 | 15 | {% block content %} 16 | {% block andablog_content %} 17 | {% endblock %} 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /andablog/templates/andablog/comments_count_snippet.html: -------------------------------------------------------------------------------- 1 | {% comment %} 2 | This template must be overriden to implement displaying comment counts. 3 | 4 | You will recieve a context object called comment_object which is the object being commented on. 5 | {% endcomment %} 6 | -------------------------------------------------------------------------------- /andablog/templates/andablog/comments_snippet.html: -------------------------------------------------------------------------------- 1 | {% comment %} 2 | This template must be overriden to implement listing comments and displaying a comment box. 3 | 4 | You will recieve a context object called comment_object which is the object being commented on. 5 | {% endcomment %} 6 | -------------------------------------------------------------------------------- /andablog/templates/andablog/entry_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "andablog/base.html" %} 2 | {% load andablog_tags %} 3 | 4 | {% block andablog_meta %} 5 | 6 | 7 | {% endblock %} 8 | 9 | {% block andablog_page_title %} 10 | {{ entry.title }} 11 | {% endblock %} 12 | 13 | {% block andablog_content %} 14 |
15 |

{{ entry.title }}{% if not entry.is_published %} Draft{% endif %}

16 | 17 |

18 | {{ entry.content }} 19 |

20 | 21 | {% include "andablog/comments_snippet.html" with comment_object=entry %} 22 |
23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /andablog/templates/andablog/entry_list.html: -------------------------------------------------------------------------------- 1 | {% extends "andablog/base.html" %} 2 | {% load andablog_tags %} 3 | 4 | {% block andablog_meta %} 5 | 6 | {% endblock %} 7 | 8 | {% block andablog_page_title %} 9 | Latest Blog Entries 10 | {% endblock %} 11 | 12 | {% block andablog_content %} 13 | {% for entry in entries %} 14 |
15 |
16 |
17 |

{{ entry.title }}{% if not entry.is_published %} Draft{% endif %}

18 | 19 |

20 | {% if entry.preview_content %} 21 | {% if entry.preview_image %} 22 | Preview image for {{ entry.title }} 23 | {% endif %} 24 | {{ entry.preview_content }} 25 | {% else %} 26 | {# Truncate derived from: Avg reading speed (3.33 words/s) * Average attention span (8s) #} 27 | {{ entry.content|truncatewords_html:26 }} (More...) {% include "andablog/comments_count_snippet.html" with comment_object=entry %} 28 | {% endif %} 29 |

30 |
31 |
32 |
33 | {% endfor %} 34 | 35 | {% include "andablog/pagination_snippet.html" with page_obj=page_obj %} 36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /andablog/templates/andablog/pagination_snippet.html: -------------------------------------------------------------------------------- 1 | {% if is_paginated %} 2 |
    3 | {% if page_obj.has_previous %} 4 |
  • <<
  • 5 |
  • <
  • 6 | {% endif %} 7 | 8 | {% for i in paginator.page_range %} 9 |
  • {{i}}
  • 10 | {% endfor %} 11 | 12 | {% if page_obj.has_next %} 13 |
  • >
  • 14 |
  • >>
  • 15 | {% endif %} 16 |
17 | {% endif %} 18 | -------------------------------------------------------------------------------- /andablog/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load trans from i18n %} 2 | 3 | 4 | 5 | 6 | {% block title %}{% block subtitle %}{% endblock %} - Untitled{% endblock %} 7 | 8 | 9 | 10 | 11 | {% block head_meta %} 12 | {% endblock %} 13 | 14 | {% block head_styles %} 15 | {% endblock %} 16 | 17 | {% block head_scripts %} 18 | {% endblock %} 19 | 20 | 21 | 22 | {% if messages %} 23 |
24 | Messages: 25 |
    26 | {% for message in messages %} 27 |
  • {{message}}
  • 28 | {% endfor %} 29 |
30 |
31 | {% endif %} 32 | 33 |
34 | {% block content %}{% endblock %} 35 |
36 | 37 | {% block body_scripts %}{% endblock %} 38 | 39 | 40 | -------------------------------------------------------------------------------- /andablog/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /andablog/templatetags/andablog_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.utils.safestring import mark_safe 3 | import six 4 | 5 | register = template.Library() 6 | 7 | 8 | @register.filter(name='authordisplay') 9 | def author_display(author, *args): 10 | """Returns either the linked or not-linked profile name.""" 11 | 12 | # Call get_absolute_url or a function returning none if not defined 13 | url = getattr(author, 'get_absolute_url', lambda: None)() 14 | # get_short_name or unicode representation 15 | short_name = getattr(author, 'get_short_name', lambda: six.text_type(author))() 16 | if url: 17 | return mark_safe('{}'.format(url, short_name)) 18 | else: 19 | return short_name 20 | -------------------------------------------------------------------------------- /andablog/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramblin-dev/django-andablog/e15b9af397d05ab3fd1649020a798df3b7df5041/andablog/tests/__init__.py -------------------------------------------------------------------------------- /andablog/tests/test_entry_detail.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from django.test import TestCase 5 | 6 | from andablog import models 7 | 8 | 9 | class TestEntryDetail(TestCase): 10 | 11 | def setUp(self): 12 | self.entry_1 = models.Entry.objects.create(title=u'Welcome!', is_published=True) 13 | 14 | self.url = self.entry_1.get_absolute_url() 15 | 16 | def test_published(self): 17 | """A published entry should be shown""" 18 | response = self.client.get(self.url) 19 | 20 | self.assertEqual(response.status_code, 200) 21 | self.assertEqual(self.entry_1, response.context['entry']) 22 | self.assertNumQueries(1) 23 | 24 | def test_unpublished(self): 25 | """An unpublished entry should not be found""" 26 | self.entry_1.is_published = False 27 | self.entry_1.save() 28 | 29 | response = self.client.get(self.url) 30 | 31 | self.assertEqual(response.status_code, 404) 32 | -------------------------------------------------------------------------------- /andablog/tests/test_entry_forms.py: -------------------------------------------------------------------------------- 1 | from django.forms import ModelForm 2 | from django.test import TestCase 3 | 4 | from andablog import models 5 | 6 | 7 | class TestEntryEditing(TestCase): 8 | 9 | def setUp(self): 10 | self.entry = models.Entry.objects.create(title=u'Welcome!', content='### Some Content', is_published=False) 11 | 12 | class EntryForm(ModelForm): 13 | class Meta: 14 | model = models.Entry 15 | fields = ['title', 'content', 'is_published'] 16 | 17 | self.form_cls = EntryForm 18 | 19 | def test_form_editing(self): 20 | """Should be able to properly edit an entry within a model form""" 21 | update = { 22 | 'title': 'Last Post (Final)', 23 | 'content': '### Goodbye!', 24 | 'is_published': True, 25 | } 26 | 27 | form = self.form_cls(update, instance=self.entry) 28 | 29 | form.save() 30 | 31 | actual = models.Entry.objects.get(pk=self.entry.pk) 32 | self.assertEquals(actual.title, update['title']) 33 | self.assertEquals(actual.content.raw, update['content']) 34 | self.assertIsNotNone(actual.published_timestamp) 35 | 36 | 37 | class TestEntryCreation(TestCase): 38 | 39 | def setUp(self): 40 | class EntryForm(ModelForm): 41 | class Meta: 42 | model = models.Entry 43 | fields = ['title', 'content', 'is_published'] 44 | 45 | self.form_cls = EntryForm 46 | 47 | def test_form_create(self): 48 | """Should be able to properly create an entry within a model form""" 49 | create = { 50 | 'title': 'Last Post (Final)', 51 | 'content': '### Goodbye!', 52 | 'is_published': False, 53 | } 54 | 55 | form = self.form_cls(create) 56 | print(form.errors) 57 | 58 | form.save() 59 | 60 | actual = models.Entry.objects.get(slug='last-post-final') 61 | self.assertEquals(actual.title, create['title']) 62 | self.assertEquals(actual.content.raw, create['content']) 63 | self.assertIsNone(actual.published_timestamp) 64 | -------------------------------------------------------------------------------- /andablog/tests/test_entry_listing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from django.urls import reverse 5 | from django.test import TestCase 6 | 7 | from andablog import models 8 | 9 | 10 | class TestEntryListing(TestCase): 11 | 12 | def setUp(self): 13 | self.post_1 = models.Entry.objects.create(title=u'Welcome!', is_published=True) 14 | self.post_2 = models.Entry.objects.create(title=u'Busy Busy', is_published=True) 15 | self.post_3 = models.Entry.objects.create(title=u'Last Post', is_published=True) 16 | self.post_4 = models.Entry.objects.create(title=u'Back again!') 17 | 18 | self.url = reverse('andablog:entrylist') 19 | 20 | def test_anonymous_get(self): 21 | """Only published entries should be listed by descending published timestamp""" 22 | response = self.client.get(self.url) 23 | 24 | expected_slugs = ['last-post', 'busy-busy', 'welcome'] 25 | actual_slugs = [entry.slug for entry in response.context['entries']] 26 | 27 | self.assertEqual(response.status_code, 200) 28 | self.assertEqual(actual_slugs, expected_slugs) 29 | self.assertNumQueries(1) 30 | -------------------------------------------------------------------------------- /andablog/tests/test_feeds.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import six 5 | from django.urls import reverse 6 | from django.template.defaultfilters import truncatewords 7 | from django.test import TestCase 8 | 9 | from andablog import models, feeds 10 | 11 | 12 | class TestLatestEntriesFeed(TestCase): 13 | 14 | def setUp(self): 15 | self.post_1 = models.Entry.objects.create(title=u'Welcome!', is_published=True) 16 | self.post_2 = models.Entry.objects.create(title=u'Busy Busy', is_published=True) 17 | self.post_3 = models.Entry.objects.create(title=u'Last Post', is_published=True) 18 | self.post_4 = models.Entry.objects.create(title=u'Back again!') 19 | 20 | self.feed = feeds.LatestEntriesFeed() 21 | 22 | def test_feed_properties(self): 23 | """The base entry feed should return the last 10 entries by default""" 24 | actual_entries = self.feed.items() 25 | 26 | expected_slugs = ['last-post', 'busy-busy', 'welcome'] 27 | actual_slugs = [entry.slug for entry in actual_entries] 28 | 29 | self.assertEqual(self.feed.link(), reverse('andablog:entrylist')) 30 | self.assertEqual(actual_slugs, expected_slugs) 31 | self.assertNumQueries(1) 32 | 33 | def test_feed_max(self): 34 | """Should only return ten by default""" 35 | for x in range(8): 36 | models.Entry.objects.create(title=u'Ni' + six.text_type(x), is_published=True) 37 | 38 | self.assertEqual(self.feed.items().count(), 10) 39 | 40 | 41 | class TestLatestEntriesFeedItem(TestCase): 42 | 43 | def setUp(self): 44 | self.entry = models.Entry.objects.create(title=u'Welcome!', is_published=True) 45 | 46 | self.feed = feeds.LatestEntriesFeed() 47 | 48 | def test_item_properites(self): 49 | """A base entry feed should implement all item specific properties""" 50 | expected = ( 51 | self.entry.published_timestamp, 52 | self.entry.title, 53 | None, 54 | None, 55 | None, 56 | truncatewords(self.entry.content, 26) 57 | ) 58 | actual = ( 59 | self.feed.item_pubdate(self.entry), 60 | self.feed.item_title(self.entry), 61 | self.feed.item_author_name(self.entry), 62 | self.feed.item_author_email(self.entry), 63 | self.feed.item_author_link(self.entry), 64 | self.feed.item_description(self.entry), 65 | ) 66 | 67 | #NOTE: Populated author properties testing is covered in the demo site tests 68 | self.assertEqual(expected, actual) 69 | -------------------------------------------------------------------------------- /andablog/tests/test_models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from django.test import TestCase 5 | from django.utils import timezone 6 | from django.utils.safestring import SafeText 7 | 8 | from andablog import models 9 | 10 | 11 | class TestEntryModel(TestCase): 12 | """ 13 | Author Note: We don't do much, if any testing, surrounding the Entry author attribute. 14 | As all of the integration code for them is in the templates. 15 | 16 | TODO: For 1.7, And-a-Blog should provide a Django system check that ensures that get_short_name and 17 | get_absolute_url have been implemented on the user model as that is what the templates rely on. 18 | """ 19 | 20 | def setUp(self): 21 | self.entry = models.Entry.objects.create(title=u'First post!', content=u'The best post on the internet.') 22 | 23 | def test_slug_creation(self): 24 | """The slug field should automatically get set from the title during post creation""" 25 | self.assertEqual(self.entry.slug, 'first-post') 26 | 27 | def test__insert_timestamp(self): 28 | """Ensure the returned value contains a timestamp without going over the max length""" 29 | # When given the `first-post` slug. 30 | result = self.entry._insert_timestamp(self.entry.slug) 31 | self.assertEqual(len(result.split('-')), 3) 32 | 33 | # When given a string > 255 characters. 34 | slug = '-'.join(['a'] * 250) 35 | result = self.entry._insert_timestamp(slug) 36 | self.assertLess(len(result), 255) 37 | 38 | def test_long_slugs_should_not_get_split_midword(self): 39 | """The slug should not get split mid-word.""" 40 | self.entry.title = SafeText("Please tell me where everyone is getting their assumptions about me?" * 100) 41 | self.entry.save() 42 | # The ending should not be a split word. 43 | self.assertEqual(self.entry.slug[-25:], 'everyone-is-getting-their') 44 | 45 | def test_duplicate_long_slugs_should_get_a_timestamp(self): 46 | """If a long title has a shortened slug that is a duplicate, it should have a timestamp""" 47 | self.entry.title = SafeText("Here's a really long title, for testing slug character restrictions") 48 | self.entry.save() 49 | 50 | duplicate_entry = models.Entry.objects.create(title=self.entry.title, content=self.entry.content) 51 | 52 | self.assertNotEqual(self.entry.slug, duplicate_entry.slug) 53 | # This is not ideal, but a portion of the original slug is in the duplicate 54 | self.assertIn(self.entry.slug[:25], duplicate_entry.slug) 55 | 56 | def test_new_duplicate(self): 57 | """The slug value should automatically be made unique if the slug is taken""" 58 | duplicate_entry = models.Entry.objects.create(title=self.entry.title, content=self.entry.content) 59 | 60 | self.assertNotEqual(self.entry.slug, duplicate_entry.slug) 61 | self.assertIn(self.entry.slug, duplicate_entry.slug) 62 | 63 | def test_title_rename_to_duplicate(self): 64 | """Upon title rename, the slug value should automatically be made unique if the slug is taken""" 65 | new_entry_2 = models.Entry.objects.create(title=u'Second post!', content=u'Second best post on the internet.') 66 | 67 | # Rename 68 | new_entry_2.title = self.entry.title 69 | new_entry_2.save() 70 | 71 | self.assertNotEqual(self.entry.slug, new_entry_2.slug) 72 | self.assertIn(self.entry.slug, new_entry_2.slug) 73 | 74 | def test_publishing(self): 75 | """Test publishing an entry""" 76 | self.assertFalse(self.entry.is_published) 77 | self.assertFalse(self.entry.published_timestamp) 78 | 79 | moment_ago = timezone.now() 80 | 81 | self.entry.is_published = True 82 | self.entry.save() 83 | 84 | self.assertGreaterEqual(self.entry.published_timestamp, moment_ago) 85 | 86 | def test_unpublishing(self): 87 | """Should be able to pull an entry after it has been published""" 88 | self.entry.is_published = True 89 | self.entry.save() 90 | 91 | self.entry.is_published = False 92 | self.entry.save() 93 | 94 | self.assertFalse(self.entry.is_published) 95 | self.assertIsNone(self.entry.published_timestamp) 96 | -------------------------------------------------------------------------------- /andablog/tests/test_sitemaps.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from django.test import TestCase 5 | from django.utils import timezone 6 | 7 | from andablog import models, sitemaps 8 | 9 | 10 | class TestEntrySitemap(TestCase): 11 | 12 | def setUp(self): 13 | self.post_1 = models.Entry.objects.create(title=u'Welcome!', is_published=True) 14 | self.post_2 = models.Entry.objects.create(title=u'Busy Busy', is_published=True) 15 | self.post_3 = models.Entry.objects.create(title=u'Last Post', is_published=True) 16 | self.post_4 = models.Entry.objects.create(title=u'Back again!') 17 | self.entry_map = sitemaps.EntrySitemap() 18 | 19 | def test_items(self): 20 | """Only published entries should be listed by descending published timestamp""" 21 | actual_entries = self.entry_map.items() 22 | 23 | expected_slugs = ['last-post', 'busy-busy', 'welcome'] 24 | actual_slugs = [entry.slug for entry in actual_entries] 25 | 26 | self.assertEqual(actual_slugs, expected_slugs) 27 | self.assertNumQueries(1) 28 | 29 | def test_last_modified(self): 30 | """Should be able to get the last update from an entry""" 31 | actual_update = self.entry_map.lastmod(self.post_3) 32 | 33 | now = timezone.now() 34 | self.assertGreaterEqual(now, actual_update) 35 | self.assertGreaterEqual(actual_update, self.post_1.modified) 36 | -------------------------------------------------------------------------------- /andablog/tests/test_templatetags.py: -------------------------------------------------------------------------------- 1 | from django.test import SimpleTestCase 2 | from django.utils.safestring import SafeString 3 | 4 | from .. templatetags import andablog_tags 5 | 6 | 7 | class TestAuthorDisplay(SimpleTestCase): 8 | 9 | def setUp(self): 10 | class MockAuthor(object): 11 | def get_short_name(self): 12 | return 'ShyGuy' 13 | self.author = MockAuthor() 14 | 15 | def test_none_link(self): 16 | """Test that our template tag returns just the short name when we get None for the URL""" 17 | def mock_get_url(): 18 | return None 19 | self.author.get_absolute_url = mock_get_url 20 | 21 | display = andablog_tags.author_display(self.author) 22 | self.assertEqual(display, 'ShyGuy') 23 | 24 | def test_empty_link(self): 25 | """Test that our template tag returns just the short name when we get "" for the URL""" 26 | def mock_get_url(): 27 | return "" 28 | self.author.get_absolute_url = mock_get_url 29 | 30 | display = andablog_tags.author_display(self.author) 31 | self.assertEqual(display, 'ShyGuy') 32 | 33 | def test_no_link_function(self): 34 | """Test that our template tag returns just the short name when we can not get the URL""" 35 | display = andablog_tags.author_display(self.author) 36 | self.assertEqual(display, 'ShyGuy') 37 | 38 | def test_profile_display(self): 39 | """Test that our template tag returns the short name hyperlinked to the URL""" 40 | def mock_get_url(): 41 | return 'http://example.com/profile/shyguy' 42 | self.author.get_absolute_url = mock_get_url 43 | 44 | display = andablog_tags.author_display(self.author) 45 | self.assertEqual(display, SafeString('ShyGuy')) 46 | 47 | def test_no_short_name(self): 48 | """Test that our template tag uses the unicode representation when there is no short name""" 49 | class MockAuthor(object): 50 | pass 51 | self.author = MockAuthor() 52 | 53 | display = andablog_tags.author_display(self.author) 54 | self.assertIn('MockAuthor object', display) 55 | -------------------------------------------------------------------------------- /andablog/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from . import views 4 | 5 | app_name = 'andablog' 6 | urlpatterns = [ 7 | url('^$', views.EntriesList.as_view(), name='entrylist'), 8 | url(r'^(?P[A-Za-z0-9-_]+)/$', views.EntryDetail.as_view(), name='entrydetail'), 9 | ] -------------------------------------------------------------------------------- /andablog/views.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Q 2 | from django.views.generic import ListView, DetailView 3 | 4 | from . import models 5 | 6 | 7 | class EntriesList(ListView): 8 | 9 | model = models.Entry 10 | template_name = 'andablog/entry_list.html' 11 | context_object_name = 'entries' 12 | paginate_by = 10 13 | paginate_orphans = 5 14 | 15 | def get_queryset(self): 16 | queryset = super(EntriesList, self).get_queryset().filter( 17 | Q(is_published=True) | Q(author__isnull=False, author=self.request.user.id)) 18 | return queryset.order_by('is_published', '-published_timestamp') # Put 'drafts' first. 19 | 20 | 21 | class EntryDetail(DetailView): 22 | 23 | model = models.Entry 24 | template_name = 'andablog/entry_detail.html' 25 | context_object_name = 'entry' 26 | slug_field = 'slug' 27 | 28 | def get_queryset(self): 29 | return super(EntryDetail, self).get_queryset().filter( 30 | Q(is_published=True) | Q(author__isnull=False, author=self.request.user.id)) 31 | -------------------------------------------------------------------------------- /build.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import os 3 | 4 | import pyntofdjango 5 | MODULE_PATH = os.path.abspath(__file__) 6 | pyntofdjango.setup_pod(MODULE_PATH) 7 | 8 | from pynt import task 9 | from pyntofdjango.tasks import python, pip, clean, delete_venv, create_venv, recreate_venv, manage, test_tox, \ 10 | runserver, dumpdata, migrate, docs, venv_bin 11 | from pyntofdjango import utils, project, paths 12 | from pyntcontrib import safe_cd 13 | 14 | 15 | @task() 16 | def test_venv(): 17 | """Runs all tests on venv""" 18 | with safe_cd('demo'): 19 | project.execute_manage('test', 'andablog') 20 | project.execute_manage('test') 21 | 22 | @task() 23 | def loadalldatas(): 24 | """Loads all demo fixtures.""" 25 | dependency_order = ['common', 'profiles', 'blog', 'democomments'] 26 | for app in dependency_order: 27 | project.recursive_load(os.path.join(paths.project_paths.manage_root, app)) 28 | 29 | 30 | @task() 31 | def reset_db(): 32 | """Recreates the development db""" 33 | project.execute_manage('reset_db', '--noinput') 34 | 35 | 36 | @task() 37 | def rebuild_db(): 38 | """Wipes, migrates and loads fixtures""" 39 | reset_db() 40 | migrate() 41 | loadalldatas() 42 | 43 | 44 | @task() 45 | def rundocserver(): 46 | """Runs the sphinx-autobuild server""" 47 | with safe_cd('docs'): 48 | project.venv_execute('sphinx-autobuild', '.', '_build/html') 49 | -------------------------------------------------------------------------------- /demo/blog/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramblin-dev/django-andablog/e15b9af397d05ab3fd1649020a798df3b7df5041/demo/blog/__init__.py -------------------------------------------------------------------------------- /demo/blog/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /demo/blog/feeds.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse_lazy 2 | from andablog.feeds import LatestEntriesFeed 3 | 4 | 5 | class LatestBlogEntries(LatestEntriesFeed): 6 | feed_copyright = 'Example Org' 7 | title = 'Latest Blog Entries' 8 | description = 'Updates on the latest blog entries from example.com.' 9 | link = reverse_lazy('andablog:entrylist') 10 | -------------------------------------------------------------------------------- /demo/blog/fixtures/tags_for_three_entries.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "fields": { 4 | "name": "goodbye", 5 | "slug": "goodbye" 6 | }, 7 | "model": "taggit.tag", 8 | "pk": 2 9 | }, 10 | { 11 | "fields": { 12 | "name": "your thoughts are mine", 13 | "slug": "your-thoughts-are-mine" 14 | }, 15 | "model": "taggit.tag", 16 | "pk": 3 17 | }, 18 | { 19 | "fields": { 20 | "name": "hello", 21 | "slug": "hello" 22 | }, 23 | "model": "taggit.tag", 24 | "pk": 4 25 | }, 26 | { 27 | "fields": { 28 | "name": "cliche", 29 | "slug": "cliche" 30 | }, 31 | "model": "taggit.tag", 32 | "pk": 6 33 | }, 34 | { 35 | "fields": { 36 | "tag": 6, 37 | "content_type": 16, 38 | "object_id": 1 39 | }, 40 | "model": "taggit.taggeditem", 41 | "pk": 7 42 | }, 43 | { 44 | "fields": { 45 | "tag": 3, 46 | "content_type": 16, 47 | "object_id": 2 48 | }, 49 | "model": "taggit.taggeditem", 50 | "pk": 8 51 | }, 52 | { 53 | "fields": { 54 | "tag": 4, 55 | "content_type": 16, 56 | "object_id": 2 57 | }, 58 | "model": "taggit.taggeditem", 59 | "pk": 9 60 | }, 61 | { 62 | "fields": { 63 | "tag": 6, 64 | "content_type": 16, 65 | "object_id": 2 66 | }, 67 | "model": "taggit.taggeditem", 68 | "pk": 10 69 | }, 70 | { 71 | "fields": { 72 | "tag": 2, 73 | "content_type": 16, 74 | "object_id": 3 75 | }, 76 | "model": "taggit.taggeditem", 77 | "pk": 11 78 | }, 79 | { 80 | "fields": { 81 | "tag": 6, 82 | "content_type": 16, 83 | "object_id": 3 84 | }, 85 | "model": "taggit.taggeditem", 86 | "pk": 12 87 | } 88 | ] 89 | -------------------------------------------------------------------------------- /demo/blog/fixtures/three_published_entries.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "fields": { 4 | "author": 6, 5 | "title": "Busy busy...", 6 | "created": "2014-10-11T13:00:48.992Z", 7 | "published_timestamp": "2014-10-11T13:00:48.994Z", 8 | "modified": "2014-11-03T21:32:18.659Z", 9 | "content": "Sorry it has been a long time since my last post. I have been really busy watching TV, playing video games and drinking beer. ![busy worm](/media/andablog/images/inchworm.png \"busy worm\")", 10 | "preview_content": "A post full of excuses. Helpful for me (the author) but not you (the reader). Thanks!", 11 | "preview_image": "", 12 | "_preview_content_rendered": "

A post full of excuses. Helpful for me (the author) but not you (the reader). Thanks!

", 13 | "_content_rendered": "

Sorry it has been a long time since my last post. I have been really busy watching TV, playing video games and drinking beer. \"busy

", 14 | "slug": "busy-busy", 15 | "is_published": true 16 | }, 17 | "model": "andablog.entry", 18 | "pk": 1 19 | }, 20 | { 21 | "fields": { 22 | "author": 6, 23 | "title": "Welcome!", 24 | "created": "2014-10-11T12:57:03.807Z", 25 | "published_timestamp": "2014-10-11T12:57:03.809Z", 26 | "modified": "2014-11-03T21:23:40.375Z", 27 | "content": "Hi, with this blog I intend to share my personal details and those of my friends. Sign up if you would like your personal details divulged as well.\r\n\r\n![guy waving welcome](/media/andablog/images/welcome.png \"welcome\")", 28 | "_content_rendered": "

Hi, with this blog I intend to share my personal details and those of my friends. Sign up if you would like your personal details divulged as well.

\n

\"guy

", 29 | "slug": "welcome", 30 | "is_published": true 31 | }, 32 | "model": "andablog.entry", 33 | "pk": 2 34 | }, 35 | { 36 | "fields": { 37 | "author": 6, 38 | "title": "Last post", 39 | "created": "2014-10-11T13:14:15.201Z", 40 | "published_timestamp": "2014-10-11T13:14:15.204Z", 41 | "modified": "2014-11-03T21:40:33.922Z", 42 | "content": "Hey everyone, I have recently received a heart felt letter from a Nigerian prince. \r\n\r\n![crown](/media/andablog/images/crown.png \"crown\")\r\n\r\nThe letter contained a plea for monetary help in claiming his late father's throne from his estranged brother. Included was a promise that I will receive a large amount of money in thanks once the throne is secure. Not only did I immediately send him the required funds but I have also decided to liquidate my assets in order to travel to Nigeria so that I may personally assist with the succession.\r\n\r\n![Rich and struting](/media/andablog/images/rich_strut.png \"Rich and struting\")\r\n\r\nAs a result, I fear I won't have time for this blog any more. I wish you all the best of luck. ", 43 | "_content_rendered": "

Hey everyone, I have recently received a heart felt letter from a Nigerian prince.

\n

\"crown\"

\n

The letter contained a plea for monetary help in claiming his late father's throne from his estranged brother. Included was a promise that I will receive a large amount of money in thanks once the throne is secure. Not only did I immediately send him the required funds but I have also decided to liquidate my assets in order to travel to Nigeria so that I may personally assist with the succession.

\n

\"Rich

\n

As a result, I fear I won't have time for this blog any more. I wish you all the best of luck.

", 44 | "preview_content": "A story of a neglected work of art and a new fortune discovered. Inside you will find:\r\n\r\n- A new opportunity\r\n- A farewell", 45 | "preview_image": "andablog/images/SendOff-300px.png", 46 | "_preview_content_rendered": "

A story of a neglected work of art and a new fortune discovered. Inside you will find:

\n
    \n
  • A new opportunity
  • \n
  • A farewell
  • \n
", 47 | "slug": "last-post", 48 | "is_published": true 49 | }, 50 | "model": "andablog.entry", 51 | "pk": 3 52 | }, 53 | { 54 | "fields": { 55 | "entry": 2, 56 | "image": "andablog/images/welcome.png", 57 | "modified": "2014-11-03T21:23:15.595Z", 58 | "created": "2014-11-03T21:23:15.550Z" 59 | }, 60 | "model": "andablog.entryimage", 61 | "pk": 2 62 | }, 63 | { 64 | "fields": { 65 | "entry": 1, 66 | "image": "andablog/images/inchworm.png", 67 | "modified": "2014-11-03T21:31:19.453Z", 68 | "created": "2014-11-03T21:31:19.408Z" 69 | }, 70 | "model": "andablog.entryimage", 71 | "pk": 3 72 | }, 73 | { 74 | "fields": { 75 | "entry": 3, 76 | "image": "andablog/images/crown.png", 77 | "modified": "2014-11-03T21:39:32.171Z", 78 | "created": "2014-11-03T21:39:32.154Z" 79 | }, 80 | "model": "andablog.entryimage", 81 | "pk": 4 82 | }, 83 | { 84 | "fields": { 85 | "entry": 3, 86 | "image": "andablog/images/rich_strut.png", 87 | "modified": "2014-11-03T21:39:32.177Z", 88 | "created": "2014-11-03T21:39:32.156Z" 89 | }, 90 | "model": "andablog.entryimage", 91 | "pk": 5 92 | } 93 | ] 94 | -------------------------------------------------------------------------------- /demo/blog/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramblin-dev/django-andablog/e15b9af397d05ab3fd1649020a798df3b7df5041/demo/blog/migrations/__init__.py -------------------------------------------------------------------------------- /demo/blog/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /demo/blog/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | Since we rely on an underlying blog engine we are primarily 3 | testing the integration points of the chosen blog engine. E.g. 4 | * Are the URLs from the blog engine hooked up correctly? 5 | * Are we including the blog data into the sitemap? 6 | * Are we exposing a blog feed? 7 | """ 8 | from django.urls import reverse 9 | from django.test import TestCase 10 | 11 | from andablog import models as blogmodels 12 | 13 | from .feeds import LatestBlogEntries 14 | 15 | 16 | class EntryListingTests(TestCase): 17 | """Posts or not, we just want to make sure we are hooking this up properly""" 18 | 19 | def setUp(self): 20 | self.url = reverse('andablog:entrylist') 21 | 22 | def test_rendering(self): 23 | """The listing should render properly""" 24 | response = self.client.get(self.url) 25 | 26 | self.assertEqual(response.status_code, 200) 27 | 28 | 29 | class LatestEntriesFeed(TestCase): 30 | """We want to make sure we hooked up the entry feed properly""" 31 | 32 | fixtures = ['three_users', 'three_profiles', 'three_published_entries'] 33 | 34 | def setUp(self): 35 | self.feed = LatestBlogEntries() 36 | 37 | def test_author_details(self): 38 | """Test that our custom user is integrating properly""" 39 | an_entry = blogmodels.Entry.objects.get(slug='last-post') 40 | 41 | expected = ( 42 | an_entry.published_timestamp, 43 | an_entry.author.profile_name, 44 | an_entry.author.email, 45 | an_entry.author.get_absolute_url() 46 | ) 47 | 48 | actual = ( 49 | self.feed.item_pubdate(an_entry), 50 | self.feed.item_author_name(an_entry), 51 | self.feed.item_author_email(an_entry), 52 | self.feed.item_author_link(an_entry), 53 | ) 54 | 55 | self.assertEqual(expected, actual) 56 | 57 | def test_url(self): 58 | """Should be able to get the feed items by URL""" 59 | url = reverse('blog-entry-feed') 60 | 61 | response = self.client.get(url) 62 | self.assertEqual(response.status_code, 200) 63 | 64 | 65 | class TestAuthorEntryListing(TestCase): 66 | """An author looking at the entry listing""" 67 | 68 | fixtures = ['three_users', 'three_profiles', 'three_published_entries'] 69 | 70 | def setUp(self): 71 | an_entry = blogmodels.Entry.objects.get(slug='last-post') 72 | an_entry.published_timestamp = None 73 | an_entry.is_published = False 74 | an_entry.save() 75 | 76 | self.client.login(username='agent0014@example.com', password='secret') 77 | 78 | self.url = reverse('andablog:entrylist') 79 | 80 | def test_draft_listed(self): 81 | """Our author should see the draft entry.""" 82 | response = self.client.get(self.url) 83 | 84 | expected_slugs = ['last-post', 'busy-busy', 'welcome'] 85 | actual_slugs = [entry.slug for entry in response.context['entries']] 86 | 87 | self.assertEqual(response.status_code, 200) 88 | self.assertEqual(actual_slugs, expected_slugs) 89 | self.assertNumQueries(1) 90 | 91 | 92 | class TestAuthorEntryDetail(TestCase): 93 | """An author looking at the entry detail""" 94 | 95 | fixtures = ['three_users', 'three_profiles', 'three_published_entries'] 96 | 97 | def setUp(self): 98 | self.an_entry = blogmodels.Entry.objects.get(slug='last-post') 99 | self.an_entry.published_timestamp = None 100 | self.an_entry.is_published = False 101 | self.an_entry.save() 102 | 103 | self.client.login(username='agent0014@example.com', password='secret') 104 | 105 | self.url = reverse('andablog:entrydetail', args=[self.an_entry.slug]) 106 | 107 | def test_draft_detail(self): 108 | """Our author should be able to view the draft entry.""" 109 | response = self.client.get(self.url) 110 | 111 | self.assertEqual(response.status_code, 200) 112 | self.assertEqual(self.an_entry.slug, response.context['entry'].slug) 113 | self.assertNumQueries(1) 114 | 115 | -------------------------------------------------------------------------------- /demo/blog/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | from .feeds import LatestBlogEntries 3 | 4 | urlpatterns = [ 5 | url(r'^latest/entries/$', LatestBlogEntries(), name='blog-entry-feed'), 6 | url(r'^', include('andablog.urls', namespace='andablog')), 7 | ] -------------------------------------------------------------------------------- /demo/blog/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /demo/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramblin-dev/django-andablog/e15b9af397d05ab3fd1649020a798df3b7df5041/demo/common/__init__.py -------------------------------------------------------------------------------- /demo/common/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from authtools.admin import UserAdmin 4 | 5 | from .models import User 6 | 7 | admin.site.register(User, UserAdmin) 8 | -------------------------------------------------------------------------------- /demo/common/fixtures/admin_user.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "fields": { 4 | "name": "Bill Lumbergh", 5 | "is_active": true, 6 | "slug": "the-boss", 7 | "is_superuser": true, 8 | "is_staff": true, 9 | "last_login": "2014-10-15T21:59:58.420Z", 10 | "groups": [], 11 | "user_permissions": [], 12 | "password": "pbkdf2_sha256$12000$xEvKElLT3Ys9$DAdBBjkTh8V67Gp7cUYefk0QHJqxHq/ggXi5jlm11Z4=", 13 | "email": "admin@example.com", 14 | "profile_name": "The Boss", 15 | "date_joined": "2014-10-15T21:59:58.420Z" 16 | }, 17 | "model": "common.user", 18 | "pk": 100 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /demo/common/fixtures/one_user.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "fields": { 4 | "name": "Clark Jerome Kent", 5 | "is_active": true, 6 | "slug": "superman", 7 | "is_superuser": false, 8 | "is_staff": false, 9 | "last_login": "2014-10-15T21:59:58.420Z", 10 | "groups": [], 11 | "user_permissions": [], 12 | "password": "pbkdf2_sha256$12000$xEvKElLT3Ys9$DAdBBjkTh8V67Gp7cUYefk0QHJqxHq/ggXi5jlm11Z4=", 13 | "email": "superman@example.com", 14 | "profile_name": "Superman", 15 | "date_joined": "2014-10-15T21:59:58.420Z" 16 | }, 17 | "model": "common.user", 18 | "pk": 4 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /demo/common/fixtures/three_users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "fields": { 4 | "name": "Clark Jerome Kent", 5 | "is_active": true, 6 | "slug": "superman", 7 | "is_superuser": false, 8 | "is_staff": false, 9 | "last_login": "2014-10-15T21:59:58.420Z", 10 | "groups": [], 11 | "user_permissions": [], 12 | "password": "pbkdf2_sha256$12000$xEvKElLT3Ys9$DAdBBjkTh8V67Gp7cUYefk0QHJqxHq/ggXi5jlm11Z4=", 13 | "email": "superman@example.com", 14 | "profile_name": "Superman", 15 | "date_joined": "2014-10-15T21:59:58.420Z" 16 | }, 17 | "model": "common.user", 18 | "pk": 4 19 | }, 20 | { 21 | "fields": { 22 | "name": "Diana Prince", 23 | "is_active": true, 24 | "slug": "wonder-woman", 25 | "is_superuser": false, 26 | "is_staff": false, 27 | "last_login": "2014-10-15T22:02:00.492Z", 28 | "groups": [], 29 | "user_permissions": [], 30 | "password": "pbkdf2_sha256$12000$DsAiaSyDq2jy$o5riydGwbMMhKk9eiRvLT6BUY4b89fgWcJk1JBy5svE=", 31 | "email": "wonderwoman@example.com", 32 | "profile_name": "Wonder Woman", 33 | "date_joined": "2014-10-15T22:02:00.492Z" 34 | }, 35 | "model": "common.user", 36 | "pk": 5 37 | }, 38 | { 39 | "fields": { 40 | "name": "Mark Edward Whitacre", 41 | "is_active": true, 42 | "slug": "agent-0014", 43 | "is_superuser": false, 44 | "is_staff": true, 45 | "last_login": "2014-10-15T22:12:27.930Z", 46 | "groups": [], 47 | "user_permissions": [ 48 | 43, 49 | 44, 50 | 45, 51 | 46, 52 | 47, 53 | 48 54 | ], 55 | "password": "pbkdf2_sha256$12000$OaNyUE9FBwbC$93o+uMiPW1/HIdNrkIG8wmvRdWaVr2szInyCBc7O/6g=", 56 | "email": "agent0014@example.com", 57 | "profile_name": "Agent 0014", 58 | "date_joined": "2014-10-15T22:12:27.930Z" 59 | }, 60 | "model": "common.user", 61 | "pk": 6 62 | } 63 | ] 64 | -------------------------------------------------------------------------------- /demo/common/forms.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramblin-dev/django-andablog/e15b9af397d05ab3fd1649020a798df3b7df5041/demo/common/forms.py -------------------------------------------------------------------------------- /demo/common/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramblin-dev/django-andablog/e15b9af397d05ab3fd1649020a798df3b7df5041/demo/common/management/__init__.py -------------------------------------------------------------------------------- /demo/common/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramblin-dev/django-andablog/e15b9af397d05ab3fd1649020a798df3b7df5041/demo/common/management/commands/__init__.py -------------------------------------------------------------------------------- /demo/common/management/commands/check_missing_migrations.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from django.apps import apps 4 | from django.conf import settings 5 | from django.core.management.base import BaseCommand 6 | from django.db import connections 7 | from django.db.migrations.autodetector import MigrationAutodetector 8 | from django.db.migrations.executor import MigrationExecutor 9 | from django.db.migrations.state import ProjectState 10 | from django.db.utils import OperationalError 11 | 12 | 13 | class Command(BaseCommand): 14 | """ 15 | Detect if any apps have missing migration files 16 | 17 | (not necessaily applied though) 18 | """ 19 | help = "Detect if any apps have missing migration files" 20 | 21 | def handle(self, *args, **kwargs): 22 | 23 | changed = set() 24 | ignore_list = ['authtools'] # dependencies that we don't care about migrations for (usually for testing only) 25 | 26 | self.stdout.write("Checking...") 27 | for db in settings.DATABASES.keys(): 28 | 29 | try: 30 | executor = MigrationExecutor(connections[db]) 31 | except OperationalError: 32 | sys.exit("Unable to check migrations: cannot connect to database\n") 33 | 34 | autodetector = MigrationAutodetector( 35 | executor.loader.project_state(), 36 | ProjectState.from_apps(apps), 37 | ) 38 | 39 | changed.update(autodetector.changes(graph=executor.loader.graph).keys()) 40 | 41 | for ignore in ignore_list: 42 | if ignore in changed: 43 | changed.remove(ignore) 44 | 45 | if changed: 46 | sys.exit("Apps with model changes but no corresponding migration file: %(changed)s\n" % { 47 | 'changed': list(changed) 48 | }) 49 | else: 50 | sys.stdout.write("All migration files present\n") 51 | -------------------------------------------------------------------------------- /demo/common/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import django.utils.timezone 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('auth', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='User', 17 | fields=[ 18 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 19 | ('password', models.CharField(max_length=128, verbose_name='password')), 20 | ('last_login', models.DateTimeField(default=django.utils.timezone.now, verbose_name='last login')), 21 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 22 | ('email', models.EmailField(unique=True, max_length=255, verbose_name='email address', db_index=True)), 23 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 24 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 25 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 26 | ('name', models.CharField(max_length=100)), 27 | ('profile_name', models.CharField(unique=True, max_length=20, verbose_name=b'profile name')), 28 | ('slug', models.SlugField(unique=True)), 29 | ('groups', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Group', blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of his/her group.', verbose_name='groups')), 30 | ('user_permissions', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Permission', blank=True, help_text='Specific permissions for this user.', verbose_name='user permissions')), 31 | ], 32 | options={ 33 | 'ordering': ['email'], 34 | 'abstract': False, 35 | }, 36 | bases=(models.Model,), 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /demo/common/migrations/0002_auto_20150507_1708.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | from django import VERSION as DJANGO_VERSION 6 | 7 | 8 | def get_operations(): 9 | """ 10 | This will break things if you upgrade Django to 1.8 having already applied this migration in 1.7. 11 | Since this is for a demo site it doesn't really matter (simply blow away the DB if you want to go to 1.8) 12 | 13 | Our demo site is a unusual in that we want to run it's tests (for integration testing) in multiple Django versions. 14 | Typical sites don't have to worry about that sort of thing. 15 | """ 16 | compatible = (1, 8) <= DJANGO_VERSION < (1, 10) 17 | if not compatible: 18 | return [] 19 | 20 | return [ 21 | migrations.AlterField( 22 | model_name='user', 23 | name='groups', 24 | field=models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Group', blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', verbose_name='groups'), 25 | ), 26 | migrations.AlterField( 27 | model_name='user', 28 | name='last_login', 29 | field=models.DateTimeField(null=True, verbose_name='last login', blank=True), 30 | ), 31 | ] 32 | 33 | 34 | class Migration(migrations.Migration): 35 | 36 | dependencies = [ 37 | ('common', '0001_initial'), 38 | ] 39 | 40 | operations = get_operations() -------------------------------------------------------------------------------- /demo/common/migrations/0003_auto_20150831_2043.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('common', '0002_auto_20150507_1708'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='user', 16 | name='profile_name', 17 | field=models.CharField(verbose_name='profile name', max_length=20, unique=True), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /demo/common/migrations/0004_auto_20151208_0047.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9 on 2015-12-08 00:47 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('common', '0003_auto_20150831_2043'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='user', 17 | name='email', 18 | field=models.EmailField(max_length=255, unique=True, verbose_name='email address'), 19 | ), 20 | migrations.AlterField( 21 | model_name='user', 22 | name='is_active', 23 | field=models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active'), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /demo/common/migrations/0005_auto_20160917_0035.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.1 on 2016-09-17 00:35 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('common', '0004_auto_20151208_0047'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='user', 17 | name='groups', 18 | field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups'), 19 | ), 20 | migrations.AlterField( 21 | model_name='user', 22 | name='last_login', 23 | field=models.DateTimeField(blank=True, null=True, verbose_name='last login'), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /demo/common/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramblin-dev/django-andablog/e15b9af397d05ab3fd1649020a798df3b7df5041/demo/common/migrations/__init__.py -------------------------------------------------------------------------------- /demo/common/models.py: -------------------------------------------------------------------------------- 1 | import six 2 | from django.db import models 3 | 4 | from authtools.models import AbstractEmailUser 5 | from django.template.defaultfilters import slugify 6 | 7 | 8 | class User(AbstractEmailUser): 9 | name = models.CharField(max_length=100) 10 | profile_name = models.CharField('profile name', max_length=20, unique=True) 11 | slug = models.SlugField(max_length=50, unique=True) 12 | 13 | REQUIRED_FIELDS = ['name', 'profile_name'] # Base already includes email 14 | 15 | def save(self, *args, **kwargs): 16 | self.slug = slugify(self.profile_name) 17 | super(User, self).save() 18 | 19 | def get_full_name(self): 20 | return self.name 21 | 22 | def get_short_name(self): 23 | """Required by And-a-Blog for displaying author of a (blog/comment)""" 24 | return self.profile_name 25 | 26 | def get_absolute_url(self): 27 | """Since it's provided And-a-Blog will use this to link to an author's profile""" 28 | return self.userprofile.get_absolute_url() 29 | 30 | def __unicode__(self): 31 | return six.text_type(self.get_short_name()) 32 | -------------------------------------------------------------------------------- /demo/common/tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from django.test import TestCase 5 | from django.utils.text import slugify 6 | 7 | from . import models 8 | 9 | 10 | class TestCustomUserModel(TestCase): 11 | 12 | def setUp(self): 13 | self.user = models.User.objects.create( 14 | name=u'Clark Jerome Kent', 15 | profile_name=u'Superman', 16 | email=u'manofsteel1938@example.com', 17 | ) 18 | 19 | def test_slug_creation(self): 20 | """The slug field should automatically get set from the profile name during user creation""" 21 | self.assertEqual(self.user.slug, slugify(self.user.profile_name)) 22 | 23 | def test_short_name(self): 24 | """The short name function should be the profile name""" 25 | self.assertEqual(self.user.profile_name, self.user.get_short_name()) 26 | -------------------------------------------------------------------------------- /demo/demo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramblin-dev/django-andablog/e15b9af397d05ab3fd1649020a798df3b7df5041/demo/demo/__init__.py -------------------------------------------------------------------------------- /demo/demo/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for demo project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.6/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/1.6/ref/settings/ 9 | """ 10 | from os.path import normpath, join 11 | 12 | from django import VERSION as DJANGO_VERSION 13 | 14 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 15 | import os 16 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 17 | 18 | # Absolute filesystem path to the top-level project folder: 19 | SITE_ROOT = os.path.dirname(os.path.join(BASE_DIR, 'demo')) 20 | 21 | # Quick-start development settings - unsuitable for production 22 | # See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/ 23 | 24 | # SECURITY WARNING: keep the secret key used in production secret! 25 | SECRET_KEY = '1g&e2*#444qj833xb2wr0%r0xj2$b_o_d_03nw&zkq%f=1xq(u' 26 | 27 | # SECURITY WARNING: don't run with debug turned on in production! 28 | DEBUG = True 29 | 30 | """ TEMPLATE CONFIGURATION """ 31 | 32 | TEMPLATES = [ 33 | { 34 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 35 | 'DIRS': [ 36 | os.path.normpath(os.path.join(SITE_ROOT, 'templates')), 37 | ], 38 | 'OPTIONS': { 39 | 'debug': True, 40 | 'context_processors': [ 41 | 'django.contrib.auth.context_processors.auth', 42 | 'django.template.context_processors.debug', 43 | 'django.template.context_processors.i18n', 44 | 'django.template.context_processors.media', 45 | 'django.template.context_processors.static', 46 | 'django.template.context_processors.tz', 47 | 'django.contrib.messages.context_processors.messages', 48 | 'django.template.context_processors.request', # Required for django comment next url, allauth 49 | ], 50 | 'loaders': [ 51 | 'django.template.loaders.filesystem.Loader', 52 | 'django.template.loaders.app_directories.Loader', 53 | ] 54 | }, 55 | }, 56 | ] 57 | 58 | ########## END TEMPLATE CONFIGURATION 59 | 60 | ALLOWED_HOSTS = [] 61 | 62 | """ APP CONFIGURATION """ 63 | DJANGO_APPS = ( 64 | 'django.contrib.admin', 65 | 'django.contrib.auth', 66 | 'django.contrib.contenttypes', 67 | 'django.contrib.sessions', 68 | 'django.contrib.messages', 69 | 'django.contrib.staticfiles', 70 | 'django.contrib.sites', # Required for comments and allauth 71 | 'django.contrib.sitemaps', 72 | ) 73 | 74 | THIRD_PARTY_APPS = ( 75 | 'authtools', # Custom User classes 76 | 'allauth', # Login using email 77 | 'allauth.account', 78 | 'allauth.socialaccount', 79 | 'andablog', # The blog engine 80 | 'django_extensions', # Misc Tools, we use it for the handy sql reset 81 | 'django_comments', # Replacement for Django contrib comments 82 | 'bootstrapform', # Required for bootstrap templates 83 | "taggit", # Blog req: For tags 84 | ) 85 | 86 | THIRD_PARTY_APPS += ('debug_toolbar.apps.DebugToolbarConfig',) 87 | 88 | # Apps specific for this project go here. 89 | LOCAL_APPS = ( 90 | 'common', 91 | 'profiles', 92 | 'democomments', 93 | 'blog', 94 | ) 95 | 96 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps 97 | INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS 98 | ########## END APP CONFIGURATION 99 | 100 | MIDDLEWARE = ( 101 | 'debug_toolbar.middleware.DebugToolbarMiddleware', 102 | 'django.contrib.sessions.middleware.SessionMiddleware', 103 | 'django.middleware.common.CommonMiddleware', 104 | 'django.middleware.csrf.CsrfViewMiddleware', 105 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 106 | 'django.contrib.messages.middleware.MessageMiddleware', 107 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 108 | ) 109 | 110 | AUTHENTICATION_BACKENDS = ( 111 | "django.contrib.auth.backends.ModelBackend", # Needed to login by username in Django admin 112 | # `allauth` specific authentication methods, such as login by e-mail 113 | "allauth.account.auth_backends.AuthenticationBackend", 114 | ) 115 | 116 | ROOT_URLCONF = 'demo.urls' 117 | 118 | WSGI_APPLICATION = 'demo.wsgi.application' 119 | 120 | 121 | # Database 122 | # https://docs.djangoproject.com/en/1.6/ref/settings/#databases 123 | 124 | DATABASES = { 125 | 'default': { 126 | 'ENGINE': 'django.db.backends.sqlite3', 127 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 128 | } 129 | } 130 | 131 | # Internationalization 132 | # https://docs.djangoproject.com/en/1.6/topics/i18n/ 133 | 134 | LANGUAGE_CODE = 'en-us' 135 | 136 | TIME_ZONE = 'UTC' 137 | 138 | USE_I18N = True 139 | 140 | USE_L10N = True 141 | 142 | USE_TZ = True 143 | 144 | """ MEDIA CONFIGURATION """ 145 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#media-root 146 | MEDIA_ROOT = normpath(join(SITE_ROOT, 'media')) 147 | 148 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#media-url 149 | MEDIA_URL = '/media/' 150 | ########## END MEDIA CONFIGURATION 151 | 152 | """ STATIC FILE CONFIGURATION """ 153 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url 154 | STATIC_URL = '/static/' 155 | 156 | # See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS 157 | STATICFILES_DIRS = ( 158 | os.path.normpath(os.path.join(SITE_ROOT, 'static')), 159 | ) 160 | 161 | # See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders 162 | STATICFILES_FINDERS = ( 163 | 'django.contrib.staticfiles.finders.FileSystemFinder', 164 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 165 | ) 166 | 167 | DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' 168 | 169 | STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage' 170 | ########## END STATIC FILE CONFIGURATION 171 | 172 | """ CUSTOM USER CONFIGURATION """ 173 | AUTH_USER_MODEL = 'common.User' 174 | 175 | """ All Auth """ 176 | ACCOUNT_AUTHENTICATION_METHOD = 'email' 177 | ACCOUNT_EMAIL_REQUIRED = True 178 | ACCOUNT_UNIQUE_EMAIL = True 179 | ACCOUNT_USERNAME_REQUIRED = False 180 | ACCOUNT_USER_MODEL_USERNAME_FIELD = None 181 | ACCOUNT_USER_MODEL_EMAIL_FIELD = "email" 182 | ACCOUNT_USER_DISPLAY = lambda user: user.get_short_name() 183 | ACCOUNT_EMAIL_VERIFICATION = "optional" 184 | 185 | # Django Auth 186 | LOGIN_REDIRECT_URL = '/profile/' 187 | 188 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#site-id 189 | SITE_ID = 1 190 | 191 | """ Comments Configuration """ 192 | COMMENTS_APP = 'democomments' 193 | -------------------------------------------------------------------------------- /demo/demo/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls import include, url 3 | from django.views.generic import TemplateView 4 | from django.contrib import admin 5 | from django.conf.urls.static import static 6 | from django.contrib.sitemaps.views import sitemap 7 | 8 | from andablog.sitemaps import EntrySitemap 9 | 10 | from profiles.sitemaps import UserProfileSitemap 11 | 12 | admin.autodiscover() 13 | 14 | sitemaps = { 15 | 'profiles': UserProfileSitemap, 16 | 'blog': EntrySitemap, 17 | } 18 | 19 | urlpatterns = [ 20 | url(r'^$', TemplateView.as_view(template_name='home.html')), 21 | url(r'^accounts/', include('allauth.urls')), # All Auth 22 | url(r'^blog/', include('blog.urls')), 23 | url(r'^profile/', include('profiles.urls')), 24 | url(r'^admin/', admin.site.urls), 25 | url(r'^comments/', include('django_comments.urls')), 26 | 27 | url(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps}), 28 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) # Static media hosting in debug mode 29 | 30 | if settings.DEBUG: 31 | import debug_toolbar 32 | urlpatterns.append(url(r'^__debug__/', include(debug_toolbar.urls))) 33 | -------------------------------------------------------------------------------- /demo/demo/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for demo project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.6/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings") 12 | 13 | from django.core.wsgi import get_wsgi_application 14 | application = get_wsgi_application() 15 | -------------------------------------------------------------------------------- /demo/democomments/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | NOTE: Can not import models at module level because all app model loading must first be finished. 3 | """ 4 | 5 | def get_model(): 6 | from . import models 7 | return models.DemoComment 8 | 9 | 10 | def get_form(): 11 | from . import forms 12 | return forms.DemoCommentForm 13 | -------------------------------------------------------------------------------- /demo/democomments/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import DemoComment 4 | 5 | 6 | class DemoCommentAdmin(admin.ModelAdmin): 7 | list_display = ( 8 | 'user', 9 | 'submit_date', 10 | ) 11 | search_fields = ( 12 | 'user__profile_name', 13 | 'user__email', 14 | 'comment', 15 | ) 16 | 17 | admin.site.register(DemoComment, DemoCommentAdmin) 18 | -------------------------------------------------------------------------------- /demo/democomments/fixtures/four_comments.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "fields": { 4 | "comment": "Great Krypton!", 5 | "user_url": "", 6 | "submit_date": "2014-11-03T21:48:03.582Z", 7 | "ip_address": "127.0.0.1", 8 | "object_pk": "2", 9 | "site": 1, 10 | "is_removed": false, 11 | "user": 4, 12 | "content_type": 13, 13 | "is_public": true, 14 | "user_name": "Clark Jerome Kent", 15 | "user_email": "superman@example.com" 16 | }, 17 | "model": "django_comments.comment", 18 | "pk": 1 19 | }, 20 | { 21 | "fields": { 22 | "comment": "Truth, Justice and the American Way.", 23 | "user_url": "", 24 | "submit_date": "2014-11-03T21:48:50.645Z", 25 | "ip_address": "127.0.0.1", 26 | "object_pk": "1", 27 | "site": 1, 28 | "is_removed": false, 29 | "user": 4, 30 | "content_type": 13, 31 | "is_public": true, 32 | "user_name": "Clark Jerome Kent", 33 | "user_email": "superman@example.com" 34 | }, 35 | "model": "django_comments.comment", 36 | "pk": 2 37 | }, 38 | { 39 | "fields": { 40 | "comment": "I am Diana, princess of the Amazons.", 41 | "user_url": "", 42 | "submit_date": "2014-11-03T21:49:44.333Z", 43 | "ip_address": "127.0.0.1", 44 | "object_pk": "2", 45 | "site": 1, 46 | "is_removed": false, 47 | "user": 5, 48 | "content_type": 13, 49 | "is_public": true, 50 | "user_name": "Diana Prince", 51 | "user_email": "wonderwoman@example.com" 52 | }, 53 | "model": "django_comments.comment", 54 | "pk": 3 55 | }, 56 | { 57 | "fields": { 58 | "comment": "Merciful Minerva.", 59 | "user_url": "", 60 | "submit_date": "2014-11-03T21:51:16.084Z", 61 | "ip_address": "127.0.0.1", 62 | "object_pk": "1", 63 | "site": 1, 64 | "is_removed": false, 65 | "user": 5, 66 | "content_type": 13, 67 | "is_public": true, 68 | "user_name": "Diana Prince", 69 | "user_email": "wonderwoman@example.com" 70 | }, 71 | "model": "django_comments.comment", 72 | "pk": 4 73 | } 74 | ] 75 | -------------------------------------------------------------------------------- /demo/democomments/forms.py: -------------------------------------------------------------------------------- 1 | from django_comments.forms import CommentForm 2 | from django import forms 3 | 4 | from .models import DemoComment 5 | 6 | 7 | class DemoCommentForm(CommentForm): 8 | 9 | def __init__(self, *args, **kwargs): 10 | super(DemoCommentForm, self).__init__(*args, **kwargs) 11 | self.fields['comment'].widget = forms.Textarea(attrs={'rows': 2}) 12 | 13 | def get_comment_model(self): 14 | return DemoComment 15 | -------------------------------------------------------------------------------- /demo/democomments/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9 on 2015-12-07 23:17 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | ('contenttypes', '0002_remove_content_type_name'), 16 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 17 | ('sites', '0001_initial'), 18 | ] 19 | 20 | operations = [ 21 | migrations.CreateModel( 22 | name='DemoComment', 23 | fields=[ 24 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 25 | ('object_pk', models.TextField(verbose_name='object ID')), 26 | ('user_name', models.CharField(blank=True, max_length=50, verbose_name="user's name")), 27 | ('user_email', models.EmailField(blank=True, max_length=254, verbose_name="user's email address")), 28 | ('user_url', models.URLField(blank=True, verbose_name="user's URL")), 29 | ('comment', models.TextField(max_length=3000, verbose_name='comment')), 30 | ('submit_date', models.DateTimeField(default=None, verbose_name='date/time submitted')), 31 | ('ip_address', models.GenericIPAddressField(blank=True, null=True, unpack_ipv4=True, verbose_name='IP address')), 32 | ('is_public', models.BooleanField(default=True, help_text='Uncheck this box to make the comment effectively disappear from the site.', verbose_name='is public')), 33 | ('is_removed', models.BooleanField(default=False, help_text='Check this box if the comment is inappropriate. A "This comment has been removed" message will be displayed instead.', verbose_name='is removed')), 34 | ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='content_type_set_for_democomment', to='contenttypes.ContentType', verbose_name='content type')), 35 | ('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.Site')), 36 | ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='democomment_comments', to=settings.AUTH_USER_MODEL, verbose_name='user')), 37 | ], 38 | options={ 39 | 'ordering': ('submit_date',), 40 | 'verbose_name': 'comment', 41 | 'verbose_name_plural': 'comments', 42 | 'permissions': [('can_moderate', 'Can moderate comments')], 43 | }, 44 | ), 45 | ] 46 | -------------------------------------------------------------------------------- /demo/democomments/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramblin-dev/django-andablog/e15b9af397d05ab3fd1649020a798df3b7df5041/demo/democomments/migrations/__init__.py -------------------------------------------------------------------------------- /demo/democomments/models.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import models 3 | from django.utils import timezone 4 | from django.utils.translation import ugettext_lazy as _ 5 | 6 | from django_comments.managers import CommentManager 7 | from django_comments.models import BaseCommentAbstractModel 8 | 9 | COMMENT_MAX_LENGTH = getattr(settings, 'COMMENT_MAX_LENGTH', 3000) 10 | 11 | 12 | class RelatedCommentManager(CommentManager): 13 | 14 | def get_queryset(self): 15 | return super(RelatedCommentManager, self).get_queryset().select_related( 16 | 'user', 17 | 'user__userprofile' 18 | ) 19 | 20 | 21 | # Ended up not using the concrete model because of this new limitation: https://code.djangoproject.com/ticket/22872 22 | # Not a big deal as this is mainly meant for a demo of a custom comment solution anyway. 23 | class DemoComment(BaseCommentAbstractModel): 24 | 25 | # Who posted this comment? If ``user`` is set then it was an authenticated 26 | # user; otherwise at least user_name should have been set and the comment 27 | # was posted by a non-authenticated user. 28 | user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user'), 29 | blank=True, null=True, related_name="%(class)s_comments", on_delete=models.CASCADE) 30 | user_name = models.CharField(_("user's name"), max_length=50, blank=True) 31 | # Explicit `max_length` to apply both to Django 1.7 and 1.8+. 32 | user_email = models.EmailField(_("user's email address"), max_length=254, 33 | blank=True) 34 | user_url = models.URLField(_("user's URL"), blank=True) 35 | 36 | comment = models.TextField(_('comment'), max_length=COMMENT_MAX_LENGTH) 37 | 38 | # Metadata about the comment 39 | submit_date = models.DateTimeField(_('date/time submitted'), default=None) 40 | ip_address = models.GenericIPAddressField(_('IP address'), unpack_ipv4=True, blank=True, null=True) 41 | is_public = models.BooleanField(_('is public'), default=True, 42 | help_text=_('Uncheck this box to make the comment effectively ' 43 | 'disappear from the site.')) 44 | is_removed = models.BooleanField(_('is removed'), default=False, 45 | help_text=_('Check this box if the comment is inappropriate. ' 46 | 'A "This comment has been removed" message will ' 47 | 'be displayed instead.')) 48 | 49 | class Meta: 50 | ordering = ('submit_date',) 51 | permissions = [("can_moderate", "Can moderate comments")] 52 | verbose_name = _('comment') 53 | verbose_name_plural = _('comments') 54 | 55 | objects = RelatedCommentManager() 56 | 57 | def __str__(self): 58 | return "%s: %s..." % (self.name, self.comment[:50]) 59 | 60 | def save(self, *args, **kwargs): 61 | if self.submit_date is None: 62 | self.submit_date = timezone.now() 63 | super(DemoComment, self).save(*args, **kwargs) 64 | 65 | def _get_userinfo(self): 66 | """ 67 | Get a dictionary that pulls together information about the poster 68 | safely for both authenticated and non-authenticated comments. 69 | 70 | This dict will have ``name``, ``email``, and ``url`` fields. 71 | """ 72 | if not hasattr(self, "_userinfo"): 73 | userinfo = { 74 | "name": self.user_name, 75 | "email": self.user_email, 76 | "url": self.user_url 77 | } 78 | if self.user_id: 79 | u = self.user 80 | if u.email: 81 | userinfo["email"] = u.email 82 | 83 | # If the user has a full name, use that for the user name. 84 | # However, a given user_name overrides the raw user.username, 85 | # so only use that if this comment has no associated name. 86 | if u.get_full_name(): 87 | userinfo["name"] = self.user.get_full_name() 88 | elif not self.user_name: 89 | userinfo["name"] = u.get_username() 90 | self._userinfo = userinfo 91 | return self._userinfo 92 | 93 | userinfo = property(_get_userinfo, doc=_get_userinfo.__doc__) 94 | 95 | def _get_name(self): 96 | return self.userinfo["name"] 97 | 98 | def _set_name(self, val): 99 | if self.user_id: 100 | raise AttributeError(_("This comment was posted by an authenticated " 101 | "user and thus the name is read-only.")) 102 | self.user_name = val 103 | 104 | name = property(_get_name, _set_name, doc="The name of the user who posted this comment") 105 | 106 | def _get_email(self): 107 | return self.userinfo["email"] 108 | 109 | def _set_email(self, val): 110 | if self.user_id: 111 | raise AttributeError(_("This comment was posted by an authenticated " 112 | "user and thus the email is read-only.")) 113 | self.user_email = val 114 | 115 | email = property(_get_email, _set_email, doc="The email of the user who posted this comment") 116 | 117 | def _get_url(self): 118 | return self.userinfo["url"] 119 | 120 | def _set_url(self, val): 121 | self.user_url = val 122 | 123 | url = property(_get_url, _set_url, doc="The URL given by the user who posted this comment") 124 | 125 | def get_absolute_url(self, anchor_pattern="#c%(id)s"): 126 | return self.get_content_object_url() + (anchor_pattern % self.__dict__) 127 | 128 | def get_as_text(self): 129 | """ 130 | Return this comment as plain text. Useful for emails. 131 | """ 132 | d = { 133 | 'user': self.user or self.name, 134 | 'date': self.submit_date, 135 | 'comment': self.comment, 136 | 'domain': self.site.domain, 137 | 'url': self.get_absolute_url() 138 | } 139 | return _('Posted by %(user)s at %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s') % d 140 | -------------------------------------------------------------------------------- /demo/democomments/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /demo/democomments/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /demo/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /demo/media/andablog/images/SendOff-300px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramblin-dev/django-andablog/e15b9af397d05ab3fd1649020a798df3b7df5041/demo/media/andablog/images/SendOff-300px.png -------------------------------------------------------------------------------- /demo/media/andablog/images/crown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramblin-dev/django-andablog/e15b9af397d05ab3fd1649020a798df3b7df5041/demo/media/andablog/images/crown.png -------------------------------------------------------------------------------- /demo/media/andablog/images/inchworm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramblin-dev/django-andablog/e15b9af397d05ab3fd1649020a798df3b7df5041/demo/media/andablog/images/inchworm.png -------------------------------------------------------------------------------- /demo/media/andablog/images/rich_strut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramblin-dev/django-andablog/e15b9af397d05ab3fd1649020a798df3b7df5041/demo/media/andablog/images/rich_strut.png -------------------------------------------------------------------------------- /demo/media/andablog/images/welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramblin-dev/django-andablog/e15b9af397d05ab3fd1649020a798df3b7df5041/demo/media/andablog/images/welcome.png -------------------------------------------------------------------------------- /demo/profiles/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramblin-dev/django-andablog/e15b9af397d05ab3fd1649020a798df3b7df5041/demo/profiles/__init__.py -------------------------------------------------------------------------------- /demo/profiles/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import UserProfile 4 | 5 | 6 | class UserProfileAdmin(admin.ModelAdmin): 7 | 8 | list_display = ( 9 | 'user', 10 | 'greatest_fear', 11 | ) 12 | search_fields = ( 13 | 'user__email', 14 | 'user__name', 15 | 'user__profile_name', 16 | 'user__slug', 17 | 'greatest_fear', 18 | 'home', 19 | ) 20 | 21 | admin.site.register(UserProfile, UserProfileAdmin) 22 | -------------------------------------------------------------------------------- /demo/profiles/fixtures/admin_profile.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "fields": { 4 | "home": "The corner office", 5 | "greatest_fear": "Working Saturdays", 6 | "user": 100 7 | }, 8 | "model": "profiles.userprofile", 9 | "pk": 100 10 | } 11 | ] 12 | -------------------------------------------------------------------------------- /demo/profiles/fixtures/one_profile.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "fields": { 4 | "home": "The Fortress of Solitude", 5 | "greatest_fear": "Kryptonite", 6 | "user": 4 7 | }, 8 | "model": "profiles.userprofile", 9 | "pk": 1 10 | } 11 | ] 12 | -------------------------------------------------------------------------------- /demo/profiles/fixtures/three_profiles.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "fields": { 4 | "home": "The Fortress of Solitude", 5 | "greatest_fear": "Kryptonite", 6 | "user": 4 7 | }, 8 | "model": "profiles.userprofile", 9 | "pk": 1 10 | }, 11 | { 12 | "fields": { 13 | "home": "Themyscira", 14 | "greatest_fear": "Bound gauntlets", 15 | "user": 5 16 | }, 17 | "model": "profiles.userprofile", 18 | "pk": 2 19 | }, 20 | { 21 | "fields": { 22 | "home": "Decatur, Illinois", 23 | "greatest_fear": "Matt Damon", 24 | "user": 6 25 | }, 26 | "model": "profiles.userprofile", 27 | "pk": 3 28 | } 29 | ] 30 | -------------------------------------------------------------------------------- /demo/profiles/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import django.utils.timezone 6 | from django.conf import settings 7 | import model_utils.fields 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='UserProfile', 19 | fields=[ 20 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 21 | ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), 22 | ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), 23 | ('greatest_fear', models.CharField(max_length=100, null=True, blank=True)), 24 | ('home', models.TextField(null=True, blank=True)), 25 | ('user', models.OneToOneField(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), 26 | ], 27 | options={ 28 | 'abstract': False, 29 | }, 30 | bases=(models.Model,), 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /demo/profiles/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramblin-dev/django-andablog/e15b9af397d05ab3fd1649020a798df3b7df5041/demo/profiles/migrations/__init__.py -------------------------------------------------------------------------------- /demo/profiles/models.py: -------------------------------------------------------------------------------- 1 | import six 2 | from django.urls import reverse 3 | from django.db import models 4 | from django.conf import settings 5 | from model_utils.models import TimeStampedModel 6 | 7 | 8 | class UserProfile(TimeStampedModel): 9 | 10 | user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) 11 | greatest_fear = models.CharField( 12 | blank=True, 13 | null=True, 14 | max_length=100, 15 | ) 16 | home = models.TextField( 17 | blank=True, 18 | null=True, 19 | ) 20 | 21 | def get_short_name(self): 22 | return self.user 23 | 24 | def get_absolute_url(self): 25 | return reverse('profile-detail', args=[str(self.user.slug)]) 26 | 27 | def __unicode__(self): 28 | return six.text_type(self.get_short_name()) 29 | -------------------------------------------------------------------------------- /demo/profiles/sitemaps.py: -------------------------------------------------------------------------------- 1 | from django.contrib.sitemaps import Sitemap 2 | 3 | from . import models 4 | 5 | 6 | class UserProfileSitemap(Sitemap): 7 | 8 | changefreq = "weekly" 9 | priority = 0.5 10 | protocol = 'http' 11 | 12 | def items(self): 13 | return models.UserProfile.objects.filter(user__is_active=True).order_by('-user__date_joined') 14 | 15 | def lastmod(self, obj): 16 | return obj.modified 17 | -------------------------------------------------------------------------------- /demo/profiles/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from . import models 4 | from . import sitemaps 5 | from django.utils import timezone 6 | 7 | 8 | class TestProfilePage(TestCase): 9 | 10 | fixtures = ['one_user', 'one_profile'] 11 | 12 | def setUp(self): 13 | self.profile = models.UserProfile.objects.get(user__slug=u'superman') 14 | self.url = self.profile.get_absolute_url() 15 | 16 | def test_anonymous_get(self): 17 | """Should be able to view a user's profile page""" 18 | response = self.client.get(self.url) 19 | 20 | self.assertEqual(response.status_code, 200) 21 | self.assertEqual(response.context['profile'], self.profile) 22 | self.assertNumQueries(1) 23 | 24 | 25 | class TestUserProfileSitemap(TestCase): 26 | 27 | fixtures = ['three_users', 'three_profiles'] 28 | 29 | def setUp(self): 30 | self.userprofile_map = sitemaps.UserProfileSitemap() 31 | self.wonder_woman = models.UserProfile.objects.get(user__slug='wonder-woman') 32 | 33 | def test_items(self): 34 | """All active user profiles should be listed, sorted by most recently joined""" 35 | # It's a trap! 36 | self.wonder_woman.user.is_active = False 37 | self.wonder_woman.user.save() 38 | 39 | actual_profiles = self.userprofile_map.items() 40 | 41 | expected_slugs = ['agent-0014', 'superman'] 42 | actual_slugs = [profile.user.slug for profile in actual_profiles] 43 | 44 | self.assertEqual(actual_slugs, expected_slugs) 45 | self.assertNumQueries(1) 46 | 47 | def test_last_modified(self): 48 | """Should be able to get the last update from a profile""" 49 | superman = models.UserProfile.objects.get(user__slug='superman') 50 | 51 | actual_update = self.userprofile_map.lastmod(self.wonder_woman) 52 | 53 | now = timezone.now() 54 | self.assertGreaterEqual(now, actual_update) 55 | self.assertGreaterEqual(actual_update, superman.modified) 56 | -------------------------------------------------------------------------------- /demo/profiles/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | url(r'^$', views.profile_redirect, name='profile-detail-redirect'), 7 | url(r'^(?P[A-Za-z0-9-_]+)/$', views.UserProfileDetail.as_view(), name='profile-detail'), 8 | ] 9 | -------------------------------------------------------------------------------- /demo/profiles/views.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from django.http import HttpResponseRedirect 3 | from django.views.generic import DetailView 4 | 5 | from . import models 6 | 7 | 8 | class UserProfileDetail(DetailView): 9 | model = models.UserProfile 10 | context_object_name = 'profile' 11 | slug_field = 'user__slug' 12 | 13 | 14 | def profile_redirect(request): 15 | if request.user.is_authenticated: 16 | url = reverse('profile-detail', args=[str(request.user.slug)]) 17 | return HttpResponseRedirect(url) 18 | -------------------------------------------------------------------------------- /demo/static/css/demo.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Globals 3 | */ 4 | 5 | body { 6 | font-family: Georgia, "Times New Roman", Times, serif; 7 | color: #555; 8 | } 9 | 10 | h1, .h1, 11 | h2, .h2, 12 | h3, .h3, 13 | h4, .h4, 14 | h5, .h5, 15 | h6, .h6 { 16 | margin-top: 0; 17 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 18 | font-weight: normal; 19 | color: #333; 20 | } 21 | 22 | 23 | /* 24 | * Override Bootstrap's default container. 25 | */ 26 | 27 | @media (min-width: 1200px) { 28 | .container { 29 | width: 970px; 30 | } 31 | } 32 | 33 | 34 | /* 35 | * Masthead for nav 36 | */ 37 | 38 | .demo-masthead { 39 | background-color: #428bca; 40 | -webkit-box-shadow: inset 0 -2px 5px rgba(0,0,0,.1); 41 | box-shadow: inset 0 -2px 5px rgba(0,0,0,.1); 42 | } 43 | 44 | /* Nav links */ 45 | .demo-nav-item { 46 | position: relative; 47 | display: inline-block; 48 | padding: 10px; 49 | font-weight: 500; 50 | color: #cdddeb; 51 | } 52 | .demo-nav-item:hover, 53 | .demo-nav-item:focus { 54 | color: #fff; 55 | text-decoration: none; 56 | } 57 | 58 | /* Active state gets a caret at the bottom */ 59 | .demo-nav .active { 60 | color: #fff; 61 | } 62 | .demo-nav .active:after { 63 | position: absolute; 64 | bottom: 0; 65 | left: 50%; 66 | width: 0; 67 | height: 0; 68 | margin-left: -5px; 69 | vertical-align: middle; 70 | content: " "; 71 | border-right: 5px solid transparent; 72 | border-bottom: 5px solid; 73 | border-left: 5px solid transparent; 74 | } 75 | 76 | /* 77 | * Footer 78 | */ 79 | 80 | .demo-footer { 81 | padding: 40px 0; 82 | color: #999; 83 | text-align: center; 84 | background-color: #f9f9f9; 85 | border-top: 1px solid #e5e5e5; 86 | } 87 | .demo-footer p:last-child { 88 | margin-bottom: 0; 89 | } 90 | 91 | /* Comments */ 92 | #id_honeypot { 93 | display: none; 94 | } 95 | -------------------------------------------------------------------------------- /demo/static/js/ie10-viewport-bug-workaround.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * IE10 viewport hack for Surface/desktop Windows 8 bug 3 | * Copyright 2014 Twitter, Inc. 4 | * Licensed under the Creative Commons Attribution 3.0 Unported License. For 5 | * details, see http://creativecommons.org/licenses/by/3.0/. 6 | */ 7 | 8 | // See the Getting Started docs for more information: 9 | // http://getbootstrap.com/getting-started/#support-ie10-width 10 | 11 | (function () { 12 | 'use strict'; 13 | if (navigator.userAgent.match(/IEMobile\/10\.0/)) { 14 | var msViewportStyle = document.createElement('style') 15 | msViewportStyle.appendChild( 16 | document.createTextNode( 17 | '@-ms-viewport{width:auto!important}' 18 | ) 19 | ) 20 | document.querySelector('head').appendChild(msViewportStyle) 21 | } 22 | })(); 23 | -------------------------------------------------------------------------------- /demo/templates/404.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /demo/templates/andablog/base.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | 4 | {% block subtitle %} 5 | {% block andablog_page_title %} 6 | {% endblock %} 7 | {% endblock %} 8 | 9 | {% block head_styles %} 10 | {% block andablog_head_extra %} 11 | 12 | {% endblock %} 13 | {% endblock %} 14 | 15 | {% block menu-blog-class %}active{% endblock %} 16 | 17 | {% block content %} 18 | {% block andablog_content %} 19 | {% endblock %} 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /demo/templates/andablog/comments_count_snippet.html: -------------------------------------------------------------------------------- 1 | {% load comments %} 2 | 3 | {% get_comment_count for comment_object as comment_count %} 4 | {% if comment_count > 0 %} 5 | ({{ comment_count }} comments) 6 | {% else %} 7 | (Comment) 8 | {% endif %} 9 | -------------------------------------------------------------------------------- /demo/templates/andablog/comments_snippet.html: -------------------------------------------------------------------------------- 1 | {% load comments %} 2 | {% load bootstrap %} 3 | 4 |
5 | 6 | {% get_comment_count for comment_object as comment_count %} 7 | {% if comment_count > 0 %} 8 |
9 |

10 | All Comments ({{ comment_count }}) 11 |

12 | 13 | {% render_comment_list for comment_object %} 14 |
15 | {% endif %} 16 | 17 |
18 | {% if user.is_authenticated %} 19 | {% get_comment_form for comment_object as form %} 20 |
21 | {% csrf_token %} 22 | {{ form.comment | bootstrap }} 23 | {{ form.honeypot }} 24 | {{ form.content_type }} 25 | {{ form.object_pk }} 26 | {{ form.timestamp }} 27 | {{ form.security_hash }} 28 | 29 | 30 |
31 | {% else %} 32 | Want to comment? Simply login as one of our users superman@example.com, wonderwoman@example.com or our blogger, agent0014@example.com. 33 | Let's just say their passwords are secret. 34 | {% endif %} 35 |
36 | -------------------------------------------------------------------------------- /demo/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load trans from i18n %} 2 | {% load static %} 3 | 4 | 5 | 6 | 7 | {% block title %}{% block subtitle %}{% endblock %} - Demo{% endblock %} 8 | 9 | 10 | 11 | 12 | {% block head_meta %} 13 | {% endblock %} 14 | 15 | 16 | 17 | 18 | 19 | 23 | 24 | 25 | 26 | {% block head_styles %} 27 | {% endblock %} 28 | 29 | {% block head_scripts %} 30 | {% endblock %} 31 | 32 | 33 | 34 |
35 |
36 | 41 |
42 |
43 | 44 |
45 | {% if messages %} 46 |
47 |

Messages:

48 |
49 |
    50 | {% for message in messages %} 51 |
  • {{message}}
  • 52 | {% endfor %} 53 |
54 |
55 |
56 | {% endif %} 57 | 58 |

59 | {% block content %}{% endblock %} 60 |

61 |
62 | 63 | 69 | 70 | {% comment %} 71 | Bootstrap core JavaScript 72 | ================================================== 73 | {% endcomment %} 74 | 75 | 76 | 77 | 78 | 79 | {% block body_scripts %}{% endblock %} 80 | 81 | 82 | -------------------------------------------------------------------------------- /demo/templates/comments/list.html: -------------------------------------------------------------------------------- 1 | {% load andablog_tags %} 2 | 3 |
    4 | {% for comment in comment_list %} 5 |
  • 6 |
    7 |

    {{ comment.user|authordisplay }} - {{ comment.submit_date|date:"SHORT_DATETIME_FORMAT" }} (direct link)

    8 | 9 |

    {{ comment.comment }}

    10 |
    11 |
  • 12 | {% endfor %} 13 |
14 | -------------------------------------------------------------------------------- /demo/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block subtitle %}Home{% endblock %} 4 | 5 | {% block menu-home-class %}active{% endblock %} 6 | 7 | {% block content %} 8 |

Comming soon!

9 |

Tired of making deals in smokey and cramped back rooms? Claustrophobic or not a smoker? Look no further.

10 |

Welcome to example.com. We are building an easy and safe place for company reps to to buy and sell customer data, fix prices or organize golf tournaments.

11 | 12 |

Where can I find updates?

13 |

You will find the latest updates on our blog.

14 | 15 | {# Geocitiesesque banner for ambiance. #} 16 |
                                                                                         .
 17 |                                                                                         ;',,,,,,:@`
 18 |                                                                                       +,,,,,,,,,,,,'`
 19 |                                                                                      ',,,,,,,,,,,,,,,+
 20 |                                                                                     :,,,,,,,,,,,,,,,,,@
 21 |                                                                                    :,,,,,,,,,,,,,,,,,,,@
 22 |                                                                                   :,,,,,,,,,;;;,,,,,,,,,@
 23 |                                                                                  :,,,,,,@@@@@@@@@@+,,,,,,@
 24 |                                                                                 :,,,,:@@@@@@###@@@@@#,,,,,@
 25 |                                                                                :,,,,@@@@;,,,,,,,,,@@@@:,,,,@
 26 |                                                                               :,,,,@@@;,,,,,,,,,,,,,@@@+,,,,@
 27 |                                                                              :,,,,@@@,,,,,,,,,,,,,,,,;@@+,,,,@
 28 |                                                                             :,,,,@@@,,,,,,,,,,,,,,,,,,:@@+,,,,@
 29 |                                                                            :,,,,@@@,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 30 |                                                                           :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 31 |                                                                          :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 32 |                                                                         :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 33 |                                                                        :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 34 |                                                                       :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 35 |                                                                      :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 36 |                                                                     :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 37 |                                                                    :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 38 |                                                                   :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 39 |                                                                  :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 40 |                                                                 :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 41 |                                                                :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 42 |                                                               :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 43 |                                                              :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 44 |                                                             :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 45 |                                                            :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 46 |                                                           :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 47 |                                                          :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 48 |                                                         :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 49 |                                                        :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 50 |                                                       :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 51 |                                                      :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 52 |                                                     :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 53 |                                                    :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 54 |                                                   :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 55 |                                                  :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 56 |                                                 :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 57 |                                                :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 58 |                                               :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 59 |                                              :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 60 |                                             :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 61 |                                            :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 62 |                                           :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 63 |                                          :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 64 |                                         :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 65 |                                        :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 66 |                                       :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 67 |                                      :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 68 |                                     :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 69 |                                    :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 70 |                                   :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 71 |                                  :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 72 |                                 :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 73 |                                :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 74 |                               :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 75 |                              :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 76 |                             :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 77 |                            :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 78 |                           :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 79 |                          :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 80 |                         :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@@,,,,@@@,,@@@#,,,#@@#,,@@@@@@@:,,,#@@@@@@@@,,@@@@@@@@+,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 81 |                        :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@@,,,,@@@,,@@@@,,,#@@#,,@@@@@@@@;,,#@@@@@@@@,,@@@@@@@@@#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 82 |                       :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@@,,,,@@@,,@@@@@,,#@@#,,@@@##@@@@,,#@@@;;;;;,,@@@@,,@@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 83 |                      :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@@,,,,@@@,,@@@@@#,#@@#,,@@@,,,@@@;,#@@@,,,,,,,@@@@,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 84 |                     :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@@,,,,@@@,,@@@@@@:#@@#,,@@@,,,@@@#,#@@@@@@@@,,@@@@'+@@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 85 |                    :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@@,,,,@@@,,@@@,@@@#@@#,,@@@,,,@@@@,#@@@@@@@@,,@@@@@@@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 86 |                   :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@@,,,,@@@,,@@@,+@@@@@#,,@@@,,,@@@#,#@@@,,,,,,,@@@@#@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 87 |                  :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@@:,,,@@@,,@@@,,@@@@@#,,@@@,,,@@@;,#@@@,,,,,,,@@@@,@@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 88 |                 :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@@@++@@@@,,@@@,,,@@@@#,,@@@@@@@@@,,#@@@@@@@@',@@@@,,@@@#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 89 |                :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@@@@@@@,,,@@@,,,'@@@#,,@@@@@@@@:,,#@@@@@@@@',@@@@,,#@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 90 |               :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@@@@@,,,,@@@,,,,@@@#,,@@@@@@#,,,,#@@@@@@@@',@@@@,,,@@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 91 |              :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 92 |             :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 93 |            :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 94 |           :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@+,,,,@
 95 |          :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@',,,,@
 96 |         :,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@:,,,,@
 97 |        ;,,,,;@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,'@@,,,,,+
 98 |       #,,,,,@@:,,,,,,,,,,,,,:@@@@@',,,,,;@@@@@;,,,,@@@,,,,@@@,,,:@@@@@',,@@@@@@@@@@',@@@@@@@@:,,@@@@,,,@@@@,,,,@@@@@#,,,@@@@@@@@@@,'@@@,,,,'@@@@@:,,,,@@@,,,,@@@,,,,,,,,,,,,@@+,,,,,
 99 |       ,,,,,;@@,,,,,,,,,,,,,;@@@@@@@@,,,@@@@@@@@@,,,@@@@,,,@@@,,;@@@@@@@+,@@@@@@@@@@',@@@@@@@@@,,@@@@,,,@@@@,,,@@@@@@@@,,@@@@@@@@@@,'@@@,,,@@@@@@@@#,,,@@@@,,,@@@,,,,,,,,,,,,,@@,,,,,#
100 |      @,,,,,@@,,,,,,,,,,,,,,@@@@;+@@@,,'@@@#;@@@@:,,@@@@',,@@@,,@@@,,:@@@,@@@@@@@@@@',@@@,,,@@@#,@@@@,,,@@@@,,@@@@''@@@+,@@@@@@@@@@,'@@@,,#@@@+;@@@@,,,@@@@:,,@@@,,,,,,,,,,,,,@@;,,,,,
101 |      :,,,,,@@,,,,,,,,,,,,,'@@@,,,'@',,@@@+,,,@@@@,,@@@@@,,@@@,,@@@@:,,,,,,,,,@@@,,,,,@@@,,,@@@@,@@@@,,,@@@@,,@@@,,,,@+,,,,,@@@',,,,'@@@,,@@@;,,,@@@@,,@@@@@,,@@@,,,,,,,,,,,,,:@@,,,,,+
102 |      ,,,,,:@@,,,,,,,,,,,,,@@@@,,,,,,,,@@@,,,,:@@@,,@@@@@@,@@@,,'@@@@@@',,,,,,@@@,,,,,@@@''#@@@:,@@@@,,,@@@@,;@@@,,,,,,,,,,,@@@',,,,'@@@,,@@@,,,,'@@@,,@@@@@@,@@@,,,,,,,,,,,,,,@@,,,,,'
103 |     :,,,,,+@',,,,,,,,,,,,,@@@#,,,,,,,,@@@,,,,,@@@,,@@@@@@'@@@,,,'@@@@@@@,,,,,@@@,,,,,@@@@@@@@',,@@@@,,,@@@@,'@@@,,,,,,,,,,,@@@',,,,'@@@,,@@@,,,,;@@@,,@@@@@@;@@@,,,,,,,,,,,,,,@@,,,,,,
104 |     +,,,,,@@,,,,,,,,,,,,,,@@@@,,,,:,,,@@@,,,,;@@@,,@@@,@@@@@@,,,,,,'@@@@,,,,,@@@,,,,,@@@'@@@@,,,@@@@,,,@@@@,;@@@,,,,;,,,,,,@@@',,,,'@@@,,@@@,,,,'@@@,,@@@,@@@@@@,,,,,,,,,,,,,,@@,,,,,,
105 |     @,,,,,@@,,,,,,,,,,,,,,'@@@,,,#@@#,@@@#,,,@@@@,,@@@,:@@@@@,,@@@,,,@@@;,,,,@@@,,,,,@@@,,@@@@,,+@@@,,,@@@#,,@@@,,,:@@@,,,,@@@',,,,'@@@,,@@@',,,@@@@,,@@@,;@@@@@,,,,,,,,,,,,,,@@,,,,,,
106 |     +,,,,,@@,,,,,,,,,,,,,,,@@@@'@@@@,,'@@@@'@@@@:,,@@@,,@@@@@,,@@@;,,@@@,,,,,@@@,,,,,@@@,,'@@@,,,@@@@'@@@@,,,@@@@'#@@@',,,,@@@',,,,'@@@,,+@@@#'@@@@,,,@@@,,@@@@@,,,,,,,,,,,,,,@@,,,,,,
107 |     :,,,,,#@;,,,,,,,,,,,,,,:@@@@@@@#,,,@@@@@@@@#,,,@@@,,,@@@@,,'@@@@@@@#,,,,,@@@,,,,,@@@,,,@@@@,,@@@@@@@@@,,,,@@@@@@@@,,,,,@@@',,,,'@@@,,,@@@@@@@@+,,,@@@,,,@@@@,,,,,,,,,,,,,,@@,,,,,,
108 |      ,,,,,;@@,,,,,,,,,,,,,,,,@@@@@;,,,,,;@@@@@:,,,,@@@,,,:@@@,,,;@@@@@;,,,,,,@@@,,,,,@@@,,,;@@@:,,;@@@@@;,,,,,,#@@@@+,,,,,,@@@',,,,'@@@,,,,;@@@@@,,,,,@@@,,,:@@@,,,,,,,,,,,,,,@@,,,,,;
109 |      ,,,,,,@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@,,,,,#
110 |      @,,,,,@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@;,,,,,
111 |       ,,,,,'@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@,,,,,+
112 |       @,,,,,@@:,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,,`
113 |        :,,,,;@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,;@@,,,,,#
114 |         ,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,:@@:,,,,#
115 |         `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@+,,,,#
116 |          `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,@,,,,,,,,,,,,,,,,,,,,,,,@;,,,,,,,,,,@,,,,,,,,,,,,,,,,,,,,,,,@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
117 |           `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,@,,,,,,,,,,,,,,,,,,,,,,,@;,,,,,,,,,,@,,,,,,,,,,,,,,,,,,,,,,,@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
118 |            `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,@,,,,,,,,,,,,,,,,,,,,,,,@;,,,,,,,,,,@,,,,,,,,,,,,,,,,,,,,,,,@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
119 |             `,,,,,@@@,,,,,,,,,,,,,,,,,,#@@@#,,@:@@@+,,,@@@@+,,,;@@@@,,@;,,@#,,,,,,@#@@@,,,,@@@@@,,,'@@@@,,@,,,@+,,,,,,@@@@:,,,@@@@',,,'@@@@,,,@:@@@;,,,,,,,,,,,,,,,,,,@@#,,,,#
120 |              `,,,,,@@@,,,,,,,,,,,,,,,,;@,,,@;,@@,,;@,,@@,,,@;,,@:,,@#,@;,@#,,,,,,,@@,,#@,,@+,,,@,,:@,,,@',@,,@+,,,,,,#@,,;@,,@@,,,@:,:@,,,@@,,@@,,+@,,,,,,,,,,,,,,,,,@@#,,,,#
121 |               `,,,,,@@@,,,,,,,,,,,,,,,@',,,',,@:,,,@,,@,,,,;@,@@,,,;:,@;@#,,,,,,,,@,,,,@,,,,,,,@;,@#,,,;:,@,@+,,,,,,,#@,,,,,,@,,,,#@,@#,,,,@,,@,,,,@,,,,,,,,,,,,,,,,@@#,,,,#
122 |                `,,,,,@@@,,,,,,,,,,,,,,@,,,,,,,@,,,,@,,@@@@@@@,@',,,,,,@@@#,,,,,,,,@,,,,@;,,'@@@@;,@;,,,,,,@@@+,,,,,,,,@@@+,,,@,,,,;@,@;,,,,@,,@,,,,@,,,,,,,,,,,,,,,@@#,,,,#
123 |                 `,,,,,@@@,,,,,,,,,,,,,@,,,,,,,@,,,,@,,@,,,,,,,@',,,,,,@#,@,,,,,,,,@,,,,@:,@@;,,@;,@;,,,,,,@+:@,,,,,,,,,,+@@:,@,,,,;@,@;,,,,@,,@,,,,@,,,,,,,,,,,,,,@@#,,,,#
124 |                  `,,,,,@@@,,,,,,,,,,,,@+,,,#@,@,,,,@,,@:,,,;#,@@,,,;@,@;,#@,,,,,,,@,,,,@,,@,,,,@;,@#,,,'@,@,,@@,,,,,,#+,,,@#,@:,,,@@,@@,,,:@,,@,,,,@,,,,,,,,,,,,,@@#,,,,#
125 |                   `,,,,,@@@,,,,,,,,,,,:@:,:@:,@,,,,@,,#@,,,@',,@;,,@',@;,,@+,,,,,,@@,,@@,,@:,,@@;,,@:,,@;,@,,,@',,,,,'@,,,@:,#@,,;@,,,@;,,@#,,@,,,,@,,,,,,,,,,,,@@#,,,,#
126 |                    `,,,,,@@@,,,,,,,,,,,;@@@;,,@,,,,@,,,'@@@',,,:@@@+,,@;,,:@,,,,,,@;@@@,,,'@@@:@@,,:@@@',,@,,,;@,,,,,,#@@@;,,,+@@@:,,,:@@@',,,@,,,,@,,,,,,,,,,,@@#,,,,#
127 |                     `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
128 |                      `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
129 |                       `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
130 |                        `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
131 |                         `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
132 |                          `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
133 |                           `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
134 |                            `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
135 |                             `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
136 |                              `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
137 |                               `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
138 |                                `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
139 |                                 `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
140 |                                  `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
141 |                                   `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
142 |                                    `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
143 |                                     `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
144 |                                      `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
145 |                                       `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
146 |                                        `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
147 |                                         `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
148 |                                          `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
149 |                                           `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
150 |                                            `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
151 |                                             `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
152 |                                              `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
153 |                                               `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
154 |                                                `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
155 |                                                 `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
156 |                                                  `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
157 |                                                   `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
158 |                                                    `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
159 |                                                     `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
160 |                                                      `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
161 |                                                       `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
162 |                                                        `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
163 |                                                         `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
164 |                                                          `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
165 |                                                           `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
166 |                                                            `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
167 |                                                             `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
168 |                                                              `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
169 |                                                               `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
170 |                                                                `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
171 |                                                                 `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
172 |                                                                  `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
173 |                                                                   `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
174 |                                                                    `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
175 |                                                                     `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
176 |                                                                      `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
177 |                                                                       `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
178 |                                                                        `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
179 |                                                                         `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
180 |                                                                          `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
181 |                                                                           `,,,,,@@@,,,,,,,,,,,,,,,,,,,,,@@#,,,,#
182 |                                                                            `,,,,,@@@,,,,,,,,,,,,,,,,,,,@@#,,,,#
183 |                                                                             `,,,,,@@@,,,,,,,,,,,,,,,,:@@#,,,,#
184 |                                                                              `,,,,,@@@:,,,,,,,,,,,,,#@@#,,,,#
185 |                                                                               `,,,,,@@@@:,,,,,,,,,+@@@;,,,,#
186 |                                                                                `,,,,,:@@@@@#''+@@@@@@,,,,,#
187 |                                                                                 `,,,,,,:@@@@@@@@@@#,,,,,,#
188 |                                                                                  `,,,,,,,,,;''':,,,,,,,,#
189 |                                                                                   `,,,,,,,,,,,,,,,,,,,,#
190 |                                                                                    `,,,,,,,,,,,,,,,,,,#
191 |                                                                                      ;,,,,,,,,,,,,,,,#
192 |                                                                                       #,,,,,,,,,,,,;.
193 |                                                                                         +;,,,,,,,+,
194 |                                                                                             .:.
195 |     
196 | {% endblock %} 197 | -------------------------------------------------------------------------------- /demo/templates/profiles/userprofile_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block subtitle %}{{ profile.user.profile_name }}'s Profile{% endblock %} 4 | 5 | {% block content %} 6 |

{{ profile.user.profile_name }}'s Profile

7 |

Shhh... These details were shared in confidence. Don't tell anyone.

8 |

Greatest Fear

9 |

{{ profile.greatest_fear }}

10 |

Real Name

11 |

{{ profile.user.name }}

12 |

Email

13 |

{{ profile.user.email }}

14 |

Home

15 |

{{ profile.home }}

16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /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 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DjangoAndablog.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DjangoAndablog.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/DjangoAndablog" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/DjangoAndablog" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Django Andablog documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Nov 13 11:22:52 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # on_rtd is whether we are on readthedocs.org 19 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 20 | 21 | if not on_rtd: # only import and set the theme if we're building docs locally 22 | import sphinx_rtd_theme 23 | html_theme = 'sphinx_rtd_theme' 24 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 25 | 26 | # If extensions (or modules to document with autodoc) are in another directory, 27 | # add these directories to sys.path here. If the directory is relative to the 28 | # documentation root, use os.path.abspath to make it absolute, like shown here. 29 | #sys.path.insert(0, os.path.abspath('.')) 30 | 31 | # -- General configuration ------------------------------------------------ 32 | 33 | # If your documentation needs a minimal Sphinx version, state it here. 34 | #needs_sphinx = '1.0' 35 | 36 | # Add any Sphinx extension module names here, as strings. They can be 37 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 38 | # ones. 39 | extensions = [ 40 | 'sphinx.ext.autodoc', 41 | ] 42 | 43 | # Add any paths that contain templates here, relative to this directory. 44 | templates_path = ['_templates'] 45 | 46 | # The suffix of source filenames. 47 | source_suffix = '.rst' 48 | 49 | # The encoding of source files. 50 | #source_encoding = 'utf-8-sig' 51 | 52 | # The master toctree document. 53 | master_doc = 'index' 54 | 55 | # General information about the project. 56 | project = u'Django Andablog' 57 | copyright = u'2014, Ivan VenOsdel/Wimpy Analytics LLC' 58 | 59 | # The version info for the project you're documenting, acts as replacement for 60 | # |version| and |release|, also used in various other places throughout the 61 | # built documents. 62 | # 63 | # The short X.Y version. 64 | version = '3.0.0' 65 | # The full version, including alpha/beta/rc tags. 66 | release = '3.0.0' 67 | 68 | # The language for content autogenerated by Sphinx. Refer to documentation 69 | # for a list of supported languages. 70 | #language = None 71 | 72 | # There are two options for replacing |today|: either, you set today to some 73 | # non-false value, then it is used: 74 | #today = '' 75 | # Else, today_fmt is used as the format for a strftime call. 76 | #today_fmt = '%B %d, %Y' 77 | 78 | # List of patterns, relative to source directory, that match files and 79 | # directories to ignore when looking for source files. 80 | exclude_patterns = ['_build'] 81 | 82 | # The reST default role (used for this markup: `text`) to use for all 83 | # documents. 84 | #default_role = None 85 | 86 | # If true, '()' will be appended to :func: etc. cross-reference text. 87 | #add_function_parentheses = True 88 | 89 | # If true, the current module name will be prepended to all description 90 | # unit titles (such as .. function::). 91 | #add_module_names = True 92 | 93 | # If true, sectionauthor and moduleauthor directives will be shown in the 94 | # output. They are ignored by default. 95 | #show_authors = False 96 | 97 | # The name of the Pygments (syntax highlighting) style to use. 98 | pygments_style = 'sphinx' 99 | 100 | # A list of ignored prefixes for module index sorting. 101 | #modindex_common_prefix = [] 102 | 103 | # If true, keep warnings as "system message" paragraphs in the built documents. 104 | #keep_warnings = False 105 | 106 | 107 | # -- Options for HTML output ---------------------------------------------- 108 | 109 | # The theme to use for HTML and HTML Help pages. See the documentation for 110 | # a list of builtin themes. 111 | #html_theme = 'default' # Already set above 112 | 113 | # Theme options are theme-specific and customize the look and feel of a theme 114 | # further. For a list of options available for each theme, see the 115 | # documentation. 116 | #html_theme_options = {} 117 | 118 | # Add any paths that contain custom themes here, relative to this directory. 119 | #html_theme_path = [] # Already set above 120 | 121 | # The name for this set of Sphinx documents. If None, it defaults to 122 | # " v documentation". 123 | #html_title = None 124 | 125 | # A shorter title for the navigation bar. Default is the same as html_title. 126 | #html_short_title = None 127 | 128 | # The name of an image file (relative to this directory) to place at the top 129 | # of the sidebar. 130 | #html_logo = None 131 | 132 | # The name of an image file (within the static path) to use as favicon of the 133 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 134 | # pixels large. 135 | #html_favicon = None 136 | 137 | # Add any paths that contain custom static files (such as style sheets) here, 138 | # relative to this directory. They are copied after the builtin static files, 139 | # so a file named "default.css" will overwrite the builtin "default.css". 140 | 141 | # Add any extra paths that contain custom files (such as robots.txt or 142 | # .htaccess) here, relative to this directory. These files are copied 143 | # directly to the root of the documentation. 144 | #html_extra_path = [] 145 | 146 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 147 | # using the given strftime format. 148 | #html_last_updated_fmt = '%b %d, %Y' 149 | 150 | # If true, SmartyPants will be used to convert quotes and dashes to 151 | # typographically correct entities. 152 | #html_use_smartypants = True 153 | 154 | # Custom sidebar templates, maps document names to template names. 155 | #html_sidebars = {} 156 | 157 | # Additional templates that should be rendered to pages, maps page names to 158 | # template names. 159 | #html_additional_pages = {} 160 | 161 | # If false, no module index is generated. 162 | #html_domain_indices = True 163 | 164 | # If false, no index is generated. 165 | #html_use_index = True 166 | 167 | # If true, the index is split into individual pages for each letter. 168 | #html_split_index = False 169 | 170 | # If true, links to the reST sources are added to the pages. 171 | #html_show_sourcelink = True 172 | 173 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 174 | #html_show_sphinx = True 175 | 176 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 177 | #html_show_copyright = True 178 | 179 | # If true, an OpenSearch description file will be output, and all pages will 180 | # contain a tag referring to it. The value of this option must be the 181 | # base URL from which the finished HTML is served. 182 | #html_use_opensearch = '' 183 | 184 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 185 | #html_file_suffix = None 186 | 187 | # Output file base name for HTML help builder. 188 | htmlhelp_basename = 'DjangoAndablogdoc' 189 | 190 | 191 | # -- Options for LaTeX output --------------------------------------------- 192 | 193 | latex_elements = { 194 | # The paper size ('letterpaper' or 'a4paper'). 195 | #'papersize': 'letterpaper', 196 | 197 | # The font size ('10pt', '11pt' or '12pt'). 198 | #'pointsize': '10pt', 199 | 200 | # Additional stuff for the LaTeX preamble. 201 | #'preamble': '', 202 | } 203 | 204 | # Grouping the document tree into LaTeX files. List of tuples 205 | # (source start file, target name, title, 206 | # author, documentclass [howto, manual, or own class]). 207 | latex_documents = [ 208 | ('index', 'DjangoAndablog.tex', u'Django Andablog Documentation', 209 | u'Ivan Ven Osdel', 'manual'), 210 | ] 211 | 212 | # The name of an image file (relative to this directory) to place at the top of 213 | # the title page. 214 | #latex_logo = None 215 | 216 | # For "manual" documents, if this is true, then toplevel headings are parts, 217 | # not chapters. 218 | #latex_use_parts = False 219 | 220 | # If true, show page references after internal links. 221 | #latex_show_pagerefs = False 222 | 223 | # If true, show URL addresses after external links. 224 | #latex_show_urls = False 225 | 226 | # Documents to append as an appendix to all manuals. 227 | #latex_appendices = [] 228 | 229 | # If false, no module index is generated. 230 | #latex_domain_indices = True 231 | 232 | 233 | # -- Options for manual page output --------------------------------------- 234 | 235 | # One entry per manual page. List of tuples 236 | # (source start file, name, description, authors, manual section). 237 | man_pages = [ 238 | ('index', 'andablog', u'Django Andablog Documentation', 239 | [u'Ivan Ven Osdel'], 1) 240 | ] 241 | 242 | # If true, show URL addresses after external links. 243 | #man_show_urls = False 244 | 245 | 246 | # -- Options for Texinfo output ------------------------------------------- 247 | 248 | # Grouping the document tree into Texinfo files. List of tuples 249 | # (source start file, target name, title, author, 250 | # dir menu entry, description, category) 251 | texinfo_documents = [ 252 | ('index', 'DjangoAndablog', u'Django Andablog Documentation', 253 | u'Ivan Ven Osdel', 'DjangoAndablog', 'One line description of project.', 254 | 'Miscellaneous'), 255 | ] 256 | 257 | # Documents to append as an appendix to all manuals. 258 | #texinfo_appendices = [] 259 | 260 | # If false, no module index is generated. 261 | #texinfo_domain_indices = True 262 | 263 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 264 | #texinfo_show_urls = 'footnote' 265 | 266 | # If true, do not generate a @detailmenu in the "Top" node's menu. 267 | #texinfo_no_detailmenu = False 268 | -------------------------------------------------------------------------------- /docs/demo-site.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Demo Site 3 | ========= 4 | 5 | No link? 6 | -------- 7 | 8 | Sorry, we don't yet have the demo hosted somewhere. To try out Andablog you have to pull down the source and run it locally. 9 | 10 | Running Locally 11 | --------------- 12 | 13 | The best way to test out the demo site is to set it up with all fixture data (so there is something to look at). 14 | 15 | Using build scripts 16 | ~~~~~~~~~~~~~~~~~~~ 17 | 18 | If you have the `build tools installed `_:: 19 | 20 | $ pynt create_venv 21 | $ pynt rebuild_db 22 | $ pynt runserver 23 | 24 | Manually 25 | ~~~~~~~~ 26 | 27 | 1. Create and activate a virtualenv (somewhere) 28 | 29 | 2. Change directory to the django-andablog cloned dir 30 | 31 | 3. Install Requirements:: 32 | 33 | $ pip install -r local_requirements.txt 34 | 35 | 4. Recreate the db and setup the database schema:: 36 | 37 | $ cd demo 38 | $ python manage.py reset_db 39 | $ python manage.py migrate 40 | 41 | 5. Load all fixtures 42 | 43 | Run this command for every 'fixtures' directory in the project:: 44 | 45 | $ python manage.py loaddata someapp/fixtures/*.json 46 | 47 | 6. Run the server:: 48 | 49 | $ python manage.py runserver 50 | 51 | Pre-packaged Users 52 | ------------------ 53 | 54 | The demo fixtures include the following users. All users have 'secret' for a password. 55 | 56 | Admins 57 | ~~~~~~ 58 | * admin@example.com 59 | 60 | Authors 61 | ~~~~~~~ 62 | * agent0014@example.com 63 | 64 | Regular Users 65 | ~~~~~~~~~~~~~ 66 | * superman@example.com 67 | * superwoman@example.com 68 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | =============================== 2 | Django and a Blog 3 | =============================== 4 | 5 | A blog app that is only intended to embed within an existing Django site. 6 | 7 | * Free software: BSD license `(source) `_ 8 | * Compatible with 2 most recent Django versions (see history to confirm if we are keeping up) 9 | * Compatible with Python 3.x and 2.7 10 | 11 | Getting Started 12 | --------------- 13 | 14 | .. toctree:: 15 | :maxdepth: 2 16 | 17 | install-usage 18 | 19 | Features 20 | -------- 21 | This list will likely grow slowly. Priorities are Bug Fixes > Django Release Compatibility > Bad Jokes > Features. 22 | 23 | * Blog administration through Django admin 24 | * Markdown, RST or plain text support through django-markupfield 25 | * Blog Entry tag management through django-taggit. 26 | * Template block names are prefixed as to not conflict with the those used by the site. 27 | * A URL hierarchy to include at /blog (or wherever) 28 | * A Django sitemaps EntrySitemap class 29 | * Preview content/image fields for entry for tighter control of how a blog entry looks in a listing w/fallback to truncation. 30 | * A base class for an entries feed 31 | * Utilizing a site-provided profile page as the author profile page 32 | * Easy comment integration. Simply override a template snippet 33 | * Support for custom User Models 34 | * Django migrations 35 | * Class based generic views that can be used directly 36 | * A demo application. 37 | * Django upgrade friendly: Most recently released major Django version and 1 back 38 | 39 | Not Features 40 | ------------ 41 | .. role:: strike 42 | :class: strike 43 | 44 | These features are `right out `_. If you are looking for one of them, andablog may not be right for you. 45 | 46 | * A User model. Andablog uses the settings.auth_user_model relation string for the author. 47 | * Author Profile pages. These can be implemented by the site and linked to by andablog. 48 | * Comments on blog entries. Though help is provided. In the form of a template snippet reference as well as a template tag that can be used for user display/linking. 49 | * Constructing the author display name or URL. A provided User model must implement get_short_name for author display and get_absolute_url for author profile linking. 50 | * Search. Since Andablog is only intended to be packaged with an existing site it would most likely become redundant. 51 | * Support for 3 or more Django major releases. Sorry, if you want to proceed you will have to fork until your site catches up. 52 | 53 | Trying out the demo site 54 | ------------------------ 55 | 56 | .. toctree:: 57 | :maxdepth: 2 58 | 59 | demo-site 60 | 61 | Contributing to the project 62 | --------------------------- 63 | 64 | Checkout the `project page `_ 65 | 66 | Indices and tables 67 | ================== 68 | 69 | * :ref:`genindex` 70 | * :ref:`modindex` 71 | * :ref:`search` 72 | 73 | -------------------------------------------------------------------------------- /docs/install-usage.rst: -------------------------------------------------------------------------------- 1 | ==================== 2 | Installation & Usage 3 | ==================== 4 | 5 | The easiest way to install Andablog is with pip; this will give you the latest version available on PyPi:: 6 | 7 | pip install django-andablog 8 | 9 | Unless you plan on turning off markdown rendering, you also need a package for that:: 10 | 11 | pip install Markdown 12 | 13 | If you are adventurous (or we are just slow) you can get the latest code directly from the Github repository:: 14 | 15 | pip install -e git+https://github.com/WimpyAnalytics/django-andablog.git#egg=django-andablog 16 | 17 | The master branch can generally be considered bug free though newer features may be a little half baked. 18 | For more information `see the official Python package installation tutorial `_. 19 | 20 | Django Settings 21 | --------------- 22 | 23 | 1. Check Django pre-requisites 24 | 25 | * Confirm that your site's MEDIA_ROOT and MEDIA_URL settings are correct. 26 | * Django’s site framework should be enabled. 27 | * The Django admin should be enabled if you wish to use the pre-canned blog administration tools 28 | 29 | 2. Add to your INSTALLED_APPS:: 30 | 31 | INSTALLED_APPS = ( 32 | # ... other applications, 33 | 'andablog', 34 | 'taggit', # For entry tags 35 | 'south', # Only if your site is on Django 1.6 36 | ) 37 | 38 | 3. Run the migrations:: 39 | 40 | $ python manage.py migrate 41 | 42 | 4. (Optional) Configure andablog to use a markup syntax for blog entries. 43 | 44 | For Markdown, install the Markdown pypi package and add the appropriate `Markupfield! settings `_ to your settings.py 45 | 46 | Integrating Andablog into a Site 47 | -------------------------------- 48 | The following tasks allow for all possible andablog features. Ignore the items you don't need. 49 | 50 | Included Pages 51 | ^^^^^^^^^^^^^^ 52 | To use the pages provided by andablog add something like this to your site's URL hierarchy:: 53 | 54 | (r'^blog/', include('andablog.urls', namespace='andablog')), 55 | 56 | Then modify your site's navbar to link to the blog listing. E.g. 57 | 58 |
  • Blog
  • 59 | 60 | Finally, override andablog's base template to inherit from your site's base.html. 61 | 62 | andablog/base.html 63 | 64 | .. note:: The andablog templates make no assumptions when it comes to the content of your site's template. All blocks referenced by andablog are prefixed by 'andablog' and you place them how you like. 65 | 66 | The demo app has an `example of overriding andablog's base.html `_. 67 | 68 | Blog Entry Comments 69 | ^^^^^^^^^^^^^^^^^^^ 70 | 71 | Andablog can use `contrib comments `_ or any other pluggable commenting system (such as your own). 72 | 73 | To provide andablog with comments, override the following template snippets:: 74 | 75 | andablog/comments_count_snippet.html 76 | andablog/comments_snippet.html 77 | 78 | Comments using Disqus 79 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 80 | 81 | `Disqus `_ is a service which provides commenting plug-in as a JavaScript and ``