Building a Project Budget Manager with Django (9 Part Series)
1 Deploying a Django Application ON “pythonanywhere”.
2 Project Budget Manager Tutorial Series
… 5 more parts…
3 Part 7: Deploying to PythonAnywhere
4 Building a Project Budget Manager with Django – Part 6: Advanced Features
5 Building a Project Budget Manager with Django – Part 5: Templates and Documentation
6 Building a Project Budget Manager with Django – Part 4: Deployment and Production Setup On Ubuntu Server
7 Building a Project Budget Manager with Django – Part 3: Views and Templates
8 Building a Project Budget Manager with Django – Part 2: Authentication and Models
9 Building a Project Budget Manager with Django – Part 1: Project Setup and Structure
In this third part of our series, we’ll create views and templates for our Project Budget Manager. We’ll use Tailwind CSS for styling and HTMX for dynamic interactions.
Setting Up Tailwind CSS
- First, install Tailwind CSS dependencies:
npm <span>install</span> <span>-D</span> tailwindcssnpx tailwindcss initnpm <span>install</span> <span>-D</span> tailwindcss npx tailwindcss initnpm install -D tailwindcss npx tailwindcss init
Enter fullscreen mode Exit fullscreen mode
- Create a tailwind.config.js file:
<span>/** @type {import('tailwindcss').Config} */</span><span>module</span><span>.</span><span>exports</span> <span>=</span> <span>{</span><span>content</span><span>:</span> <span>[</span><span>"</span><span>./templates/**/*.html</span><span>"</span><span>,</span><span>"</span><span>./static/**/*.js</span><span>"</span><span>,</span><span>],</span><span>theme</span><span>:</span> <span>{</span><span>extend</span><span>:</span> <span>{},</span><span>},</span><span>plugins</span><span>:</span> <span>[],</span><span>}</span><span>/** @type {import('tailwindcss').Config} */</span> <span>module</span><span>.</span><span>exports</span> <span>=</span> <span>{</span> <span>content</span><span>:</span> <span>[</span> <span>"</span><span>./templates/**/*.html</span><span>"</span><span>,</span> <span>"</span><span>./static/**/*.js</span><span>"</span><span>,</span> <span>],</span> <span>theme</span><span>:</span> <span>{</span> <span>extend</span><span>:</span> <span>{},</span> <span>},</span> <span>plugins</span><span>:</span> <span>[],</span> <span>}</span>/** @type {import('tailwindcss').Config} */ module.exports = { content: [ "./templates/**/*.html", "./static/**/*.js", ], theme: { extend: {}, }, plugins: [], }
Enter fullscreen mode Exit fullscreen mode
- Create static/css/input.css:
<span>@tailwind</span> <span>base</span><span>;</span><span>@tailwind</span> <span>components</span><span>;</span><span>@tailwind</span> <span>utilities</span><span>;</span><span>/* Custom styles */</span><span>@layer</span> <span>components</span> <span>{</span><span>.btn-primary</span> <span>{</span><span>@apply</span> <span>px-4</span> <span>py-2</span> <span>bg-blue-600</span> <span>text-white</span> <span>rounded-lg</span> <span>hover</span><span>:</span><span>bg-blue-700</span><span>;</span><span>}</span><span>.btn-secondary</span> <span>{</span><span>@apply</span> <span>px-4</span> <span>py-2</span> <span>bg-gray-600</span> <span>text-white</span> <span>rounded-lg</span> <span>hover</span><span>:</span><span>bg-gray-700</span><span>;</span><span>}</span><span>.form-input</span> <span>{</span><span>@apply</span> <span>mt-1</span> <span>block</span> <span>w-full</span> <span>rounded-md</span> <span>border-gray-300</span> <span>shadow-sm;</span><span>}</span><span>}</span><span>@tailwind</span> <span>base</span><span>;</span> <span>@tailwind</span> <span>components</span><span>;</span> <span>@tailwind</span> <span>utilities</span><span>;</span> <span>/* Custom styles */</span> <span>@layer</span> <span>components</span> <span>{</span> <span>.btn-primary</span> <span>{</span> <span>@apply</span> <span>px-4</span> <span>py-2</span> <span>bg-blue-600</span> <span>text-white</span> <span>rounded-lg</span> <span>hover</span><span>:</span><span>bg-blue-700</span><span>;</span> <span>}</span> <span>.btn-secondary</span> <span>{</span> <span>@apply</span> <span>px-4</span> <span>py-2</span> <span>bg-gray-600</span> <span>text-white</span> <span>rounded-lg</span> <span>hover</span><span>:</span><span>bg-gray-700</span><span>;</span> <span>}</span> <span>.form-input</span> <span>{</span> <span>@apply</span> <span>mt-1</span> <span>block</span> <span>w-full</span> <span>rounded-md</span> <span>border-gray-300</span> <span>shadow-sm;</span> <span>}</span> <span>}</span>@tailwind base; @tailwind components; @tailwind utilities; /* Custom styles */ @layer components { .btn-primary { @apply px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700; } .btn-secondary { @apply px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700; } .form-input { @apply mt-1 block w-full rounded-md border-gray-300 shadow-sm; } }
Enter fullscreen mode Exit fullscreen mode
Creating Base Templates
- Create templates/layout/base.html:
{% load static %}<span><!DOCTYPE html></span><span><html</span> <span>lang=</span><span>"en"</span><span>></span><span><head></span><span><meta</span> <span>charset=</span><span>"UTF-8"</span><span>></span><span><meta</span> <span>name=</span><span>"viewport"</span> <span>content=</span><span>"width=device-width, initial-scale=1.0"</span><span>></span><span><title></span>{% block title %}Project Budget Manager{% endblock %}<span></title></span><span><link</span> <span>rel=</span><span>"stylesheet"</span> <span>href=</span><span>"{% static 'css/output.css' %}"</span><span>></span><span><script </span><span>src=</span><span>"{% static 'js/htmx.min.js' %}"</span> <span>defer</span><span>></script></span>{% block extra_head %}{% endblock %}<span></head></span><span><body</span> <span>class=</span><span>"bg-gray-50"</span><span>></span>{% include "layout/nav.html" %}<span><main</span> <span>class=</span><span>"container mx-auto px-4 py-8"</span><span>></span>{% if messages %}<span><div</span> <span>class=</span><span>"messages mb-8"</span><span>></span>{% for message in messages %}<span><div</span> <span>class=</span><span>"p-4 mb-4 rounded-lg {% if message.tags == 'success' %}bg-green-100 text-green-700{% elif message.tags == 'error' %}bg-red-100 text-red-700{% else %}bg-blue-100 text-blue-700{% endif %}"</span><span>></span>{{ message }}<span></div></span>{% endfor %}<span></div></span>{% endif %}{% block content %}{% endblock %}<span></main></span><span><footer</span> <span>class=</span><span>"bg-gray-800 text-white py-8 mt-16"</span><span>></span><span><div</span> <span>class=</span><span>"container mx-auto px-4"</span><span>></span><span><p></span><span>©</span> {% now "Y" %} Project Budget Manager. All rights reserved.<span></p></span><span></div></span><span></footer></span>{% block extra_js %}{% endblock %}<span></body></span><span></html></span>{% load static %} <span><!DOCTYPE html></span> <span><html</span> <span>lang=</span><span>"en"</span><span>></span> <span><head></span> <span><meta</span> <span>charset=</span><span>"UTF-8"</span><span>></span> <span><meta</span> <span>name=</span><span>"viewport"</span> <span>content=</span><span>"width=device-width, initial-scale=1.0"</span><span>></span> <span><title></span>{% block title %}Project Budget Manager{% endblock %}<span></title></span> <span><link</span> <span>rel=</span><span>"stylesheet"</span> <span>href=</span><span>"{% static 'css/output.css' %}"</span><span>></span> <span><script </span><span>src=</span><span>"{% static 'js/htmx.min.js' %}"</span> <span>defer</span><span>></script></span> {% block extra_head %}{% endblock %} <span></head></span> <span><body</span> <span>class=</span><span>"bg-gray-50"</span><span>></span> {% include "layout/nav.html" %} <span><main</span> <span>class=</span><span>"container mx-auto px-4 py-8"</span><span>></span> {% if messages %} <span><div</span> <span>class=</span><span>"messages mb-8"</span><span>></span> {% for message in messages %} <span><div</span> <span>class=</span><span>"p-4 mb-4 rounded-lg {% if message.tags == 'success' %}bg-green-100 text-green-700{% elif message.tags == 'error' %}bg-red-100 text-red-700{% else %}bg-blue-100 text-blue-700{% endif %}"</span><span>></span> {{ message }} <span></div></span> {% endfor %} <span></div></span> {% endif %} {% block content %}{% endblock %} <span></main></span> <span><footer</span> <span>class=</span><span>"bg-gray-800 text-white py-8 mt-16"</span><span>></span> <span><div</span> <span>class=</span><span>"container mx-auto px-4"</span><span>></span> <span><p></span><span>©</span> {% now "Y" %} Project Budget Manager. All rights reserved.<span></p></span> <span></div></span> <span></footer></span> {% block extra_js %}{% endblock %} <span></body></span> <span></html></span>{% load static %} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{% block title %}Project Budget Manager{% endblock %}</title> <link rel="stylesheet" href="{% static 'css/output.css' %}"> <script src="{% static 'js/htmx.min.js' %}" defer></script> {% block extra_head %}{% endblock %} </head> <body class="bg-gray-50"> {% include "layout/nav.html" %} <main class="container mx-auto px-4 py-8"> {% if messages %} <div class="messages mb-8"> {% for message in messages %} <div class="p-4 mb-4 rounded-lg {% if message.tags == 'success' %}bg-green-100 text-green-700{% elif message.tags == 'error' %}bg-red-100 text-red-700{% else %}bg-blue-100 text-blue-700{% endif %}"> {{ message }} </div> {% endfor %} </div> {% endif %} {% block content %}{% endblock %} </main> <footer class="bg-gray-800 text-white py-8 mt-16"> <div class="container mx-auto px-4"> <p>© {% now "Y" %} Project Budget Manager. All rights reserved.</p> </div> </footer> {% block extra_js %}{% endblock %} </body> </html>
Enter fullscreen mode Exit fullscreen mode
Creating Views
- Update app/views.py with our views:
<span>from</span> <span>django.shortcuts</span> <span>import</span> <span>render</span><span>,</span> <span>redirect</span><span>,</span> <span>get_object_or_404</span><span>from</span> <span>django.contrib.auth.decorators</span> <span>import</span> <span>login_required</span><span>from</span> <span>django.contrib</span> <span>import</span> <span>messages</span><span>from</span> <span>django.http</span> <span>import</span> <span>HttpResponse</span><span>from</span> <span>.models</span> <span>import</span> <span>Project</span><span>,</span> <span>Expense</span><span>from</span> <span>.forms</span> <span>import</span> <span>ProjectForm</span><span>,</span> <span>ExpenseForm</span><span>from</span> <span>decimal</span> <span>import</span> <span>Decimal</span><span>@login_required</span><span>def</span> <span>dashboard</span><span>(</span><span>request</span><span>):</span><span>user_projects</span> <span>=</span> <span>Project</span><span>.</span><span>objects</span><span>.</span><span>filter</span><span>(</span><span>created_by</span><span>=</span><span>request</span><span>.</span><span>user</span><span>).</span><span>order_by</span><span>(</span><span>'</span><span>-created_at</span><span>'</span><span>)</span><span>assigned_projects</span> <span>=</span> <span>Project</span><span>.</span><span>objects</span><span>.</span><span>filter</span><span>(</span><span>assigned_to</span><span>=</span><span>request</span><span>.</span><span>user</span><span>).</span><span>order_by</span><span>(</span><span>'</span><span>-created_at</span><span>'</span><span>)</span><span>context</span> <span>=</span> <span>{</span><span>'</span><span>user_projects</span><span>'</span><span>:</span> <span>user_projects</span><span>,</span><span>'</span><span>assigned_projects</span><span>'</span><span>:</span> <span>assigned_projects</span><span>,</span><span>}</span><span>return</span> <span>render</span><span>(</span><span>request</span><span>,</span> <span>'</span><span>app/dashboard.html</span><span>'</span><span>,</span> <span>context</span><span>)</span><span>@login_required</span><span>def</span> <span>project_list</span><span>(</span><span>request</span><span>):</span><span>projects</span> <span>=</span> <span>Project</span><span>.</span><span>objects</span><span>.</span><span>filter</span><span>(</span><span>created_by</span><span>=</span><span>request</span><span>.</span><span>user</span><span>).</span><span>order_by</span><span>(</span><span>'</span><span>-created_at</span><span>'</span><span>)</span><span>return</span> <span>render</span><span>(</span><span>request</span><span>,</span> <span>'</span><span>app/project_list.html</span><span>'</span><span>,</span> <span>{</span><span>'</span><span>projects</span><span>'</span><span>:</span> <span>projects</span><span>})</span><span>@login_required</span><span>def</span> <span>project_detail</span><span>(</span><span>request</span><span>,</span> <span>pk</span><span>):</span><span>project</span> <span>=</span> <span>get_object_or_404</span><span>(</span><span>Project</span><span>,</span> <span>pk</span><span>=</span><span>pk</span><span>)</span><span>expenses</span> <span>=</span> <span>project</span><span>.</span><span>expenses</span><span>.</span><span>all</span><span>().</span><span>order_by</span><span>(</span><span>'</span><span>-date</span><span>'</span><span>)</span><span>context</span> <span>=</span> <span>{</span><span>'</span><span>project</span><span>'</span><span>:</span> <span>project</span><span>,</span><span>'</span><span>expenses</span><span>'</span><span>:</span> <span>expenses</span><span>,</span><span>'</span><span>total_expenses</span><span>'</span><span>:</span> <span>project</span><span>.</span><span>get_total_expenses</span><span>(),</span><span>'</span><span>budget_remaining</span><span>'</span><span>:</span> <span>project</span><span>.</span><span>get_budget_remaining</span><span>(),</span><span>}</span><span>return</span> <span>render</span><span>(</span><span>request</span><span>,</span> <span>'</span><span>app/project_detail.html</span><span>'</span><span>,</span> <span>context</span><span>)</span><span>@login_required</span><span>def</span> <span>project_create</span><span>(</span><span>request</span><span>):</span><span>if</span> <span>request</span><span>.</span><span>method</span> <span>==</span> <span>'</span><span>POST</span><span>'</span><span>:</span><span>form</span> <span>=</span> <span>ProjectForm</span><span>(</span><span>request</span><span>.</span><span>POST</span><span>)</span><span>if</span> <span>form</span><span>.</span><span>is_valid</span><span>():</span><span>project</span> <span>=</span> <span>form</span><span>.</span><span>save</span><span>(</span><span>commit</span><span>=</span><span>False</span><span>)</span><span>project</span><span>.</span><span>created_by</span> <span>=</span> <span>request</span><span>.</span><span>user</span><span>project</span><span>.</span><span>save</span><span>()</span><span>messages</span><span>.</span><span>success</span><span>(</span><span>request</span><span>,</span> <span>'</span><span>Project created successfully.</span><span>'</span><span>)</span><span>return</span> <span>redirect</span><span>(</span><span>'</span><span>project_detail</span><span>'</span><span>,</span> <span>pk</span><span>=</span><span>project</span><span>.</span><span>pk</span><span>)</span><span>else</span><span>:</span><span>form</span> <span>=</span> <span>ProjectForm</span><span>()</span><span>return</span> <span>render</span><span>(</span><span>request</span><span>,</span> <span>'</span><span>app/project_form.html</span><span>'</span><span>,</span> <span>{</span><span>'</span><span>form</span><span>'</span><span>:</span> <span>form</span><span>})</span><span>@login_required</span><span>def</span> <span>expense_create</span><span>(</span><span>request</span><span>,</span> <span>project_pk</span><span>):</span><span>project</span> <span>=</span> <span>get_object_or_404</span><span>(</span><span>Project</span><span>,</span> <span>pk</span><span>=</span><span>project_pk</span><span>)</span><span>if</span> <span>request</span><span>.</span><span>method</span> <span>==</span> <span>'</span><span>POST</span><span>'</span><span>:</span><span>form</span> <span>=</span> <span>ExpenseForm</span><span>(</span><span>request</span><span>.</span><span>POST</span><span>,</span> <span>request</span><span>.</span><span>FILES</span><span>)</span><span>if</span> <span>form</span><span>.</span><span>is_valid</span><span>():</span><span>expense</span> <span>=</span> <span>form</span><span>.</span><span>save</span><span>(</span><span>commit</span><span>=</span><span>False</span><span>)</span><span>expense</span><span>.</span><span>project</span> <span>=</span> <span>project</span><span>expense</span><span>.</span><span>created_by</span> <span>=</span> <span>request</span><span>.</span><span>user</span><span>expense</span><span>.</span><span>save</span><span>()</span><span>if</span> <span>request</span><span>.</span><span>htmx</span><span>:</span><span>return</span> <span>HttpResponse</span><span>(</span><span>f</span><span>'</span><span><div id=</span><span>"</span><span>expense-</span><span>{</span><span>expense</span><span>.</span><span>id</span><span>}</span><span>"</span><span> class=</span><span>"</span><span>expense-item</span><span>"</span><span>></span><span>'</span><span>f</span><span>'</span><span><p></span><span>{</span><span>expense</span><span>.</span><span>description</span><span>}</span><span> - $</span><span>{</span><span>expense</span><span>.</span><span>amount</span><span>}</span><span></p></div></span><span>'</span><span>)</span><span>messages</span><span>.</span><span>success</span><span>(</span><span>request</span><span>,</span> <span>'</span><span>Expense added successfully.</span><span>'</span><span>)</span><span>return</span> <span>redirect</span><span>(</span><span>'</span><span>project_detail</span><span>'</span><span>,</span> <span>pk</span><span>=</span><span>project</span><span>.</span><span>pk</span><span>)</span><span>else</span><span>:</span><span>form</span> <span>=</span> <span>ExpenseForm</span><span>()</span><span>context</span> <span>=</span> <span>{</span><span>'</span><span>form</span><span>'</span><span>:</span> <span>form</span><span>,</span><span>'</span><span>project</span><span>'</span><span>:</span> <span>project</span><span>,</span><span>}</span><span>return</span> <span>render</span><span>(</span><span>request</span><span>,</span> <span>'</span><span>app/expense_form.html</span><span>'</span><span>,</span> <span>context</span><span>)</span><span>from</span> <span>django.shortcuts</span> <span>import</span> <span>render</span><span>,</span> <span>redirect</span><span>,</span> <span>get_object_or_404</span> <span>from</span> <span>django.contrib.auth.decorators</span> <span>import</span> <span>login_required</span> <span>from</span> <span>django.contrib</span> <span>import</span> <span>messages</span> <span>from</span> <span>django.http</span> <span>import</span> <span>HttpResponse</span> <span>from</span> <span>.models</span> <span>import</span> <span>Project</span><span>,</span> <span>Expense</span> <span>from</span> <span>.forms</span> <span>import</span> <span>ProjectForm</span><span>,</span> <span>ExpenseForm</span> <span>from</span> <span>decimal</span> <span>import</span> <span>Decimal</span> <span>@login_required</span> <span>def</span> <span>dashboard</span><span>(</span><span>request</span><span>):</span> <span>user_projects</span> <span>=</span> <span>Project</span><span>.</span><span>objects</span><span>.</span><span>filter</span><span>(</span> <span>created_by</span><span>=</span><span>request</span><span>.</span><span>user</span> <span>).</span><span>order_by</span><span>(</span><span>'</span><span>-created_at</span><span>'</span><span>)</span> <span>assigned_projects</span> <span>=</span> <span>Project</span><span>.</span><span>objects</span><span>.</span><span>filter</span><span>(</span> <span>assigned_to</span><span>=</span><span>request</span><span>.</span><span>user</span> <span>).</span><span>order_by</span><span>(</span><span>'</span><span>-created_at</span><span>'</span><span>)</span> <span>context</span> <span>=</span> <span>{</span> <span>'</span><span>user_projects</span><span>'</span><span>:</span> <span>user_projects</span><span>,</span> <span>'</span><span>assigned_projects</span><span>'</span><span>:</span> <span>assigned_projects</span><span>,</span> <span>}</span> <span>return</span> <span>render</span><span>(</span><span>request</span><span>,</span> <span>'</span><span>app/dashboard.html</span><span>'</span><span>,</span> <span>context</span><span>)</span> <span>@login_required</span> <span>def</span> <span>project_list</span><span>(</span><span>request</span><span>):</span> <span>projects</span> <span>=</span> <span>Project</span><span>.</span><span>objects</span><span>.</span><span>filter</span><span>(</span> <span>created_by</span><span>=</span><span>request</span><span>.</span><span>user</span> <span>).</span><span>order_by</span><span>(</span><span>'</span><span>-created_at</span><span>'</span><span>)</span> <span>return</span> <span>render</span><span>(</span><span>request</span><span>,</span> <span>'</span><span>app/project_list.html</span><span>'</span><span>,</span> <span>{</span><span>'</span><span>projects</span><span>'</span><span>:</span> <span>projects</span><span>})</span> <span>@login_required</span> <span>def</span> <span>project_detail</span><span>(</span><span>request</span><span>,</span> <span>pk</span><span>):</span> <span>project</span> <span>=</span> <span>get_object_or_404</span><span>(</span><span>Project</span><span>,</span> <span>pk</span><span>=</span><span>pk</span><span>)</span> <span>expenses</span> <span>=</span> <span>project</span><span>.</span><span>expenses</span><span>.</span><span>all</span><span>().</span><span>order_by</span><span>(</span><span>'</span><span>-date</span><span>'</span><span>)</span> <span>context</span> <span>=</span> <span>{</span> <span>'</span><span>project</span><span>'</span><span>:</span> <span>project</span><span>,</span> <span>'</span><span>expenses</span><span>'</span><span>:</span> <span>expenses</span><span>,</span> <span>'</span><span>total_expenses</span><span>'</span><span>:</span> <span>project</span><span>.</span><span>get_total_expenses</span><span>(),</span> <span>'</span><span>budget_remaining</span><span>'</span><span>:</span> <span>project</span><span>.</span><span>get_budget_remaining</span><span>(),</span> <span>}</span> <span>return</span> <span>render</span><span>(</span><span>request</span><span>,</span> <span>'</span><span>app/project_detail.html</span><span>'</span><span>,</span> <span>context</span><span>)</span> <span>@login_required</span> <span>def</span> <span>project_create</span><span>(</span><span>request</span><span>):</span> <span>if</span> <span>request</span><span>.</span><span>method</span> <span>==</span> <span>'</span><span>POST</span><span>'</span><span>:</span> <span>form</span> <span>=</span> <span>ProjectForm</span><span>(</span><span>request</span><span>.</span><span>POST</span><span>)</span> <span>if</span> <span>form</span><span>.</span><span>is_valid</span><span>():</span> <span>project</span> <span>=</span> <span>form</span><span>.</span><span>save</span><span>(</span><span>commit</span><span>=</span><span>False</span><span>)</span> <span>project</span><span>.</span><span>created_by</span> <span>=</span> <span>request</span><span>.</span><span>user</span> <span>project</span><span>.</span><span>save</span><span>()</span> <span>messages</span><span>.</span><span>success</span><span>(</span><span>request</span><span>,</span> <span>'</span><span>Project created successfully.</span><span>'</span><span>)</span> <span>return</span> <span>redirect</span><span>(</span><span>'</span><span>project_detail</span><span>'</span><span>,</span> <span>pk</span><span>=</span><span>project</span><span>.</span><span>pk</span><span>)</span> <span>else</span><span>:</span> <span>form</span> <span>=</span> <span>ProjectForm</span><span>()</span> <span>return</span> <span>render</span><span>(</span><span>request</span><span>,</span> <span>'</span><span>app/project_form.html</span><span>'</span><span>,</span> <span>{</span><span>'</span><span>form</span><span>'</span><span>:</span> <span>form</span><span>})</span> <span>@login_required</span> <span>def</span> <span>expense_create</span><span>(</span><span>request</span><span>,</span> <span>project_pk</span><span>):</span> <span>project</span> <span>=</span> <span>get_object_or_404</span><span>(</span><span>Project</span><span>,</span> <span>pk</span><span>=</span><span>project_pk</span><span>)</span> <span>if</span> <span>request</span><span>.</span><span>method</span> <span>==</span> <span>'</span><span>POST</span><span>'</span><span>:</span> <span>form</span> <span>=</span> <span>ExpenseForm</span><span>(</span><span>request</span><span>.</span><span>POST</span><span>,</span> <span>request</span><span>.</span><span>FILES</span><span>)</span> <span>if</span> <span>form</span><span>.</span><span>is_valid</span><span>():</span> <span>expense</span> <span>=</span> <span>form</span><span>.</span><span>save</span><span>(</span><span>commit</span><span>=</span><span>False</span><span>)</span> <span>expense</span><span>.</span><span>project</span> <span>=</span> <span>project</span> <span>expense</span><span>.</span><span>created_by</span> <span>=</span> <span>request</span><span>.</span><span>user</span> <span>expense</span><span>.</span><span>save</span><span>()</span> <span>if</span> <span>request</span><span>.</span><span>htmx</span><span>:</span> <span>return</span> <span>HttpResponse</span><span>(</span> <span>f</span><span>'</span><span><div id=</span><span>"</span><span>expense-</span><span>{</span><span>expense</span><span>.</span><span>id</span><span>}</span><span>"</span><span> class=</span><span>"</span><span>expense-item</span><span>"</span><span>></span><span>'</span> <span>f</span><span>'</span><span><p></span><span>{</span><span>expense</span><span>.</span><span>description</span><span>}</span><span> - $</span><span>{</span><span>expense</span><span>.</span><span>amount</span><span>}</span><span></p></div></span><span>'</span> <span>)</span> <span>messages</span><span>.</span><span>success</span><span>(</span><span>request</span><span>,</span> <span>'</span><span>Expense added successfully.</span><span>'</span><span>)</span> <span>return</span> <span>redirect</span><span>(</span><span>'</span><span>project_detail</span><span>'</span><span>,</span> <span>pk</span><span>=</span><span>project</span><span>.</span><span>pk</span><span>)</span> <span>else</span><span>:</span> <span>form</span> <span>=</span> <span>ExpenseForm</span><span>()</span> <span>context</span> <span>=</span> <span>{</span> <span>'</span><span>form</span><span>'</span><span>:</span> <span>form</span><span>,</span> <span>'</span><span>project</span><span>'</span><span>:</span> <span>project</span><span>,</span> <span>}</span> <span>return</span> <span>render</span><span>(</span><span>request</span><span>,</span> <span>'</span><span>app/expense_form.html</span><span>'</span><span>,</span> <span>context</span><span>)</span>from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth.decorators import login_required from django.contrib import messages from django.http import HttpResponse from .models import Project, Expense from .forms import ProjectForm, ExpenseForm from decimal import Decimal @login_required def dashboard(request): user_projects = Project.objects.filter( created_by=request.user ).order_by('-created_at') assigned_projects = Project.objects.filter( assigned_to=request.user ).order_by('-created_at') context = { 'user_projects': user_projects, 'assigned_projects': assigned_projects, } return render(request, 'app/dashboard.html', context) @login_required def project_list(request): projects = Project.objects.filter( created_by=request.user ).order_by('-created_at') return render(request, 'app/project_list.html', {'projects': projects}) @login_required def project_detail(request, pk): project = get_object_or_404(Project, pk=pk) expenses = project.expenses.all().order_by('-date') context = { 'project': project, 'expenses': expenses, 'total_expenses': project.get_total_expenses(), 'budget_remaining': project.get_budget_remaining(), } return render(request, 'app/project_detail.html', context) @login_required def project_create(request): if request.method == 'POST': form = ProjectForm(request.POST) if form.is_valid(): project = form.save(commit=False) project.created_by = request.user project.save() messages.success(request, 'Project created successfully.') return redirect('project_detail', pk=project.pk) else: form = ProjectForm() return render(request, 'app/project_form.html', {'form': form}) @login_required def expense_create(request, project_pk): project = get_object_or_404(Project, pk=project_pk) if request.method == 'POST': form = ExpenseForm(request.POST, request.FILES) if form.is_valid(): expense = form.save(commit=False) expense.project = project expense.created_by = request.user expense.save() if request.htmx: return HttpResponse( f'<div id="expense-{expense.id}" class="expense-item">' f'<p>{expense.description} - ${expense.amount}</p></div>' ) messages.success(request, 'Expense added successfully.') return redirect('project_detail', pk=project.pk) else: form = ExpenseForm() context = { 'form': form, 'project': project, } return render(request, 'app/expense_form.html', context)
Enter fullscreen mode Exit fullscreen mode
- Create app/forms.py:
<span>from</span> <span>django</span> <span>import</span> <span>forms</span><span>from</span> <span>.models</span> <span>import</span> <span>Project</span><span>,</span> <span>Expense</span><span>class</span> <span>ProjectForm</span><span>(</span><span>forms</span><span>.</span><span>ModelForm</span><span>):</span><span>class</span> <span>Meta</span><span>:</span><span>model</span> <span>=</span> <span>Project</span><span>fields</span> <span>=</span> <span>[</span><span>'</span><span>title</span><span>'</span><span>,</span> <span>'</span><span>description</span><span>'</span><span>,</span> <span>'</span><span>total_budget</span><span>'</span><span>,</span> <span>'</span><span>start_date</span><span>'</span><span>,</span> <span>'</span><span>end_date</span><span>'</span><span>,</span> <span>'</span><span>assigned_to</span><span>'</span><span>]</span><span>widgets</span> <span>=</span> <span>{</span><span>'</span><span>start_date</span><span>'</span><span>:</span> <span>forms</span><span>.</span><span>DateInput</span><span>(</span><span>attrs</span><span>=</span><span>{</span><span>'</span><span>type</span><span>'</span><span>:</span> <span>'</span><span>date</span><span>'</span><span>}),</span><span>'</span><span>end_date</span><span>'</span><span>:</span> <span>forms</span><span>.</span><span>DateInput</span><span>(</span><span>attrs</span><span>=</span><span>{</span><span>'</span><span>type</span><span>'</span><span>:</span> <span>'</span><span>date</span><span>'</span><span>}),</span><span>}</span><span>class</span> <span>ExpenseForm</span><span>(</span><span>forms</span><span>.</span><span>ModelForm</span><span>):</span><span>class</span> <span>Meta</span><span>:</span><span>model</span> <span>=</span> <span>Expense</span><span>fields</span> <span>=</span> <span>[</span><span>'</span><span>description</span><span>'</span><span>,</span> <span>'</span><span>amount</span><span>'</span><span>,</span> <span>'</span><span>category</span><span>'</span><span>,</span> <span>'</span><span>date</span><span>'</span><span>,</span> <span>'</span><span>receipt</span><span>'</span><span>]</span><span>widgets</span> <span>=</span> <span>{</span><span>'</span><span>date</span><span>'</span><span>:</span> <span>forms</span><span>.</span><span>DateInput</span><span>(</span><span>attrs</span><span>=</span><span>{</span><span>'</span><span>type</span><span>'</span><span>:</span> <span>'</span><span>date</span><span>'</span><span>}),</span><span>}</span><span>from</span> <span>django</span> <span>import</span> <span>forms</span> <span>from</span> <span>.models</span> <span>import</span> <span>Project</span><span>,</span> <span>Expense</span> <span>class</span> <span>ProjectForm</span><span>(</span><span>forms</span><span>.</span><span>ModelForm</span><span>):</span> <span>class</span> <span>Meta</span><span>:</span> <span>model</span> <span>=</span> <span>Project</span> <span>fields</span> <span>=</span> <span>[</span><span>'</span><span>title</span><span>'</span><span>,</span> <span>'</span><span>description</span><span>'</span><span>,</span> <span>'</span><span>total_budget</span><span>'</span><span>,</span> <span>'</span><span>start_date</span><span>'</span><span>,</span> <span>'</span><span>end_date</span><span>'</span><span>,</span> <span>'</span><span>assigned_to</span><span>'</span><span>]</span> <span>widgets</span> <span>=</span> <span>{</span> <span>'</span><span>start_date</span><span>'</span><span>:</span> <span>forms</span><span>.</span><span>DateInput</span><span>(</span><span>attrs</span><span>=</span><span>{</span><span>'</span><span>type</span><span>'</span><span>:</span> <span>'</span><span>date</span><span>'</span><span>}),</span> <span>'</span><span>end_date</span><span>'</span><span>:</span> <span>forms</span><span>.</span><span>DateInput</span><span>(</span><span>attrs</span><span>=</span><span>{</span><span>'</span><span>type</span><span>'</span><span>:</span> <span>'</span><span>date</span><span>'</span><span>}),</span> <span>}</span> <span>class</span> <span>ExpenseForm</span><span>(</span><span>forms</span><span>.</span><span>ModelForm</span><span>):</span> <span>class</span> <span>Meta</span><span>:</span> <span>model</span> <span>=</span> <span>Expense</span> <span>fields</span> <span>=</span> <span>[</span><span>'</span><span>description</span><span>'</span><span>,</span> <span>'</span><span>amount</span><span>'</span><span>,</span> <span>'</span><span>category</span><span>'</span><span>,</span> <span>'</span><span>date</span><span>'</span><span>,</span> <span>'</span><span>receipt</span><span>'</span><span>]</span> <span>widgets</span> <span>=</span> <span>{</span> <span>'</span><span>date</span><span>'</span><span>:</span> <span>forms</span><span>.</span><span>DateInput</span><span>(</span><span>attrs</span><span>=</span><span>{</span><span>'</span><span>type</span><span>'</span><span>:</span> <span>'</span><span>date</span><span>'</span><span>}),</span> <span>}</span>from django import forms from .models import Project, Expense class ProjectForm(forms.ModelForm): class Meta: model = Project fields = ['title', 'description', 'total_budget', 'start_date', 'end_date', 'assigned_to'] widgets = { 'start_date': forms.DateInput(attrs={'type': 'date'}), 'end_date': forms.DateInput(attrs={'type': 'date'}), } class ExpenseForm(forms.ModelForm): class Meta: model = Expense fields = ['description', 'amount', 'category', 'date', 'receipt'] widgets = { 'date': forms.DateInput(attrs={'type': 'date'}), }
Enter fullscreen mode Exit fullscreen mode
Creating Templates
- Create templates/app/dashboard.html:
{% extends "layout/base.html" %}{% block title %}Dashboard - Project Budget Manager{% endblock %}{% block content %}<span><div</span> <span>class=</span><span>"grid grid-cols-1 md:grid-cols-2 gap-8"</span><span>></span><span><div></span><span><h2</span> <span>class=</span><span>"text-2xl font-bold mb-4"</span><span>></span>Your Projects<span></h2></span>{% if user_projects %}{% for project in user_projects %}<span><div</span> <span>class=</span><span>"bg-white p-6 rounded-lg shadow-md mb-4"</span><span>></span><span><h3</span> <span>class=</span><span>"text-xl font-semibold mb-2"</span><span>></span><span><a</span> <span>href=</span><span>"{% url 'project_detail' pk=project.pk %}"</span> <span>class=</span><span>"text-blue-600 hover:text-blue-800"</span><span>></span>{{ project.title }}<span></a></span><span></h3></span><span><p</span> <span>class=</span><span>"text-gray-600 mb-2"</span><span>></span>{{ project.description|truncatewords:30 }}<span></p></span><span><div</span> <span>class=</span><span>"flex justify-between items-center"</span><span>></span><span><span</span> <span>class=</span><span>"text-sm text-gray-500"</span><span>></span>Budget: ${{ project.total_budget }}<span></span></span><span><span</span> <span>class=</span><span>"px-3 py-1 rounded-full text-sm {% if project.status == 'approved' %}bg-green-100 text-green-800 {% elif project.status == 'pending' %}bg-yellow-100 text-yellow-800 {% elif project.status == 'rejected' %}bg-red-100 text-red-800 {% else %}bg-gray-100 text-gray-800{% endif %}"</span><span>></span>{{ project.get_status_display }}<span></span></span><span></div></span><span></div></span>{% endfor %}{% else %}<span><p</span> <span>class=</span><span>"text-gray-600"</span><span>></span>No projects created yet.<span></p></span>{% endif %}<span><a</span> <span>href=</span><span>"{% url 'project_create' %}"</span> <span>class=</span><span>"btn-primary inline-block mt-4"</span><span>></span>Create New Project<span></a></span><span></div></span><span><div></span><span><h2</span> <span>class=</span><span>"text-2xl font-bold mb-4"</span><span>></span>Assigned Projects<span></h2></span>{% if assigned_projects %}{% for project in assigned_projects %}<span><div</span> <span>class=</span><span>"bg-white p-6 rounded-lg shadow-md mb-4"</span><span>></span><span><h3</span> <span>class=</span><span>"text-xl font-semibold mb-2"</span><span>></span><span><a</span> <span>href=</span><span>"{% url 'project_detail' pk=project.pk %}"</span> <span>class=</span><span>"text-blue-600 hover:text-blue-800"</span><span>></span>{{ project.title }}<span></a></span><span></h3></span><span><p</span> <span>class=</span><span>"text-gray-600 mb-2"</span><span>></span>{{ project.description|truncatewords:30 }}<span></p></span><span><div</span> <span>class=</span><span>"flex justify-between items-center"</span><span>></span><span><span</span> <span>class=</span><span>"text-sm text-gray-500"</span><span>></span>Created by: {{ project.created_by.get_full_name|default:project.created_by.username }}<span></span></span><span><span</span> <span>class=</span><span>"px-3 py-1 rounded-full text-sm {% if project.status == 'approved' %}bg-green-100 text-green-800 {% elif project.status == 'pending' %}bg-yellow-100 text-yellow-800 {% elif project.status == 'rejected' %}bg-red-100 text-red-800 {% else %}bg-gray-100 text-gray-800{% endif %}"</span><span>></span>{{ project.get_status_display }}<span></span></span><span></div></span><span></div></span>{% endfor %}{% else %}<span><p</span> <span>class=</span><span>"text-gray-600"</span><span>></span>No projects assigned to you.<span></p></span>{% endif %}<span></div></span><span></div></span>{% endblock %}{% extends "layout/base.html" %} {% block title %}Dashboard - Project Budget Manager{% endblock %} {% block content %} <span><div</span> <span>class=</span><span>"grid grid-cols-1 md:grid-cols-2 gap-8"</span><span>></span> <span><div></span> <span><h2</span> <span>class=</span><span>"text-2xl font-bold mb-4"</span><span>></span>Your Projects<span></h2></span> {% if user_projects %} {% for project in user_projects %} <span><div</span> <span>class=</span><span>"bg-white p-6 rounded-lg shadow-md mb-4"</span><span>></span> <span><h3</span> <span>class=</span><span>"text-xl font-semibold mb-2"</span><span>></span> <span><a</span> <span>href=</span><span>"{% url 'project_detail' pk=project.pk %}"</span> <span>class=</span><span>"text-blue-600 hover:text-blue-800"</span><span>></span> {{ project.title }} <span></a></span> <span></h3></span> <span><p</span> <span>class=</span><span>"text-gray-600 mb-2"</span><span>></span>{{ project.description|truncatewords:30 }}<span></p></span> <span><div</span> <span>class=</span><span>"flex justify-between items-center"</span><span>></span> <span><span</span> <span>class=</span><span>"text-sm text-gray-500"</span><span>></span>Budget: ${{ project.total_budget }}<span></span></span> <span><span</span> <span>class=</span><span>"px-3 py-1 rounded-full text-sm {% if project.status == 'approved' %}bg-green-100 text-green-800 {% elif project.status == 'pending' %}bg-yellow-100 text-yellow-800 {% elif project.status == 'rejected' %}bg-red-100 text-red-800 {% else %}bg-gray-100 text-gray-800{% endif %}"</span><span>></span> {{ project.get_status_display }} <span></span></span> <span></div></span> <span></div></span> {% endfor %} {% else %} <span><p</span> <span>class=</span><span>"text-gray-600"</span><span>></span>No projects created yet.<span></p></span> {% endif %} <span><a</span> <span>href=</span><span>"{% url 'project_create' %}"</span> <span>class=</span><span>"btn-primary inline-block mt-4"</span><span>></span> Create New Project <span></a></span> <span></div></span> <span><div></span> <span><h2</span> <span>class=</span><span>"text-2xl font-bold mb-4"</span><span>></span>Assigned Projects<span></h2></span> {% if assigned_projects %} {% for project in assigned_projects %} <span><div</span> <span>class=</span><span>"bg-white p-6 rounded-lg shadow-md mb-4"</span><span>></span> <span><h3</span> <span>class=</span><span>"text-xl font-semibold mb-2"</span><span>></span> <span><a</span> <span>href=</span><span>"{% url 'project_detail' pk=project.pk %}"</span> <span>class=</span><span>"text-blue-600 hover:text-blue-800"</span><span>></span> {{ project.title }} <span></a></span> <span></h3></span> <span><p</span> <span>class=</span><span>"text-gray-600 mb-2"</span><span>></span>{{ project.description|truncatewords:30 }}<span></p></span> <span><div</span> <span>class=</span><span>"flex justify-between items-center"</span><span>></span> <span><span</span> <span>class=</span><span>"text-sm text-gray-500"</span><span>></span>Created by: {{ project.created_by.get_full_name|default:project.created_by.username }}<span></span></span> <span><span</span> <span>class=</span><span>"px-3 py-1 rounded-full text-sm {% if project.status == 'approved' %}bg-green-100 text-green-800 {% elif project.status == 'pending' %}bg-yellow-100 text-yellow-800 {% elif project.status == 'rejected' %}bg-red-100 text-red-800 {% else %}bg-gray-100 text-gray-800{% endif %}"</span><span>></span> {{ project.get_status_display }} <span></span></span> <span></div></span> <span></div></span> {% endfor %} {% else %} <span><p</span> <span>class=</span><span>"text-gray-600"</span><span>></span>No projects assigned to you.<span></p></span> {% endif %} <span></div></span> <span></div></span> {% endblock %}{% extends "layout/base.html" %} {% block title %}Dashboard - Project Budget Manager{% endblock %} {% block content %} <div class="grid grid-cols-1 md:grid-cols-2 gap-8"> <div> <h2 class="text-2xl font-bold mb-4">Your Projects</h2> {% if user_projects %} {% for project in user_projects %} <div class="bg-white p-6 rounded-lg shadow-md mb-4"> <h3 class="text-xl font-semibold mb-2"> <a href="{% url 'project_detail' pk=project.pk %}" class="text-blue-600 hover:text-blue-800"> {{ project.title }} </a> </h3> <p class="text-gray-600 mb-2">{{ project.description|truncatewords:30 }}</p> <div class="flex justify-between items-center"> <span class="text-sm text-gray-500">Budget: ${{ project.total_budget }}</span> <span class="px-3 py-1 rounded-full text-sm {% if project.status == 'approved' %}bg-green-100 text-green-800 {% elif project.status == 'pending' %}bg-yellow-100 text-yellow-800 {% elif project.status == 'rejected' %}bg-red-100 text-red-800 {% else %}bg-gray-100 text-gray-800{% endif %}"> {{ project.get_status_display }} </span> </div> </div> {% endfor %} {% else %} <p class="text-gray-600">No projects created yet.</p> {% endif %} <a href="{% url 'project_create' %}" class="btn-primary inline-block mt-4"> Create New Project </a> </div> <div> <h2 class="text-2xl font-bold mb-4">Assigned Projects</h2> {% if assigned_projects %} {% for project in assigned_projects %} <div class="bg-white p-6 rounded-lg shadow-md mb-4"> <h3 class="text-xl font-semibold mb-2"> <a href="{% url 'project_detail' pk=project.pk %}" class="text-blue-600 hover:text-blue-800"> {{ project.title }} </a> </h3> <p class="text-gray-600 mb-2">{{ project.description|truncatewords:30 }}</p> <div class="flex justify-between items-center"> <span class="text-sm text-gray-500">Created by: {{ project.created_by.get_full_name|default:project.created_by.username }}</span> <span class="px-3 py-1 rounded-full text-sm {% if project.status == 'approved' %}bg-green-100 text-green-800 {% elif project.status == 'pending' %}bg-yellow-100 text-yellow-800 {% elif project.status == 'rejected' %}bg-red-100 text-red-800 {% else %}bg-gray-100 text-gray-800{% endif %}"> {{ project.get_status_display }} </span> </div> </div> {% endfor %} {% else %} <p class="text-gray-600">No projects assigned to you.</p> {% endif %} </div> </div> {% endblock %}
Enter fullscreen mode Exit fullscreen mode
Setting Up URLs
Update app/urls.py:
<span>from</span> <span>django.urls</span> <span>import</span> <span>path</span><span>from</span> <span>.</span> <span>import</span> <span>views</span><span>app_name</span> <span>=</span> <span>'</span><span>app</span><span>'</span><span>urlpatterns</span> <span>=</span> <span>[</span><span>path</span><span>(</span><span>''</span><span>,</span> <span>views</span><span>.</span><span>dashboard</span><span>,</span> <span>name</span><span>=</span><span>'</span><span>dashboard</span><span>'</span><span>),</span><span>path</span><span>(</span><span>'</span><span>projects/</span><span>'</span><span>,</span> <span>views</span><span>.</span><span>project_list</span><span>,</span> <span>name</span><span>=</span><span>'</span><span>project_list</span><span>'</span><span>),</span><span>path</span><span>(</span><span>'</span><span>projects/create/</span><span>'</span><span>,</span> <span>views</span><span>.</span><span>project_create</span><span>,</span> <span>name</span><span>=</span><span>'</span><span>project_create</span><span>'</span><span>),</span><span>path</span><span>(</span><span>'</span><span>projects/<int:pk>/</span><span>'</span><span>,</span> <span>views</span><span>.</span><span>project_detail</span><span>,</span> <span>name</span><span>=</span><span>'</span><span>project_detail</span><span>'</span><span>),</span><span>path</span><span>(</span><span>'</span><span>projects/<int:project_pk>/expenses/create/</span><span>'</span><span>,</span><span>views</span><span>.</span><span>expense_create</span><span>,</span> <span>name</span><span>=</span><span>'</span><span>expense_create</span><span>'</span><span>),</span><span>]</span><span>from</span> <span>django.urls</span> <span>import</span> <span>path</span> <span>from</span> <span>.</span> <span>import</span> <span>views</span> <span>app_name</span> <span>=</span> <span>'</span><span>app</span><span>'</span> <span>urlpatterns</span> <span>=</span> <span>[</span> <span>path</span><span>(</span><span>''</span><span>,</span> <span>views</span><span>.</span><span>dashboard</span><span>,</span> <span>name</span><span>=</span><span>'</span><span>dashboard</span><span>'</span><span>),</span> <span>path</span><span>(</span><span>'</span><span>projects/</span><span>'</span><span>,</span> <span>views</span><span>.</span><span>project_list</span><span>,</span> <span>name</span><span>=</span><span>'</span><span>project_list</span><span>'</span><span>),</span> <span>path</span><span>(</span><span>'</span><span>projects/create/</span><span>'</span><span>,</span> <span>views</span><span>.</span><span>project_create</span><span>,</span> <span>name</span><span>=</span><span>'</span><span>project_create</span><span>'</span><span>),</span> <span>path</span><span>(</span><span>'</span><span>projects/<int:pk>/</span><span>'</span><span>,</span> <span>views</span><span>.</span><span>project_detail</span><span>,</span> <span>name</span><span>=</span><span>'</span><span>project_detail</span><span>'</span><span>),</span> <span>path</span><span>(</span><span>'</span><span>projects/<int:project_pk>/expenses/create/</span><span>'</span><span>,</span> <span>views</span><span>.</span><span>expense_create</span><span>,</span> <span>name</span><span>=</span><span>'</span><span>expense_create</span><span>'</span><span>),</span> <span>]</span>from django.urls import path from . import views app_name = 'app' urlpatterns = [ path('', views.dashboard, name='dashboard'), path('projects/', views.project_list, name='project_list'), path('projects/create/', views.project_create, name='project_create'), path('projects/<int:pk>/', views.project_detail, name='project_detail'), path('projects/<int:project_pk>/expenses/create/', views.expense_create, name='expense_create'), ]
Enter fullscreen mode Exit fullscreen mode
Next Steps
In Part 4 of this series, we’ll:
- Implement project approval workflow
- Add email notifications
- Create project reports and analytics
- Set up production deployment
Resources
This article is part of the “Building a Project Budget Manager with Django” series. Check out Part 1 and Part 2 if you haven’t already!
Building a Project Budget Manager with Django (9 Part Series)
1 Deploying a Django Application ON “pythonanywhere”.
2 Project Budget Manager Tutorial Series
… 5 more parts…
3 Part 7: Deploying to PythonAnywhere
4 Building a Project Budget Manager with Django – Part 6: Advanced Features
5 Building a Project Budget Manager with Django – Part 5: Templates and Documentation
6 Building a Project Budget Manager with Django – Part 4: Deployment and Production Setup On Ubuntu Server
7 Building a Project Budget Manager with Django – Part 3: Views and Templates
8 Building a Project Budget Manager with Django – Part 2: Authentication and Models
9 Building a Project Budget Manager with Django – Part 1: Project Setup and Structure
原文链接:Building a Project Budget Manager with Django – Part 3: Views and Templates
暂无评论内容