├── .dockerignore ├── python_django_blog ├── __init__.py ├── articles │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── apps.py │ ├── forms.py │ ├── urls.py │ ├── models.py │ ├── views.py │ └── tests.py ├── locale │ └── ru │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── templates │ ├── index.html │ ├── articles │ │ ├── delete.html │ │ ├── create.html │ │ ├── update.html │ │ ├── detail.html │ │ └── index.html │ ├── about.html │ └── layouts │ │ ├── confirm_delete.html │ │ ├── form.html │ │ └── application.html ├── views.py ├── fixtures │ ├── test_data.json │ └── articles.json ├── tests.py ├── asgi.py ├── wsgi.py ├── utils.py ├── urls.py └── settings.py ├── .coveragerc ├── .env.example ├── .gitignore ├── docker-compose.yml ├── ruff.toml ├── Dockerfile ├── pyproject.toml ├── manage.py ├── Makefile ├── README.md ├── .github ├── workflows │ └── pyci.yml └── dependabot.yml └── uv.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | .venv 2 | -------------------------------------------------------------------------------- /python_django_blog/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python_django_blog/articles/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = python_django_blog 3 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | SECRET_KEY=somekey 2 | DEBUG=True 3 | -------------------------------------------------------------------------------- /python_django_blog/articles/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .venv/ 2 | *.sqlite3 3 | __pycache__/ 4 | .env 5 | .coverage 6 | htmlcov/ 7 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | build: . 4 | volumes: 5 | - .:/app 6 | ports: 7 | - 8000:8000 8 | command: make start 9 | -------------------------------------------------------------------------------- /python_django_blog/locale/ru/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-components/python-django-blog/HEAD/python_django_blog/locale/ru/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /python_django_blog/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/application.html' %} 2 | {% load i18n %} 3 | 4 | {% block header %} 5 | {% translate "Python Django Blog" %} 6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /python_django_blog/templates/articles/delete.html: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/confirm_delete.html' %} 2 | {% load i18n %} 3 | 4 | {% block header %} 5 | {% translate "Delete article" %} 6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /python_django_blog/articles/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ArticlesConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'python_django_blog.articles' 7 | -------------------------------------------------------------------------------- /python_django_blog/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import TemplateView 2 | 3 | 4 | class IndexView(TemplateView): 5 | template_name = 'index.html' 6 | 7 | 8 | class AboutView(TemplateView): 9 | template_name = 'about.html' 10 | -------------------------------------------------------------------------------- /python_django_blog/templates/articles/create.html: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/form.html' %} 2 | {% load i18n %} 3 | 4 | {% block header %} 5 | {% translate "Create article" %} 6 | {% endblock %} 7 | 8 | {% block button_value %}{% translate "Create" %}{% endblock %} 9 | -------------------------------------------------------------------------------- /python_django_blog/templates/articles/update.html: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/form.html' %} 2 | {% load i18n %} 3 | 4 | {% block header %} 5 | {% translate "Update article" %} 6 | {% endblock %} 7 | 8 | {% block button_value %}{% translate "Update" %}{% endblock %} 9 | -------------------------------------------------------------------------------- /python_django_blog/articles/forms.py: -------------------------------------------------------------------------------- 1 | from django.forms import ModelForm 2 | from python_django_blog.articles.models import Article 3 | 4 | 5 | class ArticleForm(ModelForm): 6 | 7 | class Meta: 8 | model = Article 9 | fields = ['name', 'description'] 10 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | line-length = 80 2 | exclude = ["migrations", "settings.py", "manage.py"] 3 | 4 | [lint.per-file-ignores] 5 | # init modules can contain the local imports, logic, unused imports 6 | "__init__.py" = ["F401"] 7 | 8 | [lint] 9 | preview = true 10 | select = ["E", "F", "C90"] 11 | -------------------------------------------------------------------------------- /python_django_blog/fixtures/test_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "articles": { 3 | "new": { 4 | "name": "article new", 5 | "description": "description new" 6 | }, 7 | "existing": { 8 | "name": "article two", 9 | "description": "description two" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /python_django_blog/templates/about.html: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/application.html' %} 2 | {% load i18n %} 3 | 4 | {% block header %} 5 | {% translate "About the blog" %} 6 | {% endblock %} 7 | 8 | {% block content %} 9 | {% translate "Experimenting with Django on Hexlet" %} 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.13.2-slim 2 | 3 | RUN apt-get update && apt-get install -yq make gettext 4 | 5 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ 6 | 7 | WORKDIR /app 8 | 9 | COPY . . 10 | 11 | RUN uv sync 12 | 13 | CMD ["bash", "-c", "uv run manage.py migrate && uv run gunicorn python_django_blog.wsgi --log-file -"] 14 | -------------------------------------------------------------------------------- /python_django_blog/templates/layouts/confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/application.html' %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |

{% translate "Are you sure you want to delete" %} {{ object }}?

6 |
7 | {% csrf_token %} 8 | 9 |
10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /python_django_blog/templates/layouts/form.html: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/application.html' %} 2 | {% load bootstrap4 %} 3 | 4 | {% block content %} 5 |
6 | {% csrf_token %} 7 | {% bootstrap_form form %} 8 | {% buttons %} 9 | 10 | {% endbuttons %} 11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /python_django_blog/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.urls import reverse 3 | 4 | 5 | class AppTest(TestCase): 6 | 7 | def test_index_page(self): 8 | response = self.client.get(reverse('root')) 9 | self.assertEqual(response.status_code, 200) 10 | 11 | def test_about_page(self): 12 | response = self.client.get(reverse('about')) 13 | self.assertEqual(response.status_code, 200) 14 | -------------------------------------------------------------------------------- /python_django_blog/templates/articles/detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/application.html' %} 2 | {% load i18n %} 3 | 4 | {% block header %} 5 | {% translate "Show article" %} 6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 |
11 |

{{ article.name }}

12 |
13 |
14 |

{{ article.description }}

15 |
16 |
17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /python_django_blog/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for python_django_blog 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.2/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', 'python_django_blog.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /python_django_blog/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for python_django_blog 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.2/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', 'python_django_blog.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /python_django_blog/articles/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from python_django_blog.articles import views 3 | 4 | app_name = 'articles' 5 | 6 | urlpatterns = [ 7 | path('', views.IndexView.as_view(), name='index'), 8 | path('create/', views.ArticleCreate.as_view(), name='create'), 9 | path('/delete/', views.ArticleDelete.as_view(), name='delete'), 10 | path('/update/', views.ArticleUpdate.as_view(), name='update'), 11 | path('/', views.ArticleDetail.as_view(), name='detail'), 12 | ] 13 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "python-django-blog" 3 | version = "0.1.0" 4 | description = "A simple Django project that implements a simple blog" 5 | authors = [{name="Hexlet Team "}] 6 | license = "ISC" 7 | readme = "README.md" 8 | requires-python = ">=3.13" 9 | dependencies = [ 10 | "dj-database-url>=2.3.0", 11 | "django>=5.1.7", 12 | "django-bootstrap4>=25.1", 13 | "gunicorn>=23.0.0", 14 | "python-dotenv>=1.1.0", 15 | "whitenoise>=6.9.0", 16 | ] 17 | 18 | [dependency-groups] 19 | dev = [ 20 | "ruff>=0.11.2", 21 | ] 22 | -------------------------------------------------------------------------------- /python_django_blog/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | 5 | def get_fixture_path(file_name): 6 | current_dir = os.path.dirname(os.path.abspath(__file__)) 7 | return os.path.join(current_dir, 'fixtures', file_name) 8 | 9 | 10 | def read(file_path): 11 | with open(file_path, 'r') as file: 12 | content = file.read() 13 | return content 14 | 15 | 16 | def get_fixture_data(file_name): 17 | content = read(get_fixture_path(file_name)) 18 | return json.loads(content) 19 | 20 | 21 | def get_test_data(): 22 | return get_fixture_data('test_data.json') 23 | -------------------------------------------------------------------------------- /python_django_blog/articles/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.urls import reverse 3 | from django.utils import timezone 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | 7 | class Article(models.Model): 8 | name = models.CharField(_('name'), max_length=100, unique=True) 9 | description = models.TextField(_('description')) 10 | created_at = models.DateTimeField(_('created date'), default=timezone.now) 11 | 12 | def get_absolute_url(self): 13 | return reverse('articles:index') 14 | 15 | def __str__(self): 16 | return self.name 17 | -------------------------------------------------------------------------------- /python_django_blog/fixtures/articles.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "articles.article", 4 | "pk": 1, 5 | "fields": { 6 | "name": "article one", 7 | "description": "description one", 8 | "created_at": "2021-01-11T18:33:34.779Z" 9 | } 10 | }, 11 | { 12 | "model": "articles.article", 13 | "pk": 2, 14 | "fields": { 15 | "name": "article two", 16 | "description": "description two", 17 | "created_at": "2021-01-11T18:33:41.728Z" 18 | } 19 | }, 20 | { 21 | "model": "articles.article", 22 | "pk": 3, 23 | "fields": { 24 | "name": "article three", 25 | "description": "description two", 26 | "created_at": "2021-01-11T18:33:46.499Z" 27 | } 28 | } 29 | ] 30 | -------------------------------------------------------------------------------- /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 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'python_django_blog.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | compose-setup: compose-build compose-install 2 | 3 | compose-build: 4 | docker compose build 5 | 6 | compose-install: 7 | docker compose run app make install 8 | 9 | compose-bash: 10 | docker compose run app bash 11 | 12 | compose: 13 | docker compose up 14 | 15 | compose-lint: 16 | docker compose run app make lint 17 | 18 | compose-test: 19 | docker compose run app make test 20 | 21 | install: 22 | uv sync 23 | 24 | migrate: 25 | uv run manage.py migrate 26 | 27 | setup: 28 | cp -n .env.example .env || true 29 | make install 30 | make migrate 31 | 32 | start: 33 | uv run manage.py runserver 0.0.0.0:8000 34 | 35 | lint: 36 | uv run ruff check . 37 | 38 | test: 39 | uv run manage.py test 40 | 41 | check: test lint 42 | 43 | test-coverage: 44 | uv run coverage run manage.py test python_django_blog 45 | uv run coverage html 46 | uv run coverage report 47 | -------------------------------------------------------------------------------- /python_django_blog/articles/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.3 on 2021-05-30 10:06 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Article', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('name', models.CharField(max_length=100, unique=True, verbose_name='name')), 20 | ('description', models.TextField(verbose_name='description')), 21 | ('created_at', models.DateTimeField(default=django.utils.timezone.now, verbose_name='created date')), 22 | ], 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /python_django_blog/urls.py: -------------------------------------------------------------------------------- 1 | """python_django_blog URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.2/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 | 17 | from django.urls import include, path 18 | from python_django_blog import views 19 | 20 | urlpatterns = [ 21 | path('', views.IndexView.as_view(), name='root'), 22 | path('about/', views.AboutView.as_view(), name='about'), 23 | path('articles/', include('python_django_blog.articles.urls')), 24 | ] 25 | -------------------------------------------------------------------------------- /python_django_blog/articles/views.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse_lazy 2 | from django.views.generic import ( 3 | CreateView, 4 | DeleteView, 5 | DetailView, 6 | ListView, 7 | UpdateView, 8 | ) 9 | 10 | from python_django_blog.articles.forms import ArticleForm 11 | from python_django_blog.articles.models import Article 12 | 13 | 14 | class IndexView(ListView): 15 | model = Article 16 | template_name = "articles/index.html" 17 | 18 | 19 | class ArticleCreate(CreateView): 20 | model = Article 21 | form_class = ArticleForm 22 | template_name = "articles/create.html" 23 | 24 | 25 | class ArticleUpdate(UpdateView): 26 | model = Article 27 | form_class = ArticleForm 28 | template_name = "articles/update.html" 29 | 30 | 31 | class ArticleDelete(DeleteView): 32 | model = Article 33 | success_url = reverse_lazy("articles:index") 34 | template_name = "articles/delete.html" 35 | 36 | 37 | class ArticleDetail(DetailView): 38 | model = Article 39 | template_name = "articles/detail.html" 40 | -------------------------------------------------------------------------------- /python_django_blog/templates/layouts/application.html: -------------------------------------------------------------------------------- 1 | {% load bootstrap4 %} 2 | {% load i18n %} 3 | {% get_current_language as LANGUAGE_CODE %} 4 | 5 | 6 | 7 | 8 | 9 | {% translate "Django" %} 10 | {% bootstrap_css %} 11 | 12 | 13 |
14 | 25 |
26 |
27 |

28 | {% block header %} 29 | {% endblock %} 30 |

31 | {% block content %} 32 | {% endblock %} 33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /python_django_blog/templates/articles/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/application.html' %} 2 | {% load i18n %} 3 | 4 | {% block header %} 5 | {% translate "List of Articles" %} 6 | {% endblock %} 7 | 8 | {% block content %} 9 | {% translate "Create article" %} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% for article in article_list %} 21 | 22 | 23 | 26 | 27 | 32 | 33 | {% endfor %} 34 | 35 |
{% translate "id"|upper %}{% translate "name"|capfirst %}{% translate "created date"|capfirst %}
{{ article.id }} 24 | {{ article.name }} 25 | {{ article.created_at|date:"d.m.Y H:i" }} 28 | {% translate "Update" %} 29 |
30 | {% translate "Delete" %} 31 |
36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-django-blog 2 | 3 | [![github action status](https://github.com/hexlet-components/python-django-blog/workflows/Python%20CI/badge.svg)](https://github.com/hexlet-components/python-django-blog/actions) 4 | 5 | [Demo on Heroku](https://python-django-blog.hexlet.app) 6 | 7 | ## Requirements 8 | 9 | * Python 3.13+ 10 | * uv 11 | * GNU Make 12 | 13 | ## Setup 14 | 15 | ```bash 16 | make setup 17 | ``` 18 | 19 | ## Run server 20 | 21 | ```bash 22 | make start 23 | # Open http://localhost:8000 24 | ``` 25 | 26 | ## Check codestyle 27 | 28 | ```bash 29 | make lint 30 | ``` 31 | 32 | ## Run tests 33 | 34 | ```bash 35 | make test 36 | make test-coverage # run tests with coverage report 37 | ``` 38 | 39 | --- 40 | 41 | [![Hexlet Ltd. logo](https://raw.githubusercontent.com/Hexlet/assets/master/images/hexlet_logo128.png)](https://hexlet.io?utm_source=github&utm_medium=link&utm_campaign=python-django-blog) 42 | 43 | This repository is created and maintained by the team and the community of Hexlet, an educational project. [Read more about Hexlet](https://hexlet.io?utm_source=github&utm_medium=link&utm_campaign=python-django-blog). 44 | 45 | See most active contributors on [hexlet-friends](https://friends.hexlet.io/). 46 | -------------------------------------------------------------------------------- /.github/workflows/pyci.yml: -------------------------------------------------------------------------------- 1 | name: Python CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-python@v5 18 | with: 19 | python-version: '3.13' 20 | 21 | - name: Install dependencies 22 | run: | 23 | pip install uv 24 | make setup 25 | 26 | - name: Run tests and linter 27 | run: | 28 | make check 29 | 30 | deploy: 31 | needs: build 32 | runs-on: ubuntu-latest 33 | if: ${{ github.event_name == 'push' }} 34 | 35 | steps: 36 | - uses: actions/checkout@v4 37 | - uses: docker/setup-buildx-action@v3 38 | 39 | - name: Log in to GitHub Container Registry 40 | uses: docker/login-action@v3 41 | with: 42 | registry: ghcr.io 43 | username: ${{ github.actor }} 44 | password: ${{ secrets.GITHUB_TOKEN }} 45 | 46 | - name: Build and push 47 | uses: docker/build-push-action@v6 48 | with: 49 | context: . 50 | push: true 51 | cache-from: ghcr.io/${{ github.repository }}:latest 52 | cache-to: type=inline 53 | tags: ghcr.io/${{ github.repository }}:latest 54 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | # === GitHub Actions === 5 | - package-ecosystem: github-actions 6 | directory: / 7 | schedule: 8 | interval: weekly 9 | labels: [dependencies, automated, actions] 10 | groups: 11 | actions-minor-patch: 12 | update-types: [minor, patch] 13 | actions-major: 14 | update-types: [major] 15 | # === Python dev (pip) === 16 | - package-ecosystem: pip 17 | directory: / 18 | schedule: 19 | interval: weekly 20 | labels: [dependencies, automated, dev] 21 | groups: 22 | pip-dev-minor-patch: 23 | patterns: 24 | - pytest* 25 | - flake8* 26 | - black 27 | - isort 28 | - mypy* 29 | - coverage* 30 | - tox* 31 | - ruff 32 | - pylint* 33 | - bandit 34 | - pre-commit 35 | - sphinx* 36 | - mkdocs* 37 | update-types: [minor, patch] 38 | # === Python runtime (pip) === 39 | - package-ecosystem: pip 40 | directory: / 41 | schedule: 42 | interval: weekly 43 | labels: [dependencies, automated, runtime] 44 | groups: 45 | pip-runtime-minor-patch: 46 | update-types: [minor, patch] 47 | # === Docker === 48 | - package-ecosystem: docker 49 | directory: / 50 | schedule: 51 | interval: weekly 52 | labels: [dependencies, automated, docker] 53 | -------------------------------------------------------------------------------- /python_django_blog/locale/ru/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2021-05-30 10:11+0000\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 20 | "%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n" 21 | "%100>=11 && n%100<=14)? 2 : 3);\n" 22 | #: python_django_blog/articles/models.py:8 23 | #: python_django_blog/templates/articles/index.html:14 24 | msgid "name" 25 | msgstr "наименование" 26 | 27 | #: python_django_blog/articles/models.py:9 28 | msgid "description" 29 | msgstr "описание" 30 | 31 | #: python_django_blog/articles/models.py:10 32 | #: python_django_blog/templates/articles/index.html:15 33 | msgid "created date" 34 | msgstr "дата создания" 35 | 36 | #: python_django_blog/templates/about.html:5 37 | msgid "About the blog" 38 | msgstr "О блоге" 39 | 40 | #: python_django_blog/templates/about.html:9 41 | msgid "Experimenting with Django on Hexlet" 42 | msgstr "Эксперименты с Django на Хекслете" 43 | 44 | #: python_django_blog/templates/articles/create.html:5 45 | #: python_django_blog/templates/articles/index.html:9 46 | msgid "Create article" 47 | msgstr "Создать статью" 48 | 49 | #: python_django_blog/templates/articles/create.html:8 50 | msgid "Create" 51 | msgstr "Создать" 52 | 53 | #: python_django_blog/templates/articles/delete.html:5 54 | msgid "Delete article" 55 | msgstr "Удаление статьи" 56 | 57 | #: python_django_blog/templates/articles/detail.html:5 58 | msgid "Show article" 59 | msgstr "Просмотр статьи" 60 | 61 | #: python_django_blog/templates/articles/index.html:5 62 | msgid "List of Articles" 63 | msgstr "Список статей" 64 | 65 | #: python_django_blog/templates/articles/index.html:13 66 | msgid "id" 67 | msgstr "id" 68 | 69 | #: python_django_blog/templates/articles/index.html:28 70 | #: python_django_blog/templates/articles/update.html:8 71 | msgid "Update" 72 | msgstr "Изменить" 73 | 74 | #: python_django_blog/templates/articles/index.html:30 75 | msgid "Delete" 76 | msgstr "Удалить" 77 | 78 | #: python_django_blog/templates/articles/update.html:5 79 | msgid "Update article" 80 | msgstr "Изменение статьи" 81 | 82 | #: python_django_blog/templates/index.html:5 83 | msgid "Python Django Blog" 84 | msgstr "Python Django Blog" 85 | 86 | #: python_django_blog/templates/layouts/application.html:9 87 | msgid "Django" 88 | msgstr "Django" 89 | 90 | #: python_django_blog/templates/layouts/application.html:16 91 | msgid "Home" 92 | msgstr "Главная" 93 | 94 | #: python_django_blog/templates/layouts/application.html:19 95 | msgid "About" 96 | msgstr "О блоге" 97 | 98 | #: python_django_blog/templates/layouts/application.html:22 99 | msgid "Articles" 100 | msgstr "Статьи" 101 | 102 | #: python_django_blog/templates/layouts/confirm_delete.html:5 103 | msgid "Are you sure you want to delete" 104 | msgstr "Вы уверены, что хотите удалить" 105 | 106 | #: python_django_blog/templates/layouts/confirm_delete.html:8 107 | msgid "Yes, delete" 108 | msgstr "Да, удалить" 109 | -------------------------------------------------------------------------------- /python_django_blog/articles/tests.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ObjectDoesNotExist 2 | from django.test import TestCase 3 | from django.urls import reverse 4 | 5 | from python_django_blog.articles.models import Article 6 | from python_django_blog.utils import get_test_data 7 | 8 | 9 | class ArticleTest(TestCase): 10 | fixtures = ["articles.json"] 11 | 12 | @classmethod 13 | def setUpTestData(cls): 14 | cls.test_data = get_test_data() 15 | 16 | def assertArticle(self, article, article_data): 17 | self.assertEqual(article.__str__(), article_data["name"]) 18 | self.assertEqual(article.name, article_data["name"]) 19 | self.assertEqual(article.description, article_data["description"]) 20 | 21 | def test_index_page(self): 22 | response = self.client.get(reverse("articles:index")) 23 | self.assertEqual(response.status_code, 200) 24 | 25 | articles = Article.objects.all() 26 | self.assertQuerySetEqual( 27 | response.context["article_list"], 28 | articles, 29 | ordered=False, 30 | ) 31 | 32 | def test_create_page(self): 33 | response = self.client.get(reverse("articles:create")) 34 | self.assertEqual(response.status_code, 200) 35 | 36 | def test_create(self): 37 | new_article_data = self.test_data["articles"]["new"] 38 | response = self.client.post( 39 | reverse("articles:create"), new_article_data 40 | ) 41 | 42 | self.assertRedirects(response, reverse("articles:index")) 43 | created_article = Article.objects.get(name=new_article_data["name"]) 44 | self.assertArticle(created_article, new_article_data) 45 | 46 | def test_update_page(self): 47 | exist_article_data = self.test_data["articles"]["existing"] 48 | exist_article = Article.objects.get(name=exist_article_data["name"]) 49 | response = self.client.get( 50 | reverse("articles:update", args=[exist_article.pk]) 51 | ) 52 | 53 | self.assertEqual(response.status_code, 200) 54 | 55 | def test_update(self): 56 | exist_article_data = self.test_data["articles"]["existing"] 57 | new_article_data = self.test_data["articles"]["new"] 58 | exist_article = Article.objects.get(name=exist_article_data["name"]) 59 | response = self.client.post( 60 | reverse("articles:update", args=[exist_article.pk]), 61 | new_article_data, 62 | ) 63 | 64 | self.assertRedirects(response, reverse("articles:index")) 65 | updated_article = Article.objects.get(name=new_article_data["name"]) 66 | self.assertArticle(updated_article, new_article_data) 67 | 68 | def test_delete_page(self): 69 | exist_article_data = self.test_data["articles"]["existing"] 70 | exist_article = Article.objects.get(name=exist_article_data["name"]) 71 | response = self.client.get( 72 | reverse("articles:delete", args=[exist_article.pk]) 73 | ) 74 | 75 | self.assertEqual(response.status_code, 200) 76 | 77 | def test_delete(self): 78 | exist_article_data = self.test_data["articles"]["existing"] 79 | exist_article = Article.objects.get(name=exist_article_data["name"]) 80 | response = self.client.post( 81 | reverse("articles:delete", args=[exist_article.pk]) 82 | ) 83 | 84 | self.assertRedirects(response, reverse("articles:index")) 85 | with self.assertRaises(ObjectDoesNotExist): 86 | Article.objects.get(name=exist_article_data["name"]) 87 | 88 | def test_detail_page(self): 89 | exist_article_data = self.test_data["articles"]["existing"] 90 | exist_article = Article.objects.get(name=exist_article_data["name"]) 91 | response = self.client.get( 92 | reverse("articles:detail", args=[exist_article.pk]) 93 | ) 94 | 95 | self.assertEqual(response.status_code, 200) 96 | -------------------------------------------------------------------------------- /python_django_blog/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for python_django_blog project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2.3. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | 13 | import os 14 | from pathlib import Path 15 | from dotenv import load_dotenv 16 | import dj_database_url 17 | 18 | load_dotenv() 19 | 20 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 21 | BASE_DIR = Path(__file__).resolve().parent.parent 22 | 23 | 24 | # Quick-start development settings - unsuitable for production 25 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 26 | 27 | # SECURITY WARNING: keep the secret key used in production secret! 28 | SECRET_KEY = os.getenv('SECRET_KEY') 29 | 30 | # SECURITY WARNING: don't run with debug turned on in production! 31 | DEBUG = os.getenv('DEBUG', 'False') == 'True' 32 | 33 | ALLOWED_HOSTS = ['*'] 34 | 35 | 36 | # Application definition 37 | 38 | INSTALLED_APPS = [ 39 | 'whitenoise.runserver_nostatic', 40 | 'django.contrib.admin', 41 | 'django.contrib.auth', 42 | 'django.contrib.contenttypes', 43 | 'django.contrib.sessions', 44 | 'django.contrib.messages', 45 | 'django.contrib.staticfiles', 46 | 'bootstrap4', 47 | 'python_django_blog', 48 | 'python_django_blog.articles', 49 | ] 50 | 51 | MIDDLEWARE = [ 52 | 'django.middleware.security.SecurityMiddleware', 53 | 'whitenoise.middleware.WhiteNoiseMiddleware', 54 | 'django.contrib.sessions.middleware.SessionMiddleware', 55 | 'django.middleware.common.CommonMiddleware', 56 | 'django.middleware.csrf.CsrfViewMiddleware', 57 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 58 | 'django.contrib.messages.middleware.MessageMiddleware', 59 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 60 | ] 61 | 62 | ROOT_URLCONF = 'python_django_blog.urls' 63 | 64 | TEMPLATES = [ 65 | { 66 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 67 | 'DIRS': [], 68 | 'APP_DIRS': True, 69 | 'OPTIONS': { 70 | 'context_processors': [ 71 | 'django.template.context_processors.debug', 72 | 'django.template.context_processors.request', 73 | 'django.contrib.auth.context_processors.auth', 74 | 'django.contrib.messages.context_processors.messages', 75 | ], 76 | }, 77 | }, 78 | ] 79 | 80 | WSGI_APPLICATION = 'python_django_blog.wsgi.application' 81 | 82 | 83 | # Database 84 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 85 | 86 | DATABASES = { 87 | "default": { 88 | "ENGINE": "django.db.backends.sqlite3", 89 | "NAME": os.path.join(BASE_DIR, "db.sqlite3"), 90 | } 91 | } 92 | 93 | db_from_env = dj_database_url.config(conn_max_age=600) 94 | DATABASES["default"].update(db_from_env) 95 | 96 | # Password validation 97 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 98 | 99 | AUTH_PASSWORD_VALIDATORS = [ 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 102 | }, 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 105 | }, 106 | { 107 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 108 | }, 109 | { 110 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 111 | }, 112 | ] 113 | 114 | 115 | # Internationalization 116 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 117 | 118 | LANGUAGE_CODE = 'ru-ru' 119 | 120 | TIME_ZONE = 'UTC' 121 | 122 | USE_I18N = True 123 | 124 | USE_L10N = False 125 | 126 | USE_TZ = True 127 | 128 | 129 | # Static files (CSS, JavaScript, Images) 130 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 131 | 132 | STATIC_URL = '/static/' 133 | STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') 134 | STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' 135 | 136 | # Default primary key field type 137 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 138 | 139 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 140 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 1 3 | requires-python = ">=3.13" 4 | 5 | [[package]] 6 | name = "asgiref" 7 | version = "3.8.1" 8 | source = { registry = "https://pypi.org/simple" } 9 | sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186 } 10 | wheels = [ 11 | { url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828 }, 12 | ] 13 | 14 | [[package]] 15 | name = "beautifulsoup4" 16 | version = "4.13.3" 17 | source = { registry = "https://pypi.org/simple" } 18 | dependencies = [ 19 | { name = "soupsieve" }, 20 | { name = "typing-extensions" }, 21 | ] 22 | sdist = { url = "https://files.pythonhosted.org/packages/f0/3c/adaf39ce1fb4afdd21b611e3d530b183bb7759c9b673d60db0e347fd4439/beautifulsoup4-4.13.3.tar.gz", hash = "sha256:1bd32405dacc920b42b83ba01644747ed77456a65760e285fbc47633ceddaf8b", size = 619516 } 23 | wheels = [ 24 | { url = "https://files.pythonhosted.org/packages/f9/49/6abb616eb3cbab6a7cca303dc02fdf3836de2e0b834bf966a7f5271a34d8/beautifulsoup4-4.13.3-py3-none-any.whl", hash = "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16", size = 186015 }, 25 | ] 26 | 27 | [[package]] 28 | name = "dj-database-url" 29 | version = "2.3.0" 30 | source = { registry = "https://pypi.org/simple" } 31 | dependencies = [ 32 | { name = "django" }, 33 | { name = "typing-extensions" }, 34 | ] 35 | sdist = { url = "https://files.pythonhosted.org/packages/98/9f/fc9905758256af4f68a55da94ab78a13e7775074edfdcaddd757d4921686/dj_database_url-2.3.0.tar.gz", hash = "sha256:ae52e8e634186b57e5a45e445da5dc407a819c2ceed8a53d1fac004cc5288787", size = 10980 } 36 | wheels = [ 37 | { url = "https://files.pythonhosted.org/packages/e5/91/641a4e5c8903ed59f6cbcce571003bba9c5d2f731759c31db0ba83bb0bdb/dj_database_url-2.3.0-py3-none-any.whl", hash = "sha256:bb0d414ba0ac5cd62773ec7f86f8cc378a9dbb00a80884c2fc08cc570452521e", size = 7793 }, 38 | ] 39 | 40 | [[package]] 41 | name = "django" 42 | version = "5.1.7" 43 | source = { registry = "https://pypi.org/simple" } 44 | dependencies = [ 45 | { name = "asgiref" }, 46 | { name = "sqlparse" }, 47 | { name = "tzdata", marker = "sys_platform == 'win32'" }, 48 | ] 49 | sdist = { url = "https://files.pythonhosted.org/packages/5f/57/11186e493ddc5a5e92cc7924a6363f7d4c2b645f7d7cb04a26a63f9bfb8b/Django-5.1.7.tar.gz", hash = "sha256:30de4ee43a98e5d3da36a9002f287ff400b43ca51791920bfb35f6917bfe041c", size = 10716510 } 50 | wheels = [ 51 | { url = "https://files.pythonhosted.org/packages/ba/0f/7e042df3d462d39ae01b27a09ee76653692442bc3701fbfa6cb38e12889d/Django-5.1.7-py3-none-any.whl", hash = "sha256:1323617cb624add820cb9611cdcc788312d250824f92ca6048fda8625514af2b", size = 8276912 }, 52 | ] 53 | 54 | [[package]] 55 | name = "django-bootstrap4" 56 | version = "25.1" 57 | source = { registry = "https://pypi.org/simple" } 58 | dependencies = [ 59 | { name = "beautifulsoup4" }, 60 | { name = "django" }, 61 | ] 62 | sdist = { url = "https://files.pythonhosted.org/packages/cf/bb/e2f15ae18598d81d0ef4dd51e9abc6cd42bbd1398cb36d3f28201572b756/django_bootstrap4-25.1.tar.gz", hash = "sha256:6e05d17dda5922ec643e40bb804c7f399ec262c3723a4e5aef5bd550b3ff4ffa", size = 108902 } 63 | wheels = [ 64 | { url = "https://files.pythonhosted.org/packages/0a/13/007e451caec1406da56f65d1a9fcbfe76bbc7dd4b9653dcab4713f0564dd/django_bootstrap4-25.1-py3-none-any.whl", hash = "sha256:9eb574b6ddd0ae2e8b9da13746e1d084a797b8962cb8e0284c937c97e1938f29", size = 25329 }, 65 | ] 66 | 67 | [[package]] 68 | name = "gunicorn" 69 | version = "23.0.0" 70 | source = { registry = "https://pypi.org/simple" } 71 | dependencies = [ 72 | { name = "packaging" }, 73 | ] 74 | sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031 } 75 | wheels = [ 76 | { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029 }, 77 | ] 78 | 79 | [[package]] 80 | name = "packaging" 81 | version = "24.2" 82 | source = { registry = "https://pypi.org/simple" } 83 | sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } 84 | wheels = [ 85 | { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, 86 | ] 87 | 88 | [[package]] 89 | name = "python-django-blog" 90 | version = "0.1.0" 91 | source = { virtual = "." } 92 | dependencies = [ 93 | { name = "dj-database-url" }, 94 | { name = "django" }, 95 | { name = "django-bootstrap4" }, 96 | { name = "gunicorn" }, 97 | { name = "python-dotenv" }, 98 | { name = "whitenoise" }, 99 | ] 100 | 101 | [package.dev-dependencies] 102 | dev = [ 103 | { name = "ruff" }, 104 | ] 105 | 106 | [package.metadata] 107 | requires-dist = [ 108 | { name = "dj-database-url", specifier = ">=2.3.0" }, 109 | { name = "django", specifier = ">=5.1.7" }, 110 | { name = "django-bootstrap4", specifier = ">=25.1" }, 111 | { name = "gunicorn", specifier = ">=23.0.0" }, 112 | { name = "python-dotenv", specifier = ">=1.1.0" }, 113 | { name = "whitenoise", specifier = ">=6.9.0" }, 114 | ] 115 | 116 | [package.metadata.requires-dev] 117 | dev = [{ name = "ruff", specifier = ">=0.11.2" }] 118 | 119 | [[package]] 120 | name = "python-dotenv" 121 | version = "1.1.0" 122 | source = { registry = "https://pypi.org/simple" } 123 | sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 } 124 | wheels = [ 125 | { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, 126 | ] 127 | 128 | [[package]] 129 | name = "ruff" 130 | version = "0.11.2" 131 | source = { registry = "https://pypi.org/simple" } 132 | sdist = { url = "https://files.pythonhosted.org/packages/90/61/fb87430f040e4e577e784e325351186976516faef17d6fcd921fe28edfd7/ruff-0.11.2.tar.gz", hash = "sha256:ec47591497d5a1050175bdf4e1a4e6272cddff7da88a2ad595e1e326041d8d94", size = 3857511 } 133 | wheels = [ 134 | { url = "https://files.pythonhosted.org/packages/62/99/102578506f0f5fa29fd7e0df0a273864f79af044757aef73d1cae0afe6ad/ruff-0.11.2-py3-none-linux_armv6l.whl", hash = "sha256:c69e20ea49e973f3afec2c06376eb56045709f0212615c1adb0eda35e8a4e477", size = 10113146 }, 135 | { url = "https://files.pythonhosted.org/packages/74/ad/5cd4ba58ab602a579997a8494b96f10f316e874d7c435bcc1a92e6da1b12/ruff-0.11.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2c5424cc1c4eb1d8ecabe6d4f1b70470b4f24a0c0171356290b1953ad8f0e272", size = 10867092 }, 136 | { url = "https://files.pythonhosted.org/packages/fc/3e/d3f13619e1d152c7b600a38c1a035e833e794c6625c9a6cea6f63dbf3af4/ruff-0.11.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ecf20854cc73f42171eedb66f006a43d0a21bfb98a2523a809931cda569552d9", size = 10224082 }, 137 | { url = "https://files.pythonhosted.org/packages/90/06/f77b3d790d24a93f38e3806216f263974909888fd1e826717c3ec956bbcd/ruff-0.11.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c543bf65d5d27240321604cee0633a70c6c25c9a2f2492efa9f6d4b8e4199bb", size = 10394818 }, 138 | { url = "https://files.pythonhosted.org/packages/99/7f/78aa431d3ddebfc2418cd95b786642557ba8b3cb578c075239da9ce97ff9/ruff-0.11.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20967168cc21195db5830b9224be0e964cc9c8ecf3b5a9e3ce19876e8d3a96e3", size = 9952251 }, 139 | { url = "https://files.pythonhosted.org/packages/30/3e/f11186d1ddfaca438c3bbff73c6a2fdb5b60e6450cc466129c694b0ab7a2/ruff-0.11.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:955a9ce63483999d9f0b8f0b4a3ad669e53484232853054cc8b9d51ab4c5de74", size = 11563566 }, 140 | { url = "https://files.pythonhosted.org/packages/22/6c/6ca91befbc0a6539ee133d9a9ce60b1a354db12c3c5d11cfdbf77140f851/ruff-0.11.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:86b3a27c38b8fce73bcd262b0de32e9a6801b76d52cdb3ae4c914515f0cef608", size = 12208721 }, 141 | { url = "https://files.pythonhosted.org/packages/19/b0/24516a3b850d55b17c03fc399b681c6a549d06ce665915721dc5d6458a5c/ruff-0.11.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3b66a03b248c9fcd9d64d445bafdf1589326bee6fc5c8e92d7562e58883e30f", size = 11662274 }, 142 | { url = "https://files.pythonhosted.org/packages/d7/65/76be06d28ecb7c6070280cef2bcb20c98fbf99ff60b1c57d2fb9b8771348/ruff-0.11.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0397c2672db015be5aa3d4dac54c69aa012429097ff219392c018e21f5085147", size = 13792284 }, 143 | { url = "https://files.pythonhosted.org/packages/ce/d2/4ceed7147e05852876f3b5f3fdc23f878ce2b7e0b90dd6e698bda3d20787/ruff-0.11.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869bcf3f9abf6457fbe39b5a37333aa4eecc52a3b99c98827ccc371a8e5b6f1b", size = 11327861 }, 144 | { url = "https://files.pythonhosted.org/packages/c4/78/4935ecba13706fd60ebe0e3dc50371f2bdc3d9bc80e68adc32ff93914534/ruff-0.11.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2a2b50ca35457ba785cd8c93ebbe529467594087b527a08d487cf0ee7b3087e9", size = 10276560 }, 145 | { url = "https://files.pythonhosted.org/packages/81/7f/1b2435c3f5245d410bb5dc80f13ec796454c21fbda12b77d7588d5cf4e29/ruff-0.11.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7c69c74bf53ddcfbc22e6eb2f31211df7f65054bfc1f72288fc71e5f82db3eab", size = 9945091 }, 146 | { url = "https://files.pythonhosted.org/packages/39/c4/692284c07e6bf2b31d82bb8c32f8840f9d0627d92983edaac991a2b66c0a/ruff-0.11.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6e8fb75e14560f7cf53b15bbc55baf5ecbe373dd5f3aab96ff7aa7777edd7630", size = 10977133 }, 147 | { url = "https://files.pythonhosted.org/packages/94/cf/8ab81cb7dd7a3b0a3960c2769825038f3adcd75faf46dd6376086df8b128/ruff-0.11.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:842a472d7b4d6f5924e9297aa38149e5dcb1e628773b70e6387ae2c97a63c58f", size = 11378514 }, 148 | { url = "https://files.pythonhosted.org/packages/d9/3a/a647fa4f316482dacf2fd68e8a386327a33d6eabd8eb2f9a0c3d291ec549/ruff-0.11.2-py3-none-win32.whl", hash = "sha256:aca01ccd0eb5eb7156b324cfaa088586f06a86d9e5314b0eb330cb48415097cc", size = 10319835 }, 149 | { url = "https://files.pythonhosted.org/packages/86/54/3c12d3af58012a5e2cd7ebdbe9983f4834af3f8cbea0e8a8c74fa1e23b2b/ruff-0.11.2-py3-none-win_amd64.whl", hash = "sha256:3170150172a8f994136c0c66f494edf199a0bbea7a409f649e4bc8f4d7084080", size = 11373713 }, 150 | { url = "https://files.pythonhosted.org/packages/d6/d4/dd813703af8a1e2ac33bf3feb27e8a5ad514c9f219df80c64d69807e7f71/ruff-0.11.2-py3-none-win_arm64.whl", hash = "sha256:52933095158ff328f4c77af3d74f0379e34fd52f175144cefc1b192e7ccd32b4", size = 10441990 }, 151 | ] 152 | 153 | [[package]] 154 | name = "soupsieve" 155 | version = "2.6" 156 | source = { registry = "https://pypi.org/simple" } 157 | sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569 } 158 | wheels = [ 159 | { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186 }, 160 | ] 161 | 162 | [[package]] 163 | name = "sqlparse" 164 | version = "0.5.3" 165 | source = { registry = "https://pypi.org/simple" } 166 | sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999 } 167 | wheels = [ 168 | { url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415 }, 169 | ] 170 | 171 | [[package]] 172 | name = "typing-extensions" 173 | version = "4.13.0" 174 | source = { registry = "https://pypi.org/simple" } 175 | sdist = { url = "https://files.pythonhosted.org/packages/0e/3e/b00a62db91a83fff600de219b6ea9908e6918664899a2d85db222f4fbf19/typing_extensions-4.13.0.tar.gz", hash = "sha256:0a4ac55a5820789d87e297727d229866c9650f6521b64206413c4fbada24d95b", size = 106520 } 176 | wheels = [ 177 | { url = "https://files.pythonhosted.org/packages/e0/86/39b65d676ec5732de17b7e3c476e45bb80ec64eb50737a8dce1a4178aba1/typing_extensions-4.13.0-py3-none-any.whl", hash = "sha256:c8dd92cc0d6425a97c18fbb9d1954e5ff92c1ca881a309c45f06ebc0b79058e5", size = 45683 }, 178 | ] 179 | 180 | [[package]] 181 | name = "tzdata" 182 | version = "2025.2" 183 | source = { registry = "https://pypi.org/simple" } 184 | sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 } 185 | wheels = [ 186 | { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 }, 187 | ] 188 | 189 | [[package]] 190 | name = "whitenoise" 191 | version = "6.9.0" 192 | source = { registry = "https://pypi.org/simple" } 193 | sdist = { url = "https://files.pythonhosted.org/packages/b9/cf/c15c2f21aee6b22a9f6fc9be3f7e477e2442ec22848273db7f4eb73d6162/whitenoise-6.9.0.tar.gz", hash = "sha256:8c4a7c9d384694990c26f3047e118c691557481d624f069b7f7752a2f735d609", size = 25920 } 194 | wheels = [ 195 | { url = "https://files.pythonhosted.org/packages/64/b2/2ce9263149fbde9701d352bda24ea1362c154e196d2fda2201f18fc585d7/whitenoise-6.9.0-py3-none-any.whl", hash = "sha256:c8a489049b7ee9889617bb4c274a153f3d979e8f51d2efd0f5b403caf41c57df", size = 20161 }, 196 | ] 197 | --------------------------------------------------------------------------------