feat: support app
ci/woodpecker/push/woodpecker Pipeline was successful
Details
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
parent
d2fc48f399
commit
328b44e729
|
@ -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")
|
||||
|
|
|
@ -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,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SupportConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "support"
|
|
@ -0,0 +1,3 @@
|
|||
from django.db import models
|
||||
|
||||
# Create your models here.
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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>
|
|
@ -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)
|
|
@ -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"),
|
||||
]
|
|
@ -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)
|
|
@ -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)
|
Loading…
Reference in New Issue