feat: support app
ci/woodpecker/push/woodpecker Pipeline was successful Details

Summary
    Support app shows links to user to create new issue on the Hostea
    meta repository(configurable via settings.py) and view open issues.

    (auto)Redirection via dashboard to new issue page on Gitea and issue
    tracker repository is setup. In future, a form will be exposed
    within the dashboard itself to streamline support related workflows.
wip-payments
Aravinth Manivannan 2022-06-17 18:12:02 +05:30
parent d2fc48f399
commit 328b44e729
Signed by: realaravinth
GPG Key ID: AD9F0F08E855ED88
15 changed files with 358 additions and 0 deletions

View File

@ -43,6 +43,7 @@ INSTALLED_APPS = [
"django.contrib.staticfiles",
"accounts",
"dash",
"support",
]
MIDDLEWARE = [
@ -136,6 +137,11 @@ HOSTEA = {
"RESTRICT_NEW_INTEGRATION_INSTALLATION": True,
"INSTANCE_MAINTAINER_CONTACT": "contact@hostea.example.org",
"ACCOUNTS": {"MAX_VERIFICATION_TOLERANCE_PERIOD": 60 * 60 * 24}, # in seconds
"META": {
"GITEA_INSTANCE": "https://gitea.hostea.org",
"GITEA_ORG_NAME": "Hostea",
"SUPPORT_REPOSITORY": "support",
},
}
EMAIL_CONFIG = env.email("EMAIL_URL", default="smtp://admin:password@localhost:10025")

View File

@ -19,5 +19,6 @@ from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("dash/", include("dash.urls")),
path("support/", include("support.urls")),
path("", include("accounts.urls")),
]

0
support/__init__.py Normal file
View File

3
support/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
support/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class SupportConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "support"

View File

3
support/models.py Normal file
View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View File

@ -0,0 +1,6 @@
{% extends 'dash/common/base.html' %}
{% block dash %}
{% include "common/components/error.html" %}
<h2>{{ title }}</h2>
{% include "common/components/error.html" %}
{% endblock %}

View File

@ -0,0 +1,9 @@
{% extends 'dash/common/base.html' %} {% block dash %}
<h2>{{ title }}</h2>
<p>
You will be redirected to Hostea's issue tracker
<span id="timer">momentarily</span>. If not, please click
<a id="redirect-url" href="{{ support.list }}">here.</a>
</p>
{% include "support/redirect.html" %} {% endblock %}

View File

@ -0,0 +1,9 @@
{% extends 'dash/common/base.html' %} {% block dash %}
<h2>{{ title }}</h2>
<p>
You will be redirected to Hostea's issue tracker
<span id="timer">momentarily</span>. If not, please click
<a id="redirect-url" href="{{ support.new }}">here.</a>
</p>
{% include "support/redirect.html" %} {% endblock %}

View File

@ -0,0 +1,38 @@
<script>
const redirectElement = document.getElementById("redirect-url");
const timerElement = document.getElementById("timer");
let timerHandler = null;
const duration = 5;
if (redirectElement) {
if (redirectElement.href) {
console.log("found redirection URI");
if (timerElement) {
const setTime = (time) => {
let text = `in ${time} seconds`;
timerElement.innerText = text;
};
setTime(duration);
const decr = () => {
let timer = Number.parseInt(timerElement.innerText.split(" ")[1]);
timer -= 1;
if (timer < 0) {
timer = 0;
}
setTime(timer);
};
timerHandler = setInterval(decr, 1000);
}
window.setTimeout(() => {
if (timerHandler) {
clearInterval(timerHandler);
}
window.location.assign(redirectElement.href);
}, duration * 1000);
}
}
</script>

138
support/tests.py Normal file
View File

@ -0,0 +1,138 @@
# Copyright © 2022 Aravinth Manivannan <realaravinth@batsense.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.contrib.auth import get_user_model
from django.utils.http import urlencode
from django.urls import reverse
from django.test import TestCase, Client, override_settings
from django.contrib.auth import authenticate
from django.conf import settings
from .utils import IssueTracker
hostea_issue_tracker_settings = settings.HOSTEA
hostea_issue_tracker_settings["META"] = {
"GITEA_INSTANCE": "https://gitea.hostea.org",
"GITEA_ORG_NAME": "Hostea",
"SUPPORT_REPOSITORY": "support",
}
@override_settings(HOSTEA=hostea_issue_tracker_settings)
class IssueTrackerTests(TestCase):
"""
Test IssueTracker utility
"""
def test_defaults(self):
"""
Verify default credentials; all further tests are based on defaults set
"""
it = IssueTracker()
self.assertEqual(it.config["GITEA_INSTANCE"], "https://gitea.hostea.org")
self.assertEqual(it.config["GITEA_ORG_NAME"], "Hostea")
self.assertEqual(it.config["SUPPORT_REPOSITORY"], "support")
def test_uri_builders(self):
"""
Verify default credentials; all further tests are based on defaults set
"""
it = IssueTracker()
self.assertEqual(
it.get_issue_tracker(), "https://gitea.hostea.org/Hostea/support/issues"
)
self.assertEqual(
it.open_issue(), "https://gitea.hostea.org/Hostea/support/issues/new"
)
class SupportWorks(TestCase):
"""
Tests create new app view
"""
def setUp(self):
self.password = "password121231"
self.username = "suport_user"
self.email = f"{self.username}@example.org"
self.user = get_user_model().objects.create(
username=self.username,
email=self.email,
)
self.user.set_password(self.password)
self.user.save()
def test_dash_is_protected(self):
"""
Tests if support templates render
"""
# default LOGIN redirect URI that is used by @login_required decorator is
# /accounts/login. There's a redirection endpoint at /accounts/login/ that
# will redirect user to /login. Hence the /accounts prefix
def redirect_login_uri(uri: str) -> str:
return f"/accounts{reverse('accounts.login')}?next={uri}"
urls = [
reverse("support.home"),
reverse("support.new"),
reverse("support.view"),
]
for i in urls:
print(f"[*] Testing URI: {i}")
resp = self.client.get(i)
self.assertEqual(resp.status_code, 302)
expected = redirect_login_uri(i)
self.assertEqual(resp.headers["location"], expected)
def test_dash_home_renders(self):
"""
Tests if login template renders
"""
c = Client()
# username login works
payload = {
"login": self.username,
"password": self.password,
}
resp = c.post(reverse("accounts.login"), payload)
self.assertEqual(resp.status_code, 302)
self.assertEqual(resp.headers["location"], reverse("accounts.home"))
urls = [
reverse("support.home"),
reverse("support.new"),
reverse("support.view"),
]
for i in urls:
print(f"[*] Testing URI: {i}")
resp = c.get(i)
self.assertEqual(resp.status_code, 200)
self.assertEqual(b"Billing" in resp.content, True)
self.assertEqual(b"Support" in resp.content, True)
self.assertEqual(b"Logout" in resp.content, True)
# new issue view
resp = c.get(reverse("support.new"))
self.assertEqual(resp.status_code, 200)
it = IssueTracker()
new_issue = str.encode(it.open_issue())
self.assertEqual(new_issue in resp.content, True)
# list issues view
resp = c.get(reverse("support.new"))
self.assertEqual(resp.status_code, 200)
issue_tracker = str.encode(it.get_issue_tracker())
self.assertEqual(issue_tracker in resp.content, True)

24
support/urls.py Normal file
View File

@ -0,0 +1,24 @@
# Copyright © 2022 Aravinth Manivannan <realaravinth@batsense.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.contrib import admin
from django.urls import path, include
from .views import home, new_ticket, view_tickets
urlpatterns = [
path("new/", new_ticket, name="support.new"),
path("view/", view_tickets, name="support.view"),
path("", home, name="support.home"),
]

48
support/utils.py Normal file
View File

@ -0,0 +1,48 @@
# Copyright © 2022 Aravinth Manivannan <realaravinth@batsense.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from urllib.parse import urlparse, urlunparse
from django.conf import settings
class IssueTracker:
"""
Hostea support repository Issue tracker URL generation stuff
"""
def __init__(self):
self.config = settings.HOSTEA["META"]
self.instance = urlparse(self.config["GITEA_INSTANCE"])
self.repo = (
f"{self.config['GITEA_ORG_NAME']}/{self.config['SUPPORT_REPOSITORY']}"
)
self.issues = f"{self.repo}/issues"
def __path(self, path=str):
i = self.instance
return urlunparse((i.scheme, i.netloc, path, "", "", ""))
def get_issue_tracker(self):
"""
Get issue tracker URL
"""
return self.__path(path=self.issues)
def open_issue(self):
"""
Get open new issue URL
"""
path = f"{self.issues}/new"
return self.__path(path=path)

67
support/views.py Normal file
View File

@ -0,0 +1,67 @@
# Copyright © 2022 Aravinth Manivannan <realaravinth@batsense.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from .utils import IssueTracker
def default_ctx(title: str, username: str):
"""
Default context for all dashboard pages
"""
it = IssueTracker()
return {
"title": title,
"username": username,
"open_support": "open",
"support": {"list": it.get_issue_tracker(), "new": it.open_issue()},
}
@login_required
def home(request):
"""
Support page view
"""
PAGE_TITLE = "Support"
username = request.user
ctx = default_ctx(title=PAGE_TITLE, username=username.username)
return render(request, "support/index.html", context=ctx)
@login_required
def new_ticket(request):
"""
Support page view
"""
PAGE_TITLE = "New Ticket"
username = request.user
it = IssueTracker()
ctx = default_ctx(title=PAGE_TITLE, username=username.username)
return render(request, "support/new.html", context=ctx)
@login_required
def view_tickets(request):
"""
Support page view
"""
PAGE_TITLE = "Opened Tickets"
username = request.user
it = IssueTracker()
ctx = default_ctx(title=PAGE_TITLE, username=username.username)
ctx["support"] = {"list": it.get_issue_tracker(), "new": it.open_issue()}
return render(request, "support/list.html", context=ctx)