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…
	
	Add table
		
		Reference in a new issue