From cc3224bb60dda78c15014a03ebe28adbd2e5078c Mon Sep 17 00:00:00 2001 From: Ben Sturmfels Date: Tue, 24 Oct 2023 23:55:14 +1100 Subject: [PATCH] usethesource: Add data models, admin and frontend comment editing --- www/conservancy/settings.py | 2 +- www/usethesource/admin.py | 26 ++++++ www/usethesource/apps.py | 6 ++ www/usethesource/migrations/0001_initial.py | 44 ++++++++++ www/usethesource/migrations/__init__.py | 0 www/usethesource/models.py | 29 +++++++ .../add_comment_button_partial.html | 3 + .../usethesource/cancel_button_partial.html | 1 + .../templates/usethesource/candidate.html | 35 ++++---- .../usethesource/comment_deleted.html | 3 + .../templates/usethesource/comment_form.html | 8 ++ .../usethesource/comment_partial.html | 7 ++ .../templates/usethesource/download.html | 6 +- .../usethesource/edit_comment_form.html | 8 ++ .../templates/usethesource/landing_page.html | 18 ++--- .../usethesource/returned_comment.html | 5 ++ .../usethesource/save_button_partial.html | 1 + www/usethesource/urls.py | 9 ++- www/usethesource/views.py | 80 +++++++++++++++++-- 19 files changed, 251 insertions(+), 40 deletions(-) create mode 100644 www/usethesource/admin.py create mode 100644 www/usethesource/apps.py create mode 100644 www/usethesource/migrations/0001_initial.py create mode 100644 www/usethesource/migrations/__init__.py create mode 100644 www/usethesource/models.py create mode 100644 www/usethesource/templates/usethesource/add_comment_button_partial.html create mode 100644 www/usethesource/templates/usethesource/cancel_button_partial.html create mode 100644 www/usethesource/templates/usethesource/comment_deleted.html create mode 100644 www/usethesource/templates/usethesource/comment_form.html create mode 100644 www/usethesource/templates/usethesource/comment_partial.html create mode 100644 www/usethesource/templates/usethesource/edit_comment_form.html create mode 100644 www/usethesource/templates/usethesource/returned_comment.html create mode 100644 www/usethesource/templates/usethesource/save_button_partial.html diff --git a/www/conservancy/settings.py b/www/conservancy/settings.py index 2f26462e..3fd906f8 100644 --- a/www/conservancy/settings.py +++ b/www/conservancy/settings.py @@ -100,7 +100,7 @@ INSTALLED_APPS = [ 'conservancy.apps.assignment', 'conservancy.apps.fossy', 'podjango', - 'usethesource', + 'usethesource.apps.UseTheSourceConfig', ] DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' diff --git a/www/usethesource/admin.py b/www/usethesource/admin.py new file mode 100644 index 00000000..ae5df7f3 --- /dev/null +++ b/www/usethesource/admin.py @@ -0,0 +1,26 @@ +from django.contrib import admin + +from .models import Candidate, Comment + + +class CommentInline(admin.TabularInline): + model = Comment + fields = ['user', 'message'] + extra = 0 + + +@admin.register(Candidate) +class CandidateAdmin(admin.ModelAdmin): + list_display = ['name', 'vendor', 'device', 'release_date'] + fields = [ + 'name', + 'slug', + 'vendor', + 'device', + 'release_date', + 'source_url', + 'binary_url', + 'description', + ] + inlines = [CommentInline] + prepopulated_fields = {'slug': ['name']} diff --git a/www/usethesource/apps.py b/www/usethesource/apps.py new file mode 100644 index 00000000..e7322c9a --- /dev/null +++ b/www/usethesource/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UseTheSourceConfig(AppConfig): + default_auto_field = 'django.db.models.AutoField' + name = 'usethesource' diff --git a/www/usethesource/migrations/0001_initial.py b/www/usethesource/migrations/0001_initial.py new file mode 100644 index 00000000..81a4958e --- /dev/null +++ b/www/usethesource/migrations/0001_initial.py @@ -0,0 +1,44 @@ +# Generated by Django 3.2.19 on 2023-10-24 08:53 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Candidate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50, verbose_name='Candidate name')), + ('slug', models.SlugField(unique=True)), + ('vendor', models.CharField(max_length=50, verbose_name='Vendor name')), + ('device', models.CharField(max_length=50, verbose_name='Device name')), + ('release_date', models.DateField()), + ('description', models.TextField()), + ('source_url', models.URLField()), + ('binary_url', models.URLField(blank=True)), + ], + ), + migrations.CreateModel( + name='Comment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('time', models.DateTimeField(auto_now_add=True)), + ('message', models.TextField()), + ('candidate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='usethesource.candidate')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['id'], + }, + ), + ] diff --git a/www/usethesource/migrations/__init__.py b/www/usethesource/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/www/usethesource/models.py b/www/usethesource/models.py new file mode 100644 index 00000000..743efd6a --- /dev/null +++ b/www/usethesource/models.py @@ -0,0 +1,29 @@ +from django.contrib.auth.models import User +from django.db import models + + +class Candidate(models.Model): + name = models.CharField('Candidate name', max_length=50) + slug = models.SlugField(max_length=50, unique=True) + vendor = models.CharField('Vendor name', max_length=50) + device = models.CharField('Device name', max_length=50) + release_date = models.DateField() + description = models.TextField() + source_url = models.URLField() + binary_url = models.URLField(blank=True) + + def __str__(self): + return self.name + + +class Comment(models.Model): + candidate = models.ForeignKey(Candidate, on_delete=models.CASCADE) + user = models.ForeignKey(User, on_delete=models.PROTECT) + time = models.DateTimeField(auto_now_add=True) + message = models.TextField() + + def __str__(self): + return f'{self.candidate.name}, {self.user}, {self.time}' + + class Meta: + ordering = ['id'] diff --git a/www/usethesource/templates/usethesource/add_comment_button_partial.html b/www/usethesource/templates/usethesource/add_comment_button_partial.html new file mode 100644 index 00000000..1a370e98 --- /dev/null +++ b/www/usethesource/templates/usethesource/add_comment_button_partial.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/www/usethesource/templates/usethesource/cancel_button_partial.html b/www/usethesource/templates/usethesource/cancel_button_partial.html new file mode 100644 index 00000000..a8dd444b --- /dev/null +++ b/www/usethesource/templates/usethesource/cancel_button_partial.html @@ -0,0 +1 @@ + diff --git a/www/usethesource/templates/usethesource/candidate.html b/www/usethesource/templates/usethesource/candidate.html index 1dca667b..8d396d22 100644 --- a/www/usethesource/templates/usethesource/candidate.html +++ b/www/usethesource/templates/usethesource/candidate.html @@ -1,35 +1,36 @@ {% extends "usethesource/base.html" %} +{% block head %} + {{ block.super }} + +{% endblock %} + {% block content %} {{ block.super }}
-

Linksys WRG987-v2

-

Vendor: Linksys

-

Device: WRG987

-

Released: Oct 20, 2023

+

{{ candidate.name }}

+

Vendor: {{ candidate.vendor }}

+

Device: {{ candidate.device }}

+

Released: {{ candidate.release_date }}

-

The WRG1830 Wireless Router Gateway (WRG) is a component of the ZFR183x Pro Series Wireless Field Bus System. The WRG1830 provides BACnet IP connectivity to compatible Johnson Controls Field Controllers, VAV Controllers, Thermostats, and Sensors over a wireless mesh network.

+

{{ candidate.description }}

Comments

-

Denver — Oct 20, 2023:
- I was able to run the autogen and configure scripts, but the build failed with a missing dependency XYZ.

+ {% for comment in candidate.comment_set.all %} + {% include "usethesource/comment_partial.html" %} + {% endfor %} -
-
- -
-
- -
-
+ {% if user.is_staff %} + {% include "usethesource/add_comment_button_partial.html" %} + {% endif %}
{% endblock content %} diff --git a/www/usethesource/templates/usethesource/comment_deleted.html b/www/usethesource/templates/usethesource/comment_deleted.html new file mode 100644 index 00000000..f6ed46bc --- /dev/null +++ b/www/usethesource/templates/usethesource/comment_deleted.html @@ -0,0 +1,3 @@ +{% if add and user.is_staff %} + {% include 'usethesource/add_comment_button_partial.html' %} +{% endif %} diff --git a/www/usethesource/templates/usethesource/comment_form.html b/www/usethesource/templates/usethesource/comment_form.html new file mode 100644 index 00000000..cfda8b4d --- /dev/null +++ b/www/usethesource/templates/usethesource/comment_form.html @@ -0,0 +1,8 @@ +
+ {% csrf_token %} + {{ form.message }} +
+ + {% include 'usethesource/save_button_partial.html' %} +
+
diff --git a/www/usethesource/templates/usethesource/comment_partial.html b/www/usethesource/templates/usethesource/comment_partial.html new file mode 100644 index 00000000..ccbfc0c6 --- /dev/null +++ b/www/usethesource/templates/usethesource/comment_partial.html @@ -0,0 +1,7 @@ +
{{ comment.user }} — {{ comment.time }} + {% if user.is_staff %} + edit + delete + {% endif %} +
{{ comment.message|linebreaks }} +
diff --git a/www/usethesource/templates/usethesource/download.html b/www/usethesource/templates/usethesource/download.html index 791e5421..792441c5 100644 --- a/www/usethesource/templates/usethesource/download.html +++ b/www/usethesource/templates/usethesource/download.html @@ -5,13 +5,13 @@

Download

-

File: xyz-image.tar.gz

+

File: {{ download_url }}

By downloading this, you agree to use it only for the purposes of GPL compliance checking, unless otherwise permitted by the license.

- Back - Download + Back + Download
{% endblock content %} diff --git a/www/usethesource/templates/usethesource/edit_comment_form.html b/www/usethesource/templates/usethesource/edit_comment_form.html new file mode 100644 index 00000000..e03998bc --- /dev/null +++ b/www/usethesource/templates/usethesource/edit_comment_form.html @@ -0,0 +1,8 @@ +
+ {% csrf_token %} + {{ form.message }} +
+ + {% include 'usethesource/save_button_partial.html' %} +
+
diff --git a/www/usethesource/templates/usethesource/landing_page.html b/www/usethesource/templates/usethesource/landing_page.html index 5dcc0c2e..2432e6ff 100644 --- a/www/usethesource/templates/usethesource/landing_page.html +++ b/www/usethesource/templates/usethesource/landing_page.html @@ -9,16 +9,12 @@

Candidates

-
-

Linksys WRG987-v2

-

Released Oct 20, 2023

-

The WRG1830 Wireless Router Gateway (WRG) is a component of the ZFR183x Pro Series Wireless Field Bus System. The WRG1830 provides BACnet IP connectivity to compatible Johnson Controls Field Controllers, VAV Controllers, Thermostats, and Sensors over a wireless mesh network.

-
- -
-

Linksys WRG987-v2

-

Released Oct 20, 2023

-

The WRG1830 Wireless Router Gateway (WRG) is a component of the ZFR183x Pro Series Wireless Field Bus System. The WRG1830 provides BACnet IP connectivity to compatible Johnson Controls Field Controllers, VAV Controllers, Thermostats, and Sensors over a wireless mesh network.

-
+ {% for candidate in candidates %} +
+

{{ candidate.name }}

+

Released {{ candidate.release_date }}

+

{{ candidate.description }}

+
+ {% endfor %}
{% endblock content %} diff --git a/www/usethesource/templates/usethesource/returned_comment.html b/www/usethesource/templates/usethesource/returned_comment.html new file mode 100644 index 00000000..62e91ab4 --- /dev/null +++ b/www/usethesource/templates/usethesource/returned_comment.html @@ -0,0 +1,5 @@ +{% include 'usethesource/comment_partial.html' %} + +{% if add and user.is_staff %} + {% include 'usethesource/add_comment_button_partial.html' %} +{% endif %} diff --git a/www/usethesource/templates/usethesource/save_button_partial.html b/www/usethesource/templates/usethesource/save_button_partial.html new file mode 100644 index 00000000..8ae5cdff --- /dev/null +++ b/www/usethesource/templates/usethesource/save_button_partial.html @@ -0,0 +1 @@ + diff --git a/www/usethesource/urls.py b/www/usethesource/urls.py index e3f03bfa..0eb550c3 100644 --- a/www/usethesource/urls.py +++ b/www/usethesource/urls.py @@ -5,6 +5,11 @@ from . import views app_name = 'usethesource' urlpatterns = [ path('', views.landing_page, name='landing'), - path('candidate/', views.candidate_page, name='candidate'), - path('download/', views.download_page, name='download'), + path('candidate//', views.candidate_page, name='candidate'), + path('download///', views.download_page, name='download'), + path('add-comment//', views.create_comment, name='add_comment'), + path('edit-comment//', views.edit_comment, name='edit_comment'), + path('comment///', views.view_comment, name='view_comment'), + path('delete-comment///', views.delete_comment, name='delete_comment'), + path('add-button//', views.add_button, name='add_button'), ] diff --git a/www/usethesource/views.py b/www/usethesource/views.py index 64a0e1c6..e9740178 100644 --- a/www/usethesource/views.py +++ b/www/usethesource/views.py @@ -1,13 +1,81 @@ -from django.shortcuts import render +from django import forms +from django.contrib.admin.views.decorators import staff_member_required +from django.shortcuts import get_object_or_404, redirect, render + +from .models import Candidate, Comment def landing_page(request): - return render(request, 'usethesource/landing_page.html', {}) + candidates = Candidate.objects.all() + return render(request, 'usethesource/landing_page.html', {'candidates': candidates}) -def candidate_page(request): - return render(request, 'usethesource/candidate.html', {}) +def candidate_page(request, slug): + candidate = get_object_or_404(Candidate, slug=slug) + return render(request, 'usethesource/candidate.html', {'candidate': candidate, 'add': True}) -def download_page(request): - return render(request, 'usethesource/download.html', {}) +def download_page(request, slug, download_type): + candidate = get_object_or_404(Candidate, slug=slug) + url = candidate.source_url if download_type == 'source' else candidate.binary_url + return render( + request, + 'usethesource/download.html', + {'candidate': candidate, 'download_url': url}, + ) + + +class CommentForm(forms.ModelForm): + class Meta: + model = Comment + fields = ['message'] + + +@staff_member_required +def create_comment(request, slug): + candidate = get_object_or_404(Candidate, slug=slug) + if request.method == 'GET': + form = CommentForm() + else: + form = CommentForm(request.POST) + if form.is_valid(): + instance = form.save(commit=False) + instance.candidate = candidate + instance.user = request.user + instance.save() + return redirect('usethesource:view_comment', comment_id=instance.id, show_add='true') + return render(request, 'usethesource/comment_form.html', {'form': form, 'candidate': candidate}) + + +@staff_member_required +def edit_comment(request, comment_id): + comment = get_object_or_404(Comment, id=comment_id) + if request.method == 'GET': + form = CommentForm(instance=comment) + else: + form = CommentForm(request.POST, instance=comment) + if form.is_valid(): + instance = form.save(commit=False) + instance.save() + return redirect('usethesource:view_comment', comment_id=instance.id, show_add='false') + return render(request, 'usethesource/edit_comment_form.html', {'form': form, 'comment': comment}) + + +@staff_member_required +def view_comment(request, comment_id, show_add): + show_add = show_add == 'true' + comment = get_object_or_404(Comment, id=comment_id) + return render(request, 'usethesource/returned_comment.html', {'comment': comment, 'candidate': comment.candidate, 'add': show_add}) + + +@staff_member_required +def delete_comment(request, comment_id, show_add): + show_add = show_add == 'true' + Comment.objects.filter(id=comment_id).delete() + return render(request, 'usethesource/comment_deleted.html', {'comment': None, 'add': show_add}) + + +@staff_member_required +def add_button(request, slug): + candidate = get_object_or_404(Candidate, slug=slug) + return render(request, 'usethesource/add_comment_button_partial.html', {'candidate': candidate})