mirror of
https://github.com/jointakahe/takahe.git
synced 2025-01-22 12:28:06 +00:00
Even more refactor
This commit is contained in:
parent
0225c6e8ba
commit
ef08013541
47 changed files with 566 additions and 690 deletions
24
activities/migrations/0014_post_content_vector_gin.py
Normal file
24
activities/migrations/0014_post_content_vector_gin.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 4.2 on 2023-04-29 18:49
|
||||
|
||||
import django.contrib.postgres.indexes
|
||||
import django.contrib.postgres.search
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("activities", "0013_postattachment_author"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name="post",
|
||||
index=django.contrib.postgres.indexes.GinIndex(
|
||||
django.contrib.postgres.search.SearchVector(
|
||||
"content", config="english"
|
||||
),
|
||||
name="content_vector_gin",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -11,6 +11,7 @@ import httpx
|
|||
import urlman
|
||||
from asgiref.sync import async_to_sync, sync_to_async
|
||||
from django.contrib.postgres.indexes import GinIndex
|
||||
from django.contrib.postgres.search import SearchVector
|
||||
from django.db import models, transaction
|
||||
from django.template import loader
|
||||
from django.template.defaultfilters import linebreaks_filter
|
||||
|
@ -312,6 +313,10 @@ class Post(StatorModel):
|
|||
class Meta:
|
||||
indexes = [
|
||||
GinIndex(fields=["hashtags"], name="hashtags_gin"),
|
||||
GinIndex(
|
||||
SearchVector("content", config="english"),
|
||||
name="content_vector_gin",
|
||||
),
|
||||
models.Index(
|
||||
fields=["visibility", "local", "published"],
|
||||
name="ix_post_local_public_published",
|
||||
|
|
|
@ -123,6 +123,12 @@ class SearchService:
|
|||
results.add(hashtag)
|
||||
return results
|
||||
|
||||
def search_post_content(self):
|
||||
"""
|
||||
Searches for posts on an identity via full text search
|
||||
"""
|
||||
return self.identity.posts.filter(content__search=self.query)[:50]
|
||||
|
||||
def search_all(self):
|
||||
"""
|
||||
Returns all possible results for a search
|
||||
|
|
|
@ -122,12 +122,7 @@ class Compose(FormView):
|
|||
] = self.identity.config_identity.default_post_visibility
|
||||
if self.reply_to:
|
||||
initial["reply_to"] = self.reply_to.pk
|
||||
if self.reply_to.visibility == Post.Visibilities.public:
|
||||
initial[
|
||||
"visibility"
|
||||
] = self.identity.config_identity.default_reply_visibility
|
||||
else:
|
||||
initial["visibility"] = self.reply_to.visibility
|
||||
initial["visibility"] = self.reply_to.visibility
|
||||
initial["content_warning"] = self.reply_to.summary
|
||||
# Build a set of mentions for the content to start as
|
||||
mentioned = {self.reply_to.author}
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
from django import forms
|
||||
from django.views.generic import FormView
|
||||
|
||||
from activities.services import SearchService
|
||||
|
||||
|
||||
class Search(FormView):
|
||||
|
||||
template_name = "activities/search.html"
|
||||
|
||||
class form_class(forms.Form):
|
||||
query = forms.CharField(
|
||||
help_text="Search for:\nA user by @username@domain or their profile URL\nA hashtag by #tagname\nA post by its URL",
|
||||
widget=forms.TextInput(attrs={"type": "search", "autofocus": "autofocus"}),
|
||||
)
|
||||
|
||||
def form_valid(self, form):
|
||||
searcher = SearchService(form.cleaned_data["query"], identity=None)
|
||||
# Render results
|
||||
context = self.get_context_data(form=form)
|
||||
context["results"] = searcher.search_all()
|
||||
return self.render_to_response(context)
|
36
core/migrations/0002_domain_config.py
Normal file
36
core/migrations/0002_domain_config.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
# Generated by Django 4.2 on 2023-04-29 18:49
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("users", "0016_hashtagfollow"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("core", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name="config",
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="config",
|
||||
name="domain",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="configs",
|
||||
to="users.domain",
|
||||
),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="config",
|
||||
unique_together={("key", "user", "identity", "domain")},
|
||||
),
|
||||
]
|
|
@ -43,6 +43,14 @@ class Config(models.Model):
|
|||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
domain = models.ForeignKey(
|
||||
"users.domain",
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name="configs",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
json = models.JSONField(blank=True, null=True)
|
||||
image = models.ImageField(
|
||||
blank=True,
|
||||
|
@ -52,7 +60,7 @@ class Config(models.Model):
|
|||
|
||||
class Meta:
|
||||
unique_together = [
|
||||
("key", "user", "identity"),
|
||||
("key", "user", "identity", "domain"),
|
||||
]
|
||||
|
||||
system: ClassVar["Config.ConfigOptions"] # type: ignore
|
||||
|
@ -86,7 +94,7 @@ class Config(models.Model):
|
|||
"""
|
||||
return cls.load_values(
|
||||
cls.SystemOptions,
|
||||
{"identity__isnull": True, "user__isnull": True},
|
||||
{"identity__isnull": True, "user__isnull": True, "domain__isnull": True},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
@ -96,7 +104,7 @@ class Config(models.Model):
|
|||
"""
|
||||
return await sync_to_async(cls.load_values)(
|
||||
cls.SystemOptions,
|
||||
{"identity__isnull": True, "user__isnull": True},
|
||||
{"identity__isnull": True, "user__isnull": True, "domain__isnull": True},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
@ -106,7 +114,7 @@ class Config(models.Model):
|
|||
"""
|
||||
return cls.load_values(
|
||||
cls.UserOptions,
|
||||
{"identity__isnull": True, "user": user},
|
||||
{"identity__isnull": True, "user": user, "domain__isnull": True},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
@ -116,7 +124,7 @@ class Config(models.Model):
|
|||
"""
|
||||
return await sync_to_async(cls.load_values)(
|
||||
cls.UserOptions,
|
||||
{"identity__isnull": True, "user": user},
|
||||
{"identity__isnull": True, "user": user, "domain__isnull": True},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
@ -126,7 +134,7 @@ class Config(models.Model):
|
|||
"""
|
||||
return cls.load_values(
|
||||
cls.IdentityOptions,
|
||||
{"identity": identity, "user__isnull": True},
|
||||
{"identity": identity, "user__isnull": True, "domain__isnull": True},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
@ -136,7 +144,27 @@ class Config(models.Model):
|
|||
"""
|
||||
return await sync_to_async(cls.load_values)(
|
||||
cls.IdentityOptions,
|
||||
{"identity": identity, "user__isnull": True},
|
||||
{"identity": identity, "user__isnull": True, "domain__isnull": True},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def load_domain(cls, domain):
|
||||
"""
|
||||
Loads an domain config options object
|
||||
"""
|
||||
return cls.load_values(
|
||||
cls.DomainOptions,
|
||||
{"domain": domain, "user__isnull": True, "identity__isnull": True},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def aload_domain(cls, domain):
|
||||
"""
|
||||
Async loads an domain config options object
|
||||
"""
|
||||
return await sync_to_async(cls.load_values)(
|
||||
cls.DomainOptions,
|
||||
{"domain": domain, "user__isnull": True, "identity__isnull": True},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
@ -170,7 +198,7 @@ class Config(models.Model):
|
|||
key,
|
||||
value,
|
||||
cls.SystemOptions,
|
||||
{"identity__isnull": True, "user__isnull": True},
|
||||
{"identity__isnull": True, "user__isnull": True, "domain__isnull": True},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
@ -179,7 +207,7 @@ class Config(models.Model):
|
|||
key,
|
||||
value,
|
||||
cls.UserOptions,
|
||||
{"identity__isnull": True, "user": user},
|
||||
{"identity__isnull": True, "user": user, "domain__isnull": True},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
@ -188,7 +216,16 @@ class Config(models.Model):
|
|||
key,
|
||||
value,
|
||||
cls.IdentityOptions,
|
||||
{"identity": identity, "user__isnull": True},
|
||||
{"identity": identity, "user__isnull": True, "domain__isnull": True},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def set_domain(cls, domain, key, value):
|
||||
cls.set_value(
|
||||
key,
|
||||
value,
|
||||
cls.DomainOptions,
|
||||
{"domain": domain, "user__isnull": True, "identity__isnull": True},
|
||||
)
|
||||
|
||||
class SystemOptions(pydantic.BaseModel):
|
||||
|
@ -239,20 +276,22 @@ class Config(models.Model):
|
|||
custom_head: str | None
|
||||
|
||||
class UserOptions(pydantic.BaseModel):
|
||||
|
||||
pass
|
||||
light_theme: bool = False
|
||||
|
||||
class IdentityOptions(pydantic.BaseModel):
|
||||
|
||||
toot_mode: bool = False
|
||||
default_post_visibility: int = 0 # Post.Visibilities.public
|
||||
default_reply_visibility: int = 1 # Post.Visibilities.unlisted
|
||||
visible_follows: bool = True
|
||||
light_theme: bool = False
|
||||
search_enabled: bool = True
|
||||
|
||||
# wellness Options
|
||||
# Wellness Options
|
||||
visible_reaction_counts: bool = True
|
||||
expand_linked_cws: bool = True
|
||||
infinite_scroll: bool = True
|
||||
|
||||
custom_css: str | None
|
||||
class DomainOptions(pydantic.BaseModel):
|
||||
|
||||
site_name: str = ""
|
||||
site_icon: UploadedImage | None = None # type: ignore
|
||||
hide_login: bool = False
|
||||
custom_css: str = ""
|
||||
|
|
|
@ -82,13 +82,14 @@ td a {
|
|||
/* Base template styling */
|
||||
|
||||
:root {
|
||||
--color-highlight: #449c8c;
|
||||
|
||||
--color-bg-main: #26323c;
|
||||
--color-bg-menu: #2e3e4c;
|
||||
--color-bg-box: #1a2631;
|
||||
--color-bg-error: rgb(87, 32, 32);
|
||||
--color-highlight: #449c8c;
|
||||
--color-delete: #8b2821;
|
||||
--color-header-menu: rgba(0, 0, 0, 0.5);
|
||||
--color-header-menu: rgba(255, 255, 255, 0.8);
|
||||
--color-main-shadow: rgba(0, 0, 0, 0.6);
|
||||
--size-main-shadow: 50px;
|
||||
|
||||
|
@ -96,40 +97,36 @@ td a {
|
|||
--color-text-dull: #99a;
|
||||
--color-text-main: #fff;
|
||||
--color-text-link: var(--color-highlight);
|
||||
--color-text-in-highlight: #fff;
|
||||
--color-text-in-highlight: var(--color-text-main);
|
||||
|
||||
--color-input-background: #26323c;
|
||||
--color-input-background: var(--color-bg-main);
|
||||
--color-input-border: #000;
|
||||
--color-input-border-active: #444b5d;
|
||||
--color-button-secondary: #2e3e4c;
|
||||
--color-button-disabled: #7c9c97;
|
||||
|
||||
--sm-header-height: 50px;
|
||||
--sm-sidebar-width: 50px;
|
||||
|
||||
--md-sidebar-width: 250px;
|
||||
--md-header-height: 50px;
|
||||
--width-sidebar-small: 200px;
|
||||
--width-sidebar-medium: 250px;
|
||||
|
||||
--emoji-height: 1.1em;
|
||||
}
|
||||
|
||||
body.light-theme {
|
||||
--color-bg-main: #dfe3e7;
|
||||
--color-bg-menu: #cfd6dd;
|
||||
--color-bg-box: #f2f5f8;
|
||||
body.theme-light {
|
||||
--color-bg-main: #d4dee7;
|
||||
--color-bg-menu: #c0ccd8;
|
||||
--color-bg-box: #f0f3f5;
|
||||
--color-bg-error: rgb(219, 144, 144);
|
||||
--color-highlight: #449c8c;
|
||||
--color-delete: #884743;
|
||||
--color-header-menu: rgba(0, 0, 0, 0.1);
|
||||
--color-header-menu: rgba(0, 0, 0, 0.7);
|
||||
--color-main-shadow: rgba(0, 0, 0, 0.1);
|
||||
--size-main-shadow: 20px;
|
||||
|
||||
--color-text-duller: #3a3b3d;
|
||||
--color-text-dull: rgb(44, 44, 48);
|
||||
--color-text-duller: #4f5157;
|
||||
--color-text-dull: rgb(62, 62, 68);
|
||||
--color-text-main: rgb(0, 0, 0);
|
||||
--color-text-link: var(--color-highlight);
|
||||
|
||||
--color-input-background: #fff;
|
||||
--color-input-background: var(--color-bg-main);
|
||||
--color-input-border: rgb(109, 109, 109);
|
||||
--color-input-border-active: #464646;
|
||||
--color-button-secondary: #5c6770;
|
||||
|
@ -145,12 +142,16 @@ body {
|
|||
}
|
||||
|
||||
main {
|
||||
width: 1100px;
|
||||
width: 800px;
|
||||
margin: 20px auto;
|
||||
box-shadow: none;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
body.wide main {
|
||||
width: 1100px;
|
||||
}
|
||||
|
||||
footer {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
|
@ -168,7 +169,7 @@ footer a {
|
|||
header {
|
||||
display: flex;
|
||||
height: 42px;
|
||||
margin-top: -20px;
|
||||
margin: -20px 0 20px 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
@ -209,7 +210,7 @@ header menu {
|
|||
|
||||
header menu a {
|
||||
padding: 6px 10px 4px 10px;
|
||||
color: var(--color-text-main);
|
||||
color: var(--color-header-menu);
|
||||
line-height: 30px;
|
||||
border-bottom: 3px solid rgba(0, 0, 0, 0);
|
||||
margin: 0 2px;
|
||||
|
@ -374,7 +375,7 @@ nav .identity-banner a:hover {
|
|||
}
|
||||
|
||||
.settings nav {
|
||||
width: var(--md-sidebar-width);
|
||||
width: var(--width-sidebar-medium);
|
||||
background: var(--color-bg-menu);
|
||||
border-radius: 0 0 5px 0;
|
||||
}
|
||||
|
@ -400,7 +401,6 @@ img.emoji {
|
|||
/* Generic styling and sections */
|
||||
|
||||
section {
|
||||
max-width: 700px;
|
||||
background: var(--color-bg-box);
|
||||
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1);
|
||||
margin: 0 auto 45px auto;
|
||||
|
@ -742,24 +742,6 @@ div.follow {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
div.follow-hashtag {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.follow.has-reverse {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.follow .reverse-follow {
|
||||
display: block;
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
|
||||
div.follow button,
|
||||
div.follow .button {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
div.follow .actions {
|
||||
/* display: flex; */
|
||||
position: relative;
|
||||
|
@ -769,69 +751,8 @@ div.follow .actions {
|
|||
align-content: center;
|
||||
}
|
||||
|
||||
div.follow .actions a {
|
||||
border-radius: 4px;
|
||||
min-width: 40px;
|
||||
.follow .actions button {
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
div.follow .actions menu {
|
||||
display: none;
|
||||
background-color: var(--color-bg-menu);
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 43px;
|
||||
}
|
||||
|
||||
|
||||
div.follow .actions menu.enabled {
|
||||
display: block;
|
||||
min-width: 160px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
div.follow .actions menu a {
|
||||
text-align: left;
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
padding: 4px 10px;
|
||||
color: var(--color-text-dull);
|
||||
}
|
||||
|
||||
.follow .actions menu button {
|
||||
background: none !important;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
padding: 4px 10px;
|
||||
color: var(--color-text-dull);
|
||||
}
|
||||
|
||||
.follow .actions menu button i {
|
||||
margin-right: 4px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.follow .actions button:hover {
|
||||
color: var(--color-text-main);
|
||||
}
|
||||
|
||||
.follow .actions menu a i {
|
||||
margin-right: 4px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
div.follow .actions a:hover {
|
||||
color: var(--color-text-main);
|
||||
}
|
||||
|
||||
div.follow .actions a.active {
|
||||
color: var(--color-text-link);
|
||||
}
|
||||
|
||||
form.inline-menu {
|
||||
|
@ -1139,12 +1060,9 @@ button i:first-child,
|
|||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
form .field.multi-option {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
form .field.multi-option {
|
||||
form .multi-option {
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
form .option.option-row {
|
||||
|
@ -1388,7 +1306,7 @@ table.metadata td .emoji {
|
|||
.announcement {
|
||||
background-color: var(--color-highlight);
|
||||
border-radius: 5px;
|
||||
margin: 0 0 20px 0;
|
||||
margin: 10px 0 0 0;
|
||||
padding: 5px 30px 5px 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
@ -1607,7 +1525,7 @@ form .post {
|
|||
.post .actions {
|
||||
display: flex;
|
||||
position: relative;
|
||||
justify-content: space-between;
|
||||
justify-content: right;
|
||||
padding: 8px 0 0 0;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
|
@ -1623,6 +1541,12 @@ form .post {
|
|||
color: var(--color-text-dull);
|
||||
}
|
||||
|
||||
.post .actions a.no-action:hover {
|
||||
background-color: transparent;
|
||||
cursor: default;
|
||||
color: var(--color-text-dull);
|
||||
}
|
||||
|
||||
.post .actions a:hover {
|
||||
background-color: var(--color-bg-main);
|
||||
}
|
||||
|
@ -1780,37 +1704,30 @@ form .post {
|
|||
color: var(--color-text-dull);
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
main {
|
||||
max-width: 900px;
|
||||
@media (max-width: 1120px),
|
||||
(display-mode: standalone) {
|
||||
|
||||
body.wide main {
|
||||
width: 100%;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 920px),
|
||||
@media (max-width: 850px),
|
||||
(display-mode: standalone) {
|
||||
.left-column {
|
||||
margin: var(--md-header-height) var(--md-sidebar-width) 0 0;
|
||||
}
|
||||
|
||||
.right-column {
|
||||
width: var(--md-sidebar-width);
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
right: 0;
|
||||
top: var(--md-header-height);
|
||||
overflow-y: auto;
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
.right-column nav {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
margin: 20px auto 20px auto;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.settings nav {
|
||||
width: var(--width-sidebar-small);
|
||||
}
|
||||
|
||||
.post .attachments a.image img {
|
||||
|
@ -1820,72 +1737,6 @@ form .post {
|
|||
|
||||
|
||||
@media (max-width: 750px) {
|
||||
header {
|
||||
height: var(--sm-header-height);
|
||||
}
|
||||
|
||||
header menu a.identity {
|
||||
width: var(--sm-sidebar-width);
|
||||
padding: 10px 10px 0 0;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
header menu a.identity i {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
#main-content {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.left-column {
|
||||
margin: var(--sm-header-height) var(--sm-sidebar-width) 0 0;
|
||||
}
|
||||
|
||||
.right-column {
|
||||
width: var(--sm-sidebar-width);
|
||||
top: var(--sm-header-height);
|
||||
}
|
||||
|
||||
.right-column nav {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.right-column nav hr {
|
||||
display: block;
|
||||
color: var(--color-text-dull);
|
||||
margin: 20px 10px;
|
||||
}
|
||||
|
||||
.right-column nav a {
|
||||
padding: 10px 0 10px 10px;
|
||||
}
|
||||
|
||||
.right-column nav a i {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.right-column nav a .fa-solid {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.right-column nav a span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.right-column h3 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.right-column h2,
|
||||
.right-column .compose {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.right-column footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.post {
|
||||
margin-bottom: 15px;
|
||||
|
@ -1896,6 +1747,10 @@ form .post {
|
|||
|
||||
@media (max-width: 550px) {
|
||||
|
||||
main {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.post .content,
|
||||
.post .summary,
|
||||
.post .edited,
|
||||
|
|
|
@ -194,6 +194,7 @@ INSTALLED_APPS = [
|
|||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"django.contrib.postgres",
|
||||
"corsheaders",
|
||||
"django_htmx",
|
||||
"hatchway",
|
||||
|
@ -220,6 +221,7 @@ MIDDLEWARE = [
|
|||
"core.middleware.HeadersMiddleware",
|
||||
"core.middleware.ConfigLoadingMiddleware",
|
||||
"api.middleware.ApiTokenMiddleware",
|
||||
"users.middleware.DomainMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "takahe.urls"
|
||||
|
|
|
@ -6,7 +6,6 @@ from activities.views import (
|
|||
compose,
|
||||
debug,
|
||||
posts,
|
||||
search,
|
||||
timelines,
|
||||
)
|
||||
from api.views import oauth
|
||||
|
@ -27,12 +26,6 @@ urlpatterns = [
|
|||
path("", core.homepage),
|
||||
path("robots.txt", core.RobotsTxt.as_view()),
|
||||
# Activity views
|
||||
path(
|
||||
"@<handle>/notifications/",
|
||||
timelines.Notifications.as_view(),
|
||||
name="notifications",
|
||||
),
|
||||
path("search/", search.Search.as_view(), name="search"),
|
||||
path("tags/<hashtag>/", timelines.Tag.as_view(), name="tag"),
|
||||
# Settings views
|
||||
path(
|
||||
|
@ -45,6 +38,11 @@ urlpatterns = [
|
|||
settings.SecurityPage.as_view(),
|
||||
name="settings_security",
|
||||
),
|
||||
path(
|
||||
"settings/interface/",
|
||||
settings.InterfacePage.as_view(),
|
||||
name="settings_interface",
|
||||
),
|
||||
path(
|
||||
"@<handle>/settings/",
|
||||
settings.SettingsRoot.as_view(),
|
||||
|
@ -56,9 +54,9 @@ urlpatterns = [
|
|||
name="settings_profile",
|
||||
),
|
||||
path(
|
||||
"@<handle>/settings/interface/",
|
||||
settings.InterfacePage.as_view(),
|
||||
name="settings_interface",
|
||||
"@<handle>/settings/posting/",
|
||||
settings.PostingPage.as_view(),
|
||||
name="settings_posting",
|
||||
),
|
||||
path(
|
||||
"@<handle>/settings/follows/",
|
||||
|
@ -238,6 +236,12 @@ urlpatterns = [
|
|||
path("@<handle>/rss/", identity.IdentityFeed()),
|
||||
path("@<handle>/following/", identity.IdentityFollows.as_view(inbound=False)),
|
||||
path("@<handle>/followers/", identity.IdentityFollows.as_view(inbound=True)),
|
||||
path("@<handle>/search/", identity.IdentitySearch.as_view()),
|
||||
path(
|
||||
"@<handle>/notifications/",
|
||||
timelines.Notifications.as_view(),
|
||||
name="notifications",
|
||||
),
|
||||
# Posts
|
||||
path("@<handle>/compose/", compose.Compose.as_view(), name="compose"),
|
||||
path("@<handle>/posts/<int:post_id>/", posts.Individual.as_view()),
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
<a class="option" href="{{ hashtag.urls.timeline }}">
|
||||
<i class="fa-solid fa-hashtag"></i>
|
||||
<span class="handle">
|
||||
{{ hashtag.display_name }}
|
||||
</span>
|
||||
{% if not hide_stats %}
|
||||
<span>
|
||||
Post count: {{ hashtag.stats.total }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</a>
|
|
@ -1,86 +0,0 @@
|
|||
<nav>
|
||||
<a href="/" {% if current_page == "home" %}class="selected"{% endif %} title="Home">
|
||||
<i class="fa-solid fa-home"></i>
|
||||
<span>Home</span>
|
||||
</a>
|
||||
{% if request.user.is_authenticated %}
|
||||
<a href="{% url "notifications" %}" {% if current_page == "notifications" %}class="selected"{% endif %} title="Notifications">
|
||||
<i class="fa-solid fa-at"></i>
|
||||
<span>Notifications</span>
|
||||
</a>
|
||||
{% comment %}
|
||||
Not sure we want to show this quite yet
|
||||
<a href="{% url "explore" %}" {% if current_page == "explore" %}class="selected"{% endif %} title="Explore">
|
||||
<i class="fa-solid fa-hashtag"></i>
|
||||
<span>Explore</span>
|
||||
</a>
|
||||
{% endcomment %}
|
||||
<a href="{% url "local" %}" {% if current_page == "local" %}class="selected"{% endif %} title="Local">
|
||||
<i class="fa-solid fa-city"></i>
|
||||
<span>Local</span>
|
||||
</a>
|
||||
<a href="{% url "federated" %}" {% if current_page == "federated" %}class="selected"{% endif %} title="Federated">
|
||||
<i class="fa-solid fa-globe"></i>
|
||||
<span>Federated</span>
|
||||
</a>
|
||||
<a href="{% url "follows" %}" {% if section == "follows" %}class="selected"{% endif %} title="Follows">
|
||||
<i class="fa-solid fa-arrow-right-arrow-left"></i>
|
||||
<span>Follows</span>
|
||||
</a>
|
||||
<hr>
|
||||
<h3></h3>
|
||||
<a href="{% url "compose" %}" {% if top_section == "compose" %}class="selected"{% endif %} title="Compose">
|
||||
<i class="fa-solid fa-feather"></i>
|
||||
<span>Compose</span>
|
||||
</a>
|
||||
<a href="{% url "search" %}" {% if top_section == "search" %}class="selected"{% endif %} title="Search">
|
||||
<i class="fa-solid fa-search"></i>
|
||||
<span>Search</span>
|
||||
</a>
|
||||
{% if current_page == "tag" %}
|
||||
<a href="{% url "tag" hashtag.hashtag %}" class="selected" title="Tag {{ hashtag.display_name }}">
|
||||
<i class="fa-solid fa-hashtag"></i>
|
||||
<span>{{ hashtag.display_name }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url "settings" %}" {% if top_section == "settings" %}class="selected"{% endif %} title="Settings">
|
||||
<i class="fa-solid fa-gear"></i>
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
<a href="{% url "identity_select" %}" title="Select Identity">
|
||||
<i class="fa-solid fa-users-viewfinder"></i>
|
||||
<span>Select Identity</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url "local" %}" {% if current_page == "local" %}class="selected"{% endif %} title="Local Posts">
|
||||
<i class="fa-solid fa-city"></i>
|
||||
<span>Local Posts</span>
|
||||
</a>
|
||||
<a href="{% url "explore" %}" {% if current_page == "explore" %}class="selected"{% endif %} title="Explore">
|
||||
<i class="fa-solid fa-hashtag"></i>
|
||||
<span>Explore</span>
|
||||
</a>
|
||||
<h3></h3>
|
||||
{% if config.signup_allowed %}
|
||||
<a href="{% url "signup" %}" {% if current_page == "signup" %}class="selected"{% endif %} title="Create Account">
|
||||
<i class="fa-solid fa-user-plus"></i>
|
||||
<span>Create Account</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</nav>
|
||||
|
||||
{% if current_page == "home" %}
|
||||
<h2>Compose</h2>
|
||||
<form action="{% url "compose" %}" method="POST" class="compose">
|
||||
{% csrf_token %}
|
||||
{{ form.text }}
|
||||
{{ form.content_warning }}
|
||||
<input type="hidden" name="visibility" value="{{ config_identity.default_post_visibility }}">
|
||||
<div class="buttons">
|
||||
<span id="character-counter">{{ config.post_length }}</span>
|
||||
<button class="toggle" _="on click or keyup[key is 'Enter'] toggle .enabled then toggle .hidden on #id_content_warning then halt">CW</button>
|
||||
<button id="post-button">{% if config_identity.toot_mode %}Toot!{% else %}Post{% endif %}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
|
@ -74,15 +74,15 @@
|
|||
{% endif %}
|
||||
|
||||
<div class="actions">
|
||||
<a title="Replies">
|
||||
<a title="Replies" href="{% if not post.local and post.url %}{{ post.url }}{% else %}{{ post.urls.view }}{% endif %}">
|
||||
<i class="fa-solid fa-reply"></i>
|
||||
<span class="like-count">{{ post.stats_with_defaults.replies|default:"0" }}</span>
|
||||
</a>
|
||||
<a title="Likes">
|
||||
<a title="Likes" class="no-action">
|
||||
<i class="fa-solid fa-star"></i>
|
||||
<span class="like-count">{{ post.stats_with_defaults.likes|default:"0" }}</span>
|
||||
</a>
|
||||
<a title="Boosts">
|
||||
<a title="Boosts" class="no-action">
|
||||
<i class="fa-solid fa-retweet"></i>
|
||||
<span class="like-count">{{ post.stats_with_defaults.boosts|default:"0" }}</span>
|
||||
</a>
|
||||
|
@ -93,9 +93,6 @@
|
|||
<a href="{{ post.urls.view }}" role="menuitem">
|
||||
<i class="fa-solid fa-comment"></i> View Post & Replies
|
||||
</a>
|
||||
<a href="{{ post.urls.action_report }}" role="menuitem">
|
||||
<i class="fa-solid fa-flag"></i> Report
|
||||
</a>
|
||||
{% if not post.local and post.url %}
|
||||
<a href="{{ post.url }}" role="menuitem">
|
||||
<i class="fa-solid fa-arrow-up-right-from-square"></i> See Original
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
{% block title %}Debug JSON{% endblock %}
|
||||
|
||||
{% block body_class %}no-sidebar{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="POST">
|
||||
{% csrf_token %}
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}#{{ hashtag.display_name }} Timeline{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="timeline-name">Explore Trending Tags</div>
|
||||
|
||||
<section class="icon-menu">
|
||||
{% for hashtag in page_obj %}
|
||||
{% include "activities/_hashtag.html" %}
|
||||
{% empty %}
|
||||
No tags are trending yet.
|
||||
{% endfor %}
|
||||
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -1,60 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% load activity_tags %}
|
||||
|
||||
{% block subtitle %}Follows{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="view-options">
|
||||
{% if inbound %}
|
||||
<a href=".">Following ({{ num_outbound }})</a>
|
||||
<a href="." class="selected">Followers ({{ num_inbound }})</a>
|
||||
{% else %}
|
||||
<a href=".?inbound=true" class="selected">Following ({{ num_outbound }})</a>
|
||||
<a href=".?inbound=true">Followers ({{ num_inbound }})</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<section class="icon-menu">
|
||||
{% for identity in page_obj %}
|
||||
<a class="option " href="{{ identity.urls.view }}">
|
||||
<div class="option-content">
|
||||
{% include "identity/_identity_banner.html" with identity=identity link_avatar=False link_handle=False %}
|
||||
</div>
|
||||
<div class="option-actions">
|
||||
{% if identity.id in outbound_ids %}
|
||||
<span class="pill">Following</span>
|
||||
{% endif %}
|
||||
{% if identity.id in inbound_ids %}
|
||||
<span class="pill">Follows You</span>
|
||||
{% endif %}
|
||||
|
||||
{% if inbound %}
|
||||
<form action="{{ identity.urls.action }}" method="POST" class="follow">
|
||||
{% csrf_token %}
|
||||
{% if identity.id in outbound_ids %}
|
||||
<input type="hidden" name="action" value="unfollow">
|
||||
<button class="destructive">Unfollow</button>
|
||||
{% else %}
|
||||
<input type="hidden" name="action" value="follow">
|
||||
<button>Follow</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endif %}
|
||||
<time>{{ identity.follow_date | timedeltashort }} ago</time>
|
||||
</div>
|
||||
</a>
|
||||
{% empty %}
|
||||
<p class="option empty">You {% if inbound %}have no followers{% else %}are not following anyone{% endif %}.</p>
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
<div class="pagination">
|
||||
{% if page_obj.has_previous %}
|
||||
<a class="button" href=".?page={{ page_obj.previous_page_number }}{% if inbound %}&inbound=true{% endif %}">Previous Page</a>
|
||||
{% endif %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a class="button" href=".?page={{ page_obj.next_page_number }}{% if inbound %}&inbound=true{% endif %}">Next Page</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -27,22 +27,22 @@
|
|||
<section>
|
||||
<h1 class="above">Apps</h1>
|
||||
<p>
|
||||
To see your timelines, compose new messages, and follow people,
|
||||
To see your timelines, compose and edit messages, and follow people,
|
||||
you will need to use a Mastodon-compatible app. Our favourites
|
||||
are listed below.
|
||||
</p>
|
||||
<div class="flex-icons">
|
||||
<a href="#">
|
||||
<a href="https://elk.zone/">
|
||||
<img src="{% static "img/apps/elk.svg" %}" alt="Elk logo">
|
||||
<h2>Elk</h2>
|
||||
<i>Web/Mobile (Free)</i>
|
||||
</a>
|
||||
<a href="#">
|
||||
<a href="https://tusky.app/">
|
||||
<img src="{% static "img/apps/tusky.png" %}" alt="Tusky logo">
|
||||
<h2>Tusky</h2>
|
||||
<i>Android (Free)</i>
|
||||
</a>
|
||||
<a href="#">
|
||||
<a href="https://tapbots.com/ivory/">
|
||||
<img src="{% static "img/apps/ivory.webp" %}" alt="Ivory logo">
|
||||
<h2>Ivory</h2>
|
||||
<i>iOS (Paid)</i>
|
||||
|
|
|
@ -3,9 +3,6 @@
|
|||
{% block title %}Notifications{% endblock %}
|
||||
|
||||
{% block settings_content %}
|
||||
{% if page_obj.number == 1 %}
|
||||
{% include "_announcements.html" %}
|
||||
{% endif %}
|
||||
|
||||
<section class="invisible">
|
||||
<div class="view-options">
|
||||
|
@ -37,14 +34,5 @@
|
|||
No notifications yet.
|
||||
{% endfor %}
|
||||
|
||||
<div class="pagination">
|
||||
{% if page_obj.has_previous and not request.htmx %}
|
||||
<a class="button" href=".?page={{ page_obj.previous_page_number }}">Previous Page</a>
|
||||
{% endif %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a class="button" href=".?page={{ page_obj.next_page_number }}" hx-get=".?page={{ page_obj.next_page_number }}" hx-select=".left-column > *:not(.view-options)" hx-target=".pagination" hx-swap="outerHTML" {% if config_identity.infinite_scroll %}hx-trigger="revealed"{% endif %}>Next Page</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Search{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="POST">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
{% include "forms/_field.html" with field=form.query %}
|
||||
</fieldset>
|
||||
<div class="buttons">
|
||||
<button>Search</button>
|
||||
</div>
|
||||
</form>
|
||||
{% if results.identities %}
|
||||
<h2>People</h2>
|
||||
{% for identity in results.identities %}
|
||||
{% include "activities/_identity.html" %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if results.posts %}
|
||||
<h2>Posts</h2>
|
||||
<section class="icon-menu">
|
||||
{% for post in results.posts %}
|
||||
{% include "activities/_post.html" %}
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
{% if results.hashtags %}
|
||||
<h2>Hashtags</h2>
|
||||
<section class="icon-menu">
|
||||
{% for hashtag in results.hashtags %}
|
||||
{% include "activities/_hashtag.html" with hide_stats=True %}
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
{% if results and not results.identities and not results.hashtags and not results.posts %}
|
||||
<h2>No results</h2>
|
||||
<p>
|
||||
We could not find anything matching your query.
|
||||
</p>
|
||||
<p>
|
||||
If you're trying to find a post or profile on another server,
|
||||
try again in a few moments - if the other end is overloaded, it
|
||||
can take some time to fetch the details.
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -21,7 +21,7 @@
|
|||
{% endif %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a class="button" href=".?page={{ page_obj.next_page_number }} {% if config_identity.infinite_scroll %}hx-trigger="revealed"{% endif %}">Next Page</a>
|
||||
<a class="button" href=".?page={{ page_obj.next_page_number }}">Next Page</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
{% block title %}{% block subtitle %}{% endblock %} - Administration{% endblock %}
|
||||
|
||||
{% block body_class %}wide{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="settings">
|
||||
{% include "admin/_menu.html" %}
|
||||
|
|
|
@ -16,6 +16,13 @@
|
|||
{% include "forms/_field.html" with field=form.default %}
|
||||
{% include "forms/_field.html" with field=form.users %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Appearance</legend>
|
||||
{% include "forms/_field.html" with field=form.site_name %}
|
||||
{% include "forms/_field.html" with field=form.site_icon %}
|
||||
{% include "forms/_field.html" with field=form.hide_login %}
|
||||
{% include "forms/_field.html" with field=form.custom_css %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Admin Notes</legend>
|
||||
{% include "forms/_field.html" with field=form.notes %}
|
||||
|
|
|
@ -17,11 +17,6 @@
|
|||
--color-highlight: {{ config.highlight_color }};
|
||||
--color-text-link: {{ config.highlight_color }};
|
||||
}
|
||||
{% if not config_identity.visible_reaction_counts %}
|
||||
.like-count {
|
||||
display: none;
|
||||
}
|
||||
{% endif %}
|
||||
</style>
|
||||
{% if config_identity.custom_css %}
|
||||
<style>{{ config_identity.custom_css|safe }}</style>
|
||||
|
@ -32,7 +27,7 @@
|
|||
{% block extra_head %}{% endblock %}
|
||||
{% block custom_head %}{% if config.custom_head %}{{ config.custom_head|safe }}{% endif %}{% endblock %}
|
||||
</head>
|
||||
<body class="{% if config_identity.light_theme %}light-theme {% endif %}{% block body_class %}{% endblock %}" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
||||
<body class="{% if user.config_user.light_theme %}theme-light {% endif %}{% block body_class %}{% endblock %}" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
||||
<a id="skip-to-main" class="screenreader-text" href="#main-content">Skip to Content</a>
|
||||
<a id="skip-to-nav" class="screenreader-text" href="#side-navigation">Skip to Navigation</a>
|
||||
<main>
|
||||
|
@ -41,18 +36,28 @@
|
|||
<menu>
|
||||
<a class="logo" href="/">
|
||||
<img src="{{ config.site_icon }}" width="32">
|
||||
{{ config.site_name }}
|
||||
{{ request.domain.config_domain.site_name|default:config.site_name }}
|
||||
</a>
|
||||
{% if user.is_authenticated %}
|
||||
<a href="/" title="My Account"><i class="fa-solid fa-user"></i></a>
|
||||
{% else %}
|
||||
{% if identity %}
|
||||
<a href="{{ identity.urls.settings }}" title="Settings"><i class="fa-solid fa-gear"></i></a>
|
||||
{% else %}
|
||||
<a href="/settings" title="Settings"><i class="fa-solid fa-gear"></i></a>
|
||||
{% endif %}
|
||||
{% if user.admin %}
|
||||
<a href="/admin" title="Administration"><i class="fa-solid fa-screwdriver-wrench"></i></a>
|
||||
{% endif %}
|
||||
{% elif not request.domain.config_domain.hide_login %}
|
||||
<a href="{% url "login" %}" title="Login"><i class="fa-solid fa-right-to-bracket"></i></a>
|
||||
{% endif %}
|
||||
<a href="/settings" title="Settings"><i class="fa-solid fa-gear"></i></a>
|
||||
<a href="/admin" title="Administration"><i class="fa-solid fa-screwdriver-wrench"></i></a>
|
||||
</menu>
|
||||
</header>
|
||||
|
||||
{% block announcements %}
|
||||
{% include "_announcements.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -28,55 +28,65 @@
|
|||
{# fmt:on #}
|
||||
</script>
|
||||
|
||||
{% include "forms/_field.html" %}
|
||||
<div class="field">
|
||||
<div class="label-input">
|
||||
<label for="{{ field.id_for_label }}">
|
||||
{{ field.label }}
|
||||
{% if field.field.required %}<small>(Required)</small>{% endif %}
|
||||
</label>
|
||||
{% if field.help_text %}
|
||||
<p class="help">
|
||||
{{ field.help_text|safe|linebreaksbr }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{{ field.errors }}
|
||||
{{ field }}
|
||||
|
||||
<div class="field multi-option">
|
||||
<section class="icon-menu">
|
||||
{# fmt:off #}
|
||||
<span id="new_{{ field.name }}" stlye="display: none"
|
||||
_="on load
|
||||
get the (value of #id_{{ field.name }}) as Object
|
||||
set items to it
|
||||
for item in items
|
||||
set f to {{ field.name }}.addEmptyField()
|
||||
set one to the first <input.{{ field.name }}-{{ name_one }}/> in f then
|
||||
set one@value to item.{{ name_one }}
|
||||
<div class="multi-option">
|
||||
{# fmt:off #}
|
||||
<span id="new_{{ field.name }}" style="display: none"
|
||||
_="on load
|
||||
get the (value of #id_{{ field.name }}) as Object
|
||||
set items to it
|
||||
for item in items
|
||||
set f to {{ field.name }}.addEmptyField()
|
||||
set one to the first <input.{{ field.name }}-{{ name_one }}/> in f then
|
||||
set one@value to item.{{ name_one }}
|
||||
|
||||
set two to the first <input.{{ field.name }}-{{ name_two }}/> in f then
|
||||
set two@value to item.{{ name_two }}
|
||||
end
|
||||
get the (@data-min-empty of #id_{{ field.name }})
|
||||
set min_empty to it
|
||||
if items.length < min_empty then
|
||||
repeat (min_empty - items.length) times
|
||||
call {{ field.name }}.addEmptyField()
|
||||
end
|
||||
"></span>
|
||||
{# fmt:on #}
|
||||
set two to the first <input.{{ field.name }}-{{ name_two }}/> in f then
|
||||
set two@value to item.{{ name_two }}
|
||||
end
|
||||
get the (@data-min-empty of #id_{{ field.name }})
|
||||
set min_empty to it
|
||||
if items.length < min_empty then
|
||||
repeat (min_empty - items.length) times
|
||||
call {{ field.name }}.addEmptyField()
|
||||
end
|
||||
"></span>
|
||||
{# fmt:on #}
|
||||
|
||||
<div class="option">
|
||||
<span class="option-field">
|
||||
<button class="fa-solid fa-add" title="Add Row" _="on click {{ field.name }}.addEmptyField() then halt"></button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="option">
|
||||
<span class="option-field">
|
||||
<button class="fa-solid fa-add" title="Add Row" _="on click {{ field.name }}.addEmptyField() then halt"></button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div id="blank_{{ field.name }}" class="option option-row hidden">
|
||||
<span class="option-field">
|
||||
<input type=text class="{{ field.name }}-{{ name_one }}" name="{{ field.name }}_{{ name_one }}" value="">
|
||||
</span>
|
||||
<span class="option-field">
|
||||
<input type=text class="{{ field.name }}-{{ name_two }}" name="{{ field.name }}_{{ name_two }}" value="">
|
||||
</span>
|
||||
<div class="right">
|
||||
<button class="fa-solid fa-trash delete" title="Delete Row"
|
||||
_="on click remove (closest parent .option)
|
||||
then {{ field.name }}.collect{{ field.name|title }}Fields()
|
||||
then halt" />
|
||||
<div id="blank_{{ field.name }}" class="option option-row hidden">
|
||||
<span class="option-field">
|
||||
<input type=text class="{{ field.name }}-{{ name_one }}" name="{{ field.name }}_{{ name_one }}" value="">
|
||||
</span>
|
||||
<span class="option-field">
|
||||
<input type=text class="{{ field.name }}-{{ name_two }}" name="{{ field.name }}_{{ name_two }}" value="">
|
||||
</span>
|
||||
<div class="right">
|
||||
<button class="fa-solid fa-trash delete" title="Delete Row"
|
||||
_="on click remove (closest parent .option)
|
||||
then {{ field.name }}.collect{{ field.name|title }}Fields()
|
||||
then halt" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endwith %}
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
<a href="{{ identity.urls.view }}" class="handle">
|
||||
{% endif %}
|
||||
<div class="link">{{ identity.html_name_or_handle }}</div>
|
||||
<small>@{{ identity.handle }}</small>
|
||||
<small>@{{ identity.username }}@{{ identity.domain_id }}</small>
|
||||
{% if link_handle is False %}
|
||||
</div>
|
||||
{% else %}
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
<nav>
|
||||
<a href="{% url "identity_select" %}" {% if identities %}class="selected"{% endif %} title="Select Identity">
|
||||
<i class="fa-solid fa-users-viewfinder"></i>
|
||||
<span>Select Identity</span>
|
||||
</a>
|
||||
<a href="{% url "identity_create" %}" {% if form %}class="selected"{% endif %} title="Create Identity">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
<span>Create Identity</span>
|
||||
</a>
|
||||
<a href="{% url "logout" %}" title="Logout">
|
||||
<i class="fa-solid fa-right-from-bracket"></i>
|
||||
<span>Logout</span>
|
||||
</a>
|
||||
{% if request.user.admin %}
|
||||
<hr>
|
||||
<h3>Administration</h3>
|
||||
<a href="{% url "admin_domains" %}" {% if section == "domains" %}class="selected"{% endif %} title="Domains">
|
||||
<i class="fa-solid fa-globe"></i>
|
||||
<span>Domains</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</nav>
|
10
templates/identity/_tabs.html
Normal file
10
templates/identity/_tabs.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
<section class="view-options">
|
||||
<a href="{{ identity.urls.view }}" {% if not section %}class="selected"{% endif %}><strong>{{ post_count }}</strong> Posts</a>
|
||||
{% if identity.local and identity.config_identity.visible_follows %}
|
||||
<a href="{{ identity.urls.following }}" {% if not inbound and section == "follows" %}class="selected"{% endif %}><strong>{{ following_count }}</strong> Following</a>
|
||||
<a href="{{ identity.urls.followers }}" {% if inbound and section == "follows" %}class="selected"{% endif %}><strong>{{ followers_count }}</strong> Follower{{ followers_count|pluralize }}</a>
|
||||
{% endif %}
|
||||
{% if identity.local and identity.config_identity.search_enabled %}
|
||||
<a href="{{ identity.urls.search }}" {% if section == "search" %}class="selected"{% endif %}>Search</a>
|
||||
{% endif %}
|
||||
</section>
|
|
@ -1,19 +0,0 @@
|
|||
<div class="inline follow {% if inbound_follow %}has-reverse{% endif %}">
|
||||
<div class="actions" role="menubar">
|
||||
{% if request.user in identity.users.all %}
|
||||
<a href="{% url "settings_profile" handle=identity.handle %}" class="button" title="Edit Profile">
|
||||
<i class="fa-solid fa-user-edit"></i> Edit
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if request.user.admin or request.user.moderator %}
|
||||
<a title="Menu" class="menu button" _="on click or keyup[key is 'Enter'] toggle .enabled on the next <menu/> then halt" aria-haspopup="menu" tabindex="0">
|
||||
<i class="fa-solid fa-bars"></i>
|
||||
</a>
|
||||
<menu>
|
||||
<a href="{{ identity.urls.admin_edit }}" role="menuitem">
|
||||
<i class="fa-solid fa-user-gear"></i> View in Admin
|
||||
</a>
|
||||
</menu>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
|
@ -1,7 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{% block subtitle %}{% endblock %} - Settings{% endblock %}
|
||||
|
||||
{% block right_content %}
|
||||
{% include "identity/_menu.html" %}
|
||||
{% endblock %}
|
|
@ -1,12 +1,15 @@
|
|||
{% extends "identity/base.html" %}
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Create Identity{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="POST">
|
||||
<h1>Create New Identity</h1>
|
||||
<p>You can have multiple identities - they are totally separate, and share
|
||||
nothing apart from your login details. Use them for alternates, projects, and more.</p>
|
||||
<p>
|
||||
You can have multiple identities - they are totally separate, and share
|
||||
nothing apart from your login details. They can also be shared among multiple
|
||||
users, so you can use them for company or project accounts.
|
||||
</p>
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
<legend>Identity Details</legend>
|
||||
|
|
23
templates/identity/search.html
Normal file
23
templates/identity/search.html
Normal file
|
@ -0,0 +1,23 @@
|
|||
{% extends "identity/view.html" %}
|
||||
|
||||
{% block title %}Search - {{ identity }}{% endblock %}
|
||||
|
||||
{% block subcontent %}
|
||||
|
||||
<form action="." method="post">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
{% include "forms/_field.html" with field=form.query %}
|
||||
</fieldset>
|
||||
<div class="buttons">
|
||||
<button>Search</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% for post in results %}
|
||||
{% include "activities/_post.html" %}
|
||||
{% empty %}
|
||||
<p class="empty">No posts were found that match your search.</p>
|
||||
{% endfor %}
|
||||
|
||||
{% endblock %}
|
|
@ -27,7 +27,20 @@
|
|||
>
|
||||
</span>
|
||||
|
||||
{% if request.user.moderator or request.user.admin %}{% include "identity/_view_menu.html" %}{% endif %}
|
||||
<div class="inline follow">
|
||||
<div class="actions" role="menubar">
|
||||
{% if request.user in identity.users.all %}
|
||||
<a href="{% url "settings_profile" handle=identity.handle %}" class="button" title="Edit Profile">
|
||||
<i class="fa-solid fa-user-edit"></i> Settings
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if request.user.admin or request.user.moderator %}
|
||||
<a href="{% url "settings_profile" handle=identity.handle %}" class="button danger" title="View in Admin">
|
||||
<i class="fa-solid fa-screwdriver-wrench"></i> Admin
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1>{{ identity.html_name_or_handle }}</h1>
|
||||
<small>
|
||||
|
@ -72,15 +85,8 @@
|
|||
</section>
|
||||
{% else %}
|
||||
|
||||
<section class="view-options">
|
||||
<a href="{{ identity.urls.view }}" {% if not follows_page %}class="selected"{% endif %}><strong>{{ post_count }}</strong> posts</a>
|
||||
{% if identity.local and identity.config_identity.visible_follows %}
|
||||
<a href="{{ identity.urls.following }}" {% if not inbound and follows_page %}class="selected"{% endif %}><strong>{{ following_count }}</strong> following</a>
|
||||
<a href="{{ identity.urls.followers }}" {% if inbound and follows_page %}class="selected"{% endif %}><strong>{{ followers_count }}</strong> follower{{ followers_count|pluralize }}</a>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% include "identity/_tabs.html" %}
|
||||
|
||||
<section class="invisible">
|
||||
{% block subcontent %}
|
||||
|
||||
{% for post in page_obj %}
|
||||
|
@ -90,10 +96,10 @@
|
|||
{% if identity.local %}
|
||||
No posts yet.
|
||||
{% else %}
|
||||
No posts have been received/retrieved by this server yet.
|
||||
No posts have been received by this server yet.
|
||||
|
||||
{% if identity.profile_uri %}
|
||||
You might find historical posts at
|
||||
You may find historical posts at
|
||||
<a href="{{ identity.profile_uri }}">their original profile ➔</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
@ -106,11 +112,10 @@
|
|||
{% endif %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a class="button" href=".?page={{ page_obj.next_page_number }}" hx-get=".?page={{ page_obj.next_page_number }}" hx-select=".left-column > *:not(.view-options)" hx-target=".pagination" hx-swap="outerHTML" {% if config_identity.infinite_scroll %}hx-trigger="revealed"{% endif %}>Next Page</a>
|
||||
<a class="button" href=".?page={{ page_obj.next_page_number }}" hx-get=".?page={{ page_obj.next_page_number }}" hx-select=".left-column > *:not(.view-options)" hx-target=".pagination" hx-swap="outerHTML">Next Page</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,38 +1,54 @@
|
|||
<nav>
|
||||
{% if identity %}
|
||||
{% include "identity/_identity_banner.html" %}
|
||||
<h3>Settings</h3>
|
||||
<a href="{% url "settings_profile" handle=identity.handle %}" {% if section == "profile" %}class="selected"{% endif %} title="Profile">
|
||||
<i class="fa-solid fa-user"></i>
|
||||
<span>Profile</span>
|
||||
</a>
|
||||
<a href="{% url "settings_interface" handle=identity.handle %}" {% if section == "interface" %}class="selected"{% endif %} title="Interface">
|
||||
<i class="fa-solid fa-display"></i>
|
||||
<span>Interface</span>
|
||||
</a>
|
||||
<a href="{% url "settings_follows" handle=identity.handle %}" {% if section == "follows" %}class="selected"{% endif %} title="Follows">
|
||||
<i class="fa-solid fa-arrow-right-arrow-left"></i>
|
||||
<span>Follows</span>
|
||||
</a>
|
||||
<a href="{% url "settings_import_export" handle=identity.handle %}" {% if section == "importexport" %}class="selected"{% endif %} title="Interface">
|
||||
<i class="fa-solid fa-cloud-arrow-up"></i>
|
||||
<span>Import/Export</span>
|
||||
</a>
|
||||
<hr>
|
||||
<h3>Tools</h3>
|
||||
<a href="{% url "compose" handle=identity.handle %}" {% if section == "compose" %}class="selected"{% endif %} title="Compose">
|
||||
<i class="fa-solid fa-pen-to-square"></i>
|
||||
<span>Compose</span>
|
||||
</a>
|
||||
<hr>
|
||||
{% include "identity/_identity_banner.html" %}
|
||||
<h3>Identity Settings</h3>
|
||||
<a href="{% url "settings_profile" handle=identity.handle %}" {% if section == "profile" %}class="selected"{% endif %} title="Profile">
|
||||
<i class="fa-solid fa-user"></i>
|
||||
<span>Profile</span>
|
||||
</a>
|
||||
<a href="{% url "settings_posting" handle=identity.handle %}" {% if section == "posting" %}class="selected"{% endif %} title="Posting">
|
||||
<i class="fa-solid fa-message"></i>
|
||||
<span>Posting</span>
|
||||
</a>
|
||||
<a href="{% url "settings_import_export" handle=identity.handle %}" {% if section == "importexport" %}class="selected"{% endif %} title="Interface">
|
||||
<i class="fa-solid fa-cloud-arrow-up"></i>
|
||||
<span>Import/Export</span>
|
||||
</a>
|
||||
<hr>
|
||||
<h3>Tools</h3>
|
||||
<a href="{% url "settings_follows" handle=identity.handle %}" {% if section == "follows" %}class="selected"{% endif %} title="Follows">
|
||||
<i class="fa-solid fa-arrow-right-arrow-left"></i>
|
||||
<span>Follows</span>
|
||||
</a>
|
||||
<a href="{% url "compose" handle=identity.handle %}" {% if section == "compose" %}class="selected"{% endif %} title="Compose">
|
||||
<i class="fa-solid fa-pen-to-square"></i>
|
||||
<span>Compose</span>
|
||||
</a>
|
||||
<hr>
|
||||
<h3>User Settings</h3>
|
||||
<a href="{% url "settings" %}" title="Switch to User Settings">
|
||||
<i class="fa-solid fa-arrow-right"></i>
|
||||
<span>Go to User Settings</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<h3>Account</h3>
|
||||
<a href="{% url "settings_security" %}" {% if section == "security" %}class="selected"{% endif %} title="Login & Security">
|
||||
<i class="fa-solid fa-key"></i>
|
||||
<span>Login & Security</span>
|
||||
</a>
|
||||
<a href="{% url "settings_interface" %}" {% if section == "interface" %}class="selected"{% endif %} title="Interface">
|
||||
<i class="fa-solid fa-display"></i>
|
||||
<span>Interface</span>
|
||||
</a>
|
||||
<a href="{% url "logout" %}">
|
||||
<i class="fa-solid fa-right-from-bracket" title="Logout"></i>
|
||||
<span>Logout</span>
|
||||
</a>
|
||||
<hr>
|
||||
<h3>Identity Settings</h3>
|
||||
<a href="/" title="Go to Identity Select">
|
||||
<i class="fa-solid fa-arrow-right"></i>
|
||||
<span>Go to Identity Select</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
<h3>Account</h3>
|
||||
<a href="{% url "settings_security" %}" {% if section == "security" %}class="selected"{% endif %} title="Login & Security">
|
||||
<i class="fa-solid fa-key"></i>
|
||||
<span>Login & Security</span>
|
||||
</a>
|
||||
<a href="{% url "logout" %}">
|
||||
<i class="fa-solid fa-right-from-bracket" title="Logout"></i>
|
||||
<span>Logout</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
{% block title %}{% block subtitle %}{% endblock %} - Settings{% endblock %}
|
||||
|
||||
{% block body_class %}wide{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="settings">
|
||||
{% include "settings/_menu.html" %}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
{% for other_identity in page_obj %}
|
||||
<tr>
|
||||
<td class="icon">
|
||||
<a href="{{ domain.urls.edit }}" class="overlay"></a>
|
||||
<a href="{{ other_identity.urls.view }}" class="overlay"></a>
|
||||
<img
|
||||
src="{{ other_identity.local_icon_url.relative }}"
|
||||
class="icon"
|
||||
|
@ -27,20 +27,30 @@
|
|||
_="on error set my.src to generate_avatar(@data-handle)"
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ other_identity.urls.view }}" class="overlay">{{ other_identity.handle }}</a>
|
||||
<td class="name">
|
||||
<a href="{{ other_identity.urls.view }}" class="overlay"></a>
|
||||
{{ other_identity.html_name_or_handle }}
|
||||
<small>@{{ other_identity.handle }}</small>
|
||||
</td>
|
||||
<td class="stat">
|
||||
{% if identity.id in outbound_ids %}
|
||||
{% if other_identity.id in outbound_ids %}
|
||||
<span class="pill">Following</span>
|
||||
{% endif %}
|
||||
{% if identity.id in inbound_ids %}
|
||||
{% if other_identity.id in inbound_ids %}
|
||||
<span class="pill">Follows You</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="actions">
|
||||
<a href="{{ other_identity.urls.view }}" title="View"><i class="fa-solid fa-eye"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr class="empty"><td>You {% if inbound %}have no followers{% else %}are not following anyone{% endif %}.</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% if inbound %}
|
||||
{% include "admin/_pagination.html" with nouns="follower,followers" %}
|
||||
{% else %}
|
||||
{% include "admin/_pagination.html" with nouns="follow,follows" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -12,14 +12,20 @@
|
|||
{% include "forms/_field.html" with field=form.name %}
|
||||
{% include "forms/_field.html" with field=form.summary %}
|
||||
{% include "forms/_json_name_value_list.html" with field=form.metadata %}
|
||||
{% include "forms/_field.html" with field=form.discoverable %}
|
||||
{% include "forms/_field.html" with field=form.visible_follows %}
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Images</legend>
|
||||
<legend>Appearance</legend>
|
||||
{% include "forms/_field.html" with field=form.icon %}
|
||||
{% include "forms/_field.html" with field=form.image %}
|
||||
{% include "forms/_field.html" with field=form.theme %}
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Privacy</legend>
|
||||
{% include "forms/_field.html" with field=form.discoverable %}
|
||||
{% include "forms/_field.html" with field=form.visible_follows %}
|
||||
{% include "forms/_field.html" with field=form.search_enabled %}
|
||||
</fieldset>
|
||||
<div class="buttons">
|
||||
<button>Save</button>
|
||||
|
|
15
users/middleware.py
Normal file
15
users/middleware.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
from users.models import Domain
|
||||
|
||||
|
||||
class DomainMiddleware:
|
||||
"""
|
||||
Tries to attach a Domain object to every incoming request, if one matches.
|
||||
"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
request.domain = Domain.get_domain(request.META["HTTP_HOST"])
|
||||
response = self.get_response(request)
|
||||
return response
|
|
@ -1,6 +1,7 @@
|
|||
import json
|
||||
import ssl
|
||||
from typing import Optional
|
||||
from functools import cached_property
|
||||
|
||||
import httpx
|
||||
import pydantic
|
||||
|
@ -12,6 +13,7 @@ from django.db import models
|
|||
from core.exceptions import capture_message
|
||||
from stator.models import State, StateField, StateGraph, StatorModel
|
||||
from users.schemas import NodeInfo
|
||||
from core.models import Config
|
||||
|
||||
|
||||
class DomainStates(StateGraph):
|
||||
|
@ -230,3 +232,9 @@ class Domain(StatorModel):
|
|||
version = software.get("version", "unknown")
|
||||
return f"{name:.10} - {version:.10}"
|
||||
return None
|
||||
|
||||
### Config ###
|
||||
|
||||
@cached_property
|
||||
def config_domain(self) -> Config.DomainOptions:
|
||||
return Config.load_domain(self)
|
||||
|
|
|
@ -234,6 +234,7 @@ class Identity(StatorModel):
|
|||
action = "{view}action/"
|
||||
followers = "{view}followers/"
|
||||
following = "{view}following/"
|
||||
search = "{view}search/"
|
||||
activate = "{view}activate/"
|
||||
admin = "/admin/identities/"
|
||||
admin_edit = "{admin}{self.pk}/"
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import urlman
|
||||
from functools import cached_property
|
||||
from core.models import Config
|
||||
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
|
||||
from django.db import models
|
||||
|
||||
|
@ -66,3 +68,7 @@ class User(AbstractBaseUser):
|
|||
|
||||
def has_perm(self, perm):
|
||||
return self.admin
|
||||
|
||||
@cached_property
|
||||
def config_user(self) -> Config.UserOptions:
|
||||
return Config.load_user(self)
|
||||
|
|
|
@ -5,6 +5,7 @@ from django.shortcuts import get_object_or_404, redirect
|
|||
from django.utils.decorators import method_decorator
|
||||
from django.views.generic import FormView, TemplateView
|
||||
|
||||
from core.models import Config
|
||||
from users.decorators import admin_required
|
||||
from users.models import Domain, User
|
||||
|
||||
|
@ -158,6 +159,29 @@ class DomainEdit(FormView):
|
|||
extra_context = {"section": "domains"}
|
||||
|
||||
class form_class(DomainCreate.form_class):
|
||||
site_name = forms.CharField(
|
||||
help_text="Override the site name for this domain",
|
||||
required=False,
|
||||
)
|
||||
|
||||
site_icon = forms.ImageField(
|
||||
required=False,
|
||||
help_text="Override the site icon for this domain",
|
||||
)
|
||||
|
||||
hide_login = forms.BooleanField(
|
||||
help_text="If the login button should appear in the header for this domain",
|
||||
widget=forms.Select(choices=[(False, "Show"), (True, "Hide")]),
|
||||
initial=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
custom_css = forms.CharField(
|
||||
help_text="Customise the appearance of this domain\nSee our <a href='#'>documentation</a> for help on common changes\n<b>WARNING: Do not put untrusted code in here!</b>",
|
||||
widget=forms.Textarea,
|
||||
required=False,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["domain"].disabled = True
|
||||
|
@ -188,6 +212,10 @@ class DomainEdit(FormView):
|
|||
self.domain.users.set(form.cleaned_data["users"])
|
||||
if self.domain.default:
|
||||
Domain.objects.exclude(pk=self.domain.pk).update(default=False)
|
||||
Config.set_domain(self.domain, "hide_login", form.cleaned_data["hide_login"])
|
||||
Config.set_domain(self.domain, "site_name", form.cleaned_data["site_name"])
|
||||
Config.set_domain(self.domain, "site_icon", form.cleaned_data["site_icon"])
|
||||
Config.set_domain(self.domain, "custom_css", form.cleaned_data["custom_css"])
|
||||
return redirect(Domain.urls.root)
|
||||
|
||||
def get_initial(self):
|
||||
|
@ -198,6 +226,10 @@ class DomainEdit(FormView):
|
|||
"public": self.domain.public,
|
||||
"default": self.domain.default,
|
||||
"users": "\n".join(sorted(user.email for user in self.domain.users.all())),
|
||||
"site_name": self.domain.config_domain.site_name,
|
||||
"site_icon": self.domain.config_domain.site_icon,
|
||||
"hide_login": self.domain.config_domain.hide_login,
|
||||
"custom_css": self.domain.config_domain.custom_css,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ from core.models import Config
|
|||
from django.contrib.auth.decorators import login_required
|
||||
from users.models import Domain, FollowStates, Identity, IdentityStates
|
||||
from users.services import IdentityService
|
||||
from activities.services import SearchService
|
||||
from users.shortcuts import by_handle_or_404
|
||||
|
||||
|
||||
|
@ -213,17 +214,11 @@ class IdentityFollows(ListView):
|
|||
raise Http404("Hidden follows")
|
||||
return super().get(request, identity=self.identity)
|
||||
|
||||
def get_queryset(self):
|
||||
if self.inbound:
|
||||
return IdentityService(self.identity).followers()
|
||||
else:
|
||||
return IdentityService(self.identity).following()
|
||||
|
||||
def get_context_data(self):
|
||||
context = super().get_context_data()
|
||||
context["identity"] = self.identity
|
||||
context["inbound"] = self.inbound
|
||||
context["follows_page"] = True
|
||||
context["section"] = "follows"
|
||||
context["followers_count"] = self.identity.inbound_follows.filter(
|
||||
state__in=FollowStates.group_active()
|
||||
).count()
|
||||
|
@ -234,6 +229,43 @@ class IdentityFollows(ListView):
|
|||
return context
|
||||
|
||||
|
||||
class IdentitySearch(FormView):
|
||||
"""
|
||||
Allows an identity's posts to be searched.
|
||||
"""
|
||||
|
||||
template_name = "identity/search.html"
|
||||
|
||||
class form_class(forms.Form):
|
||||
query = forms.CharField(help_text="The text to search for")
|
||||
|
||||
def dispatch(self, request, handle):
|
||||
self.identity = by_handle_or_404(self.request, handle)
|
||||
if not Config.load_identity(self.identity).search_enabled:
|
||||
raise Http404("Search not enabled")
|
||||
return super().dispatch(request, identity=self.identity)
|
||||
|
||||
def form_valid(self, form):
|
||||
self.results = SearchService(
|
||||
query=form.cleaned_data["query"], identity=self.identity
|
||||
).search_post_content()
|
||||
return self.form_invalid(form)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["identity"] = self.identity
|
||||
context["section"] = "search"
|
||||
context["followers_count"] = self.identity.inbound_follows.filter(
|
||||
state__in=FollowStates.group_active()
|
||||
).count()
|
||||
context["following_count"] = self.identity.outbound_follows.filter(
|
||||
state__in=FollowStates.group_active()
|
||||
).count()
|
||||
context["post_count"] = self.identity.posts.count()
|
||||
context["results"] = getattr(self, "results", None)
|
||||
return context
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class CreateIdentity(FormView):
|
||||
template_name = "identity/create.html"
|
||||
|
|
|
@ -8,11 +8,12 @@ from users.views.settings.import_export import ( # noqa
|
|||
CsvFollowing,
|
||||
ImportExportPage,
|
||||
)
|
||||
from users.views.settings.interface import InterfacePage # noqa
|
||||
from users.views.settings.posting import PostingPage # noqa
|
||||
from users.views.settings.profile import ProfilePage # noqa
|
||||
from users.views.settings.security import SecurityPage # noqa
|
||||
from users.views.settings.settings_page import SettingsPage # noqa
|
||||
from users.views.settings.follows import FollowsPage # noqa
|
||||
from users.views.settings.interface import InterfacePage # noqa
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
|
|
|
@ -1,33 +1,16 @@
|
|||
from activities.models.post import Post
|
||||
from users.views.settings.settings_page import SettingsPage
|
||||
from users.views.settings.settings_page import UserSettingsPage
|
||||
|
||||
|
||||
class InterfacePage(SettingsPage):
|
||||
class InterfacePage(UserSettingsPage):
|
||||
section = "interface"
|
||||
|
||||
options = {
|
||||
"default_post_visibility": {
|
||||
"title": "Default Post Visibility",
|
||||
"help_text": "Visibility to use as default for new posts.",
|
||||
"choices": Post.Visibilities.choices,
|
||||
},
|
||||
"default_reply_visibility": {
|
||||
"title": "Default Reply Visibility",
|
||||
"help_text": "Visibility to use as default for replies.",
|
||||
"choices": Post.Visibilities.choices,
|
||||
},
|
||||
"custom_css": {
|
||||
"title": "Custom CSS",
|
||||
"help_text": "Theme the website however you'd like, just for you. You should probably not use this unless you know what you're doing.",
|
||||
"display": "textarea",
|
||||
},
|
||||
"light_theme": {
|
||||
"title": "Light Mode",
|
||||
"help_text": "Use a light theme rather than the default dark theme.",
|
||||
"title": "Light Theme",
|
||||
"help_text": "Use a light theme when you are logged in to the web interface",
|
||||
},
|
||||
}
|
||||
|
||||
layout = {
|
||||
"Posting": ["default_post_visibility", "default_reply_visibility"],
|
||||
"Appearance": ["light_theme", "custom_css"],
|
||||
"Appearance": ["light_theme"],
|
||||
}
|
||||
|
|
18
users/views/settings/posting.py
Normal file
18
users/views/settings/posting.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
from activities.models.post import Post
|
||||
from users.views.settings.settings_page import SettingsPage
|
||||
|
||||
|
||||
class PostingPage(SettingsPage):
|
||||
section = "posting"
|
||||
|
||||
options = {
|
||||
"default_post_visibility": {
|
||||
"title": "Default Post Visibility",
|
||||
"help_text": "Visibility to use as default for new posts.",
|
||||
"choices": Post.Visibilities.choices,
|
||||
},
|
||||
}
|
||||
|
||||
layout = {
|
||||
"Posting": ["default_post_visibility"],
|
||||
}
|
|
@ -36,20 +36,25 @@ class ProfilePage(FormView):
|
|||
required=False, help_text="Shown at the top of your profile"
|
||||
)
|
||||
discoverable = forms.BooleanField(
|
||||
help_text="If this user is visible on the frontpage and in user directories.",
|
||||
help_text="If this user is visible on the frontpage and in user directories",
|
||||
widget=forms.Select(
|
||||
choices=[(True, "Discoverable"), (False, "Not Discoverable")]
|
||||
),
|
||||
required=False,
|
||||
)
|
||||
visible_follows = forms.BooleanField(
|
||||
help_text="Whether or not to show your following and follower counts in your profile.",
|
||||
help_text="Whether or not to show your following and follower counts in your profile",
|
||||
widget=forms.Select(choices=[(True, "Visible"), (False, "Hidden")]),
|
||||
required=False,
|
||||
)
|
||||
search_enabled = forms.BooleanField(
|
||||
help_text="If a search feature is provided for your posts on the profile page\n(Disabling this will not prevent third-party search crawlers from indexing your posts)",
|
||||
widget=forms.Select(choices=[(True, "Enabled"), (False, "Disabled")]),
|
||||
required=False,
|
||||
)
|
||||
metadata = forms.JSONField(
|
||||
label="Profile Metadata Fields",
|
||||
help_text="These values will appear on your profile below your Bio",
|
||||
help_text="These values will appear on your profile below your bio",
|
||||
widget=forms.HiddenInput(attrs={"data-min-empty": 2}),
|
||||
required=False,
|
||||
)
|
||||
|
@ -84,6 +89,7 @@ class ProfilePage(FormView):
|
|||
"discoverable": self.identity.discoverable,
|
||||
"visible_follows": self.identity.config_identity.visible_follows,
|
||||
"metadata": self.identity.metadata or [],
|
||||
"search_enabled": self.identity.config_identity.search_enabled,
|
||||
}
|
||||
|
||||
def form_valid(self, form):
|
||||
|
@ -115,4 +121,7 @@ class ProfilePage(FormView):
|
|||
Config.set_identity(
|
||||
self.identity, "visible_follows", form.cleaned_data["visible_follows"]
|
||||
)
|
||||
Config.set_identity(
|
||||
self.identity, "search_enabled", form.cleaned_data["search_enabled"]
|
||||
)
|
||||
return redirect(".")
|
||||
|
|
|
@ -114,3 +114,17 @@ class SettingsPage(FormView):
|
|||
form.cleaned_data[field.name],
|
||||
)
|
||||
return redirect(".")
|
||||
|
||||
|
||||
class UserSettingsPage(SettingsPage):
|
||||
"""
|
||||
User-option oriented version of the settings page.
|
||||
"""
|
||||
|
||||
options_class = Config.UserOptions
|
||||
|
||||
def load_config(self):
|
||||
return Config.load_user(self.request.user)
|
||||
|
||||
def save_config(self, key, value):
|
||||
Config.set_user(self.request.user, key, value)
|
||||
|
|
Loading…
Reference in a new issue