├── posts ├── __init__.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── populatedb.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── tests.py ├── admin.py ├── apps.py ├── models.py ├── static │ ├── custom.css │ └── infinite.js ├── templates │ └── posts │ │ ├── _posts.html │ │ ├── _post.html │ │ ├── base.html │ │ └── post_list.html └── views.py ├── infinite ├── __init__.py ├── asgi.py ├── wsgi.py ├── urls.py └── settings.py ├── .gitignore ├── .editorconfig ├── requirements.txt ├── manage.py ├── README.md └── LICENSE /posts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /infinite/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /posts/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /posts/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /posts/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | *.sqlite3 4 | .vscode 5 | .env 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.html] 2 | indent_size = 2 3 | 4 | [*.js] 5 | indent_size = 2 -------------------------------------------------------------------------------- /posts/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /posts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.2.10 2 | Django==3.0.8 3 | pytz==2020.1 4 | sqlparse==0.3.1 5 | -------------------------------------------------------------------------------- /posts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PostsConfig(AppConfig): 5 | name = 'posts' 6 | -------------------------------------------------------------------------------- /posts/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Post(models.Model): 5 | text = models.TextField() 6 | 7 | -------------------------------------------------------------------------------- /posts/static/custom.css: -------------------------------------------------------------------------------- 1 | .posts { 2 | margin-top: 75px; 3 | } 4 | 5 | /* #sentinel { 6 | border: 1px solid red; 7 | } */ -------------------------------------------------------------------------------- /posts/templates/posts/_posts.html: -------------------------------------------------------------------------------- 1 | {% for post in posts %} 2 | {% include 'posts/_post.html' with post=post %} 3 | {% endfor %} -------------------------------------------------------------------------------- /posts/templates/posts/_post.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

6 | {{ post.text }} 7 |

8 |
9 |
10 |
11 |
-------------------------------------------------------------------------------- /infinite/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for infinite project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'infinite.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /infinite/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for infinite 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/3.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'infinite.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /posts/management/commands/populatedb.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand, CommandError 2 | from posts.models import Post 3 | 4 | class Command(BaseCommand): 5 | help = 'Populates the database. Deletes old entries' 6 | 7 | def handle(self, *args, **options): 8 | print("Deleting old entries...") 9 | Post.objects.all().delete() 10 | print("Populating...") 11 | for i in range(300): 12 | Post.objects.create(text=str(i)) 13 | print("Done.") -------------------------------------------------------------------------------- /posts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.8 on 2020-07-16 11:56 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Post', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('text', models.TextField()), 19 | ], 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /posts/templates/posts/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | infinite scroll 9 | 10 | 11 | {% block extra_scripts %} 12 | {% endblock extra_scripts%} 13 | 14 | 15 | {% block main %} 16 | {% endblock main %} 17 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'infinite.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple infinite scroll example in Django 2 | 3 | This is an example of a very simple infinite scroll implementation in Django. 4 | 5 | I blogged about this here: https://spikelantern.com/articles/simple-infinite-scroll-in-django/ 6 | 7 | Hope this helps you! 8 | 9 | ## Installation 10 | 11 | Create and activate a virtualenv like this (instructions may differ on Windows): 12 | 13 | ``` 14 | $ python -m venv myvenv 15 | $ source myvenv/bin/activate 16 | ``` 17 | 18 | Install the requirements: 19 | 20 | ``` 21 | $ pip install -r requirements.txt 22 | ``` 23 | 24 | Run migrations: 25 | 26 | ``` 27 | $ python manage.py migrate 28 | ``` 29 | 30 | Populate the database: 31 | 32 | ``` 33 | $ python manage.py populatedb 34 | ``` 35 | 36 | Now you can start the server: 37 | 38 | ``` 39 | $ python manage.py runserver 40 | ``` 41 | 42 | You can view the example at http://127.0.0.1:8000 43 | 44 | # License 45 | 46 | MIT 47 | -------------------------------------------------------------------------------- /posts/static/infinite.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const fetchPage = async (url) => { 4 | let headers = new Headers() 5 | headers.append("X-Requested-With", "XMLHttpRequest") 6 | return fetch(url, { headers }) 7 | } 8 | 9 | const appendElements = async (scrollElement, counter, baseUrl) => { 10 | let url = `${baseUrl}?page=${counter + 1}` 11 | 12 | let req = await fetchPage(url); 13 | 14 | if (req.ok) { 15 | let body = await req.text(); 16 | // Be careful of XSS if you do this. Make sure 17 | // you remove all possible sources of XSS. 18 | scrollElement.innerHTML += body; 19 | } else { 20 | end = true; 21 | } 22 | } 23 | 24 | 25 | const attachInfiniteScroll = (sentinel, scrollElement, baseUrl) => { 26 | let counter = 1; 27 | let end = false; 28 | 29 | let observer = new IntersectionObserver(async (entries) => { 30 | let bottomEntry = entries[0]; 31 | 32 | if (!end && bottomEntry.intersectionRatio > 0) { 33 | await appendElements(scrollElement, counter, baseUrl); 34 | counter += 1; 35 | } 36 | }) 37 | 38 | 39 | observer.observe(sentinel); 40 | }; -------------------------------------------------------------------------------- /infinite/urls.py: -------------------------------------------------------------------------------- 1 | """infinite URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path 18 | from django.views.generic import RedirectView 19 | from posts import views as posts_views 20 | 21 | urlpatterns = [ 22 | path('admin/', admin.site.urls), 23 | path('posts/', posts_views.post_list, name='post-list'), 24 | path('posts/create/', posts_views.create_post, name='create-post'), 25 | path('', RedirectView.as_view(url='posts/')), 26 | ] 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Yoong Kang Lim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /posts/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect 2 | from django.views.decorators.http import require_GET, require_POST 3 | from django.core.paginator import Paginator 4 | from django.http import Http404 5 | 6 | from .models import Post 7 | 8 | 9 | def is_ajax(request): 10 | """ 11 | This utility function is used, as `request.is_ajax()` is deprecated. 12 | 13 | This implements the previous functionality. Note that you need to 14 | attach this header manually if using fetch. 15 | """ 16 | return request.META.get("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest" 17 | 18 | 19 | @require_GET 20 | def post_list(request): 21 | """ 22 | List view for posts. 23 | """ 24 | all_posts = Post.objects.order_by('-pk').all() 25 | paginator = Paginator(all_posts, per_page=10) 26 | page_num = int(request.GET.get("page", 1)) 27 | if page_num > paginator.num_pages: 28 | raise Http404 29 | posts = paginator.page(page_num) 30 | if is_ajax(request): 31 | return render(request, 'posts/_posts.html', {'posts': posts}) 32 | return render(request, 'posts/post_list.html', {'posts': posts}) 33 | 34 | 35 | @require_POST 36 | def create_post(request): 37 | """ 38 | Endpoint to create posts 39 | """ 40 | text = request.POST.get("text") 41 | if text: 42 | post = Post.objects.create(text=request.POST.get("text")) 43 | return redirect('post-list') 44 | -------------------------------------------------------------------------------- /posts/templates/posts/post_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'posts/base.html' %} 2 | 3 | {% load static %} 4 | 5 | 6 | {% block extra_scripts %} 7 | 8 | {% endblock extra_scripts%} 9 | 10 | {% block main %} 11 | 12 | 13 |
14 |
15 |
16 |
17 | 18 |
19 | {% csrf_token %} 20 |
21 |
22 | 23 |
24 |
25 | 26 |
27 |
28 | 29 |
30 |
31 | 32 |
33 | 34 | 35 |
36 | {% include 'posts/_posts.html' with posts=posts %} 37 |
38 | 39 | 40 |
41 |
42 | 43 |
44 | 45 | 46 | 47 |
48 |
49 |
50 | 51 |
52 |
53 | 54 | 55 | 63 | {% endblock main %} -------------------------------------------------------------------------------- /infinite/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for infinite project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.0.8. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'gwtt1%%wcq%+p9=^5*)ke-%k#$7^dy^k877!!e_)5alakhqrp8' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'posts.apps.PostsConfig', 41 | ] 42 | 43 | MIDDLEWARE = [ 44 | 'django.middleware.security.SecurityMiddleware', 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | ] 52 | 53 | ROOT_URLCONF = 'infinite.urls' 54 | 55 | TEMPLATES = [ 56 | { 57 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 58 | 'DIRS': [], 59 | 'APP_DIRS': True, 60 | 'OPTIONS': { 61 | 'context_processors': [ 62 | 'django.template.context_processors.debug', 63 | 'django.template.context_processors.request', 64 | 'django.contrib.auth.context_processors.auth', 65 | 'django.contrib.messages.context_processors.messages', 66 | ], 67 | }, 68 | }, 69 | ] 70 | 71 | WSGI_APPLICATION = 'infinite.wsgi.application' 72 | 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases 76 | 77 | DATABASES = { 78 | 'default': { 79 | 'ENGINE': 'django.db.backends.sqlite3', 80 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 81 | } 82 | } 83 | 84 | 85 | # Password validation 86 | # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators 87 | 88 | AUTH_PASSWORD_VALIDATORS = [ 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 91 | }, 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 100 | }, 101 | ] 102 | 103 | 104 | # Internationalization 105 | # https://docs.djangoproject.com/en/3.0/topics/i18n/ 106 | 107 | LANGUAGE_CODE = 'en-us' 108 | 109 | TIME_ZONE = 'UTC' 110 | 111 | USE_I18N = True 112 | 113 | USE_L10N = True 114 | 115 | USE_TZ = True 116 | 117 | 118 | # Static files (CSS, JavaScript, Images) 119 | # https://docs.djangoproject.com/en/3.0/howto/static-files/ 120 | 121 | STATIC_URL = '/static/' 122 | --------------------------------------------------------------------------------