Skip to content

Commit

Permalink
Add Feature [276] Plugins feedback (multiple tasks) (#284)
Browse files Browse the repository at this point in the history
* added PluginVersionFeedback model

* added version_feedback_notify

* added plugin feedback received and feedback pending view and url

* updated plugin base and detail html template

* added create feedback view

* notified user when create feedback

* added version_feedback_update

* updated template and ajax requests

* never cache feedback

* use submit button for feedback update

* updated layout
  • Loading branch information
sumandari authored Nov 20, 2023
1 parent 2057611 commit c625132
Show file tree
Hide file tree
Showing 11 changed files with 1,037 additions and 2 deletions.
1 change: 1 addition & 0 deletions REQUIREMENTS-dev.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
flake8
pre-commit
freezegun
37 changes: 36 additions & 1 deletion qgis-app/plugins/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from django.forms import CharField, ModelForm, ValidationError
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from plugins.models import Plugin, PluginVersion
from plugins.models import Plugin, PluginVersion, PluginVersionFeedback
from plugins.validator import validator
from taggit.forms import *

Expand Down Expand Up @@ -208,3 +208,38 @@ def clean_package(self):
# Clean tags
self.cleaned_data["tags"] = _clean_tags(self.cleaned_data.get("tags", None))
return package


class VersionFeedbackForm(forms.Form):
"""Feedback for a plugin version"""

feedback = forms.CharField(
widget=forms.Textarea(
attrs={
"placeholder": _(
"Please provide clear feedback as a task. \n"
"You can create multiple tasks with '- [ ]'.\n"
"e.g:\n"
"- [ ] first task\n"
"- [ ] second task"
),
"rows": "5",
"class": "span12"
}
)
)

def clean(self):
super().clean()
feedback = self.cleaned_data.get('feedback')

if feedback:
lines: list = feedback.split('\n')
bullet_points: list = [
line[6:].strip() for line in lines if line.strip().startswith('- [ ]')
]
has_bullet_point = len(bullet_points) >= 1
tasks: list = bullet_points if has_bullet_point else [feedback]
self.cleaned_data['tasks'] = tasks

return self.cleaned_data
31 changes: 31 additions & 0 deletions qgis-app/plugins/migrations/0002_plugins_feedback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Generated by Django 2.2.25 on 2023-06-17 03:19

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('plugins', '0001_initial'),
]

operations = [
migrations.CreateModel(
name='PluginVersionFeedback',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('task', models.TextField(help_text='A feedback task. Please write your review as a task for this plugin.', max_length=1000, verbose_name='Task')),
('created_on', models.DateTimeField(auto_now_add=True, verbose_name='Created on')),
('completed_on', models.DateTimeField(blank=True, null=True, verbose_name='Completed on')),
('is_completed', models.BooleanField(db_index=True, default=False, verbose_name='Completed')),
('reviewer', models.ForeignKey(help_text='The user who reviewed this plugin.', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Reviewed by')),
('version', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feedback', to='plugins.PluginVersion')),
],
options={
'ordering': ['created_on'],
},
),
]
81 changes: 81 additions & 0 deletions qgis-app/plugins/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,37 @@ def get_queryset(self):
return super(ServerPlugins, self).get_queryset().filter(server=True).distinct()


class FeedbackReceivedPlugins(models.Manager):
"""
Show only unapproved plugins with a feedback
"""
def get_queryset(self):
return (
super(FeedbackReceivedPlugins, self)
.get_queryset()
.filter(
pluginversion__approved=False,
pluginversion__feedback__isnull=False
).distinct()
)


class FeedbackPendingPlugins(models.Manager):
"""
Show only unapproved plugins with a feedback
"""
def get_queryset(self):
return (
super(FeedbackPendingPlugins, self)
.get_queryset()
.filter(
pluginversion__approved=False,
pluginversion__feedback__isnull=True
).distinct()
)



class Plugin(models.Model):
"""
Plugins model
Expand Down Expand Up @@ -352,6 +383,8 @@ class Plugin(models.Model):
most_voted_objects = MostVotedPlugins()
most_rated_objects = MostRatedPlugins()
server_objects = ServerPlugins()
feedback_received_objects = FeedbackReceivedPlugins()
feedback_pending_objects = FeedbackPendingPlugins()

rating = AnonymousRatingField(
range=5, use_cookies=True, can_change_vote=True, allow_delete=True
Expand Down Expand Up @@ -769,6 +802,54 @@ def __str__(self):
return self.__unicode__()


class PluginVersionFeedback(models.Model):
"""Feedback for a plugin version."""

version = models.ForeignKey(
PluginVersion,
on_delete=models.CASCADE,
related_name="feedback"
)
reviewer = models.ForeignKey(
User,
verbose_name=_("Reviewed by"),
help_text=_("The user who reviewed this plugin."),
on_delete=models.CASCADE,
)
task = models.TextField(
verbose_name=_("Task"),
help_text=_("A feedback task. Please write your review as a task for this plugin."),
max_length=1000,
blank=False,
null=False
)
created_on = models.DateTimeField(
verbose_name=_("Created on"),
auto_now_add=True,
editable=False
)
completed_on = models.DateTimeField(
verbose_name=_("Completed on"),
blank=True,
null=True
)
is_completed = models.BooleanField(
verbose_name=_("Completed"),
default=False,
db_index=True
)

class Meta:
ordering = ["created_on"]

def save(self, *args, **kwargs):
if self.is_completed is True:
self.completed_on = datetime.datetime.now()
else:
self.completed_on = None
super(PluginVersionFeedback, self).save(*args, **kwargs)


def delete_version_package(sender, instance, **kw):
"""
Removes the zip package
Expand Down
4 changes: 4 additions & 0 deletions qgis-app/plugins/templates/plugins/plugin_base.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ <h3>{% trans "Plugins" %}</h3>
{% endif %}
{% if user.is_staff %}
<li class="staff"><a href="{% url "unapproved_plugins" %}">{% trans "Unapproved"%}</a></li>
<ul>
<li class="sub-list"><a href="{% url "feedback_received_plugins" %}">{% trans "Feedback received"%}</a></li>
<li class="sub-list"><a href="{% url "feedback_pending_plugins" %}">{% trans "Feedback pending"%}</a></li>
</ul>
<li class="staff"><a href="{% url "deprecated_plugins" %}">{% trans "Deprecated"%}</a></li>
{% endif %}
<li><a href="{% url "featured_plugins" %}">{% trans "Featured "%}</a></li>
Expand Down
3 changes: 3 additions & 0 deletions qgis-app/plugins/templates/plugins/plugin_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,9 @@ <h2>{{ object.name }}
{% if user.is_staff or user in version.plugin.approvers %}
{% if not version.approved %}<button class="btn btn-success btn-mini" type="submit" name="version_approve" id="version_approve"title="{% trans "Approve" %}"><i class="icon-thumbs-up icon-white"></i></button>{% else %}<button class="btn btn-warning btn-mini" type="submit" name="version_unapprove" id="version_unapprove" title="{% trans "Unapprove" %}"><i class="icon-thumbs-down icon-white"></i></button>{% endif %}
{% endif %}
<a class="btn {% if version.feedback.exists %}btn-warning{% else %}btn-primary{% endif %} btn-mini"
href="{% url "version_feedback" object.package_name version.version %}" title="{% trans "Feedback" %}"><i class="icon-comments icon-white"></i>
</a>
{% if user.is_staff or user in version.plugin.editors %}
<a class="btn btn-primary btn-mini" href="{% url "version_update" object.package_name version.version %}" title="{% trans "Edit" %}"><i class="icon-pencil icon-white"></i></a>&nbsp;<a class="btn btn-danger btn-mini delete" href="{% url "version_delete" object.package_name version.version %}" title="{% trans "Delete" %}"><i class="icon-remove icon-white"></i></a>{% endif %}</form>
</td>
Expand Down
170 changes: 170 additions & 0 deletions qgis-app/plugins/templates/plugins/plugin_feedback.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
{% extends 'plugins/plugin_base.html' %}{% load i18n %}
{% block content %}
{% if form.errors %}
<div class="alert alert-error">
<button type="button" class="close" data-dismiss="alert">&times;</button>
<p>{% trans "The form contains errors and cannot be submitted, please check the fields highlighted in red." %}</p>
</div>
{% endif %}
{% if form.non_field_errors %}
<div class="alert alert-error">
<button type="button" class="close" data-dismiss="alert">&times;</button>
{% for error in form.non_field_errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
<h2>{% trans "Feedback Plugin" %} {{ version.plugin.name }} {{ version.version }}</h2>
<div class="feedback-list">
<p>Please tick the checkbox when the task is completed and click the "Update" button to update status.</p>
{% for feedback in feedbacks %}
<div class=" previous-feedback {% if feedback.reviewer == request.user %}with-box{% endif %}" data-feedback-id="{{ feedback.id }}">
<input type="checkbox" class="statusCheckbox pull-left" name="statusCheckbox" data-feedback-id="{{ feedback.id }}" {% if feedback.is_completed %}checked disabled{% endif %}>
<label class="feedback">
{{ feedback.task }}
<span class="feedback-info"> &mdash;{% if feedback.reviewer.first_name %}{{ feedback.reviewer.first_name }} {{ feedback.reviewer.last_name }}{% else %}{{ feedback.reviewer.username }}{% endif %}
wrote {{ feedback.created_on|timesince }} ago
</span>
</label>
{% if feedback.reviewer == request.user %}
<button type="button" class="btn btn-danger btn-mini deleteButton pull-right" data-feedback-id="{{ feedback.id }}"><i class="icon-remove"></i></button>
{% endif %}
</div>
{% endfor %}
{% if feedbacks %}
<div class="text-center update-feedback">
<button type="button" id="updateButton" class="btn btn-primary">Update</button>
</div>
{% endif %}

{% if is_user_has_approval_rights %}
<div class="new-feedback">
<form method="post" action="{% url 'version_feedback' version.plugin.package_name version.version %}">{% csrf_token %}
<b>New Feedback</b>
{{ form.feedback }}
<div class="text-right">
<button class="btn btn-primary" type="submit">{% trans "Submit New Feedback" %}</button>
</div>
</form>
</div>
{% endif %}
</div>

{% endblock %}

{% block extrajs %}
<style>
.with-box {
border: 1px solid #e8e8e8;
padding: 5px;
border-radius: 5px;
margin-top: 5px;
}
label.feedback{
width: 90%;
display: inline-block;
vertical-align: top;
}
.feedback-info{
font-size: 0.75rem;
color: #8D8D8D;
white-space: nowrap;
}
.update-feedback {
margin-top: 10px;
}
.new-feedback{
padding: 5px;
border-radius: 5px;
margin-top: 20px;
margin-bottom: 5px;
}
input.statusCheckbox{
margin-right: 5px;
}
button#updateButton[disabled] {
background-color: #545454;
}
</style>

<script>
$(document).ready(function(){
const url = window.location.href;
// Disable submit button initially
$("#updateButton").prop("disabled", true);
// Handle checkbox change event
$(".statusCheckbox").change(function() {
// Check if any new checkbox (excluding disabled ones) is checked
const anyNewCheckboxChecked = $(".statusCheckbox:not(:disabled):checked").length > 0;
// Enable or disable the submit button based on new checkbox checked state
$("#updateButton").prop("disabled", !anyNewCheckboxChecked);
});

$('.deleteButton').on('click', function() {
const button = $(this);
const feedbackId = button.data('feedback-id');
const formData = {
'status_feedback': "deleted",
'csrfmiddlewaretoken': '{{ csrf_token }}'
};
deleteFeedback(feedbackId, formData);
});

$("#updateButton").on('click', function() {
let completedTasks = [];
$('.statusCheckbox:checked').each(function() {
const feedbackId = $(this).data('feedback-id');
completedTasks.push(feedbackId);
});
const formData = {
completed_tasks: completedTasks,
'csrfmiddlewaretoken': '{{ csrf_token }}'
};
updateStatus(formData);
});

function updateStatus(formData) {
const msg = "Update the task(s) as completed. You cannot revert the update. Please confirm."
if (confirm((msg))) {
$.ajax({
url: url + 'update/',
type: 'POST',
data: formData,
traditional: true,
success: function(response) {
if (response.success) {
$('.statusCheckbox:checked').each(function() {
$(this).prop('disabled', true);
});
$("#updateButton").prop("disabled", true);
}
},
error: function(xhr, status, error) {
console.error('Error updating status:', error);
}
});
}
}

function deleteFeedback(feedbackId, formData) {
const msg = "This task will be permanently deleted. Please confirm."
if (confirm(msg)) {
$.ajax({
type: 'POST',
url: url + feedbackId + '/',
data: formData,
success: function (response) {
if (response.success) {
const feedbackItem = $('.previous-feedback[data-feedback-id="' + feedbackId + '"]');
feedbackItem.remove();
}
},
error: function (xhr, textStatus, errorThrown) {
console.error('Error updating status:', errorThrown);
}
});
}
}
})
</script>
{% endblock %}
Loading

0 comments on commit c625132

Please sign in to comment.