From ec3309e2ba42d06f4062caf7f55eddb08d127626 Mon Sep 17 00:00:00 2001 From: Geir Arne Hjelle Date: Fri, 29 Sep 2023 12:19:30 +0200 Subject: [PATCH 1/3] Add materials for full Python 3.12 tutorial (#440) * Add files from the full Python 3.12 tutorial * Update README with information about new files * Use correct numbers for sales.py and add a quick histogram * Final QA updates --- python-312/README.md | 18 ++++- python-312/comprehension_benchmark.py | 65 +++++++++++++++++++ python-312/error-messages/shapes.py | 13 ++++ python-312/pathlib_walk.py | 20 ++++++ python-312/pathlib_walk/musicians/readme.txt | 1 + .../musicians/trumpet/armstrong.txt | 1 + .../pathlib_walk/musicians/trumpet/davis.txt | 1 + .../musicians/vocal/fitzgerald.txt | 1 + python-312/quarters.py | 5 ++ python-312/sales.py | 20 ++++++ python-312/typing/accounts.py | 42 ++++++++++++ python-312/typing/generic_stack.py | 27 ++++++++ 12 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 python-312/comprehension_benchmark.py create mode 100644 python-312/error-messages/shapes.py create mode 100644 python-312/pathlib_walk.py create mode 100644 python-312/pathlib_walk/musicians/readme.txt create mode 100644 python-312/pathlib_walk/musicians/trumpet/armstrong.txt create mode 100644 python-312/pathlib_walk/musicians/trumpet/davis.txt create mode 100644 python-312/pathlib_walk/musicians/vocal/fitzgerald.txt create mode 100644 python-312/quarters.py create mode 100644 python-312/sales.py create mode 100644 python-312/typing/accounts.py create mode 100644 python-312/typing/generic_stack.py diff --git a/python-312/README.md b/python-312/README.md index ea8679a4be..791570268f 100644 --- a/python-312/README.md +++ b/python-312/README.md @@ -12,6 +12,7 @@ Note that for the `perf` support, you'll need to build Python from source code w You can learn more about Python 3.12's new features in the following Real Python tutorials: +- [Python 3.12: Cool New Features for You to Try](https://realpython.com/python312-new-features/) - [Python 3.12 Preview: Ever Better Error Messages](https://realpython.com/python312-error-messages/) - [Python 3.12 Preview: Support For the Linux `perf` Profiler](https://realpython.com/python312-perf-profiler/) - [Python 3.12 Preview: More Intuitive and Consistent F-Strings](https://realpython.com/python312-f-strings/) @@ -38,7 +39,7 @@ You can swap the import statement to `import d from this` in either of the files SyntaxError: Did you mean to use 'from ... import ...' instead? ``` -In [`local_self.py`](error-messages/local_self.py), you can see a naive reproduction of another improved error message. Pick apart the example code to learn more about how this was implemented in Python 3.12. +In [`local_self.py`](error-messages/local_self.py), you can see a naive reproduction of another improved error message. Pick apart the example code to learn more about how this was implemented in Python 3.12. You can also run [`shapes.py`](error-messages/shapes.py) to see the _self_ error message in action. See [Ever Better Error Messages in Python 3.12](https://realpython.com/python312-error-messages/) for more information. @@ -153,6 +154,7 @@ You can then run type checks by running `pyright`. For some features, you need t You can find comparisons between the old and the new syntax for type variables in the following files, with the new 3.12 syntax shown in the commented part of the code: - [`generic_queue.py`](typing/generic_queue.py) +- [`generic_stack.py`](typing/generic_stack.py) - [`list_helpers.py`](typing/list_helpers.py) - [`concatenation.py`](typing/concatenation.py) - [`inspect_string.py`](typing/inspect_string.py) @@ -165,10 +167,24 @@ Additionally, [`typed_queue.py`](typing/typed_queue.py) shows the implementation The file [`quiz.py`](typing/quiz.py) shows how to use the new `@override` decorator. In addition to the code in the tutorial, this file includes support for reading questions from files. This is done to show that `@override` works well together with other decorators like `@classmethod`. +Similarly, [`accounts.py`](typing/accounts.py) contains the bank account classes annotated with `@override`. To play with the bank account code, you should run it interactively: `python -i accounts.py`. + #### Annotating `**kwargs` With Typed Dictionaries The file [`options.py`](typing/options.py) shows how you can use a typed dictionary to annotate variable keyword arguments. +### Inlined Comprehensions + +You can run benchmarks on comprehensions by running [`comprehension_benchmark.py`](comprehension_benchmark.py). If you have multiple versions of Python, then you should run the benchmarks on all of them and compare the results. + +### Group Iterables Into Batches With `itertools.batched()` + +Run [`quarters.py`](quarters.py) for an example showing how to group months into quarters. + +### List Files and Directories With `Path.walk()` + +Run [`pathlib_walk.py`](pathlib_walk.py) to compare `.walk()` and `.rglob()` for listing files and directories. + ## Authors - **Martin Breuss**, E-mail: [martin@realpython.com](martin@realpython.com) diff --git a/python-312/comprehension_benchmark.py b/python-312/comprehension_benchmark.py new file mode 100644 index 0000000000..7915273b72 --- /dev/null +++ b/python-312/comprehension_benchmark.py @@ -0,0 +1,65 @@ +import sys +import timeit +from functools import partial + +NUM_REPEATS = 100 + + +def convert_unit(value): + for unit in ["s", "ms", "μs", "ns"]: + if value > 1: + return f"{value:8.2f}{unit}" + value *= 1000 + + +def timer(func): + def _timer(*args, **kwargs): + tictoc = timeit.timeit( + partial(func, *args, **kwargs), number=NUM_REPEATS + ) + print(f"{func.__name__:<20} {convert_unit((tictoc) / NUM_REPEATS)}") + + return _timer + + +@timer +def plain(numbers): + return [number for number in numbers] + + +@timer +def square(numbers): + return [number**2 for number in numbers] + + +@timer +def filtered(numbers): + return [number for number in numbers if number % 2 == 0] + + +n, N = 100, 100_000 +small_numbers = range(n) +big_numbers = range(N) +repeated_numbers = [1 for _ in range(N)] + +print(sys.version) + +print("\nOne item iterable:") +plain([0]) +square([0]) +filtered([0]) + +print(f"\nSmall iterable ({n = :,}):") +plain(small_numbers) +square(small_numbers) +filtered(small_numbers) + +print(f"\nBig iterable ({N = :,}):") +plain(big_numbers) +square(big_numbers) +filtered(big_numbers) + +print(f"\nRepeated iterable ({N = :,}):") +plain(repeated_numbers) +square(repeated_numbers) +filtered(repeated_numbers) diff --git a/python-312/error-messages/shapes.py b/python-312/error-messages/shapes.py new file mode 100644 index 0000000000..3a32ddb7eb --- /dev/null +++ b/python-312/error-messages/shapes.py @@ -0,0 +1,13 @@ +from math import pi + + +class Circle: + def __init__(self, radius): + self.radius = radius + + def area(self): + return pi * radius**2 # noqa: F821 + + +if __name__ == "__main__": + Circle(5).area() diff --git a/python-312/pathlib_walk.py b/python-312/pathlib_walk.py new file mode 100644 index 0000000000..94aee3d29d --- /dev/null +++ b/python-312/pathlib_walk.py @@ -0,0 +1,20 @@ +from pathlib import Path + +data_path = Path(__file__).parent / "pathlib_walk" + + +def headline(text): + print(f"\n{text}\n{'-' * len(text)}") + + +headline("Using .rglob():") +for path in data_path.rglob("*"): + print(path) + +headline("Using .walk():") +for path, directories, files in data_path.walk(): + print(path, directories, files) + +headline("Using .walk(top_down=False):") +for path, directories, files in data_path.walk(top_down=False): + print(path, directories, files) diff --git a/python-312/pathlib_walk/musicians/readme.txt b/python-312/pathlib_walk/musicians/readme.txt new file mode 100644 index 0000000000..dce27e1b71 --- /dev/null +++ b/python-312/pathlib_walk/musicians/readme.txt @@ -0,0 +1 @@ +A directory with jazz people diff --git a/python-312/pathlib_walk/musicians/trumpet/armstrong.txt b/python-312/pathlib_walk/musicians/trumpet/armstrong.txt new file mode 100644 index 0000000000..cb86af6ce8 --- /dev/null +++ b/python-312/pathlib_walk/musicians/trumpet/armstrong.txt @@ -0,0 +1 @@ +Louie diff --git a/python-312/pathlib_walk/musicians/trumpet/davis.txt b/python-312/pathlib_walk/musicians/trumpet/davis.txt new file mode 100644 index 0000000000..8488b04bb0 --- /dev/null +++ b/python-312/pathlib_walk/musicians/trumpet/davis.txt @@ -0,0 +1 @@ +Miles diff --git a/python-312/pathlib_walk/musicians/vocal/fitzgerald.txt b/python-312/pathlib_walk/musicians/vocal/fitzgerald.txt new file mode 100644 index 0000000000..64f51c4c6a --- /dev/null +++ b/python-312/pathlib_walk/musicians/vocal/fitzgerald.txt @@ -0,0 +1 @@ +Ella diff --git a/python-312/quarters.py b/python-312/quarters.py new file mode 100644 index 0000000000..45cc70dc50 --- /dev/null +++ b/python-312/quarters.py @@ -0,0 +1,5 @@ +import calendar +import itertools + +for quarter in itertools.batched(calendar.Month, n=3): + print([month.name for month in quarter]) diff --git a/python-312/sales.py b/python-312/sales.py new file mode 100644 index 0000000000..585dc9cf6c --- /dev/null +++ b/python-312/sales.py @@ -0,0 +1,20 @@ +import calendar + +sales = { + calendar.JANUARY: 5, + calendar.FEBRUARY: 9, + calendar.MARCH: 6, + calendar.APRIL: 14, + calendar.MAY: 9, + calendar.JUNE: 8, + calendar.JULY: 15, + calendar.AUGUST: 22, + calendar.SEPTEMBER: 20, + calendar.OCTOBER: 23, +} +for month in calendar.Month: + if month in sales: + print( + f"{month.value:2d} {month.name:<10}" + f" {sales[month]:2d} {'*' * sales[month]}" + ) diff --git a/python-312/typing/accounts.py b/python-312/typing/accounts.py new file mode 100644 index 0000000000..f6d923db0a --- /dev/null +++ b/python-312/typing/accounts.py @@ -0,0 +1,42 @@ +import random +from dataclasses import dataclass +from typing import Self, override + + +def generate_account_number() -> str: + """Generate a random eleven-digit account number""" + account_number = str(random.randrange(10_000_000_000, 100_000_000_000)) + return f"{account_number[:4]}.{account_number[4:6]}.{account_number[6:]}" + + +@dataclass +class BankAccount: + account_number: str + balance: float + + @classmethod + def from_balance(cls, balance: float) -> Self: + return cls(generate_account_number(), balance) + + def deposit(self, amount: float) -> None: + self.balance += amount + + def withdraw(self, amount: float) -> None: + self.balance -= amount + + +@dataclass +class SavingsAccount(BankAccount): + interest: float + + def add_interest(self) -> None: + self.balance *= 1 + self.interest / 100 + + @classmethod + @override + def from_balance(cls, balance: float, interest: float = 1.0) -> Self: + return cls(generate_account_number(), balance, interest) + + @override + def withdraw(self, amount: float) -> None: + self.balance -= int(amount) if amount > 100 else amount diff --git a/python-312/typing/generic_stack.py b/python-312/typing/generic_stack.py new file mode 100644 index 0000000000..a829481327 --- /dev/null +++ b/python-312/typing/generic_stack.py @@ -0,0 +1,27 @@ +from typing import Generic, TypeVar + +T = TypeVar("T") + + +class Stack(Generic[T]): + def __init__(self) -> None: + self.stack: list[T] = [] + + def push(self, element: T) -> None: + self.stack.append(element) + + def pop(self) -> T: + return self.stack.pop() + + +# %% Python 3.12 + +# class Stack[T]: +# def __init__(self) -> None: +# self.stack: list[T] = [] +# +# def push(self, element: T) -> None: +# self.stack.append(element) +# +# def pop(self) -> T: +# return self.stack.pop() From a10f932c8ecbd547588874db44453176b38bd773 Mon Sep 17 00:00:00 2001 From: Philipp Acsany <68116180+acsany@users.noreply.github.com> Date: Thu, 5 Oct 2023 21:07:25 +0200 Subject: [PATCH 2/3] Add materials for the Django blog tutorial (#432) * Add materials * Final QA --------- Co-authored-by: gahjelle --- rp-blog/README.md | 57 ++++++++ rp-blog/django-blog/blog/__init__.py | 0 rp-blog/django-blog/blog/admin.py | 19 +++ rp-blog/django-blog/blog/apps.py | 6 + rp-blog/django-blog/blog/forms.py | 15 ++ .../blog/migrations/0001_initial.py | 43 ++++++ .../django-blog/blog/migrations/__init__.py | 0 rp-blog/django-blog/blog/models.py | 32 +++++ .../blog/templates/blog/category.html | 5 + .../blog/templates/blog/detail.html | 39 ++++++ .../blog/templates/blog/index.html | 22 +++ rp-blog/django-blog/blog/urls.py | 8 ++ rp-blog/django-blog/blog/views.py | 38 ++++++ rp-blog/django-blog/manage.py | 22 +++ rp-blog/django-blog/personal_blog/__init__.py | 0 rp-blog/django-blog/personal_blog/asgi.py | 16 +++ rp-blog/django-blog/personal_blog/settings.py | 128 ++++++++++++++++++ rp-blog/django-blog/personal_blog/urls.py | 7 + rp-blog/django-blog/personal_blog/wsgi.py | 16 +++ rp-blog/django-blog/templates/base.html | 15 ++ rp-blog/requirements.txt | 3 + 21 files changed, 491 insertions(+) create mode 100644 rp-blog/README.md create mode 100644 rp-blog/django-blog/blog/__init__.py create mode 100644 rp-blog/django-blog/blog/admin.py create mode 100644 rp-blog/django-blog/blog/apps.py create mode 100644 rp-blog/django-blog/blog/forms.py create mode 100644 rp-blog/django-blog/blog/migrations/0001_initial.py create mode 100644 rp-blog/django-blog/blog/migrations/__init__.py create mode 100644 rp-blog/django-blog/blog/models.py create mode 100644 rp-blog/django-blog/blog/templates/blog/category.html create mode 100644 rp-blog/django-blog/blog/templates/blog/detail.html create mode 100644 rp-blog/django-blog/blog/templates/blog/index.html create mode 100644 rp-blog/django-blog/blog/urls.py create mode 100644 rp-blog/django-blog/blog/views.py create mode 100755 rp-blog/django-blog/manage.py create mode 100644 rp-blog/django-blog/personal_blog/__init__.py create mode 100644 rp-blog/django-blog/personal_blog/asgi.py create mode 100644 rp-blog/django-blog/personal_blog/settings.py create mode 100644 rp-blog/django-blog/personal_blog/urls.py create mode 100644 rp-blog/django-blog/personal_blog/wsgi.py create mode 100644 rp-blog/django-blog/templates/base.html create mode 100644 rp-blog/requirements.txt diff --git a/rp-blog/README.md b/rp-blog/README.md new file mode 100644 index 0000000000..a63c1d50a4 --- /dev/null +++ b/rp-blog/README.md @@ -0,0 +1,57 @@ +# Create Your Own Blog With Django + +Follow the [step-by-step instructions](https://realpython.com/get-started-with-django-1/) on Real Python to build your own blog with Django. + +## Setup + +You can run the provided example project on your local machine by following the steps outlined below. + +Create a new virtual environment: + +```bash +$ python3 -m venv venv +``` + +Activate the virtual environment: + +```bash +$ source venv/bin/activate +``` + +Install the dependencies for this project if you haven't installed them yet: + +```bash +(venv) $ python -m pip install -r requirements.txt +``` + +Make and apply the migrations for the project to build your local database: + +```bash +(venv) $ python manage.py makemigrations +(venv) $ python manage.py migrate +``` + +Run the Django development server: + +```bash +(venv) $ python manage.py runserver +``` + +Navigate to `http://localhost:8000/` to see your blog in action. + +## Using the Django Admin site + +To create new posts, you need to create a superuser: + +```bash +(venv) $ python manage.py createsuperuser +Username (leave blank to use 'root'): admin +Email address: admin@example.com +Password: RealPyth0n +Password (again): RealPyth0n +Superuser created successfully. +``` + +When running the `createsuperuser` managemant command you're prompted to choose a username, provide an email address, and set a password. Use your own data for these fields and make sure to remember them. + +Navigate to `http://localhost:8000/admin` and log in with the credentials you just used to create a superuser. diff --git a/rp-blog/django-blog/blog/__init__.py b/rp-blog/django-blog/blog/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/rp-blog/django-blog/blog/admin.py b/rp-blog/django-blog/blog/admin.py new file mode 100644 index 0000000000..14b0918994 --- /dev/null +++ b/rp-blog/django-blog/blog/admin.py @@ -0,0 +1,19 @@ +from django.contrib import admin +from blog.models import Category, Comment, Post + + +class CategoryAdmin(admin.ModelAdmin): + pass + + +class PostAdmin(admin.ModelAdmin): + pass + + +class CommentAdmin(admin.ModelAdmin): + pass + + +admin.site.register(Category, CategoryAdmin) +admin.site.register(Post, PostAdmin) +admin.site.register(Comment, CommentAdmin) diff --git a/rp-blog/django-blog/blog/apps.py b/rp-blog/django-blog/blog/apps.py new file mode 100644 index 0000000000..6be26c734b --- /dev/null +++ b/rp-blog/django-blog/blog/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class BlogConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "blog" diff --git a/rp-blog/django-blog/blog/forms.py b/rp-blog/django-blog/blog/forms.py new file mode 100644 index 0000000000..3fdf2c7e31 --- /dev/null +++ b/rp-blog/django-blog/blog/forms.py @@ -0,0 +1,15 @@ +from django import forms + + +class CommentForm(forms.Form): + author = forms.CharField( + max_length=60, + widget=forms.TextInput( + attrs={"class": "form-control", "placeholder": "Your Name"} + ), + ) + body = forms.CharField( + widget=forms.Textarea( + attrs={"class": "form-control", "placeholder": "Leave a comment!"} + ) + ) diff --git a/rp-blog/django-blog/blog/migrations/0001_initial.py b/rp-blog/django-blog/blog/migrations/0001_initial.py new file mode 100644 index 0000000000..1f1bba108f --- /dev/null +++ b/rp-blog/django-blog/blog/migrations/0001_initial.py @@ -0,0 +1,43 @@ +# Generated by Django 4.2.4 on 2023-08-29 12:43 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=30)), + ], + ), + migrations.CreateModel( + name='Post', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255)), + ('body', models.TextField()), + ('created_on', models.DateTimeField(auto_now_add=True)), + ('last_modified', models.DateTimeField(auto_now=True)), + ('categories', models.ManyToManyField(related_name='posts', to='blog.category')), + ], + ), + migrations.CreateModel( + name='Comment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('author', models.CharField(max_length=60)), + ('body', models.TextField()), + ('created_on', models.DateTimeField(auto_now_add=True)), + ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.post')), + ], + ), + ] diff --git a/rp-blog/django-blog/blog/migrations/__init__.py b/rp-blog/django-blog/blog/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/rp-blog/django-blog/blog/models.py b/rp-blog/django-blog/blog/models.py new file mode 100644 index 0000000000..a3795fc4f0 --- /dev/null +++ b/rp-blog/django-blog/blog/models.py @@ -0,0 +1,32 @@ +from django.db import models + + +class Category(models.Model): + name = models.CharField(max_length=30) + + class Meta: + verbose_name_plural = "categories" + + def __str__(self): + return self.name + + +class Post(models.Model): + title = models.CharField(max_length=255) + body = models.TextField() + created_on = models.DateTimeField(auto_now_add=True) + last_modified = models.DateTimeField(auto_now=True) + categories = models.ManyToManyField("Category", related_name="posts") + + def __str__(self): + return self.title + + +class Comment(models.Model): + author = models.CharField(max_length=60) + body = models.TextField() + created_on = models.DateTimeField(auto_now_add=True) + post = models.ForeignKey("Post", on_delete=models.CASCADE) + + def __str__(self): + return f"{self.author} on '{self.post}'" diff --git a/rp-blog/django-blog/blog/templates/blog/category.html b/rp-blog/django-blog/blog/templates/blog/category.html new file mode 100644 index 0000000000..585bf2543a --- /dev/null +++ b/rp-blog/django-blog/blog/templates/blog/category.html @@ -0,0 +1,5 @@ +{% extends "blog/index.html" %} + +{% block page_title %} +

{{ category }}

+{% endblock page_title %} diff --git a/rp-blog/django-blog/blog/templates/blog/detail.html b/rp-blog/django-blog/blog/templates/blog/detail.html new file mode 100644 index 0000000000..86512c3fd2 --- /dev/null +++ b/rp-blog/django-blog/blog/templates/blog/detail.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} + +{% block page_title %} +

{{ post.title }}

+{% endblock page_title %} + +{% block page_content %} + + {{ post.created_on.date }} | Categories: + {% for category in post.categories.all %} + + {{ category.name }} + + {% endfor %} + +

{{ post.body | linebreaks }}

+ +

Leave a comment:

+
+ {% csrf_token %} +
+ {{ form.author }} +
+
+ {{ form.body }} +
+ +
+ +

Comments:

+ {% for comment in comments %} +

+ On {{ comment.created_on.date }} {{ comment.author }} wrote: +

+

+ {{ comment.body | linebreaks }} +

+ {% endfor %} +{% endblock page_content %} diff --git a/rp-blog/django-blog/blog/templates/blog/index.html b/rp-blog/django-blog/blog/templates/blog/index.html new file mode 100644 index 0000000000..eb0480b7b2 --- /dev/null +++ b/rp-blog/django-blog/blog/templates/blog/index.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} + +{% block page_title %} +

Blog Posts

+{% endblock page_title %} + +{% block page_content %} + {% block posts %} + {% for post in posts %} +

{{ post.title }}

+ + {{ post.created_on.date }} | Categories: + {% for category in post.categories.all %} + + {{ category.name }} + + {% endfor %} + +

{{ post.body | slice:":400" }}...

+ {% endfor %} + {% endblock posts %} +{% endblock page_content %} diff --git a/rp-blog/django-blog/blog/urls.py b/rp-blog/django-blog/blog/urls.py new file mode 100644 index 0000000000..5fb62222cb --- /dev/null +++ b/rp-blog/django-blog/blog/urls.py @@ -0,0 +1,8 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path("", views.blog_index, name="blog_index"), + path("post//", views.blog_detail, name="blog_detail"), + path("category//", views.blog_category, name="blog_category"), +] diff --git a/rp-blog/django-blog/blog/views.py b/rp-blog/django-blog/blog/views.py new file mode 100644 index 0000000000..e1f51f4bbc --- /dev/null +++ b/rp-blog/django-blog/blog/views.py @@ -0,0 +1,38 @@ +from django.http import HttpResponseRedirect +from django.shortcuts import render + +from blog.forms import CommentForm +from blog.models import Comment, Post + + +def blog_index(request): + posts = Post.objects.all().order_by("-created_on") + context = {"posts": posts} + return render(request, "blog/index.html", context) + + +def blog_category(request, category): + posts = Post.objects.filter(categories__name__contains=category).order_by( + "-created_on" + ) + context = {"category": category, "posts": posts} + return render(request, "blog/category.html", context) + + +def blog_detail(request, pk): + post = Post.objects.get(pk=pk) + form = CommentForm() + if request.method == "POST": + form = CommentForm(request.POST) + if form.is_valid(): + comment = Comment( + author=form.cleaned_data["author"], + body=form.cleaned_data["body"], + post=post, + ) + comment.save() + return HttpResponseRedirect(request.path_info) + comments = Comment.objects.filter(post=post) + context = {"post": post, "comments": comments, "form": form} + + return render(request, "blog/detail.html", context) diff --git a/rp-blog/django-blog/manage.py b/rp-blog/django-blog/manage.py new file mode 100755 index 0000000000..3e900181cc --- /dev/null +++ b/rp-blog/django-blog/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "personal_blog.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/rp-blog/django-blog/personal_blog/__init__.py b/rp-blog/django-blog/personal_blog/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/rp-blog/django-blog/personal_blog/asgi.py b/rp-blog/django-blog/personal_blog/asgi.py new file mode 100644 index 0000000000..8da0b6f372 --- /dev/null +++ b/rp-blog/django-blog/personal_blog/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for personal_blog project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "personal_blog.settings") + +application = get_asgi_application() diff --git a/rp-blog/django-blog/personal_blog/settings.py b/rp-blog/django-blog/personal_blog/settings.py new file mode 100644 index 0000000000..046f736a1b --- /dev/null +++ b/rp-blog/django-blog/personal_blog/settings.py @@ -0,0 +1,128 @@ +""" +Django settings for personal_blog project. + +Generated by 'django-admin startproject' using Django 4.2.4. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.2/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = ( + "django-insecure-jl==lja$kjxwf+!=yt@(2oja*6ob8jg#!n*_7#j_a+zrrk2-kr" +) + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + "blog.apps.BlogConfig", + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "personal_blog.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [ + BASE_DIR / "templates/", + ], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "personal_blog.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.2/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.2/howto/static-files/ + +STATIC_URL = "static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/rp-blog/django-blog/personal_blog/urls.py b/rp-blog/django-blog/personal_blog/urls.py new file mode 100644 index 0000000000..948559b782 --- /dev/null +++ b/rp-blog/django-blog/personal_blog/urls.py @@ -0,0 +1,7 @@ +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + path("admin/", admin.site.urls), + path("", include("blog.urls")), +] diff --git a/rp-blog/django-blog/personal_blog/wsgi.py b/rp-blog/django-blog/personal_blog/wsgi.py new file mode 100644 index 0000000000..d359bd0277 --- /dev/null +++ b/rp-blog/django-blog/personal_blog/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for personal_blog project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "personal_blog.settings") + +application = get_wsgi_application() diff --git a/rp-blog/django-blog/templates/base.html b/rp-blog/django-blog/templates/base.html new file mode 100644 index 0000000000..237f952328 --- /dev/null +++ b/rp-blog/django-blog/templates/base.html @@ -0,0 +1,15 @@ + + + + + My Personal Blog + + + +

My Personal Blog

+Home +
+{% block page_title %}{% endblock page_title %} +{% block page_content %}{% endblock page_content %} + + diff --git a/rp-blog/requirements.txt b/rp-blog/requirements.txt new file mode 100644 index 0000000000..57104a52a0 --- /dev/null +++ b/rp-blog/requirements.txt @@ -0,0 +1,3 @@ +asgiref==3.7.2 +Django==4.2.4 +sqlparse==0.4.4 From 7bcdde2d61fdad5ce4d3097b66088bc0bbf8fa7a Mon Sep 17 00:00:00 2001 From: Geir Arne Hjelle Date: Thu, 5 Oct 2023 21:12:04 +0200 Subject: [PATCH 3/3] Rename blog materials directory (#442) --- {rp-blog => build-a-blog-from-scratch-django}/README.md | 0 .../django-blog/blog/__init__.py | 0 .../django-blog/blog/admin.py | 0 .../django-blog/blog/apps.py | 0 .../django-blog/blog/forms.py | 0 .../django-blog/blog/migrations/0001_initial.py | 0 .../django-blog/blog/migrations/__init__.py | 0 .../django-blog/blog/models.py | 0 .../django-blog/blog/templates/blog/category.html | 0 .../django-blog/blog/templates/blog/detail.html | 0 .../django-blog/blog/templates/blog/index.html | 0 .../django-blog/blog/urls.py | 0 .../django-blog/blog/views.py | 0 .../django-blog/manage.py | 0 .../django-blog/personal_blog/__init__.py | 0 .../django-blog/personal_blog/asgi.py | 0 .../django-blog/personal_blog/settings.py | 0 .../django-blog/personal_blog/urls.py | 0 .../django-blog/personal_blog/wsgi.py | 0 .../django-blog/templates/base.html | 0 {rp-blog => build-a-blog-from-scratch-django}/requirements.txt | 0 21 files changed, 0 insertions(+), 0 deletions(-) rename {rp-blog => build-a-blog-from-scratch-django}/README.md (100%) rename {rp-blog => build-a-blog-from-scratch-django}/django-blog/blog/__init__.py (100%) rename {rp-blog => build-a-blog-from-scratch-django}/django-blog/blog/admin.py (100%) rename {rp-blog => build-a-blog-from-scratch-django}/django-blog/blog/apps.py (100%) rename {rp-blog => build-a-blog-from-scratch-django}/django-blog/blog/forms.py (100%) rename {rp-blog => build-a-blog-from-scratch-django}/django-blog/blog/migrations/0001_initial.py (100%) rename {rp-blog => build-a-blog-from-scratch-django}/django-blog/blog/migrations/__init__.py (100%) rename {rp-blog => build-a-blog-from-scratch-django}/django-blog/blog/models.py (100%) rename {rp-blog => build-a-blog-from-scratch-django}/django-blog/blog/templates/blog/category.html (100%) rename {rp-blog => build-a-blog-from-scratch-django}/django-blog/blog/templates/blog/detail.html (100%) rename {rp-blog => build-a-blog-from-scratch-django}/django-blog/blog/templates/blog/index.html (100%) rename {rp-blog => build-a-blog-from-scratch-django}/django-blog/blog/urls.py (100%) rename {rp-blog => build-a-blog-from-scratch-django}/django-blog/blog/views.py (100%) rename {rp-blog => build-a-blog-from-scratch-django}/django-blog/manage.py (100%) rename {rp-blog => build-a-blog-from-scratch-django}/django-blog/personal_blog/__init__.py (100%) rename {rp-blog => build-a-blog-from-scratch-django}/django-blog/personal_blog/asgi.py (100%) rename {rp-blog => build-a-blog-from-scratch-django}/django-blog/personal_blog/settings.py (100%) rename {rp-blog => build-a-blog-from-scratch-django}/django-blog/personal_blog/urls.py (100%) rename {rp-blog => build-a-blog-from-scratch-django}/django-blog/personal_blog/wsgi.py (100%) rename {rp-blog => build-a-blog-from-scratch-django}/django-blog/templates/base.html (100%) rename {rp-blog => build-a-blog-from-scratch-django}/requirements.txt (100%) diff --git a/rp-blog/README.md b/build-a-blog-from-scratch-django/README.md similarity index 100% rename from rp-blog/README.md rename to build-a-blog-from-scratch-django/README.md diff --git a/rp-blog/django-blog/blog/__init__.py b/build-a-blog-from-scratch-django/django-blog/blog/__init__.py similarity index 100% rename from rp-blog/django-blog/blog/__init__.py rename to build-a-blog-from-scratch-django/django-blog/blog/__init__.py diff --git a/rp-blog/django-blog/blog/admin.py b/build-a-blog-from-scratch-django/django-blog/blog/admin.py similarity index 100% rename from rp-blog/django-blog/blog/admin.py rename to build-a-blog-from-scratch-django/django-blog/blog/admin.py diff --git a/rp-blog/django-blog/blog/apps.py b/build-a-blog-from-scratch-django/django-blog/blog/apps.py similarity index 100% rename from rp-blog/django-blog/blog/apps.py rename to build-a-blog-from-scratch-django/django-blog/blog/apps.py diff --git a/rp-blog/django-blog/blog/forms.py b/build-a-blog-from-scratch-django/django-blog/blog/forms.py similarity index 100% rename from rp-blog/django-blog/blog/forms.py rename to build-a-blog-from-scratch-django/django-blog/blog/forms.py diff --git a/rp-blog/django-blog/blog/migrations/0001_initial.py b/build-a-blog-from-scratch-django/django-blog/blog/migrations/0001_initial.py similarity index 100% rename from rp-blog/django-blog/blog/migrations/0001_initial.py rename to build-a-blog-from-scratch-django/django-blog/blog/migrations/0001_initial.py diff --git a/rp-blog/django-blog/blog/migrations/__init__.py b/build-a-blog-from-scratch-django/django-blog/blog/migrations/__init__.py similarity index 100% rename from rp-blog/django-blog/blog/migrations/__init__.py rename to build-a-blog-from-scratch-django/django-blog/blog/migrations/__init__.py diff --git a/rp-blog/django-blog/blog/models.py b/build-a-blog-from-scratch-django/django-blog/blog/models.py similarity index 100% rename from rp-blog/django-blog/blog/models.py rename to build-a-blog-from-scratch-django/django-blog/blog/models.py diff --git a/rp-blog/django-blog/blog/templates/blog/category.html b/build-a-blog-from-scratch-django/django-blog/blog/templates/blog/category.html similarity index 100% rename from rp-blog/django-blog/blog/templates/blog/category.html rename to build-a-blog-from-scratch-django/django-blog/blog/templates/blog/category.html diff --git a/rp-blog/django-blog/blog/templates/blog/detail.html b/build-a-blog-from-scratch-django/django-blog/blog/templates/blog/detail.html similarity index 100% rename from rp-blog/django-blog/blog/templates/blog/detail.html rename to build-a-blog-from-scratch-django/django-blog/blog/templates/blog/detail.html diff --git a/rp-blog/django-blog/blog/templates/blog/index.html b/build-a-blog-from-scratch-django/django-blog/blog/templates/blog/index.html similarity index 100% rename from rp-blog/django-blog/blog/templates/blog/index.html rename to build-a-blog-from-scratch-django/django-blog/blog/templates/blog/index.html diff --git a/rp-blog/django-blog/blog/urls.py b/build-a-blog-from-scratch-django/django-blog/blog/urls.py similarity index 100% rename from rp-blog/django-blog/blog/urls.py rename to build-a-blog-from-scratch-django/django-blog/blog/urls.py diff --git a/rp-blog/django-blog/blog/views.py b/build-a-blog-from-scratch-django/django-blog/blog/views.py similarity index 100% rename from rp-blog/django-blog/blog/views.py rename to build-a-blog-from-scratch-django/django-blog/blog/views.py diff --git a/rp-blog/django-blog/manage.py b/build-a-blog-from-scratch-django/django-blog/manage.py similarity index 100% rename from rp-blog/django-blog/manage.py rename to build-a-blog-from-scratch-django/django-blog/manage.py diff --git a/rp-blog/django-blog/personal_blog/__init__.py b/build-a-blog-from-scratch-django/django-blog/personal_blog/__init__.py similarity index 100% rename from rp-blog/django-blog/personal_blog/__init__.py rename to build-a-blog-from-scratch-django/django-blog/personal_blog/__init__.py diff --git a/rp-blog/django-blog/personal_blog/asgi.py b/build-a-blog-from-scratch-django/django-blog/personal_blog/asgi.py similarity index 100% rename from rp-blog/django-blog/personal_blog/asgi.py rename to build-a-blog-from-scratch-django/django-blog/personal_blog/asgi.py diff --git a/rp-blog/django-blog/personal_blog/settings.py b/build-a-blog-from-scratch-django/django-blog/personal_blog/settings.py similarity index 100% rename from rp-blog/django-blog/personal_blog/settings.py rename to build-a-blog-from-scratch-django/django-blog/personal_blog/settings.py diff --git a/rp-blog/django-blog/personal_blog/urls.py b/build-a-blog-from-scratch-django/django-blog/personal_blog/urls.py similarity index 100% rename from rp-blog/django-blog/personal_blog/urls.py rename to build-a-blog-from-scratch-django/django-blog/personal_blog/urls.py diff --git a/rp-blog/django-blog/personal_blog/wsgi.py b/build-a-blog-from-scratch-django/django-blog/personal_blog/wsgi.py similarity index 100% rename from rp-blog/django-blog/personal_blog/wsgi.py rename to build-a-blog-from-scratch-django/django-blog/personal_blog/wsgi.py diff --git a/rp-blog/django-blog/templates/base.html b/build-a-blog-from-scratch-django/django-blog/templates/base.html similarity index 100% rename from rp-blog/django-blog/templates/base.html rename to build-a-blog-from-scratch-django/django-blog/templates/base.html diff --git a/rp-blog/requirements.txt b/build-a-blog-from-scratch-django/requirements.txt similarity index 100% rename from rp-blog/requirements.txt rename to build-a-blog-from-scratch-django/requirements.txt