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 @@
+Cancel
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 }}
+
+ Cancel
+ {% 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.
{% 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 }}
+
+ Cancel
+ {% 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
-
-
-
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.
-
-
-
-
-
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 %}
+
+
+
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 @@
+Save
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})