diff options
-rw-r--r-- | README.md | 22 | ||||
-rw-r--r-- | static/css/style.css | 11 | ||||
-rw-r--r-- | static/img/icon-admin-512.png | bin | 0 -> 48813 bytes | |||
-rw-r--r-- | static/img/icon-admin.svg | 66 | ||||
-rw-r--r-- | users/models/identity.py | 2 | ||||
-rw-r--r-- | users/views/settings/__init__.py (renamed from users/views/settings.py) | 55 | ||||
-rw-r--r-- | users/views/settings/profile.py | 63 |
7 files changed, 144 insertions, 75 deletions
@@ -1,30 +1,14 @@ ![takahē](static/img/logo-128.png) -A *very experimental* Fediverse server for microblogging/"toots". Not fully functional yet - +An *experimental* Fediverse server for microblogging/"toots". Not fully functional yet - I'm still working on making all the basic bits work! For more background and information, -see [my blog posts about it](https://aeracode.org/category/takahe/). +see [jointakahe.org]](https://jointakahe.org/). -Indended features: - -* Can run on serverless hosting (no need for worker daemons) -* Multiple account domains possible per server -* Async evented core for fan-out/delivery -* Mastodon client API compatible (eventually) ## Deployment -### Requirements: - -- **Python** 3.11 -- **PostgreSQL** 14+ -- **Lots of patience** This is *very experimental* - -### Setup - -More deployment docs will come soon! Just know that you need to run the Takahē -Django app, and then either hit `/.stator/runner/` or run `./manage.py runstator` -at least every 30 seconds. +See [the documentation](https://takahe-server.readthedocs.io) ## Roadmap diff --git a/static/css/style.css b/static/css/style.css index 426308d..1584e68 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -617,6 +617,7 @@ form .button:hover { height: auto; object-fit: cover; margin: -65px -15px 0 -15px; + border-radius: 5px 0 0 0; display: block; } @@ -633,6 +634,7 @@ h1.identity .banner { display: block; width: calc(100% + 30px); margin: -65px -15px 20px -15px; + border-radius: 5px 0 0 0; } h1.identity .icon { @@ -644,10 +646,10 @@ h1.identity .icon { h1.identity small { display: block; - font-size: 80%; + font-size: 60%; font-weight: normal; color: var(--color-text-dull); - margin: -10px 0 0 0; + margin: -5px 0 0 0; } .bio { @@ -834,6 +836,11 @@ h1.identity small { header menu a.identity { width: 50px; padding: 10px 10px 0 0; + font-size: 0; + } + + header menu a.identity i { + font-size: 22px; } .right-column { diff --git a/static/img/icon-admin-512.png b/static/img/icon-admin-512.png Binary files differnew file mode 100644 index 0000000..1a0ada9 --- /dev/null +++ b/static/img/icon-admin-512.png diff --git a/static/img/icon-admin.svg b/static/img/icon-admin.svg new file mode 100644 index 0000000..d495b6f --- /dev/null +++ b/static/img/icon-admin.svg @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + width="1024" + height="1024" + viewBox="0 0 270.93333 270.93333" + version="1.1" + id="svg5" + xml:space="preserve" + inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)" + sodipodi:docname="icon-admin.svg" + inkscape:export-filename="icon-admin-512.png" + inkscape:export-xdpi="12.000001" + inkscape:export-ydpi="12.000001" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview + id="namedview7" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + inkscape:document-units="mm" + showgrid="false" + inkscape:zoom="0.5946522" + inkscape:cx="761.78983" + inkscape:cy="461.61437" + inkscape:current-layer="layer1"><inkscape:grid + type="xygrid" + id="grid111" /></sodipodi:namedview><defs + id="defs2"><linearGradient + inkscape:collect="always" + id="linearGradient717"><stop + style="stop-color:#dc0000;stop-opacity:1;" + offset="0" + id="stop713" /><stop + style="stop-color:#b50000;stop-opacity:1" + offset="1" + id="stop715" /></linearGradient><linearGradient + inkscape:collect="always" + xlink:href="#linearGradient717" + id="linearGradient719" + x1="228.25317" + y1="14.682952" + x2="140.6004" + y2="261.17859" + gradientUnits="userSpaceOnUse" /></defs><g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1"><rect + style="fill:url(#linearGradient719);fill-opacity:1;stroke-width:0.264583" + id="rect115" + width="270.93332" + height="270.93332" + x="0" + y="0" + rx="33.866665" /><path + id="path18329" + style="opacity:1;fill:#ffffff;fill-opacity:1;stroke-width:0.264583" + d="M 81.445158 44.391606 C 71.497762 44.34326 59.998982 45.079969 48.053398 47.437415 C 15.973572 53.768324 -5.0132687e-15 69.410234 0 69.410234 L 0 270.93333 L 126.36231 270.93333 C 126.36231 270.93333 127.25234 270.93314 122.02201 253.61449 C 116.79165 236.29585 123.37369 206.01207 150.06991 203.11525 C 152.12765 202.89196 153.29599 201.80052 153.81697 200.1361 C 163.3758 199.05018 175.31632 198.19731 189.82666 198.01634 C 221.49006 197.62143 257.75843 201.03217 257.75843 201.03217 C 257.75843 201.03217 245.16914 168.19766 229.14105 143.28169 C 212.31497 117.12523 195.16587 101.64392 195.16587 101.64392 C 179.27533 82.720553 145.61897 53.85519 113.77238 47.437415 C 113.77238 47.437415 100.43564 44.483902 81.445158 44.391606 z M 94.104333 114.79403 A 17.130112 17.130112 0 0 1 111.23455 131.92425 A 17.130112 17.130112 0 0 1 94.104333 149.05447 A 17.130112 17.130112 0 0 1 76.974113 131.92425 A 17.130112 17.130112 0 0 1 94.104333 114.79403 z " /></g></svg> diff --git a/users/models/identity.py b/users/models/identity.py index 21912ac..53b6f80 100644 --- a/users/models/identity.py +++ b/users/models/identity.py @@ -162,6 +162,8 @@ class Identity(StatorModel): actor_uri, handle = async_to_sync(cls.fetch_webfinger)( f"{username}@{domain}" ) + if handle is None: + return None username, domain = handle.split("@") domain = Domain.get_remote_domain(domain) return cls.objects.create( diff --git a/users/views/settings.py b/users/views/settings/__init__.py index 1403821..65be1c5 100644 --- a/users/views/settings.py +++ b/users/views/settings/__init__.py @@ -6,10 +6,10 @@ from django.core.files import File from django.shortcuts import redirect from django.utils.decorators import method_decorator from django.views.generic import FormView, RedirectView -from PIL import Image, ImageOps from core.models.config import Config, UploadedImage from users.decorators import identity_required +from users.views.settings.profile import ProfilePage # noqa @method_decorator(identity_required, name="dispatch") @@ -120,59 +120,6 @@ class InterfacePage(SettingsPage): @method_decorator(identity_required, name="dispatch") -class ProfilePage(FormView): - """ - Lets the identity's profile be edited - """ - - template_name = "settings/profile.html" - extra_context = {"section": "profile"} - - class form_class(forms.Form): - name = forms.CharField(max_length=500) - summary = forms.CharField( - widget=forms.Textarea, - required=False, - help_text="Describe you and your interests", - label="Bio", - ) - icon = forms.ImageField( - required=False, help_text="Shown next to all your posts and activities" - ) - image = forms.ImageField( - required=False, help_text="Shown at the top of your profile" - ) - - def get_initial(self): - return { - "name": self.request.identity.name, - "summary": self.request.identity.summary, - "icon": self.request.identity.icon and self.request.identity.icon.url, - "image": self.request.identity.image and self.request.identity.image.url, - } - - def form_valid(self, form): - # Update identity name and summary - self.request.identity.name = form.cleaned_data["name"] - self.request.identity.summary = form.cleaned_data["summary"] - # Resize images - icon = form.cleaned_data.get("icon") - image = form.cleaned_data.get("image") - if isinstance(icon, File): - resized_image = ImageOps.fit(Image.open(icon), (400, 400)) - icon.open() - resized_image.save(icon) - self.request.identity.icon = icon - if isinstance(image, File): - resized_image = ImageOps.fit(Image.open(image), (1500, 500)) - image.open() - resized_image.save(image) - self.request.identity.image = image - self.request.identity.save() - return redirect(".") - - -@method_decorator(identity_required, name="dispatch") class SecurityPage(FormView): """ Lets the identity's profile be edited diff --git a/users/views/settings/profile.py b/users/views/settings/profile.py new file mode 100644 index 0000000..3b16d69 --- /dev/null +++ b/users/views/settings/profile.py @@ -0,0 +1,63 @@ +import io + +from django import forms +from django.core.files import File +from django.shortcuts import redirect +from django.utils.decorators import method_decorator +from django.views.generic import FormView +from PIL import Image, ImageOps + +from users.decorators import identity_required + + +@method_decorator(identity_required, name="dispatch") +class ProfilePage(FormView): + """ + Lets the identity's profile be edited + """ + + template_name = "settings/profile.html" + extra_context = {"section": "profile"} + + class form_class(forms.Form): + name = forms.CharField(max_length=500) + summary = forms.CharField( + widget=forms.Textarea, + required=False, + help_text="Describe you and your interests", + label="Bio", + ) + icon = forms.ImageField( + required=False, help_text="Shown next to all your posts and activities" + ) + image = forms.ImageField( + required=False, help_text="Shown at the top of your profile" + ) + + def get_initial(self): + return { + "name": self.request.identity.name, + "summary": self.request.identity.summary, + "icon": self.request.identity.icon and self.request.identity.icon.url, + "image": self.request.identity.image and self.request.identity.image.url, + } + + def form_valid(self, form): + # Update identity name and summary + self.request.identity.name = form.cleaned_data["name"] + self.request.identity.summary = form.cleaned_data["summary"] + # Resize images + icon = form.cleaned_data.get("icon") + image = form.cleaned_data.get("image") + if isinstance(icon, File): + resized_image = ImageOps.fit(Image.open(icon), (400, 400)) + new_icon_bytes = io.BytesIO() + resized_image.save(new_icon_bytes, format=icon.format) + self.request.identity.icon.save(icon.name, File(new_icon_bytes)) + if isinstance(image, File): + resized_image = ImageOps.fit(Image.open(image), (400, 400)) + new_image_bytes = io.BytesIO() + resized_image.save(new_image_bytes, format=image.format) + self.request.identity.image.save(image.name, File(new_image_bytes)) + self.request.identity.save() + return redirect(".") |