usethesource: Add data models, admin and frontend comment editing
This commit is contained in:
parent
905e5c918d
commit
cc3224bb60
19 changed files with 251 additions and 40 deletions
|
@ -100,7 +100,7 @@ INSTALLED_APPS = [
|
|||
'conservancy.apps.assignment',
|
||||
'conservancy.apps.fossy',
|
||||
'podjango',
|
||||
'usethesource',
|
||||
'usethesource.apps.UseTheSourceConfig',
|
||||
]
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||
|
|
26
www/usethesource/admin.py
Normal file
26
www/usethesource/admin.py
Normal file
|
@ -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']}
|
6
www/usethesource/apps.py
Normal file
6
www/usethesource/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class UseTheSourceConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.AutoField'
|
||||
name = 'usethesource'
|
44
www/usethesource/migrations/0001_initial.py
Normal file
44
www/usethesource/migrations/0001_initial.py
Normal file
|
@ -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'],
|
||||
},
|
||||
),
|
||||
]
|
0
www/usethesource/migrations/__init__.py
Normal file
0
www/usethesource/migrations/__init__.py
Normal file
29
www/usethesource/models.py
Normal file
29
www/usethesource/models.py
Normal file
|
@ -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']
|
|
@ -0,0 +1,3 @@
|
|||
<div class="mt2" hx-target="this" hx-swap="outerHTML">
|
||||
<button type="submit" class="f3 b white bg-green ph2" style="border: none" hx-get="{% url 'usethesource:add_comment' slug=candidate.slug %}">+</button>
|
||||
</div>
|
|
@ -0,0 +1 @@
|
|||
<button type="submit" class="b white bg-light-silver pv2 ph3" style="border: none">Cancel</button>
|
|
@ -1,35 +1,36 @@
|
|||
{% extends "usethesource/base.html" %}
|
||||
|
||||
{% block head %}
|
||||
{{ block.super }}
|
||||
<script src="https://unpkg.com/htmx.org@1.9.6"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ block.super }}
|
||||
|
||||
<section class="pa2 mt4 mb3">
|
||||
<div style="display: flex; justify-content: space-between">
|
||||
<div>
|
||||
<h2 class="f2 lh-title ttu mt0">Linksys WRG987-v2</h2>
|
||||
<p><strong>Vendor</strong>: Linksys</em></p>
|
||||
<p><strong>Device</strong>: WRG987</em></p>
|
||||
<p><strong>Released</strong>: Oct 20, 2023</em></p>
|
||||
<h2 class="f2 lh-title ttu mt0">{{ candidate.name }}</h2>
|
||||
<p><strong>Vendor</strong>: {{ candidate.vendor }}</em></p>
|
||||
<p><strong>Device</strong>: {{ candidate.device }}</em></p>
|
||||
<p><strong>Released</strong>: {{ candidate.release_date }}</em></p>
|
||||
</div>
|
||||
<div class="mt2">
|
||||
<div><a href="{% url 'usethesource:download' %}" class="white bg-green db pv2 ph3 mb2">Download source</a></div>
|
||||
<div><a href="{% url 'usethesource:download' %}" class="white bg-green db pv2 ph3">Download image</a></div>
|
||||
<div><a href="{% url 'usethesource:download' slug=candidate.slug download_type='source' %}" class="white bg-green db pv2 ph3 mb2">Download source</a></div>
|
||||
<div><a href="{% url 'usethesource:download' slug=candidate.slug download_type='binary' %}" class="white bg-green db pv2 ph3">Download image</a></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>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.</p>
|
||||
<p>{{ candidate.description }}</p>
|
||||
|
||||
<h3 class="f3 lh-title mt4">Comments</h3>
|
||||
<p><strong>Denver — Oct 20, 2023</strong>:<br>
|
||||
I was able to run the autogen and configure scripts, but the build failed with a missing dependency XYZ.</p>
|
||||
{% for comment in candidate.comment_set.all %}
|
||||
{% include "usethesource/comment_partial.html" %}
|
||||
{% endfor %}
|
||||
|
||||
<form method="post">
|
||||
<div class="mt4">
|
||||
<textarea placeholder="Add a comment..." style="width: 50em; height: 15em;"></textarea>
|
||||
</div>
|
||||
<div class="mt2">
|
||||
<button type="submit" class="b white bg-green pv2 ph3" style="border: none">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
{% if user.is_staff %}
|
||||
{% include "usethesource/add_comment_button_partial.html" %}
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock content %}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{% if add and user.is_staff %}
|
||||
{% include 'usethesource/add_comment_button_partial.html' %}
|
||||
{% endif %}
|
|
@ -0,0 +1,8 @@
|
|||
<form hx-target="this" hx-swap="outerHTML" hx-post="{% url 'usethesource:add_comment' slug=candidate.slug %}" method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.message }}
|
||||
<div class="mt2">
|
||||
<button type="submit" hx-get="{% url 'usethesource:add_button' slug=candidate.slug %}" class="b pointer white bg-light-silver pv2 ph3" style="border: none">Cancel</button>
|
||||
{% include 'usethesource/save_button_partial.html' %}
|
||||
</div>
|
||||
</form>
|
|
@ -0,0 +1,7 @@
|
|||
<div hx-target="this" hx-swap="outerHTML"><strong>{{ comment.user }} — {{ comment.time }}</strong>
|
||||
{% if user.is_staff %}
|
||||
<a href="#" class="f7 white bg-light-silver ph2" hx-get="{% url 'usethesource:edit_comment' comment_id=comment.id %}">edit</a>
|
||||
<a href="#" class="f7 white bg-light-red ph2" hx-delete="{% url 'usethesource:delete_comment' comment_id=comment.id show_add='false' %}" hx-confirm="Are you sure you want to delete this comment?">delete</a>
|
||||
{% endif %}
|
||||
<br>{{ comment.message|linebreaks }}
|
||||
</div>
|
|
@ -5,13 +5,13 @@
|
|||
|
||||
<section class="pa2 mt4 mb5">
|
||||
<h2 class="f2 lh-title ttu mt0">Download</h2>
|
||||
<p><strong>File</strong>: xyz-image.tar.gz</p>
|
||||
<p><strong>File</strong>: {{ download_url }}</p>
|
||||
|
||||
<p>By downloading this, you agree to use it only for the purposes of GPL compliance checking, unless otherwise permitted by the license.</p>
|
||||
|
||||
<div style="display: flex">
|
||||
<a href="{% url 'usethesource:candidate' %}" class="white bg-silver dib pv2 ph3 mb2 mr2">Back</a>
|
||||
<a href="{% url 'usethesource:download' %}" class="white bg-green dib pv2 ph3 mb2">Download</a>
|
||||
<a href="{% url 'usethesource:candidate' slug=candidate.slug %}" class="white bg-silver dib pv2 ph3 mb2 mr2">Back</a>
|
||||
<a href="{{ download_url }}" class="white bg-green dib pv2 ph3 mb2">Download</a>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock content %}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
<form class="mb3" hx-target="this" hx-swap="outerHTML" hx-post="{% url 'usethesource:edit_comment' comment_id=comment.id %}" method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.message }}
|
||||
<div class="mt2">
|
||||
<button type="submit" hx-get="{% url 'usethesource:view_comment' comment_id=comment.id show_add='false' %}" class="b pointer white bg-light-silver pv2 ph3" style="border: none">Cancel</button>
|
||||
{% include 'usethesource/save_button_partial.html' %}
|
||||
</div>
|
||||
</form>
|
|
@ -9,16 +9,12 @@
|
|||
|
||||
<section class="mb5">
|
||||
<h2 class="f2 lh-title ttu mt0">Candidates</h2>
|
||||
<div class="mb4">
|
||||
<h3 class="f4 lh-title mt0"><a href="{% url 'usethesource:candidate' %}">Linksys WRG987-v2</a></h3>
|
||||
<p class=""><em>Released Oct 20, 2023</em></p>
|
||||
<p class="">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.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="lh-title mt0"><a href="{% url 'usethesource:candidate' %}">Linksys WRG987-v2</a></h2>
|
||||
<p><em>Released Oct 20, 2023</em></p>
|
||||
<p>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.</p>
|
||||
</div>
|
||||
{% for candidate in candidates %}
|
||||
<div class="mb4">
|
||||
<h3 class="f4 lh-title mt0"><a href="{% url 'usethesource:candidate' slug=candidate.slug %}">{{ candidate.name }}</a></h3>
|
||||
<p><em>Released {{ candidate.release_date }}</em></p>
|
||||
<p>{{ candidate.description }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endblock content %}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{% include 'usethesource/comment_partial.html' %}
|
||||
|
||||
{% if add and user.is_staff %}
|
||||
{% include 'usethesource/add_comment_button_partial.html' %}
|
||||
{% endif %}
|
|
@ -0,0 +1 @@
|
|||
<button type="submit" class="b white bg-green pv2 ph3" style="border: none">Save</button>
|
|
@ -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/<slug:slug>/', views.candidate_page, name='candidate'),
|
||||
path('download/<slug:slug>/<download_type>/', views.download_page, name='download'),
|
||||
path('add-comment/<slug:slug>/', views.create_comment, name='add_comment'),
|
||||
path('edit-comment/<int:comment_id>/', views.edit_comment, name='edit_comment'),
|
||||
path('comment/<int:comment_id>/<show_add>/', views.view_comment, name='view_comment'),
|
||||
path('delete-comment/<int:comment_id>/<show_add>/', views.delete_comment, name='delete_comment'),
|
||||
path('add-button/<slug:slug>/', views.add_button, name='add_button'),
|
||||
]
|
||||
|
|
|
@ -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})
|
||||
|
|
Loading…
Reference in a new issue