diff --git a/.env.example b/.env.example
index 8c1b9eeb6..994ea9a16 100644
--- a/.env.example
+++ b/.env.example
@@ -21,4 +21,4 @@ LANGCHAIN_ENDPOINT="https://api.smith.langchain.com"
DATABASE_URL=postgres://user:password@localhost:5432/dbname
#Sentry DSN
-SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0
\ No newline at end of file
+SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0
diff --git a/blog/__init__.py b/blog/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/blog/admin.py b/blog/admin.py
new file mode 100644
index 000000000..e40e01a79
--- /dev/null
+++ b/blog/admin.py
@@ -0,0 +1,9 @@
+from django.contrib import admin
+
+from .models import Post
+
+
+@admin.register(Post)
+class PostAdmin(admin.ModelAdmin):
+ list_display = ("title", "author", "created_at", "image")
+ prepopulated_fields = {"slug": ("title",)}
diff --git a/blog/apps.py b/blog/apps.py
new file mode 100644
index 000000000..6be26c734
--- /dev/null
+++ b/blog/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class BlogConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "blog"
diff --git a/blog/migrations/0001_initial.py b/blog/migrations/0001_initial.py
new file mode 100644
index 000000000..087cedbdb
--- /dev/null
+++ b/blog/migrations/0001_initial.py
@@ -0,0 +1,42 @@
+# Generated by Django 5.0.8 on 2024-10-30 15:57
+
+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="Post",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("title", models.CharField(max_length=200)),
+ ("slug", models.SlugField(unique=True)),
+ ("content", models.TextField()),
+ ("created_at", models.DateTimeField(auto_now_add=True)),
+ ("updated_at", models.DateTimeField(auto_now=True)),
+ (
+ "author",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ ],
+ ),
+ ]
diff --git a/blog/migrations/0002_alter_post_slug.py b/blog/migrations/0002_alter_post_slug.py
new file mode 100644
index 000000000..06c135ace
--- /dev/null
+++ b/blog/migrations/0002_alter_post_slug.py
@@ -0,0 +1,17 @@
+# Generated by Django 5.0.8 on 2024-10-30 18:52
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("blog", "0001_initial"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="post",
+ name="slug",
+ field=models.SlugField(blank=True, unique=True),
+ ),
+ ]
diff --git a/blog/migrations/0003_post_image.py b/blog/migrations/0003_post_image.py
new file mode 100644
index 000000000..ec56eb214
--- /dev/null
+++ b/blog/migrations/0003_post_image.py
@@ -0,0 +1,19 @@
+# Generated by Django 5.0.8 on 2024-10-31 08:48
+
+import django.utils.timezone
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("blog", "0002_alter_post_slug"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="post",
+ name="image",
+ field=models.ImageField(default=django.utils.timezone.now, upload_to=""),
+ preserve_default=False,
+ ),
+ ]
diff --git a/blog/migrations/__init__.py b/blog/migrations/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/blog/models.py b/blog/models.py
new file mode 100644
index 000000000..ef84ae7fe
--- /dev/null
+++ b/blog/models.py
@@ -0,0 +1,19 @@
+from django.contrib.auth.models import User
+from django.db import models
+from django.urls import reverse
+
+
+class Post(models.Model):
+ title = models.CharField(max_length=200)
+ slug = models.SlugField(unique=True, blank=True)
+ author = models.ForeignKey(User, on_delete=models.CASCADE)
+ content = models.TextField()
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+ image = models.ImageField()
+
+ def __str__(self):
+ return self.title
+
+ def get_absolute_url(self):
+ return reverse("post_detail", kwargs={"slug": self.slug})
diff --git a/blog/templates/blog/post_delete.html b/blog/templates/blog/post_delete.html
new file mode 100644
index 000000000..d7b9aed4b
--- /dev/null
+++ b/blog/templates/blog/post_delete.html
@@ -0,0 +1,65 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+
+{% include "includes/sidenav.html" %}
+
Confirm Delete
+Are you sure you want to delete the post "{{ post.title }}"?
+
+{% endblock content %}
diff --git a/blog/templates/blog/post_details.html b/blog/templates/blog/post_details.html
new file mode 100644
index 000000000..4bcc752a5
--- /dev/null
+++ b/blog/templates/blog/post_details.html
@@ -0,0 +1,108 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+
+{% include "includes/sidenav.html" %}
+
+ {{ post.title }}
+ By {{ post.author }} on {{ post.created_at }}
+
+ {{ post.content|safe }}
+
+ {% if post.image %}
+
+
+
+ {% endif %}
+
+ {% if request.user == post.author %}
+
+ {% endif %}
+
+{% endblock content %}
diff --git a/blog/templates/blog/post_form.html b/blog/templates/blog/post_form.html
new file mode 100644
index 000000000..2dc089085
--- /dev/null
+++ b/blog/templates/blog/post_form.html
@@ -0,0 +1,87 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+
+{% include "includes/sidenav.html" %}
+
+
+
+
+
+
+
+{% endblock content %}
diff --git a/blog/templates/blog/post_list.html b/blog/templates/blog/post_list.html
new file mode 100644
index 000000000..86081a1d3
--- /dev/null
+++ b/blog/templates/blog/post_list.html
@@ -0,0 +1,81 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+
+{% include "includes/sidenav.html" %}
+Blog Posts
+
+{% endblock content %}
\ No newline at end of file
diff --git a/blog/tests.py b/blog/tests.py
new file mode 100644
index 000000000..a39b155ac
--- /dev/null
+++ b/blog/tests.py
@@ -0,0 +1 @@
+# Create your tests here.
diff --git a/blog/urls.py b/blog/urls.py
new file mode 100644
index 000000000..5f59c4974
--- /dev/null
+++ b/blog/urls.py
@@ -0,0 +1,13 @@
+from django.conf import settings
+from django.conf.urls.static import static
+from django.urls import path
+
+from . import views
+
+urlpatterns = [
+ path("", views.PostListView.as_view(), name="post_list"),
+ path("new/", views.PostCreateView.as_view(), name="post_create"),
+ path("/", views.PostDetailView.as_view(), name="post_detail"),
+ path("/edit/", views.PostUpdateView.as_view(), name="post_update"),
+ path("/delete/", views.PostDeleteView.as_view(), name="post_delete"),
+] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
diff --git a/blog/views.py b/blog/views.py
new file mode 100644
index 000000000..292640adb
--- /dev/null
+++ b/blog/views.py
@@ -0,0 +1,69 @@
+import markdown
+from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
+from django.utils.text import slugify
+from django.views import generic
+
+from .models import Post
+
+
+class PostListView(generic.ListView):
+ model = Post
+ template_name = "blog/post_list.html"
+ context_object_name = "posts"
+ paginate_by = 5
+
+
+class PostDetailView(generic.DetailView):
+ model = Post
+ template_name = "blog/post_details.html"
+
+ def get_object(self):
+ post = super().get_object()
+ post.content = markdown.markdown(post.content)
+ return post
+
+
+class PostCreateView(LoginRequiredMixin, generic.CreateView):
+ model = Post
+ fields = ["title", "content", "image"]
+ template_name = "blog/post_form.html"
+
+ def form_valid(self, form):
+ form.instance.author = self.request.user
+
+ base_slug = slugify(form.instance.title)
+ slug = base_slug
+ counter = 1
+ if slug == "new":
+ slug = f"{base_slug}-{counter}"
+ counter += 1
+ while Post.objects.filter(slug=slug).exists():
+ slug = f"{base_slug}-{counter}"
+ counter += 1
+ form.instance.slug = slug
+
+ return super().form_valid(form)
+
+
+class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, generic.UpdateView):
+ model = Post
+ fields = ["title", "content", "image"]
+ template_name = "blog/post_form.html"
+
+ def form_valid(self, form):
+ form.instance.author = self.request.user
+ return super().form_valid(form)
+
+ def test_func(self):
+ post = self.get_object()
+ return self.request.user == post.author
+
+
+class PostDeleteView(LoginRequiredMixin, UserPassesTestMixin, generic.DeleteView):
+ model = Post
+ template_name = "blog/post_delete.html"
+ success_url = "/blogs"
+
+ def test_func(self):
+ post = self.get_object()
+ return self.request.user == post.author
diff --git a/blt/settings.py b/blt/settings.py
index d38d2a3d4..6d6643466 100644
--- a/blt/settings.py
+++ b/blt/settings.py
@@ -101,6 +101,7 @@
"captcha",
"dj_rest_auth",
"dj_rest_auth.registration",
+ "blog",
)
diff --git a/blt/urls.py b/blt/urls.py
index 273fbbfdb..2d1b05699 100644
--- a/blt/urls.py
+++ b/blt/urls.py
@@ -567,7 +567,12 @@
path("api/timelogsreport/", website.views.TimeLogListAPIView, name="timelogsreport"),
path("time-logs/", website.views.TimeLogListView, name="time_logs"),
path("sizzle-daily-log/", website.views.sizzle_daily_log, name="sizzle_daily_log"),
- path("user-sizzle-report//", website.views.user_sizzle_report, name="user_sizzle_report"),
+ path("blogs/", include("blog.urls")),
+ path(
+ "user-sizzle-report//",
+ website.views.user_sizzle_report,
+ name="user_sizzle_report",
+ ),
]
if settings.DEBUG:
diff --git a/website/templates/sizzle/sizzle.html b/website/templates/sizzle/sizzle.html
index 3ce7f4115..9defe586b 100644
--- a/website/templates/sizzle/sizzle.html
+++ b/website/templates/sizzle/sizzle.html
@@ -120,7 +120,8 @@ Your Sizzle Report
{% for user in leaderboard %}
- {{ user.username }}
+ {{ user.username }}
|
{{ user.formatted_duration }} |
diff --git a/website/templates/sizzle/user_sizzle_report.html b/website/templates/sizzle/user_sizzle_report.html
index 6f796c8ea..13102a3a8 100644
--- a/website/templates/sizzle/user_sizzle_report.html
+++ b/website/templates/sizzle/user_sizzle_report.html
@@ -14,12 +14,12 @@ {{ user.username }}'s Sizzle Report
{% for log in response_data %}
-
- {{ log.date }} |
- {{ log.issue_title }} |
- {{ log.duration }} |
- {{ log.start_time }} |
-
+
+ {{ log.date }} |
+ {{ log.issue_title }} |
+ {{ log.duration }} |
+ {{ log.start_time }} |
+
{% endfor %}
diff --git a/website/views.py b/website/views.py
index f4e27bd73..ab8256156 100644
--- a/website/views.py
+++ b/website/views.py
@@ -2622,4 +2622,6 @@ def user_sizzle_report(request, username):
response_data.append(day_data)
- return render(request, "sizzle/user_sizzle_report.html", {"response_data": response_data, "user": user})
+ return render(
+ request, "sizzle/user_sizzle_report.html", {"response_data": response_data, "user": user}
+ )