]> git.menne-pb.de Git - pinpoint.git/commitdiff
Add tutorial code for django and django_rest_framework
authorJörn Menne <jmenne@fedora.de>
Thu, 16 Jan 2025 19:44:57 +0000 (20:44 +0100)
committerJörn Menne <jmenne@fedora.de>
Thu, 16 Jan 2025 19:44:57 +0000 (20:44 +0100)
28 files changed:
georeport/migrations/0006_alter_category_options_group.py [new file with mode: 0644]
georeport/migrations/0007_delete_group.py [new file with mode: 0644]
pinpoint/settings.py
pinpoint/urls.py
polls/__init__.py [new file with mode: 0644]
polls/admin.py [new file with mode: 0644]
polls/apps.py [new file with mode: 0644]
polls/migrations/0001_initial.py [new file with mode: 0644]
polls/migrations/__init__.py [new file with mode: 0644]
polls/models.py [new file with mode: 0644]
polls/templates/polls/detail.html [new file with mode: 0644]
polls/templates/polls/index.html [new file with mode: 0644]
polls/templates/polls/results.html [new file with mode: 0644]
polls/tests.py [new file with mode: 0644]
polls/urls.py [new file with mode: 0644]
polls/views.py [new file with mode: 0644]
snippets/__init__.py [new file with mode: 0644]
snippets/admin.py [new file with mode: 0644]
snippets/apps.py [new file with mode: 0644]
snippets/migrations/0001_initial.py [new file with mode: 0644]
snippets/migrations/0002_alter_snippet_owner.py [new file with mode: 0644]
snippets/migrations/__init__.py [new file with mode: 0644]
snippets/models.py [new file with mode: 0644]
snippets/permissions.py [new file with mode: 0644]
snippets/serializers.py [new file with mode: 0644]
snippets/tests.py [new file with mode: 0644]
snippets/urls.py [new file with mode: 0644]
snippets/views.py [new file with mode: 0644]

diff --git a/georeport/migrations/0006_alter_category_options_group.py b/georeport/migrations/0006_alter_category_options_group.py
new file mode 100644 (file)
index 0000000..c56fb9b
--- /dev/null
@@ -0,0 +1,31 @@
+# Generated by Django 5.1.4 on 2025-01-13 10:58
+
+import django.contrib.auth.models
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('auth', '0012_alter_user_first_name_max_length'),
+        ('georeport', '0005_category_parent'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='category',
+            options={'verbose_name_plural': 'Categories'},
+        ),
+        migrations.CreateModel(
+            name='Group',
+            fields=[
+                ('group_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='auth.group')),
+                ('categories', models.ManyToManyField(related_name='auth_groups', to='georeport.category')),
+            ],
+            bases=('auth.group',),
+            managers=[
+                ('objects', django.contrib.auth.models.GroupManager()),
+            ],
+        ),
+    ]
diff --git a/georeport/migrations/0007_delete_group.py b/georeport/migrations/0007_delete_group.py
new file mode 100644 (file)
index 0000000..89e8e14
--- /dev/null
@@ -0,0 +1,16 @@
+# Generated by Django 5.1.4 on 2025-01-15 09:09
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('georeport', '0006_alter_category_options_group'),
+    ]
+
+    operations = [
+        migrations.DeleteModel(
+            name='Group',
+        ),
+    ]
index 418743b50d7558ae1f236838229eccb1a2b8729a..84380f0806d49c88550b3ad90c3e518261eacaa3 100644 (file)
@@ -32,12 +32,15 @@ ALLOWED_HOSTS = []
 
 INSTALLED_APPS = [
     "georeport.apps.GeoreportConfig",
+    "polls.apps.PollsConfig",
     "django.contrib.admin",
     "django.contrib.auth",
     "django.contrib.contenttypes",
     "django.contrib.sessions",
     "django.contrib.messages",
     "django.contrib.staticfiles",
+    "rest_framework",
+    "snippets",
     "debug_toolbar",
 ]
 
@@ -57,7 +60,7 @@ ROOT_URLCONF = "pinpoint.urls"
 TEMPLATES = [
     {
         "BACKEND": "django.template.backends.django.DjangoTemplates",
-        "DIRS": [],
+        "DIRS": [BASE_DIR / "templates"],
         "APP_DIRS": True,
         "OPTIONS": {
             "context_processors": [
@@ -128,3 +131,7 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
 INTERNAL_IPS = [
     "127.0.0.1",
 ]
+REST_FRAMEWORK = {
+    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
+    "PAGE_SIZE": 10,
+}
index 9f6c87f31777d8cfa22a59504e6c59b882fecbf0..cae0aa37196bda381764d4d68e859e241f07c8eb 100644 (file)
@@ -21,5 +21,7 @@ from debug_toolbar.toolbar import debug_toolbar_urls
 
 urlpatterns = [
     path("admin/", admin.site.urls),
+    path("", include("snippets.urls")),
     path("georeport/", include("georeport.urls")),
+    path("polls/", include("polls.urls")),
 ] + debug_toolbar_urls()
diff --git a/polls/__init__.py b/polls/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/polls/admin.py b/polls/admin.py
new file mode 100644 (file)
index 0000000..a847220
--- /dev/null
@@ -0,0 +1,25 @@
+from django.contrib import admin
+
+# Register your models here.
+
+from .models import *
+
+
+class ChoiceInLine(admin.TabularInline):
+    model = Choice
+    extra = 0
+
+
+@admin.register(Question)
+class QuestionAdmin(admin.ModelAdmin):
+    fieldsets = [
+        (None, {"fields": ["question_text"]}),
+        ("Date Information", {"fields": ["pub_date"]}),
+    ]
+    inlines = [ChoiceInLine]
+
+    list_display = ["question_text", "pub_date", "was_published_recently"]
+    search_fields = ["question_text"]
+
+
+admin.site.register(Choice)
diff --git a/polls/apps.py b/polls/apps.py
new file mode 100644 (file)
index 0000000..5a5f94c
--- /dev/null
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class PollsConfig(AppConfig):
+    default_auto_field = 'django.db.models.BigAutoField'
+    name = 'polls'
diff --git a/polls/migrations/0001_initial.py b/polls/migrations/0001_initial.py
new file mode 100644 (file)
index 0000000..8512a5b
--- /dev/null
@@ -0,0 +1,32 @@
+# Generated by Django 5.1.4 on 2025-01-16 18:35
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Question',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('question_text', models.CharField(max_length=100)),
+                ('pub_date', models.DateTimeField(verbose_name='date published')),
+            ],
+        ),
+        migrations.CreateModel(
+            name='Choice',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('choice_text', models.CharField(max_length=200)),
+                ('votes', models.IntegerField(default=0)),
+                ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='polls.question')),
+            ],
+        ),
+    ]
diff --git a/polls/migrations/__init__.py b/polls/migrations/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/polls/models.py b/polls/models.py
new file mode 100644 (file)
index 0000000..cbcef16
--- /dev/null
@@ -0,0 +1,26 @@
+from django.db import models
+import datetime
+from django.utils import timezone
+from django.contrib import admin
+
+
+class Question(models.Model):
+    question_text = models.CharField(max_length=100)
+    pub_date = models.DateTimeField("date published")
+
+    @admin.display(boolean=True, ordering="pub_date", description="Published recently?")
+    def was_published_recently(self):
+        now = timezone.now()
+        return now - datetime.timedelta(days=1) <= self.pub_date <= now
+
+    def __str__(self):  # type: ignore
+        return self.question_text
+
+
+class Choice(models.Model):
+    question = models.ForeignKey(Question, on_delete=models.CASCADE)
+    choice_text = models.CharField(max_length=200)
+    votes = models.IntegerField(default=0)  # type:ignore
+
+    def __str__(self):  # type: ignore
+        return self.choice_text
diff --git a/polls/templates/polls/detail.html b/polls/templates/polls/detail.html
new file mode 100644 (file)
index 0000000..8d92d7c
--- /dev/null
@@ -0,0 +1,19 @@
+<html>
+    <head>
+        <title>Detail</title>
+    </head>
+    <body>
+        <form action="{% url 'polls:vote' question.id %}" method="post">
+        {% csrf_token %}
+        <fieldset>
+            <legend><h1>{{ question.question_text }}</h1></legend>
+            {% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
+            {% for choice in question.choice_set.all %}
+                <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
+                <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
+            {% endfor %}
+        </fieldset>
+        <input type="submit" value="Vote">
+        </form>  
+    </body>
+</html>
diff --git a/polls/templates/polls/index.html b/polls/templates/polls/index.html
new file mode 100644 (file)
index 0000000..d7c0dc1
--- /dev/null
@@ -0,0 +1,17 @@
+<html>
+    <head>
+        <title>Index</title>
+    </head>
+    <body>
+        {% if latest_question_list %}
+            <ul>
+                {% for question in latest_question_list %}
+            <li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text}}</a></li>
+                {% endfor %}
+            </ul>
+        {% else %}
+            <p>No polls</p>
+        {% endif %}
+
+    </body>
+</html>
diff --git a/polls/templates/polls/results.html b/polls/templates/polls/results.html
new file mode 100644 (file)
index 0000000..1837ca8
--- /dev/null
@@ -0,0 +1,15 @@
+<html>
+    <head>
+        <title>Results</title>
+    </head>
+    <body>
+        <h1>{{question.question_text}}</h1>
+
+        <ul>
+            {% for choice in question.choice_set.all %}
+                <li>{{choice.choice_text}} -- {{choice.votes }} vote {{choice.votes|pluralize}}</li>
+            {% endfor %}
+        </ul>
+        <a href="{% url 'polls:detail' question.id %}">Vote again?</a>
+    </body>
+</html>
diff --git a/polls/tests.py b/polls/tests.py
new file mode 100644 (file)
index 0000000..62b987b
--- /dev/null
@@ -0,0 +1,119 @@
+from django.urls import reverse
+from django.test import TestCase
+
+import datetime
+from django.utils import timezone
+
+from .models import Question
+
+
+class QuestionModelTests(TestCase):
+    def test_was_published_recently_with_future_question(self):
+        time = timezone.now() + datetime.timedelta(days=30)
+        future_question = Question(pub_date=time)
+        self.assertIs(future_question.was_published_recently(), False)
+
+    def test_was_published_recently_with_old_question(self):
+        """
+        was_published_recently() returns False for questions whose pub_date
+        is older than 1 day.
+        """
+        time = timezone.now() - datetime.timedelta(days=1, seconds=1)
+        old_question = Question(pub_date=time)
+        self.assertIs(old_question.was_published_recently(), False)
+
+    def test_was_published_recently_with_recent_question(self):
+        """
+        was_published_recently() returns True for questions whose pub_date
+        is within the last day.
+        """
+        time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59)
+        recent_question = Question(pub_date=time)
+        self.assertIs(recent_question.was_published_recently(), True)
+
+
+def create_question(question_text, days):
+    """
+    Create a question with the given `question_text` and published the
+    given number of `days` offset to now (negative for questions published
+    in the past, positive for questions that have yet to be published).
+    """
+    time = timezone.now() + datetime.timedelta(days=days)
+    return Question.objects.create(question_text=question_text, pub_date=time)
+
+
+class QuestionIndexViewTests(TestCase):
+    def test_no_questoin(self):
+        response = self.client.get(reverse("polls:index"))
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, "No polls")
+        self.assertQuerySetEqual(response.context["latest_question_list"], [])
+
+    def test_past_question(self):
+        """
+        Questions with a pub_date in the past are displayed on the
+        index page.
+        """
+        question = create_question(question_text="Past question.", days=-30)
+        response = self.client.get(reverse("polls:index"))
+        self.assertQuerySetEqual(
+            response.context["latest_question_list"],
+            [question],
+        )
+
+    def test_future_question(self):
+        """
+        Questions with a pub_date in the future aren't displayed on
+        the index page.
+        """
+        create_question(question_text="Future question.", days=30)
+        response = self.client.get(reverse("polls:index"))
+        self.assertContains(response, "No polls")
+        self.assertQuerySetEqual(response.context["latest_question_list"], [])
+
+    def test_future_question_and_past_question(self):
+        """
+        Even if both past and future questions exist, only past questions
+        are displayed.
+        """
+        question = create_question(question_text="Past question.", days=-30)
+        create_question(question_text="Future question.", days=30)
+        response = self.client.get(reverse("polls:index"))
+        self.assertQuerySetEqual(
+            response.context["latest_question_list"],
+            [question],
+        )
+
+    def test_two_past_questions(self):
+        """
+        The questions index page may display multiple questions.
+        """
+        question1 = create_question(question_text="Past question 1.", days=-30)
+        question2 = create_question(question_text="Past question 2.", days=-5)
+        response = self.client.get(reverse("polls:index"))
+        self.assertQuerySetEqual(
+            response.context["latest_question_list"],
+            [question2, question1],
+        )
+
+
+class QuestionDetailViewTests(TestCase):
+    def test_future_question(self):
+        """
+        The detail view of a question with a pub_date in the future
+        returns a 404 not found.
+        """
+        future_question = create_question(question_text="Future question.", days=5)
+        url = reverse("polls:detail", args=(future_question.id,))
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 404)
+
+    def test_past_question(self):
+        """
+        The detail view of a question with a pub_date in the past
+        displays the question's text.
+        """
+        past_question = create_question(question_text="Past Question.", days=-5)
+        url = reverse("polls:detail", args=(past_question.id,))
+        response = self.client.get(url)
+        self.assertContains(response, past_question.question_text)
diff --git a/polls/urls.py b/polls/urls.py
new file mode 100644 (file)
index 0000000..ce53e6f
--- /dev/null
@@ -0,0 +1,15 @@
+from django.urls import path
+
+from . import views
+
+app_name = "polls"
+urlpatterns = [
+    # ex: /polls/
+    path("", views.IndexView.as_view(), name="index"),
+    # ex: /polls/5/
+    path("<int:pk>/", views.DetailView.as_view(), name="detail"),
+    # ex: /polls/5/results/
+    path("<int:pk>/results/", views.ResultsView.as_view(), name="results"),
+    # ex: /polls/5/vote/
+    path("<int:question_id>/vote/", views.vote, name="vote"),
+]
diff --git a/polls/views.py b/polls/views.py
new file mode 100644 (file)
index 0000000..4687658
--- /dev/null
@@ -0,0 +1,56 @@
+from django.shortcuts import get_object_or_404, render
+
+from django.http import HttpResponse, HttpResponseRedirect
+from django.urls import reverse
+
+from polls.models import Question, Choice
+from django.db.models import F
+from django.views import generic
+
+from django.utils import timezone
+
+
+class IndexView(generic.ListView):
+    template_name = "polls/index.html"
+    context_object_name = "latest_question_list"
+
+    def get_queryset(self):
+        return Question.objects.filter(pub_date__lte=timezone.now()).order_by(
+            "-pub_date"
+        )[:5]
+
+
+class DetailView(generic.DetailView):
+    model = Question
+    template_name = "polls/detail.html"
+
+    def get_queryset(self):
+        return Question.objects.filter(pub_date__lte=timezone.now())
+
+
+class ResultsView(generic.DetailView):
+    model = Question
+    template_name = "polls/results.html"
+
+
+def vote(request, question_id):
+    question = get_object_or_404(Question, pk=question_id)
+    try:
+        selected_choice = question.choice_set.get(pk=request.POST["choice"])
+    except (KeyError, Choice.DoesNotExist):
+        # Redisplay the question voting form.
+        return render(
+            request,
+            "polls/detail.html",
+            {
+                "question": question,
+                "error_message": "You didn't select a choice.",
+            },
+        )
+    else:
+        selected_choice.votes = F("votes") + 1
+        selected_choice.save()
+        # Always return an HttpResponseRedirect after successfully dealing
+        # with POST data. This prevents data from being posted twice if a
+        # user hits the Back button.
+        return HttpResponseRedirect(reverse("polls:results", args=(question.id,)))
diff --git a/snippets/__init__.py b/snippets/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/snippets/admin.py b/snippets/admin.py
new file mode 100644 (file)
index 0000000..8c38f3f
--- /dev/null
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/snippets/apps.py b/snippets/apps.py
new file mode 100644 (file)
index 0000000..33dd0be
--- /dev/null
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class SnippetsConfig(AppConfig):
+    default_auto_field = 'django.db.models.BigAutoField'
+    name = 'snippets'
diff --git a/snippets/migrations/0001_initial.py b/snippets/migrations/0001_initial.py
new file mode 100644 (file)
index 0000000..212f776
--- /dev/null
@@ -0,0 +1,34 @@
+# Generated by Django 5.1.4 on 2025-01-15 09:36
+
+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='Snippet',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('created', models.DateTimeField(auto_now_add=True)),
+                ('title', models.CharField(blank=True, default='', max_length=100)),
+                ('code', models.TextField()),
+                ('linenos', models.BooleanField(default=False)),
+                ('language', models.CharField(choices=[('abap', 'ABAP'), ('abnf', 'ABNF'), ('actionscript', 'ActionScript'), ('actionscript3', 'ActionScript 3'), ('ada', 'Ada'), ('adl', 'ADL'), ('agda', 'Agda'), ('aheui', 'Aheui'), ('alloy', 'Alloy'), ('ambienttalk', 'AmbientTalk'), ('amdgpu', 'AMDGPU'), ('ampl', 'Ampl'), ('androidbp', 'Soong'), ('ansys', 'ANSYS parametric design language'), ('antlr', 'ANTLR'), ('antlr-actionscript', 'ANTLR With ActionScript Target'), ('antlr-cpp', 'ANTLR With CPP Target'), ('antlr-csharp', 'ANTLR With C# Target'), ('antlr-java', 'ANTLR With Java Target'), ('antlr-objc', 'ANTLR With ObjectiveC Target'), ('antlr-perl', 'ANTLR With Perl Target'), ('antlr-python', 'ANTLR With Python Target'), ('antlr-ruby', 'ANTLR With Ruby Target'), ('apacheconf', 'ApacheConf'), ('apl', 'APL'), ('applescript', 'AppleScript'), ('arduino', 'Arduino'), ('arrow', 'Arrow'), ('arturo', 'Arturo'), ('asc', 'ASCII armored'), ('asn1', 'ASN.1'), ('aspectj', 'AspectJ'), ('aspx-cs', 'aspx-cs'), ('aspx-vb', 'aspx-vb'), ('asymptote', 'Asymptote'), ('augeas', 'Augeas'), ('autohotkey', 'autohotkey'), ('autoit', 'AutoIt'), ('awk', 'Awk'), ('bare', 'BARE'), ('basemake', 'Base Makefile'), ('bash', 'Bash'), ('batch', 'Batchfile'), ('bbcbasic', 'BBC Basic'), ('bbcode', 'BBCode'), ('bc', 'BC'), ('bdd', 'Bdd'), ('befunge', 'Befunge'), ('berry', 'Berry'), ('bibtex', 'BibTeX'), ('blitzbasic', 'BlitzBasic'), ('blitzmax', 'BlitzMax'), ('blueprint', 'Blueprint'), ('bnf', 'BNF'), ('boa', 'Boa'), ('boo', 'Boo'), ('boogie', 'Boogie'), ('bqn', 'BQN'), ('brainfuck', 'Brainfuck'), ('bst', 'BST'), ('bugs', 'BUGS'), ('c', 'C'), ('c-objdump', 'c-objdump'), ('ca65', 'ca65 assembler'), ('cadl', 'cADL'), ('camkes', 'CAmkES'), ('capdl', 'CapDL'), ('capnp', "Cap'n Proto"), ('carbon', 'Carbon'), ('cbmbas', 'CBM BASIC V2'), ('cddl', 'CDDL'), ('ceylon', 'Ceylon'), ('cfc', 'Coldfusion CFC'), ('cfengine3', 'CFEngine3'), ('cfm', 'Coldfusion HTML'), ('cfs', 'cfstatement'), ('chaiscript', 'ChaiScript'), ('chapel', 'Chapel'), ('charmci', 'Charmci'), ('cheetah', 'Cheetah'), ('cirru', 'Cirru'), ('clay', 'Clay'), ('clean', 'Clean'), ('clojure', 'Clojure'), ('clojurescript', 'ClojureScript'), ('cmake', 'CMake'), ('cobol', 'COBOL'), ('cobolfree', 'COBOLFree'), ('codeql', 'CodeQL'), ('coffeescript', 'CoffeeScript'), ('comal', 'COMAL-80'), ('common-lisp', 'Common Lisp'), ('componentpascal', 'Component Pascal'), ('console', 'Bash Session'), ('coq', 'Coq'), ('cplint', 'cplint'), ('cpp', 'C++'), ('cpp-objdump', 'cpp-objdump'), ('cpsa', 'CPSA'), ('cr', 'Crystal'), ('crmsh', 'Crmsh'), ('croc', 'Croc'), ('cryptol', 'Cryptol'), ('csharp', 'C#'), ('csound', 'Csound Orchestra'), ('csound-document', 'Csound Document'), ('csound-score', 'Csound Score'), ('css', 'CSS'), ('css+django', 'CSS+Django/Jinja'), ('css+genshitext', 'CSS+Genshi Text'), ('css+lasso', 'CSS+Lasso'), ('css+mako', 'CSS+Mako'), ('css+mozpreproc', 'CSS+mozpreproc'), ('css+myghty', 'CSS+Myghty'), ('css+php', 'CSS+PHP'), ('css+ruby', 'CSS+Ruby'), ('css+smarty', 'CSS+Smarty'), ('css+ul4', 'CSS+UL4'), ('cuda', 'CUDA'), ('cypher', 'Cypher'), ('cython', 'Cython'), ('d', 'D'), ('d-objdump', 'd-objdump'), ('dart', 'Dart'), ('dasm16', 'DASM16'), ('dax', 'Dax'), ('debcontrol', 'Debian Control file'), ('debian.sources', 'Debian Sources file'), ('debsources', 'Debian Sourcelist'), ('delphi', 'Delphi'), ('desktop', 'Desktop file'), ('devicetree', 'Devicetree'), ('dg', 'dg'), ('diff', 'Diff'), ('django', 'Django/Jinja'), ('docker', 'Docker'), ('doscon', 'MSDOS Session'), ('dpatch', 'Darcs Patch'), ('dtd', 'DTD'), ('duel', 'Duel'), ('dylan', 'Dylan'), ('dylan-console', 'Dylan session'), ('dylan-lid', 'DylanLID'), ('earl-grey', 'Earl Grey'), ('easytrieve', 'Easytrieve'), ('ebnf', 'EBNF'), ('ec', 'eC'), ('ecl', 'ECL'), ('eiffel', 'Eiffel'), ('elixir', 'Elixir'), ('elm', 'Elm'), ('elpi', 'Elpi'), ('emacs-lisp', 'EmacsLisp'), ('email', 'E-mail'), ('erb', 'ERB'), ('erl', 'Erlang erl session'), ('erlang', 'Erlang'), ('evoque', 'Evoque'), ('execline', 'execline'), ('extempore', 'xtlang'), ('ezhil', 'Ezhil'), ('factor', 'Factor'), ('fan', 'Fantom'), ('fancy', 'Fancy'), ('felix', 'Felix'), ('fennel', 'Fennel'), ('fift', 'Fift'), ('fish', 'Fish'), ('flatline', 'Flatline'), ('floscript', 'FloScript'), ('forth', 'Forth'), ('fortran', 'Fortran'), ('fortranfixed', 'FortranFixed'), ('foxpro', 'FoxPro'), ('freefem', 'Freefem'), ('fsharp', 'F#'), ('fstar', 'FStar'), ('func', 'FunC'), ('futhark', 'Futhark'), ('gap', 'GAP'), ('gap-console', 'GAP session'), ('gas', 'GAS'), ('gcode', 'g-code'), ('gdscript', 'GDScript'), ('genshi', 'Genshi'), ('genshitext', 'Genshi Text'), ('gherkin', 'Gherkin'), ('gleam', 'Gleam'), ('glsl', 'GLSL'), ('gnuplot', 'Gnuplot'), ('go', 'Go'), ('golo', 'Golo'), ('gooddata-cl', 'GoodData-CL'), ('googlesql', 'GoogleSQL'), ('gosu', 'Gosu'), ('graphql', 'GraphQL'), ('graphviz', 'Graphviz'), ('groff', 'Groff'), ('groovy', 'Groovy'), ('gsql', 'GSQL'), ('gst', 'Gosu Template'), ('haml', 'Haml'), ('handlebars', 'Handlebars'), ('hare', 'Hare'), ('haskell', 'Haskell'), ('haxe', 'Haxe'), ('haxeml', 'Hxml'), ('hexdump', 'Hexdump'), ('hlsl', 'HLSL'), ('hsail', 'HSAIL'), ('hspec', 'Hspec'), ('html', 'HTML'), ('html+cheetah', 'HTML+Cheetah'), ('html+django', 'HTML+Django/Jinja'), ('html+evoque', 'HTML+Evoque'), ('html+genshi', 'HTML+Genshi'), ('html+handlebars', 'HTML+Handlebars'), ('html+lasso', 'HTML+Lasso'), ('html+mako', 'HTML+Mako'), ('html+myghty', 'HTML+Myghty'), ('html+ng2', 'HTML + Angular2'), ('html+php', 'HTML+PHP'), ('html+smarty', 'HTML+Smarty'), ('html+twig', 'HTML+Twig'), ('html+ul4', 'HTML+UL4'), ('html+velocity', 'HTML+Velocity'), ('http', 'HTTP'), ('hybris', 'Hybris'), ('hylang', 'Hy'), ('i6t', 'Inform 6 template'), ('icon', 'Icon'), ('idl', 'IDL'), ('idris', 'Idris'), ('iex', 'Elixir iex session'), ('igor', 'Igor'), ('inform6', 'Inform 6'), ('inform7', 'Inform 7'), ('ini', 'INI'), ('io', 'Io'), ('ioke', 'Ioke'), ('irc', 'IRC logs'), ('isabelle', 'Isabelle'), ('j', 'J'), ('jags', 'JAGS'), ('janet', 'Janet'), ('jasmin', 'Jasmin'), ('java', 'Java'), ('javascript', 'JavaScript'), ('javascript+cheetah', 'JavaScript+Cheetah'), ('javascript+django', 'JavaScript+Django/Jinja'), ('javascript+lasso', 'JavaScript+Lasso'), ('javascript+mako', 'JavaScript+Mako'), ('javascript+mozpreproc', 'Javascript+mozpreproc'), ('javascript+myghty', 'JavaScript+Myghty'), ('javascript+php', 'JavaScript+PHP'), ('javascript+ruby', 'JavaScript+Ruby'), ('javascript+smarty', 'JavaScript+Smarty'), ('jcl', 'JCL'), ('jlcon', 'Julia console'), ('jmespath', 'JMESPath'), ('js+genshitext', 'JavaScript+Genshi Text'), ('js+ul4', 'Javascript+UL4'), ('jsgf', 'JSGF'), ('jslt', 'JSLT'), ('json', 'JSON'), ('json5', 'JSON5'), ('jsonld', 'JSON-LD'), ('jsonnet', 'Jsonnet'), ('jsp', 'Java Server Page'), ('jsx', 'JSX'), ('julia', 'Julia'), ('juttle', 'Juttle'), ('k', 'K'), ('kal', 'Kal'), ('kconfig', 'Kconfig'), ('kmsg', 'Kernel log'), ('koka', 'Koka'), ('kotlin', 'Kotlin'), ('kql', 'Kusto'), ('kuin', 'Kuin'), ('lasso', 'Lasso'), ('ldapconf', 'LDAP configuration file'), ('ldif', 'LDIF'), ('lean', 'Lean'), ('lean4', 'Lean4'), ('less', 'LessCss'), ('lighttpd', 'Lighttpd configuration file'), ('lilypond', 'LilyPond'), ('limbo', 'Limbo'), ('liquid', 'liquid'), ('literate-agda', 'Literate Agda'), ('literate-cryptol', 'Literate Cryptol'), ('literate-haskell', 'Literate Haskell'), ('literate-idris', 'Literate Idris'), ('livescript', 'LiveScript'), ('llvm', 'LLVM'), ('llvm-mir', 'LLVM-MIR'), ('llvm-mir-body', 'LLVM-MIR Body'), ('logos', 'Logos'), ('logtalk', 'Logtalk'), ('lsl', 'LSL'), ('lua', 'Lua'), ('luau', 'Luau'), ('macaulay2', 'Macaulay2'), ('make', 'Makefile'), ('mako', 'Mako'), ('maple', 'Maple'), ('maql', 'MAQL'), ('markdown', 'Markdown'), ('mask', 'Mask'), ('mason', 'Mason'), ('mathematica', 'Mathematica'), ('matlab', 'Matlab'), ('matlabsession', 'Matlab session'), ('maxima', 'Maxima'), ('mcfunction', 'MCFunction'), ('mcschema', 'MCSchema'), ('meson', 'Meson'), ('mime', 'MIME'), ('minid', 'MiniD'), ('miniscript', 'MiniScript'), ('mips', 'MIPS'), ('modelica', 'Modelica'), ('modula2', 'Modula-2'), ('mojo', 'Mojo'), ('monkey', 'Monkey'), ('monte', 'Monte'), ('moocode', 'MOOCode'), ('moonscript', 'MoonScript'), ('mosel', 'Mosel'), ('mozhashpreproc', 'mozhashpreproc'), ('mozpercentpreproc', 'mozpercentpreproc'), ('mql', 'MQL'), ('mscgen', 'Mscgen'), ('mupad', 'MuPAD'), ('mxml', 'MXML'), ('myghty', 'Myghty'), ('mysql', 'MySQL'), ('nasm', 'NASM'), ('ncl', 'NCL'), ('nemerle', 'Nemerle'), ('nesc', 'nesC'), ('nestedtext', 'NestedText'), ('newlisp', 'NewLisp'), ('newspeak', 'Newspeak'), ('ng2', 'Angular2'), ('nginx', 'Nginx configuration file'), ('nimrod', 'Nimrod'), ('nit', 'Nit'), ('nixos', 'Nix'), ('nodejsrepl', 'Node.js REPL console session'), ('notmuch', 'Notmuch'), ('nsis', 'NSIS'), ('numba_ir', 'Numba_IR'), ('numpy', 'NumPy'), ('nusmv', 'NuSMV'), ('objdump', 'objdump'), ('objdump-nasm', 'objdump-nasm'), ('objective-c', 'Objective-C'), ('objective-c++', 'Objective-C++'), ('objective-j', 'Objective-J'), ('ocaml', 'OCaml'), ('octave', 'Octave'), ('odin', 'ODIN'), ('omg-idl', 'OMG Interface Definition Language'), ('ooc', 'Ooc'), ('opa', 'Opa'), ('openedge', 'OpenEdge ABL'), ('openscad', 'OpenSCAD'), ('org', 'Org Mode'), ('output', 'Text output'), ('pacmanconf', 'PacmanConf'), ('pan', 'Pan'), ('parasail', 'ParaSail'), ('pawn', 'Pawn'), ('pddl', 'PDDL'), ('peg', 'PEG'), ('perl', 'Perl'), ('perl6', 'Perl6'), ('phix', 'Phix'), ('php', 'PHP'), ('pig', 'Pig'), ('pike', 'Pike'), ('pkgconfig', 'PkgConfig'), ('plpgsql', 'PL/pgSQL'), ('pointless', 'Pointless'), ('pony', 'Pony'), ('portugol', 'Portugol'), ('postgres-explain', 'PostgreSQL EXPLAIN dialect'), ('postgresql', 'PostgreSQL SQL dialect'), ('postscript', 'PostScript'), ('pot', 'Gettext Catalog'), ('pov', 'POVRay'), ('powershell', 'PowerShell'), ('praat', 'Praat'), ('procfile', 'Procfile'), ('prolog', 'Prolog'), ('promela', 'Promela'), ('promql', 'PromQL'), ('properties', 'Properties'), ('protobuf', 'Protocol Buffer'), ('prql', 'PRQL'), ('psql', 'PostgreSQL console (psql)'), ('psysh', 'PsySH console session for PHP'), ('ptx', 'PTX'), ('pug', 'Pug'), ('puppet', 'Puppet'), ('pwsh-session', 'PowerShell Session'), ('py+ul4', 'Python+UL4'), ('py2tb', 'Python 2.x Traceback'), ('pycon', 'Python console session'), ('pypylog', 'PyPy Log'), ('pytb', 'Python Traceback'), ('python', 'Python'), ('python2', 'Python 2.x'), ('q', 'Q'), ('qbasic', 'QBasic'), ('qlik', 'Qlik'), ('qml', 'QML'), ('qvto', 'QVTO'), ('racket', 'Racket'), ('ragel', 'Ragel'), ('ragel-c', 'Ragel in C Host'), ('ragel-cpp', 'Ragel in CPP Host'), ('ragel-d', 'Ragel in D Host'), ('ragel-em', 'Embedded Ragel'), ('ragel-java', 'Ragel in Java Host'), ('ragel-objc', 'Ragel in Objective C Host'), ('ragel-ruby', 'Ragel in Ruby Host'), ('rbcon', 'Ruby irb session'), ('rconsole', 'RConsole'), ('rd', 'Rd'), ('reasonml', 'ReasonML'), ('rebol', 'REBOL'), ('red', 'Red'), ('redcode', 'Redcode'), ('registry', 'reg'), ('rego', 'Rego'), ('resourcebundle', 'ResourceBundle'), ('restructuredtext', 'reStructuredText'), ('rexx', 'Rexx'), ('rhtml', 'RHTML'), ('ride', 'Ride'), ('rita', 'Rita'), ('rng-compact', 'Relax-NG Compact'), ('roboconf-graph', 'Roboconf Graph'), ('roboconf-instances', 'Roboconf Instances'), ('robotframework', 'RobotFramework'), ('rql', 'RQL'), ('rsl', 'RSL'), ('ruby', 'Ruby'), ('rust', 'Rust'), ('sarl', 'SARL'), ('sas', 'SAS'), ('sass', 'Sass'), ('savi', 'Savi'), ('scala', 'Scala'), ('scaml', 'Scaml'), ('scdoc', 'scdoc'), ('scheme', 'Scheme'), ('scilab', 'Scilab'), ('scss', 'SCSS'), ('sed', 'Sed'), ('sgf', 'SmartGameFormat'), ('shen', 'Shen'), ('shexc', 'ShExC'), ('sieve', 'Sieve'), ('silver', 'Silver'), ('singularity', 'Singularity'), ('slash', 'Slash'), ('slim', 'Slim'), ('slurm', 'Slurm'), ('smali', 'Smali'), ('smalltalk', 'Smalltalk'), ('smarty', 'Smarty'), ('smithy', 'Smithy'), ('sml', 'Standard ML'), ('snbt', 'SNBT'), ('snobol', 'Snobol'), ('snowball', 'Snowball'), ('solidity', 'Solidity'), ('sophia', 'Sophia'), ('sp', 'SourcePawn'), ('sparql', 'SPARQL'), ('spec', 'RPMSpec'), ('spice', 'Spice'), ('splus', 'S'), ('sql', 'SQL'), ('sql+jinja', 'SQL+Jinja'), ('sqlite3', 'sqlite3con'), ('squidconf', 'SquidConf'), ('srcinfo', 'Srcinfo'), ('ssp', 'Scalate Server Page'), ('stan', 'Stan'), ('stata', 'Stata'), ('supercollider', 'SuperCollider'), ('swift', 'Swift'), ('swig', 'SWIG'), ('systemd', 'Systemd'), ('systemverilog', 'systemverilog'), ('tablegen', 'TableGen'), ('tact', 'Tact'), ('tads3', 'TADS 3'), ('tal', 'Tal'), ('tap', 'TAP'), ('tasm', 'TASM'), ('tcl', 'Tcl'), ('tcsh', 'Tcsh'), ('tcshcon', 'Tcsh Session'), ('tea', 'Tea'), ('teal', 'teal'), ('teratermmacro', 'Tera Term macro'), ('termcap', 'Termcap'), ('terminfo', 'Terminfo'), ('terraform', 'Terraform'), ('tex', 'TeX'), ('text', 'Text only'), ('thrift', 'Thrift'), ('ti', 'ThingsDB'), ('tid', 'tiddler'), ('tlb', 'Tl-b'), ('tls', 'TLS Presentation Language'), ('tnt', 'Typographic Number Theory'), ('todotxt', 'Todotxt'), ('toml', 'TOML'), ('trac-wiki', 'MoinMoin/Trac Wiki markup'), ('trafficscript', 'TrafficScript'), ('treetop', 'Treetop'), ('tsql', 'Transact-SQL'), ('tsx', 'TSX'), ('turtle', 'Turtle'), ('twig', 'Twig'), ('typescript', 'TypeScript'), ('typoscript', 'TypoScript'), ('typoscriptcssdata', 'TypoScriptCssData'), ('typoscripthtmldata', 'TypoScriptHtmlData'), ('typst', 'Typst'), ('ucode', 'ucode'), ('ul4', 'UL4'), ('unicon', 'Unicon'), ('unixconfig', 'Unix/Linux config files'), ('urbiscript', 'UrbiScript'), ('urlencoded', 'urlencoded'), ('usd', 'USD'), ('vala', 'Vala'), ('vb.net', 'VB.net'), ('vbscript', 'VBScript'), ('vcl', 'VCL'), ('vclsnippets', 'VCLSnippets'), ('vctreestatus', 'VCTreeStatus'), ('velocity', 'Velocity'), ('verifpal', 'Verifpal'), ('verilog', 'verilog'), ('vgl', 'VGL'), ('vhdl', 'vhdl'), ('vim', 'VimL'), ('visualprolog', 'Visual Prolog'), ('visualprologgrammar', 'Visual Prolog Grammar'), ('vue', 'Vue'), ('vyper', 'Vyper'), ('wast', 'WebAssembly'), ('wdiff', 'WDiff'), ('webidl', 'Web IDL'), ('wgsl', 'WebGPU Shading Language'), ('whiley', 'Whiley'), ('wikitext', 'Wikitext'), ('wowtoc', 'World of Warcraft TOC'), ('wren', 'Wren'), ('x10', 'X10'), ('xml', 'XML'), ('xml+cheetah', 'XML+Cheetah'), ('xml+django', 'XML+Django/Jinja'), ('xml+evoque', 'XML+Evoque'), ('xml+lasso', 'XML+Lasso'), ('xml+mako', 'XML+Mako'), ('xml+myghty', 'XML+Myghty'), ('xml+php', 'XML+PHP'), ('xml+ruby', 'XML+Ruby'), ('xml+smarty', 'XML+Smarty'), ('xml+ul4', 'XML+UL4'), ('xml+velocity', 'XML+Velocity'), ('xorg.conf', 'Xorg'), ('xpp', 'X++'), ('xquery', 'XQuery'), ('xslt', 'XSLT'), ('xtend', 'Xtend'), ('xul+mozpreproc', 'XUL+mozpreproc'), ('yaml', 'YAML'), ('yaml+jinja', 'YAML+Jinja'), ('yang', 'YANG'), ('yara', 'YARA'), ('zeek', 'Zeek'), ('zephir', 'Zephir'), ('zig', 'Zig'), ('zone', 'Zone')], default='python', max_length=100)),
+                ('style', models.CharField(choices=[('abap', 'abap'), ('algol', 'algol'), ('algol_nu', 'algol_nu'), ('arduino', 'arduino'), ('autumn', 'autumn'), ('borland', 'borland'), ('bw', 'bw'), ('coffee', 'coffee'), ('colorful', 'colorful'), ('default', 'default'), ('dracula', 'dracula'), ('emacs', 'emacs'), ('friendly', 'friendly'), ('friendly_grayscale', 'friendly_grayscale'), ('fruity', 'fruity'), ('github-dark', 'github-dark'), ('gruvbox-dark', 'gruvbox-dark'), ('gruvbox-light', 'gruvbox-light'), ('igor', 'igor'), ('inkpot', 'inkpot'), ('lightbulb', 'lightbulb'), ('lilypond', 'lilypond'), ('lovelace', 'lovelace'), ('manni', 'manni'), ('material', 'material'), ('monokai', 'monokai'), ('murphy', 'murphy'), ('native', 'native'), ('nord', 'nord'), ('nord-darker', 'nord-darker'), ('one-dark', 'one-dark'), ('paraiso-dark', 'paraiso-dark'), ('paraiso-light', 'paraiso-light'), ('pastie', 'pastie'), ('perldoc', 'perldoc'), ('rainbow_dash', 'rainbow_dash'), ('rrt', 'rrt'), ('sas', 'sas'), ('solarized-dark', 'solarized-dark'), ('solarized-light', 'solarized-light'), ('staroffice', 'staroffice'), ('stata-dark', 'stata-dark'), ('stata-light', 'stata-light'), ('tango', 'tango'), ('trac', 'trac'), ('vim', 'vim'), ('vs', 'vs'), ('xcode', 'xcode'), ('zenburn', 'zenburn')], default='friendly', max_length=100)),
+                ('highlighted', models.TextField()),
+                ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owner', to=settings.AUTH_USER_MODEL)),
+            ],
+            options={
+                'ordering': ['created'],
+            },
+        ),
+    ]
diff --git a/snippets/migrations/0002_alter_snippet_owner.py b/snippets/migrations/0002_alter_snippet_owner.py
new file mode 100644 (file)
index 0000000..e5ce189
--- /dev/null
@@ -0,0 +1,21 @@
+# Generated by Django 5.1.4 on 2025-01-16 18:35
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('snippets', '0001_initial'),
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='snippet',
+            name='owner',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='snippets', to=settings.AUTH_USER_MODEL),
+        ),
+    ]
diff --git a/snippets/migrations/__init__.py b/snippets/migrations/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/snippets/models.py b/snippets/models.py
new file mode 100644 (file)
index 0000000..96fbcde
--- /dev/null
@@ -0,0 +1,47 @@
+from django.db import models
+
+from pygments.formatters import HtmlFormatter
+from pygments.lexers import get_all_lexers
+from pygments.styles import get_all_styles
+
+from pygments.lexers import get_lexer_by_name
+from pygments.formatters.html import HtmlFormatter
+from pygments import highlight
+
+
+LEXERS = [item for item in get_all_lexers() if item[1]]
+LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS])
+STYLE_CHOICES = sorted([(item, item) for item in get_all_styles()])
+
+
+class Snippet(models.Model):
+    created = models.DateTimeField(auto_now_add=True)
+    title = models.CharField(max_length=100, blank=True, default="")
+    code = models.TextField()
+    linenos = models.BooleanField(default=False)  # type: ignore
+    language = models.CharField(
+        choices=LANGUAGE_CHOICES, default="python", max_length=100
+    )
+    style = models.CharField(choices=STYLE_CHOICES, default="friendly", max_length=100)
+
+    owner = models.ForeignKey(
+        "auth.User", related_name="snippets", on_delete=models.CASCADE
+    )
+    highlighted = models.TextField()
+
+    def save(self, *args, **kwargs):
+        """
+        Use the `pygments` library to create a highlighted HTML
+        representation of the code snippet.
+        """
+        lexer = get_lexer_by_name(self.language)
+        linenos = "table" if self.linenos else False
+        options = {"title": self.title} if self.title else {}
+        formatter = HtmlFormatter(
+            style=self.style, linenos=linenos, full=True, **options
+        )
+        self.highlighted = highlight(self.code, lexer, formatter)
+        super().save(*args, **kwargs)
+
+    class Meta:
+        ordering = ["created"]
diff --git a/snippets/permissions.py b/snippets/permissions.py
new file mode 100644 (file)
index 0000000..1b9e8fe
--- /dev/null
@@ -0,0 +1,16 @@
+from rest_framework import permissions
+
+
+class IsOwnerOrReadOnly(permissions.BasePermission):
+    """
+    Custom permission to only allow owner of an object to edit it.
+    """
+
+    def has_object_permission(self, request, view, obj):
+        # Read permission are allowed to any request
+        # so we'll always allow GET, HEAD, or OPTIONS requests
+        if request.method in permissions.SAFE_METHODS:
+            return True
+
+        # Write permissions are only allowed to the owner of the snippet
+        return obj.owner == request.user
diff --git a/snippets/serializers.py b/snippets/serializers.py
new file mode 100644 (file)
index 0000000..4b6eafc
--- /dev/null
@@ -0,0 +1,36 @@
+from django.contrib.auth.models import User
+from .models import Snippet
+from rest_framework import serializers
+
+
+class SnippetSerializer(serializers.HyperlinkedModelSerializer):
+    owner = serializers.ReadOnlyField(source="owner.username")
+    hightlight = serializers.HyperlinkedIdentityField(
+        view_name="snippet-highlight", format="html"
+    )
+
+    class Meta:
+        model = Snippet
+        fields = [
+            "url",
+            "id",
+            "hightlight",
+            "owner",
+            "title",
+            "code",
+            "linenos",
+            "language",
+            "style",
+        ]
+
+
+class UserSerializer(serializers.HyperlinkedModelSerializer):
+    snippets = serializers.HyperlinkedRelatedField(
+        many=True,
+        view_name="snippet-detail",
+        read_only=True,
+    )
+
+    class Meta:
+        model = User
+        fields = ["url", "id", "username", "snippets"]
diff --git a/snippets/tests.py b/snippets/tests.py
new file mode 100644 (file)
index 0000000..7ce503c
--- /dev/null
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/snippets/urls.py b/snippets/urls.py
new file mode 100644 (file)
index 0000000..695c1e6
--- /dev/null
@@ -0,0 +1,17 @@
+from functools import partial
+from django.urls import path, include
+from rest_framework import renderers
+from rest_framework.decorators import renderer_classes
+from . import views
+from rest_framework.urlpatterns import format_suffix_patterns
+
+from .views import SnippetViewSet, UserViewSet
+
+from rest_framework.routers import DefaultRouter
+
+router = DefaultRouter()
+
+router.register(r"snippets", SnippetViewSet, basename="snippet")
+router.register(r"users", UserViewSet, basename="user")
+
+urlpatterns = [path("", include(router.urls))]
diff --git a/snippets/views.py b/snippets/views.py
new file mode 100644 (file)
index 0000000..b6f4a54
--- /dev/null
@@ -0,0 +1,40 @@
+from django.contrib.auth.models import User
+from rest_framework import permissions, renderers, viewsets
+from rest_framework.decorators import api_view, action
+from rest_framework.reverse import reverse
+from rest_framework.views import Response
+
+from snippets.permissions import IsOwnerOrReadOnly
+
+from .serializers import SnippetSerializer, UserSerializer
+from snippets import serializers
+from .models import Snippet
+
+
+@api_view(["GET"])
+def api_root(request, format=None):
+    return Response(
+        {
+            "users": reverse("user-list", request=request, format=format),
+            "snippets": reverse("snippet-list", request=request, format=format),
+        }
+    )
+
+
+class SnippetViewSet(viewsets.ModelViewSet):
+    queryset = Snippet.objects.all()  # type: ignore
+    serializer_class = SnippetSerializer
+    permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]
+
+    @action(detail=True, renderer_classes=[renderers.StaticHTMLRenderer])
+    def highlight(self, request, *args, **kwargs):
+        snippet = self.get_object()
+        return Response(snippet.highlighted)
+
+    def perform_create(self, serializer):
+        serializer.save(owner=self.request.user)
+
+
+class UserViewSet(viewsets.ReadOnlyModelViewSet):
+    queryset = User.objects.all()  # type:ignore
+    serializer_class = UserSerializer