diff --git a/articles/admin.py b/articles/admin.py index 6d53b53..78cfcd3 100644 --- a/articles/admin.py +++ b/articles/admin.py @@ -1,6 +1,37 @@ from django.contrib import admin +from django.contrib.admin import register from django.contrib.auth.admin import UserAdmin -from .models import User +from .models import Article, User admin.site.register(User, UserAdmin) + + +@register(Article) +class ArticleAdmin(admin.ModelAdmin): + list_display = [ + "title", + "status", + "author", + "created_at", + "published_at", + "updated_at", + ] + list_display_links = ["title"] + list_filter = ["status"] + date_hierarchy = "created_at" + fieldsets = [ + ( + "Metadata", + { + "fields": ( + "title", + "status", + ("created_at", "published_at", "updated_at"), + "author", + ) + }, + ), + ("Content", {"fields": ("content",)}), + ] + readonly_fields = ["created_at", "updated_at"] diff --git a/articles/migrations/0001_initial.py b/articles/migrations/0001_initial.py new file mode 100644 index 0000000..8880233 --- /dev/null +++ b/articles/migrations/0001_initial.py @@ -0,0 +1,130 @@ +# Generated by Django 3.1 on 2020-08-14 12:56 + +import django.contrib.auth.models +import django.contrib.auth.validators +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ] + + operations = [ + migrations.CreateModel( + name="User", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=150, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "email", + models.EmailField( + blank=True, max_length=254, verbose_name="email address" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.Group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.Permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "verbose_name": "user", + "verbose_name_plural": "users", + "abstract": False, + }, + managers=[("objects", django.contrib.auth.models.UserManager()),], + ), + ] diff --git a/articles/migrations/0002_article.py b/articles/migrations/0002_article.py new file mode 100644 index 0000000..2029669 --- /dev/null +++ b/articles/migrations/0002_article.py @@ -0,0 +1,50 @@ +# Generated by Django 3.1 on 2020-08-14 13:16 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("articles", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="Article", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=255)), + ("content", models.TextField()), + ( + "status", + models.CharField( + choices=[("draft", "Draft"), ("published", "Published")], + default="draft", + max_length=15, + ), + ), + ("published_at", models.DateTimeField(null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "author", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={"ordering": ["-published_at"],}, + ), + ] diff --git a/articles/migrations/0003_auto_20200814_1522.py b/articles/migrations/0003_auto_20200814_1522.py new file mode 100644 index 0000000..601011b --- /dev/null +++ b/articles/migrations/0003_auto_20200814_1522.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2020-08-14 13:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("articles", "0002_article"), + ] + + operations = [ + migrations.AlterField( + model_name="article", + name="published_at", + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/articles/models.py b/articles/models.py index 7e21811..4f55131 100644 --- a/articles/models.py +++ b/articles/models.py @@ -1,6 +1,35 @@ +import markdown from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): pass + + +class Article(models.Model): + DRAFT = "draft" + PUBLISHED = "published" + STATUS_CHOICES = [ + (DRAFT, "Draft"), + (PUBLISHED, "Published"), + ] + title = models.CharField(max_length=255) + content = models.TextField() + status = models.CharField(max_length=15, choices=STATUS_CHOICES, default=DRAFT) + published_at = models.DateTimeField(null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + author = models.ForeignKey(User, on_delete=models.PROTECT) + + class Meta: + ordering = ["-published_at"] + + def get_abstract(self): + md = markdown.Markdown(extensions=["extra"]) + html = md.convert(self.content) + return html.split("")[0] + + def get_formatted_content(self): + md = markdown.Markdown(extensions=["extra"]) + return md.convert(self.content) diff --git a/articles/static/style.css b/articles/static/style.css new file mode 100644 index 0000000..4c9cc2f --- /dev/null +++ b/articles/static/style.css @@ -0,0 +1,6 @@ +.content { + max-width: 640px; + margin-left: auto; + margin-right: auto; + font-family: Arial, sans-serif; +} diff --git a/articles/templates/articles/article_detail.html b/articles/templates/articles/article_detail.html new file mode 100644 index 0000000..7e45337 --- /dev/null +++ b/articles/templates/articles/article_detail.html @@ -0,0 +1,12 @@ +{% extends 'articles/base.html' %} + +{% block content %} +

{{ article.title }}

+

By: {{ article.author }}

+

Published at: {{ article.published_at }}

+

Updated at: {{ article.updated_at }}

+ +
+ {{ article.get_formatted_content|safe }} +
+{% endblock %} diff --git a/articles/templates/articles/article_list.html b/articles/templates/articles/article_list.html new file mode 100644 index 0000000..f93356e --- /dev/null +++ b/articles/templates/articles/article_list.html @@ -0,0 +1,12 @@ +{% extends 'articles/base.html' %} + +{% block content %} +

Articles list

+ {% for article in articles %} +
+

{{ article.title }}

+

{{ article.get_abstract|safe }}

+

Read more

+
+ {% endfor %} +{% endblock %} diff --git a/articles/templates/articles/base.html b/articles/templates/articles/base.html new file mode 100644 index 0000000..3f187b6 --- /dev/null +++ b/articles/templates/articles/base.html @@ -0,0 +1,15 @@ +{% load static %} + + + + + Gabnotes + + + +
+ {% block content %} + {% endblock %} +
+ + diff --git a/articles/views.py b/articles/views.py index 91ea44a..5c5d847 100644 --- a/articles/views.py +++ b/articles/views.py @@ -1,3 +1,14 @@ -from django.shortcuts import render +from django.views import generic -# Create your views here. +from articles.models import Article + + +class ArticlesListView(generic.ListView): + model = Article + paginate_by = 15 + context_object_name = "articles" + + +class ArticleDetailView(generic.DetailView): + model = Article + context_object_name = "article" diff --git a/blog/settings.py b/blog/settings.py index efa915e..cb80f11 100644 --- a/blog/settings.py +++ b/blog/settings.py @@ -37,6 +37,7 @@ INSTALLED_APPS = [ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "articles", ] MIDDLEWARE = [ @@ -99,7 +100,7 @@ AUTH_PASSWORD_VALIDATORS = [ LANGUAGE_CODE = "en-us" -TIME_ZONE = "UTC" +TIME_ZONE = "Europe/Paris" USE_I18N = True diff --git a/blog/urls.py b/blog/urls.py index 9250580..4008530 100644 --- a/blog/urls.py +++ b/blog/urls.py @@ -16,6 +16,10 @@ Including another URLconf from django.contrib import admin from django.urls import path +from articles import views + urlpatterns = [ path("admin/", admin.site.urls), + path("", views.ArticlesListView.as_view(), name="articles-list"), + path("", views.ArticleDetailView.as_view(), name="article-detail"), ] diff --git a/requirements.txt b/requirements.txt index 6add173..957549f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ django==3.1 pre-commit==2.6.0 +markdown==3.2.2