mirror of https://github.com/iSoron/uhabits.git
This commit introduces the initial web version of the habits tracker application, built with Django. It provides the core functionality for tracking habits, including user authentication, habit management, and repetition logging. Features implemented: - User authentication (login, logout, signup) using django-allauth. - Core habit management features (CRUD operations for habits). - A main dashboard that displays a list of habits and their repetitions for the last 5 days. - A habit details page with a calendar view of repetitions for the current month. - A management command to send email reminders for habits. - Functionality to export habit data to a CSV file. - Unit tests for the core features. This version addresses the feedback from the code review, including fixing the template structure, implementing repetition logging, and fixing the calendar view.pull/2220/head
parent
5aa8744ef4
commit
3a5bf60a30
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class HabitsConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "habits"
|
@ -0,0 +1,25 @@
|
|||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.core.mail import send_mail
|
||||||
|
from django.utils import timezone
|
||||||
|
from habits.models import Habit
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Sends habit reminders to users'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
now = timezone.now()
|
||||||
|
habits_to_remind = Habit.objects.filter(
|
||||||
|
send_reminder=True,
|
||||||
|
reminder_time__hour=now.hour,
|
||||||
|
reminder_time__minute=now.minute
|
||||||
|
)
|
||||||
|
|
||||||
|
for habit in habits_to_remind:
|
||||||
|
send_mail(
|
||||||
|
f'Reminder: {habit.name}',
|
||||||
|
f'This is a reminder to complete your habit: {habit.name}',
|
||||||
|
'from@example.com',
|
||||||
|
[habit.user.email],
|
||||||
|
fail_silently=False,
|
||||||
|
)
|
||||||
|
self.stdout.write(self.style.SUCCESS(f'Successfully sent reminder for "{habit.name}" to {habit.user.email}'))
|
@ -0,0 +1,63 @@
|
|||||||
|
# Generated by Django 5.2.6 on 2025-09-13 17:33
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Habit",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=255)),
|
||||||
|
("description", models.TextField(blank=True)),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Repetition",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("date", models.DateField()),
|
||||||
|
("value", models.IntegerField(default=1)),
|
||||||
|
(
|
||||||
|
"habit",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to="habits.habit"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.2.6 on 2025-09-13 17:39
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("habits", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="habit",
|
||||||
|
name="reminder_time",
|
||||||
|
field=models.TimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="habit",
|
||||||
|
name="send_reminder",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,21 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
class Habit(models.Model):
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
description = models.TextField(blank=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
send_reminder = models.BooleanField(default=False)
|
||||||
|
reminder_time = models.TimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Repetition(models.Model):
|
||||||
|
habit = models.ForeignKey(Habit, on_delete=models.CASCADE)
|
||||||
|
date = models.DateField()
|
||||||
|
value = models.IntegerField(default=1)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.habit.name} - {self.date}"
|
@ -0,0 +1,10 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>Are you sure you want to delete "{{ object.name }}"?</h2>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit">Confirm Delete</button>
|
||||||
|
<a href="{% url 'habits:habit_detail' object.pk %}">Cancel</a>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,36 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>{{ object.name }}</h2>
|
||||||
|
<p>{{ object.description }}</p>
|
||||||
|
|
||||||
|
<h3>Calendar</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Mon</th>
|
||||||
|
<th>Tue</th>
|
||||||
|
<th>Wed</th>
|
||||||
|
<th>Thu</th>
|
||||||
|
<th>Fri</th>
|
||||||
|
<th>Sat</th>
|
||||||
|
<th>Sun</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for week in month_days %}
|
||||||
|
<tr>
|
||||||
|
{% for day in week %}
|
||||||
|
<td class="{% if day.month != current_month %}not-current-month{% endif %} {% if day in reps %}has-rep{% endif %}">
|
||||||
|
{% if day.month == current_month %}
|
||||||
|
{{ day.day }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<a href="{% url 'habits:habit_list' %}">Back to Habits</a>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,10 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>{% if object %}Edit Habit{% else %}Create Habit{% endif %}</h2>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<button type="submit">Save</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,45 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load habit_tags %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>My Habits</h2>
|
||||||
|
<a href="{% url 'habits:habit_create' %}">Create New Habit</a>
|
||||||
|
<a href="{% url 'habits:export_csv' %}">Export to CSV</a>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Habit</th>
|
||||||
|
{% for day in days %}
|
||||||
|
<th>{{ day|date:"D" }}</th>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for habit, reps in habits_with_reps %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'habits:habit_detail' habit.pk %}">{{ habit.name }}</a>
|
||||||
|
<a href="{% url 'habits:habit_update' habit.pk %}">Edit</a>
|
||||||
|
<a href="{% url 'habits:habit_delete' habit.pk %}">Delete</a>
|
||||||
|
</td>
|
||||||
|
{% for day in days %}
|
||||||
|
<td>
|
||||||
|
<form class="log-repetition-form" method="post" action="{% url 'habits:log_repetition' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="habit_id" value="{{ habit.pk }}">
|
||||||
|
<input type="hidden" name="date" value="{{ day|date:'Y-m-d' }}">
|
||||||
|
<button type="submit">
|
||||||
|
{% if reps|get_item:day %}
|
||||||
|
✓
|
||||||
|
{% else %}
|
||||||
|
×
|
||||||
|
{% endif %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,7 @@
|
|||||||
|
from django import template
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def get_item(dictionary, key):
|
||||||
|
return dictionary.get(key)
|
@ -0,0 +1,43 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from .models import Habit, Repetition
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
class HabitTestCase(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.user = User.objects.create_user(username='testuser', password='password')
|
||||||
|
self.client.login(username='testuser', password='password')
|
||||||
|
self.habit = Habit.objects.create(user=self.user, name='Test Habit')
|
||||||
|
self.repetition = Repetition.objects.create(habit=self.habit, date='2025-01-01')
|
||||||
|
|
||||||
|
def test_habit_list_view(self):
|
||||||
|
response = self.client.get(reverse('habits:habit_list'))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, 'Test Habit')
|
||||||
|
|
||||||
|
def test_habit_detail_view(self):
|
||||||
|
response = self.client.get(reverse('habits:habit_detail', args=[self.habit.pk]))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, 'Test Habit')
|
||||||
|
|
||||||
|
def test_habit_create_view(self):
|
||||||
|
response = self.client.post(reverse('habits:habit_create'), {'name': 'New Habit'})
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertTrue(Habit.objects.filter(name='New Habit').exists())
|
||||||
|
|
||||||
|
def test_habit_update_view(self):
|
||||||
|
response = self.client.post(reverse('habits:habit_update', args=[self.habit.pk]), {'name': 'Updated Habit'})
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.habit.refresh_from_db()
|
||||||
|
self.assertEqual(self.habit.name, 'Updated Habit')
|
||||||
|
|
||||||
|
def test_habit_delete_view(self):
|
||||||
|
response = self.client.post(reverse('habits:habit_delete', args=[self.habit.pk]))
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertFalse(Habit.objects.filter(pk=self.habit.pk).exists())
|
||||||
|
|
||||||
|
def test_export_csv_view(self):
|
||||||
|
response = self.client.get(reverse('habits:export_csv'))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response['Content-Type'], 'text/csv')
|
||||||
|
self.assertIn('Test Habit,2025-01-01,1', response.content.decode())
|
@ -0,0 +1,14 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = 'habits'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', views.HabitListView.as_view(), name='habit_list'),
|
||||||
|
path('<int:pk>/', views.HabitDetailView.as_view(), name='habit_detail'),
|
||||||
|
path('create/', views.HabitCreateView.as_view(), name='habit_create'),
|
||||||
|
path('<int:pk>/update/', views.HabitUpdateView.as_view(), name='habit_update'),
|
||||||
|
path('<int:pk>/delete/', views.HabitDeleteView.as_view(), name='habit_delete'),
|
||||||
|
path('export/csv/', views.ExportCSVView.as_view(), name='export_csv'),
|
||||||
|
path('log/', views.LogRepetitionView.as_view(), name='log_repetition'),
|
||||||
|
]
|
@ -0,0 +1,102 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView, View
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from .models import Habit
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
import csv
|
||||||
|
from django.http import HttpResponse
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
class HabitListView(LoginRequiredMixin, ListView):
|
||||||
|
model = Habit
|
||||||
|
template_name = 'habits/habit_list.html'
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return Habit.objects.filter(user=self.request.user)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
today = timezone.now().date()
|
||||||
|
days = [today - datetime.timedelta(days=i) for i in range(5)]
|
||||||
|
context['days'] = days
|
||||||
|
|
||||||
|
habits_with_reps = []
|
||||||
|
for habit in context['object_list']:
|
||||||
|
reps = {rep.date: rep for rep in habit.repetition_set.filter(date__in=days)}
|
||||||
|
habits_with_reps.append((habit, reps))
|
||||||
|
|
||||||
|
context['habits_with_reps'] = habits_with_reps
|
||||||
|
return context
|
||||||
|
|
||||||
|
import calendar
|
||||||
|
|
||||||
|
class HabitDetailView(LoginRequiredMixin, DetailView):
|
||||||
|
model = Habit
|
||||||
|
template_name = 'habits/habit_detail.html'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
today = timezone.now().date()
|
||||||
|
cal = calendar.Calendar()
|
||||||
|
month_days = cal.monthdatescalendar(today.year, today.month)
|
||||||
|
|
||||||
|
reps = {rep.date for rep in self.object.repetition_set.filter(date__year=today.year, date__month=today.month)}
|
||||||
|
|
||||||
|
context['month_days'] = month_days
|
||||||
|
context['reps'] = reps
|
||||||
|
context['current_month'] = today.month
|
||||||
|
return context
|
||||||
|
|
||||||
|
class HabitCreateView(LoginRequiredMixin, CreateView):
|
||||||
|
model = Habit
|
||||||
|
fields = ['name', 'description']
|
||||||
|
template_name = 'habits/habit_form.html'
|
||||||
|
success_url = reverse_lazy('habits:habit_list')
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
form.instance.user = self.request.user
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
class HabitUpdateView(LoginRequiredMixin, UpdateView):
|
||||||
|
model = Habit
|
||||||
|
fields = ['name', 'description']
|
||||||
|
template_name = 'habits/habit_form.html'
|
||||||
|
success_url = reverse_lazy('habits:habit_list')
|
||||||
|
|
||||||
|
class HabitDeleteView(LoginRequiredMixin, DeleteView):
|
||||||
|
model = Habit
|
||||||
|
template_name = 'habits/habit_confirm_delete.html'
|
||||||
|
success_url = reverse_lazy('habits:habit_list')
|
||||||
|
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from .models import Repetition
|
||||||
|
|
||||||
|
class ExportCSVView(LoginRequiredMixin, View):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
response = HttpResponse(content_type='text/csv')
|
||||||
|
response['Content-Disposition'] = 'attachment; filename="habits.csv"'
|
||||||
|
|
||||||
|
writer = csv.writer(response)
|
||||||
|
writer.writerow(['Habit', 'Date', 'Value'])
|
||||||
|
|
||||||
|
habits = Habit.objects.filter(user=request.user)
|
||||||
|
for habit in habits:
|
||||||
|
for repetition in habit.repetition_set.all():
|
||||||
|
writer.writerow([habit.name, repetition.date, repetition.value])
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
class LogRepetitionView(LoginRequiredMixin, View):
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
habit_id = request.POST.get('habit_id')
|
||||||
|
date_str = request.POST.get('date')
|
||||||
|
date = datetime.datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||||
|
habit = get_object_or_404(Habit, pk=habit_id, user=request.user)
|
||||||
|
|
||||||
|
rep, created = Repetition.objects.get_or_create(habit=habit, date=date)
|
||||||
|
if not created:
|
||||||
|
rep.delete()
|
||||||
|
|
||||||
|
return HttpResponse(status=204)
|
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
ASGI config for habits_tracker project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "habits_tracker.settings")
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
@ -0,0 +1,140 @@
|
|||||||
|
"""
|
||||||
|
Django settings for habits_tracker project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 5.2.6.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.2/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/5.2/ref/settings/
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = "django-insecure-ck4e+hvwh86i8b*-v0d0uwn$le-o=qvu)f(ss(%6arverkar8f"
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = []
|
||||||
|
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
"django.contrib.admin",
|
||||||
|
"django.contrib.auth",
|
||||||
|
"django.contrib.contenttypes",
|
||||||
|
"django.contrib.sessions",
|
||||||
|
"django.contrib.messages",
|
||||||
|
"django.contrib.staticfiles",
|
||||||
|
"habits",
|
||||||
|
# The following apps are required by django-allauth
|
||||||
|
'allauth',
|
||||||
|
'allauth.account',
|
||||||
|
'allauth.socialaccount',
|
||||||
|
]
|
||||||
|
|
||||||
|
AUTHENTICATION_BACKENDS = [
|
||||||
|
# Needed to login by username in Django admin, regardless of `allauth`
|
||||||
|
'django.contrib.auth.backends.ModelBackend',
|
||||||
|
|
||||||
|
# `allauth` specific authentication methods, such as login by e-mail
|
||||||
|
'allauth.account.auth_backends.AuthenticationBackend',
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
"django.middleware.security.SecurityMiddleware",
|
||||||
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
|
"django.middleware.common.CommonMiddleware",
|
||||||
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
|
"allauth.account.middleware.AccountMiddleware",
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = "habits_tracker.urls"
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
|
"DIRS": [BASE_DIR / 'templates'],
|
||||||
|
"APP_DIRS": True,
|
||||||
|
"OPTIONS": {
|
||||||
|
"context_processors": [
|
||||||
|
"django.template.context_processors.request",
|
||||||
|
"django.contrib.auth.context_processors.auth",
|
||||||
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = "habits_tracker.wsgi.application"
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
|
"NAME": BASE_DIR / "db.sqlite3",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = "en-us"
|
||||||
|
|
||||||
|
TIME_ZONE = "UTC"
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = "static/"
|
||||||
|
|
||||||
|
# Default primary key field type
|
||||||
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
|
||||||
|
SITE_ID = 1
|
||||||
|
|
||||||
|
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
@ -0,0 +1,25 @@
|
|||||||
|
"""
|
||||||
|
URL configuration for habits_tracker project.
|
||||||
|
|
||||||
|
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||||
|
https://docs.djangoproject.com/en/5.2/topics/http/urls/
|
||||||
|
Examples:
|
||||||
|
Function views
|
||||||
|
1. Add an import: from my_app import views
|
||||||
|
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||||
|
Class-based views
|
||||||
|
1. Add an import: from other_app.views import Home
|
||||||
|
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||||
|
Including another URLconf
|
||||||
|
1. Import the include() function: from django.urls import include, path
|
||||||
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import path, include
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("admin/", admin.site.urls),
|
||||||
|
path("accounts/", include("allauth.urls")),
|
||||||
|
path("habits/", include("habits.urls", namespace="habits")),
|
||||||
|
]
|
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
WSGI config for habits_tracker project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "habits_tracker.settings")
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "habits_tracker.settings")
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
@ -0,0 +1,10 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>Log In</h2>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<button type="submit">Log In</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,10 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>Sign Up</h2>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<button type="submit">Sign Up</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
Loading…
Reference in new issue