plain.support 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plain_support-0.1.0/.gitignore +13 -0
- plain_support-0.1.0/PKG-INFO +20 -0
- plain_support-0.1.0/README.md +9 -0
- plain_support-0.1.0/plain/support/README.md +7 -0
- plain_support-0.1.0/plain/support/assets/support/embed.js +87 -0
- plain_support-0.1.0/plain/support/assets/support/iframe.js +28 -0
- plain_support-0.1.0/plain/support/config.py +6 -0
- plain_support-0.1.0/plain/support/default_settings.py +4 -0
- plain_support-0.1.0/plain/support/forms.py +66 -0
- plain_support-0.1.0/plain/support/migrations/0001_initial.py +43 -0
- plain_support-0.1.0/plain/support/migrations/0002_alter_supportformentry_options.py +16 -0
- plain_support-0.1.0/plain/support/migrations/__init__.py +0 -0
- plain_support-0.1.0/plain/support/models.py +25 -0
- plain_support-0.1.0/plain/support/staff.py +20 -0
- plain_support-0.1.0/plain/support/templates/mail/support_form_entry.html +1 -0
- plain_support-0.1.0/plain/support/templates/support/forms/default.html +37 -0
- plain_support-0.1.0/plain/support/templates/support/iframe.html +13 -0
- plain_support-0.1.0/plain/support/templates/support/page.html +11 -0
- plain_support-0.1.0/plain/support/templates/support/success/default.html +1 -0
- plain_support-0.1.0/plain/support/urls.py +12 -0
- plain_support-0.1.0/plain/support/views.py +59 -0
- plain_support-0.1.0/pyproject.toml +19 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plain.support
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Basic support tools for Plain.
|
|
5
|
+
Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: plain-auth<1.0.0
|
|
8
|
+
Requires-Dist: plain-models<1.0.0
|
|
9
|
+
Requires-Dist: plain<1.0.0
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
<!-- This file is compiled from plain-support/plain/support/README.md. Do not edit this file directly. -->
|
|
13
|
+
|
|
14
|
+
# plain-support
|
|
15
|
+
|
|
16
|
+
Captcha...
|
|
17
|
+
|
|
18
|
+
## Security considerations
|
|
19
|
+
|
|
20
|
+
Most support forms allow you to type in an email address. Be careful, because anybody can pretend to be anybody else at this point. Converations either need to continue over email (which confirms they have access to the email account), or include a verification step (emailing a code to the email address, for example).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<!-- This file is compiled from plain-support/plain/support/README.md. Do not edit this file directly. -->
|
|
2
|
+
|
|
3
|
+
# plain-support
|
|
4
|
+
|
|
5
|
+
Captcha...
|
|
6
|
+
|
|
7
|
+
## Security considerations
|
|
8
|
+
|
|
9
|
+
Most support forms allow you to type in an email address. Be careful, because anybody can pretend to be anybody else at this point. Converations either need to continue over email (which confirms they have access to the email account), or include a verification step (emailing a code to the email address, for example).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# plain-support
|
|
2
|
+
|
|
3
|
+
Captcha...
|
|
4
|
+
|
|
5
|
+
## Security considerations
|
|
6
|
+
|
|
7
|
+
Most support forms allow you to type in an email address. Be careful, because anybody can pretend to be anybody else at this point. Converations either need to continue over email (which confirms they have access to the email account), or include a verification step (emailing a code to the email address, for example).
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
var container = document.createElement('div');
|
|
2
|
+
document.currentScript.parentNode.insertBefore(container, document.currentScript);
|
|
3
|
+
|
|
4
|
+
// Build the iframe url based on the script src
|
|
5
|
+
// (replace the .js extension with /iframe/)
|
|
6
|
+
var src = document.currentScript.src;
|
|
7
|
+
var origin = new URL(src).origin;
|
|
8
|
+
var iframeSrc = src.replace(/\.js$/, '/iframe/');
|
|
9
|
+
|
|
10
|
+
var iframe = document.createElement('iframe');
|
|
11
|
+
iframe.src = iframeSrc;
|
|
12
|
+
iframe.width = "100%";
|
|
13
|
+
iframe.height = "auto";
|
|
14
|
+
iframe.style.border = "none";
|
|
15
|
+
iframe.style.display = 'none';
|
|
16
|
+
|
|
17
|
+
// Insert or select a loading div
|
|
18
|
+
var loading;
|
|
19
|
+
var loadingData = document.currentScript.getAttribute('data-loading');
|
|
20
|
+
if (!loadingData) {
|
|
21
|
+
var svg = '<svg width="40px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200"><circle fill="none" stroke-opacity="1" stroke="#000000" stroke-width=".5" cx="100" cy="100" r="0"><animate attributeName="r" calcMode="spline" dur="1" values="1;80" keyTimes="0;1" keySplines="0 .2 .5 1" repeatCount="indefinite"></animate><animate attributeName="stroke-width" calcMode="spline" dur="1" values="0;25" keyTimes="0;1" keySplines="0 .2 .5 1" repeatCount="indefinite"></animate><animate attributeName="stroke-opacity" calcMode="spline" dur="1" values="1;0" keyTimes="0;1" keySplines="0 .2 .5 1" repeatCount="indefinite"></animate></circle></svg>';
|
|
22
|
+
loading = document.createElement('div');
|
|
23
|
+
loading.style.display = 'flex';
|
|
24
|
+
loading.style.justifyContent = 'center';
|
|
25
|
+
loading.innerHTML = svg;
|
|
26
|
+
} else if (loadingData.startsWith("#")) {
|
|
27
|
+
loading = document.querySelector(loadingData);
|
|
28
|
+
} else if (!loading) {
|
|
29
|
+
loading = document.createElement('div');
|
|
30
|
+
loading.textContent = loadingData;
|
|
31
|
+
loading.style.color = "black";
|
|
32
|
+
}
|
|
33
|
+
container.appendChild(loading);
|
|
34
|
+
|
|
35
|
+
// Insert or select an error div
|
|
36
|
+
var error;
|
|
37
|
+
var errorData = document.currentScript.getAttribute('data-error');
|
|
38
|
+
if (!errorData) {
|
|
39
|
+
error = document.createElement('div');
|
|
40
|
+
error.textContent = "There was an error. Please email us directly.";
|
|
41
|
+
error.style.color = "black";
|
|
42
|
+
error.style.display = 'none';
|
|
43
|
+
} else if (errorData.startsWith("#")) {
|
|
44
|
+
error = document.querySelector(errorData);
|
|
45
|
+
error.style.display = 'none';
|
|
46
|
+
} else {
|
|
47
|
+
error = document.createElement('div');
|
|
48
|
+
error.textContent = errorData;
|
|
49
|
+
error.style.color = "black";
|
|
50
|
+
error.style.display = 'none';
|
|
51
|
+
}
|
|
52
|
+
container.appendChild(error);
|
|
53
|
+
|
|
54
|
+
// Keep track of whether the iframe has told us it is loaded
|
|
55
|
+
var iframeLoaded = false;
|
|
56
|
+
|
|
57
|
+
// Listen for postMessage events from the iframe
|
|
58
|
+
window.addEventListener('message', (event) => {
|
|
59
|
+
if (event.origin !== origin) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (event.data.type === 'setHeight') {
|
|
64
|
+
iframe.style.height = `${event.data.height}px`;
|
|
65
|
+
} else if (event.data.type === 'iframeLoaded') {
|
|
66
|
+
// The iframe has loaded, show it
|
|
67
|
+
iframe.style.display = 'block';
|
|
68
|
+
loading.style.display = 'none';
|
|
69
|
+
|
|
70
|
+
iframeLoaded = true;
|
|
71
|
+
|
|
72
|
+
error.style.display = 'none';
|
|
73
|
+
loading.style.display = 'none';
|
|
74
|
+
iframe.style.display = 'block';
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
container.appendChild(iframe);
|
|
79
|
+
|
|
80
|
+
// If the iframe hasn't send a loaded message after 5 seconds, render an error emssage
|
|
81
|
+
setTimeout(() => {
|
|
82
|
+
if (!iframeLoaded) {
|
|
83
|
+
error.style.display = 'block';
|
|
84
|
+
loading.style.display = 'none';
|
|
85
|
+
iframe.style.display = 'none';
|
|
86
|
+
}
|
|
87
|
+
}, 5000);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
2
|
+
function sendHeight(height) {
|
|
3
|
+
window.parent.postMessage({ type: 'setHeight', height: height }, "*");
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
let lastHeight = 0;
|
|
7
|
+
|
|
8
|
+
function calculateAndSendHeight() {
|
|
9
|
+
const height = document.documentElement.scrollHeight;
|
|
10
|
+
if (height !== lastHeight) {
|
|
11
|
+
lastHeight = height;
|
|
12
|
+
sendHeight(height);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Observe DOM changes
|
|
17
|
+
const observer = new MutationObserver(calculateAndSendHeight);
|
|
18
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
19
|
+
|
|
20
|
+
// Recalculate height on window resize
|
|
21
|
+
window.addEventListener('resize', calculateAndSendHeight);
|
|
22
|
+
|
|
23
|
+
// Send initial height
|
|
24
|
+
calculateAndSendHeight();
|
|
25
|
+
|
|
26
|
+
// Tell the embed.js that we loaded the iframe successfully (no other good way to do this)
|
|
27
|
+
window.parent.postMessage({ type: 'iframeLoaded' }, '*');
|
|
28
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from plain.auth import get_user_model
|
|
2
|
+
from plain.mail import TemplateEmail
|
|
3
|
+
from plain.models.forms import ModelForm
|
|
4
|
+
from plain.runtime import settings
|
|
5
|
+
|
|
6
|
+
from .models import SupportFormEntry
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SupportForm(ModelForm):
|
|
10
|
+
"""
|
|
11
|
+
The form is the customization point for users.
|
|
12
|
+
So any behavior modifications should be possible here.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
class Meta:
|
|
16
|
+
model = SupportFormEntry
|
|
17
|
+
fields = ["name", "email", "message"]
|
|
18
|
+
|
|
19
|
+
def __init__(self, user, form_slug, *args, **kwargs):
|
|
20
|
+
super().__init__(*args, **kwargs)
|
|
21
|
+
self.user = user # User provided directly by authed request
|
|
22
|
+
self.form_slug = form_slug
|
|
23
|
+
if self.user:
|
|
24
|
+
self.fields["email"].initial = user.email
|
|
25
|
+
|
|
26
|
+
def find_user(self):
|
|
27
|
+
# If the user isn't logged in (typical in an iframe, depending on session cookie settings),
|
|
28
|
+
# we can still try to look them up by email
|
|
29
|
+
# to associate the entry with them.
|
|
30
|
+
#
|
|
31
|
+
# Note that since they aren't logged in, this doesn't necessarily
|
|
32
|
+
# confirm that this wasn't an impersonation attempt.
|
|
33
|
+
# Subsequent conversations over email will confirm that they have access to the email.
|
|
34
|
+
email = self.cleaned_data.get("email")
|
|
35
|
+
if not email:
|
|
36
|
+
return None
|
|
37
|
+
UserModel = get_user_model()
|
|
38
|
+
try:
|
|
39
|
+
return UserModel.objects.get(email=email)
|
|
40
|
+
except UserModel.DoesNotExist:
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
def save(self, commit=True):
|
|
44
|
+
instance = super().save(commit=False)
|
|
45
|
+
instance.user = self.user or self.find_user()
|
|
46
|
+
instance.form_slug = self.form_slug
|
|
47
|
+
if commit:
|
|
48
|
+
instance.save()
|
|
49
|
+
return instance
|
|
50
|
+
|
|
51
|
+
def notify(self, instance):
|
|
52
|
+
"""
|
|
53
|
+
Notify the support team of a new support form entry.
|
|
54
|
+
|
|
55
|
+
Sends an immediate email by default.
|
|
56
|
+
"""
|
|
57
|
+
email = TemplateEmail(
|
|
58
|
+
template="support_form_entry",
|
|
59
|
+
subject=f"Support request from {instance.name}",
|
|
60
|
+
to=[settings.SUPPORT_EMAIL],
|
|
61
|
+
reply_to=[instance.email],
|
|
62
|
+
context={
|
|
63
|
+
"support_form_entry": instance,
|
|
64
|
+
},
|
|
65
|
+
)
|
|
66
|
+
email.send()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Generated by Plain 0.15.0 on 2025-01-07 18:45
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
import plain.models.deletion
|
|
6
|
+
from plain import models
|
|
7
|
+
from plain.models import migrations
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Migration(migrations.Migration):
|
|
11
|
+
initial = True
|
|
12
|
+
|
|
13
|
+
dependencies = [
|
|
14
|
+
("users", "0001_reset"),
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
operations = [
|
|
18
|
+
migrations.CreateModel(
|
|
19
|
+
name="SupportFormEntry",
|
|
20
|
+
fields=[
|
|
21
|
+
("id", models.BigAutoField(auto_created=True, primary_key=True)),
|
|
22
|
+
(
|
|
23
|
+
"uuid",
|
|
24
|
+
models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
|
|
25
|
+
),
|
|
26
|
+
("name", models.CharField(max_length=255)),
|
|
27
|
+
("email", models.EmailField(max_length=254)),
|
|
28
|
+
("message", models.TextField()),
|
|
29
|
+
("created_at", models.DateTimeField(auto_now_add=True)),
|
|
30
|
+
("form_slug", models.CharField(max_length=255)),
|
|
31
|
+
(
|
|
32
|
+
"user",
|
|
33
|
+
models.ForeignKey(
|
|
34
|
+
blank=True,
|
|
35
|
+
null=True,
|
|
36
|
+
on_delete=plain.models.deletion.SET_NULL,
|
|
37
|
+
related_name="support_form_entries",
|
|
38
|
+
to="users.user",
|
|
39
|
+
),
|
|
40
|
+
),
|
|
41
|
+
],
|
|
42
|
+
),
|
|
43
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Generated by Plain 0.15.0 on 2025-01-07 21:37
|
|
2
|
+
|
|
3
|
+
from plain.models import migrations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
dependencies = [
|
|
8
|
+
("plainsupport", "0001_initial"),
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
operations = [
|
|
12
|
+
migrations.AlterModelOptions(
|
|
13
|
+
name="supportformentry",
|
|
14
|
+
options={"ordering": ["-created_at"]},
|
|
15
|
+
),
|
|
16
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
from plain import models
|
|
4
|
+
from plain.runtime import settings
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SupportFormEntry(models.Model):
|
|
8
|
+
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
|
|
9
|
+
user = models.ForeignKey(
|
|
10
|
+
settings.AUTH_USER_MODEL,
|
|
11
|
+
on_delete=models.SET_NULL,
|
|
12
|
+
related_name="support_form_entries",
|
|
13
|
+
null=True,
|
|
14
|
+
blank=True,
|
|
15
|
+
)
|
|
16
|
+
name = models.CharField(max_length=255)
|
|
17
|
+
email = models.EmailField()
|
|
18
|
+
message = models.TextField()
|
|
19
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
20
|
+
form_slug = models.CharField(max_length=255)
|
|
21
|
+
# referrer? source? session?
|
|
22
|
+
# extra_data
|
|
23
|
+
|
|
24
|
+
class Meta:
|
|
25
|
+
ordering = ["-created_at"]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from plain.staff.views import (
|
|
2
|
+
StaffModelDetailView,
|
|
3
|
+
StaffModelListView,
|
|
4
|
+
StaffModelViewset,
|
|
5
|
+
register_viewset,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
from .models import SupportFormEntry
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@register_viewset
|
|
12
|
+
class PageviewStaff(StaffModelViewset):
|
|
13
|
+
class ListView(StaffModelListView):
|
|
14
|
+
model = SupportFormEntry
|
|
15
|
+
nav_section = "Support"
|
|
16
|
+
title = "Form entries"
|
|
17
|
+
fields = ["user", "email", "name", "form_slug", "created_at"]
|
|
18
|
+
|
|
19
|
+
class DetailView(StaffModelDetailView):
|
|
20
|
+
model = SupportFormEntry
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div style="white-space: pre-line;">{{ support_form_entry.message }}</div>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<div class="flex justify-center p-2">
|
|
2
|
+
<form class="max-w-2xl w-full" method="post" action="{{ form_action }}">
|
|
3
|
+
{{ csrf_input }}
|
|
4
|
+
<div class="flex space-x-4">
|
|
5
|
+
<div class="flex-1 space-y-1">
|
|
6
|
+
<label class="font-medium" for="{{ form.name.html_id }}">Name</label>
|
|
7
|
+
<input
|
|
8
|
+
id="{{ form.name.html_id }}"
|
|
9
|
+
class="block w-full rounded-sm text-black border-gray-300 border"
|
|
10
|
+
required
|
|
11
|
+
type="text"
|
|
12
|
+
name="{{ form.name.html_name }}"
|
|
13
|
+
value="{{ form.name.value() or '' }}">
|
|
14
|
+
</div>
|
|
15
|
+
<div class="flex-1 space-y-1">
|
|
16
|
+
<label class="font-medium" for="{{ form.email.html_id }}">Email</label>
|
|
17
|
+
<input
|
|
18
|
+
id="{{ form.email.html_id }}"
|
|
19
|
+
class="block w-full rounded-sm text-black border-gray-300 border"
|
|
20
|
+
required
|
|
21
|
+
type="email"
|
|
22
|
+
name="{{ form.email.html_name }}"
|
|
23
|
+
value="{{ form.email.value() or '' }}">
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="mt-4 space-y-1">
|
|
27
|
+
<label class="font-medium" for="{{ form.message.html_id }}">Message</label>
|
|
28
|
+
<textarea
|
|
29
|
+
id="{{ form.message.html_id }}"
|
|
30
|
+
class="block w-full text-black rounded-sm border-gray-300 border"
|
|
31
|
+
rows="10"
|
|
32
|
+
required
|
|
33
|
+
name="{{ form.message.html_name }}">{{ form.message.value() or '' }}</textarea>
|
|
34
|
+
</div>
|
|
35
|
+
<button class="mt-8 text-lg text-white block text-center bg-black hover:bg-black/80 w-full py-2 px-3 rounded-sm" type="submit">Submit</button>
|
|
36
|
+
</form>
|
|
37
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<html>
|
|
2
|
+
<head>
|
|
3
|
+
{% tailwind_css %}
|
|
4
|
+
<script src="{{ asset('support/iframe.js') }}"></script>
|
|
5
|
+
</head>
|
|
6
|
+
<body>
|
|
7
|
+
{% if success %}
|
|
8
|
+
{% include success_template_name %}
|
|
9
|
+
{% else %}
|
|
10
|
+
{% include form_template_name %}
|
|
11
|
+
{% endif %}
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div class="text-center">Your message has been sent.</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from plain.urls import path
|
|
2
|
+
|
|
3
|
+
from . import views
|
|
4
|
+
|
|
5
|
+
default_namespace = "support"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
urlpatterns = [
|
|
9
|
+
path("form/<slug:form_slug>.js", views.SupportFormJSView),
|
|
10
|
+
path("form/<slug:form_slug>/iframe/", views.SupportIFrameView, name="iframe"),
|
|
11
|
+
path("form/<slug:form_slug>/", views.SupportFormView, name="form"),
|
|
12
|
+
]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from plain.assets.urls import get_asset_url
|
|
2
|
+
from plain.http import ResponseRedirect
|
|
3
|
+
from plain.runtime import settings
|
|
4
|
+
from plain.utils.module_loading import import_string
|
|
5
|
+
from plain.views import FormView, View
|
|
6
|
+
from plain.views.csrf import CsrfExemptViewMixin
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SupportFormView(FormView):
|
|
10
|
+
template_name = "support/page.html"
|
|
11
|
+
|
|
12
|
+
def get_form(self):
|
|
13
|
+
form_slug = self.url_kwargs["form_slug"]
|
|
14
|
+
form_class = import_string(settings.SUPPORT_FORMS[form_slug])
|
|
15
|
+
return form_class(**self.get_form_kwargs())
|
|
16
|
+
|
|
17
|
+
def get_template_context(self):
|
|
18
|
+
context = super().get_template_context()
|
|
19
|
+
form_slug = self.url_kwargs["form_slug"]
|
|
20
|
+
context["form_action"] = self.request.build_absolute_uri()
|
|
21
|
+
context["form_template_name"] = f"support/forms/{form_slug}.html"
|
|
22
|
+
context["success_template_name"] = f"support/success/{form_slug}.html"
|
|
23
|
+
context["success"] = self.request.GET.get("success") == "true"
|
|
24
|
+
return context
|
|
25
|
+
|
|
26
|
+
def get_form_kwargs(self):
|
|
27
|
+
kwargs = super().get_form_kwargs()
|
|
28
|
+
kwargs["user"] = self.request.user
|
|
29
|
+
kwargs["form_slug"] = self.url_kwargs["form_slug"]
|
|
30
|
+
return kwargs
|
|
31
|
+
|
|
32
|
+
def form_valid(self, form):
|
|
33
|
+
entry = form.save()
|
|
34
|
+
form.notify(entry)
|
|
35
|
+
return super().form_valid(form)
|
|
36
|
+
|
|
37
|
+
def get_success_url(self, form):
|
|
38
|
+
# Redirect to the same view and template so we
|
|
39
|
+
# don't have to create two additional views for iframe and non-iframe.
|
|
40
|
+
return "?success=true"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SupportIFrameView(CsrfExemptViewMixin, SupportFormView):
|
|
44
|
+
template_name = "support/iframe.html"
|
|
45
|
+
|
|
46
|
+
def get_response(self):
|
|
47
|
+
response = super().get_response()
|
|
48
|
+
|
|
49
|
+
# X-Frame-Options are typically in DEFAULT_RESPONSE_HEADERS,
|
|
50
|
+
# which will know to drop the header completely if an empty string.
|
|
51
|
+
# We can't del/pop it because DEFAULT_RESPONSE_HEADERS may add it back.
|
|
52
|
+
response.headers["X-Frame-Options"] = ""
|
|
53
|
+
|
|
54
|
+
return response
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class SupportFormJSView(View):
|
|
58
|
+
def get(self):
|
|
59
|
+
return ResponseRedirect(get_asset_url("support/embed.js"))
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "plain.support"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Basic support tools for Plain."
|
|
5
|
+
authors = [{name = "Dave Gaeddert", email = "dave.gaeddert@dropseed.dev"}]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"plain<1.0.0",
|
|
10
|
+
"plain.models<1.0.0",
|
|
11
|
+
"plain.auth<1.0.0",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[tool.hatch.build.targets.wheel]
|
|
15
|
+
packages = ["plain"]
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["hatchling"]
|
|
19
|
+
build-backend = "hatchling.build"
|