├── 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 |
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 |
--------------------------------------------------------------------------------