From c2a411497196f1eb76ebec782228e7a73657a354 Mon Sep 17 00:00:00 2001 From: SethAngell Date: Sun, 10 Jul 2022 15:45:29 -0400 Subject: [PATCH 1/4] Added Invite Model and forms for sending an invite --- .github/workflows/deploy-to-prod.yml | 48 +++++ .github/workflows/notify.yml | 37 ++++ app/TheCompound/settings.py | 7 + app/TheCompound/urls.py | 1 + app/accounts/forms.py | 9 +- ...omain_name_alter_domain_user_invitation.py | 60 ++++++ app/accounts/models.py | 14 +- .../accounts/emails/plaintext-invite.txt | 11 + .../accounts/emails/styled-invite.html | 11 + .../templates/accounts/invite_user.html | 64 ++++++ app/accounts/urls.py | 7 + app/accounts/views.py | 50 ++++- .../0004_alter_blogpost_parent_blog.py | 21 ++ app/jstoolchain/package-lock.json | 37 ++++ app/jstoolchain/package.json | 1 + app/jstoolchain/tailwind.config.js | 1 + app/project_static/css/tailwind-output.css | 203 ++++++++++++++++-- 17 files changed, 557 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/deploy-to-prod.yml create mode 100644 .github/workflows/notify.yml create mode 100644 app/accounts/migrations/0004_alter_domain_name_alter_domain_user_invitation.py create mode 100644 app/accounts/templates/accounts/emails/plaintext-invite.txt create mode 100644 app/accounts/templates/accounts/emails/styled-invite.html create mode 100644 app/accounts/templates/accounts/invite_user.html create mode 100644 app/accounts/urls.py create mode 100644 app/blog/migrations/0004_alter_blogpost_parent_blog.py diff --git a/.github/workflows/deploy-to-prod.yml b/.github/workflows/deploy-to-prod.yml new file mode 100644 index 0000000..ad3bd35 --- /dev/null +++ b/.github/workflows/deploy-to-prod.yml @@ -0,0 +1,48 @@ +name: Deploy To Production + +on: + workflow_run: + workflows: ["Build and Publish"] + types: + - completed + +jobs: + shut-down-current-version: + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} + steps: + - name: Retrieving up to date docker-compose.yaml + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.SSH_HOST }} + username: ${{ secrets.SSH_USER }} + password: ${{ secrets.SSH_PASSWORD }} + port: ${{ secrets.SSH_PORT }} + script: (cd /home/TheCompound && docker-compose down) + + download-updated-docker-compose: + needs: shut-down-current-version + runs-on: ubuntu-latest + steps: + - name: Retrieving up to date docker-compose.yaml + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.SSH_HOST }} + username: ${{ secrets.SSH_USER }} + password: ${{ secrets.SSH_PASSWORD }} + port: ${{ secrets.SSH_PORT }} + script: curl -o /home/TheCompound/docker-compose.yml https://raw.githubusercontent.com/SethAngell/TheVirtualCompound/main/.ci/docker-compose.yml + + spin-up-new-version-of-the-compound: + needs: [shut-down-current-version, download-updated-docker-compose] + if: always() + runs-on: ubuntu-latest + steps: + - name: Retrieving up to date docker-compose.yaml + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.SSH_HOST }} + username: ${{ secrets.SSH_USER }} + password: ${{ secrets.SSH_PASSWORD }} + port: ${{ secrets.SSH_PORT }} + script: (cd /home/TheCompound && docker-compose up -d) diff --git a/.github/workflows/notify.yml b/.github/workflows/notify.yml new file mode 100644 index 0000000..0315208 --- /dev/null +++ b/.github/workflows/notify.yml @@ -0,0 +1,37 @@ +name: Notify + +# Only trigger, when the build workflow succeeded +on: + workflow_run: + workflows: ["Deploy To Production"] + types: + - completed + +jobs: + see-if-site-is-back-online: + name: healthcheck + runs-on: ubuntu-latest + steps: + - name: Give site some time to collectstatic and deploy + run: sleep 60s + shell: bash + - name: Attempt to connect to personal landing page + run: curl --connect-timeout 30 --retry 10 --retry-delay 5 https://sethangell.com + shell: bash + + send-notification: + runs-on: ubuntu-latest + needs: see-if-site-is-back-online + if: always() + steps: + - name: Post Pipeline Status + uses: twilio-labs/actions-sms@v1 + with: + fromPhoneNumber: ${{ secrets.FROM_NUMBER }} + toPhoneNumber: ${{ secrets.TO_NUMBER }} + message: "The Virtual Compound CI pipeline is complete. Status of Deployment is ${{ github.event.workflow_run.conclusion }}. Status of post deployment healthcheck is ${{ steps.healthcheck.conclusion }}. See https://sethangell.com" + env: + TWILIO_ACCOUNT_SID: ${{ secrets.TWILIO_ACCOUNT_SID }} + TWILIO_API_KEY: ${{ secrets.TWILIO_API_KEY }} + TWILIO_API_SECRET: ${{ secrets.TWILIO_API_SECRET }} + diff --git a/app/TheCompound/settings.py b/app/TheCompound/settings.py index af9c0c8..bb54251 100644 --- a/app/TheCompound/settings.py +++ b/app/TheCompound/settings.py @@ -171,6 +171,13 @@ BASE_DIR / "project_static", ] +# =============================== +# = = = = Email Settings = = = = +if DEBUG is True: + EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" +else: + raise NotImplementedError("Prod Email Is Not Yet Available") + # =============================== # = = = Deployment Settings = = = if DEBUG is False: diff --git a/app/TheCompound/urls.py b/app/TheCompound/urls.py index 6257c09..f7ecf08 100644 --- a/app/TheCompound/urls.py +++ b/app/TheCompound/urls.py @@ -23,6 +23,7 @@ path("admin/", admin.site.urls), path("", include("landing_page.urls")), path("blog/", include("blog.urls")), + path("accounts/", include("accounts.urls")), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/app/accounts/forms.py b/app/accounts/forms.py index fc37e01..06a74c6 100644 --- a/app/accounts/forms.py +++ b/app/accounts/forms.py @@ -1,6 +1,7 @@ +from django import forms from django.contrib.auth.forms import UserChangeForm, UserCreationForm -from .models import CustomUser +from .models import CustomUser, Invitation class CustomUserCreationForm(UserCreationForm): @@ -19,3 +20,9 @@ class Meta: "email", "name", ) + + +class NewInvitationForm(forms.ModelForm): + class Meta: + model = Invitation + fields = ("linked_domain", "email") diff --git a/app/accounts/migrations/0004_alter_domain_name_alter_domain_user_invitation.py b/app/accounts/migrations/0004_alter_domain_name_alter_domain_user_invitation.py new file mode 100644 index 0000000..856cd1c --- /dev/null +++ b/app/accounts/migrations/0004_alter_domain_name_alter_domain_user_invitation.py @@ -0,0 +1,60 @@ +# Generated by Django 4.0.2 on 2022-07-10 15:51 + +import uuid + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("accounts", "0003_customuser_name"), + ] + + operations = [ + migrations.AlterField( + model_name="domain", + name="name", + field=models.CharField(max_length=128), + ), + migrations.AlterField( + model_name="domain", + name="user", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.CreateModel( + name="Invitation", + fields=[ + ( + "email", + models.EmailField( + max_length=254, + primary_key=True, + serialize=False, + unique=True, + verbose_name="email address", + ), + ), + ( + "invitation_code", + models.UUIDField(default=uuid.uuid4, editable=False), + ), + ( + "linked_domain", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="accounts.domain", + ), + ), + ], + ), + ] diff --git a/app/accounts/models.py b/app/accounts/models.py index dc3d1c8..fd0f91a 100644 --- a/app/accounts/models.py +++ b/app/accounts/models.py @@ -1,3 +1,5 @@ +import uuid + from django.conf import settings from django.contrib.auth.models import AbstractUser from django.db import models @@ -21,8 +23,18 @@ def __str__(self): class Domain(models.Model): - user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True + ) name = models.CharField(max_length=128) def __str__(self): return self.name + + +class Invitation(models.Model): + email = models.EmailField(_("email address"), unique=True, primary_key=True) + invitation_code = models.UUIDField(default=uuid.uuid4, editable=False) + linked_domain = models.ForeignKey( + Domain, on_delete=models.CASCADE, blank=True, null=True + ) diff --git a/app/accounts/templates/accounts/emails/plaintext-invite.txt b/app/accounts/templates/accounts/emails/plaintext-invite.txt new file mode 100644 index 0000000..6c9cca8 --- /dev/null +++ b/app/accounts/templates/accounts/emails/plaintext-invite.txt @@ -0,0 +1,11 @@ +Hey! Welcome To The Site + +Create an account here link-to-web.site + +Youre unique signup code is {{ invite.invitation_code }} + +Make sure to signup with your emails address: {{ invite.email }} + +Your site will be available at https://{{ invite.domain.name }} + +Bye Now! \ No newline at end of file diff --git a/app/accounts/templates/accounts/emails/styled-invite.html b/app/accounts/templates/accounts/emails/styled-invite.html new file mode 100644 index 0000000..c7ddb1e --- /dev/null +++ b/app/accounts/templates/accounts/emails/styled-invite.html @@ -0,0 +1,11 @@ +

Hey! Welcome To The Site

+ +

Create an account here link-to-web.site

+ +

Youre unique signup code is {{ invite.invitation_code }}

+ +

Make sure to signup with your emails address: {{ invite.email }}

+ +

Your site will be available at https://{{ invite.linked_domain.name }}

+ +Bye Now! \ No newline at end of file diff --git a/app/accounts/templates/accounts/invite_user.html b/app/accounts/templates/accounts/invite_user.html new file mode 100644 index 0000000..f8b902b --- /dev/null +++ b/app/accounts/templates/accounts/invite_user.html @@ -0,0 +1,64 @@ +{% extends "_base.html" %} +{% load static %} +{% load timeline_helper %} +{% block meta %} + + + +{% endblock meta %} + +{% block links %} + +{% endblock %} +{% block style %} + h1, h2, a { + font-family: 'Rubik', sans-serif; + } + + p { + font-family: 'Karla', sans-serif; + } + +{% endblock %} +{% block font %} + + +{% endblock %} +{% block title %}New Invite{% endblock title %} +{% block content %} +
+
+
+ + + +

It's kinda quiet in here. Let's invite some people!

+
+ +
+ {% csrf_token %} +
+ + + + +
+
+
+
+ +{% endblock content %} \ No newline at end of file diff --git a/app/accounts/urls.py b/app/accounts/urls.py new file mode 100644 index 0000000..6833588 --- /dev/null +++ b/app/accounts/urls.py @@ -0,0 +1,7 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + path("new_invite/", views.create_new_invite, name="new_user_invitation"), +] diff --git a/app/accounts/views.py b/app/accounts/views.py index 91ea44a..c841f9f 100644 --- a/app/accounts/views.py +++ b/app/accounts/views.py @@ -1,3 +1,51 @@ +from xml import dom + +from django.core.mail import EmailMultiAlternatives +from django.http import HttpResponseRedirect from django.shortcuts import render +from django.template import Context +from django.template.loader import get_template + +from .forms import NewInvitationForm +from .models import Domain, Invitation + + +def create_new_invite(request): + # if this is a POST request we need to process the form data + if request.method == "POST": + # create a form instance and populate it with data from the request: + form = NewInvitationForm(request.POST) + # check whether it's valid: + if form.is_valid(): + # process the data in form.cleaned_data as required + print("Hi!") + new_invite = form.save() + send_invite_email(new_invite) + # redirect to a new URL: + return HttpResponseRedirect("/") + else: + print(form.errors) + + # if a GET (or any other method) we'll create a blank form + else: + form = NewInvitationForm() + + domains = Domain.objects.filter(user=None) + context = {"form": form, "domains": domains} + + return render(request, "accounts/invite_user.html", context) + + +def send_invite_email(new_invitation): + print("email sent!") + plaintext = get_template("accounts/emails/plaintext-invite.txt") + htmly = get_template("accounts/emails/styled-invite.html") + + d = {"invite": new_invitation} -# Create your views here. + subject, from_email, to = "Welcome!", "seth@doublel.studio", new_invitation.email + text_content = plaintext.render(d) + html_content = htmly.render(d) + msg = EmailMultiAlternatives(subject, text_content, from_email, [to]) + msg.attach_alternative(html_content, "text/html") + msg.send() diff --git a/app/blog/migrations/0004_alter_blogpost_parent_blog.py b/app/blog/migrations/0004_alter_blogpost_parent_blog.py new file mode 100644 index 0000000..42afa53 --- /dev/null +++ b/app/blog/migrations/0004_alter_blogpost_parent_blog.py @@ -0,0 +1,21 @@ +# Generated by Django 4.0.2 on 2022-07-10 15:51 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("blog", "0003_blogpost_parent_blog_alter_blog_blog_description"), + ] + + operations = [ + migrations.AlterField( + model_name="blogpost", + name="parent_blog", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="blog.blog" + ), + ), + ] diff --git a/app/jstoolchain/package-lock.json b/app/jstoolchain/package-lock.json index 831abdd..2f7fbcd 100644 --- a/app/jstoolchain/package-lock.json +++ b/app/jstoolchain/package-lock.json @@ -12,6 +12,7 @@ "tailwindcss": "^3.1.4" }, "devDependencies": { + "@tailwindcss/forms": "^0.5.2", "@tailwindcss/typography": "^0.5.2" } }, @@ -47,6 +48,18 @@ "node": ">= 8" } }, + "node_modules/@tailwindcss/forms": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.2.tgz", + "integrity": "sha512-pSrFeJB6Bg1Mrg9CdQW3+hqZXAKsBrSG9MAfFLKy1pVA4Mb4W7C0k7mEhlmS2Dfo/otxrQOET7NJiJ9RrS563w==", + "dev": true, + "dependencies": { + "mini-svg-data-uri": "^1.2.3" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1" + } + }, "node_modules/@tailwindcss/typography": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.2.tgz", @@ -636,6 +649,15 @@ "node": ">=8.6" } }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "dev": true, + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, "node_modules/minimist": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", @@ -1245,6 +1267,15 @@ "fastq": "^1.6.0" } }, + "@tailwindcss/forms": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.2.tgz", + "integrity": "sha512-pSrFeJB6Bg1Mrg9CdQW3+hqZXAKsBrSG9MAfFLKy1pVA4Mb4W7C0k7mEhlmS2Dfo/otxrQOET7NJiJ9RrS563w==", + "dev": true, + "requires": { + "mini-svg-data-uri": "^1.2.3" + } + }, "@tailwindcss/typography": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.2.tgz", @@ -1639,6 +1670,12 @@ "picomatch": "^2.3.1" } }, + "mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "dev": true + }, "minimist": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", diff --git a/app/jstoolchain/package.json b/app/jstoolchain/package.json index ae68a87..9f8adf1 100644 --- a/app/jstoolchain/package.json +++ b/app/jstoolchain/package.json @@ -10,6 +10,7 @@ "tailwindcss": "^3.1.4" }, "devDependencies": { + "@tailwindcss/forms": "^0.5.2", "@tailwindcss/typography": "^0.5.2" } } diff --git a/app/jstoolchain/tailwind.config.js b/app/jstoolchain/tailwind.config.js index 26a12a7..2208731 100644 --- a/app/jstoolchain/tailwind.config.js +++ b/app/jstoolchain/tailwind.config.js @@ -17,6 +17,7 @@ module.exports = { }, teal: colors.teal, slate: colors.slate, + pink: colors.pink, }, extend: {}, diff --git a/app/project_static/css/tailwind-output.css b/app/project_static/css/tailwind-output.css index 19f4a2f..eb9e137 100644 --- a/app/project_static/css/tailwind-output.css +++ b/app/project_static/css/tailwind-output.css @@ -1047,6 +1047,9 @@ video { --tw-prose-invert-th-borders: #475569; --tw-prose-invert-td-borders: #334155; } +.invisible { + visibility: hidden; +} .static { position: static; } @@ -1077,6 +1080,15 @@ video { .mt-5 { margin-top: 1.25rem; } +.mb-5 { + margin-bottom: 1.25rem; +} +.mt-1 { + margin-top: 0.25rem; +} +.mt-2 { + margin-top: 0.5rem; +} .mt-3 { margin-top: 0.75rem; } @@ -1104,6 +1116,14 @@ video { .h-screen { height: 100vh; } +.h-fit { + height: -webkit-fit-content; + height: -moz-fit-content; + height: fit-content; +} +.h-6 { + height: 1.5rem; +} .h-full { height: 100%; } @@ -1113,14 +1133,17 @@ video { .w-11\/12 { width: 91.666667%; } -.w-full { - width: 100%; -} .w-fit { width: -webkit-fit-content; width: -moz-fit-content; width: fit-content; } +.w-6 { + width: 1.5rem; +} +.w-full { + width: 100%; +} .w-44 { width: 11rem; } @@ -1142,6 +1165,11 @@ video { .basis-1\/2 { flex-basis: 50%; } +.appearance-none { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} .grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } @@ -1154,6 +1182,9 @@ video { .content-center { align-content: center; } +.items-start { + align-items: flex-start; +} .items-center { align-items: center; } @@ -1169,6 +1200,11 @@ video { .gap-y-2 { row-gap: 0.5rem; } +.space-x-1 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0.25rem * var(--tw-space-x-reverse)); + margin-left: calc(0.25rem * calc(1 - var(--tw-space-x-reverse))); +} .space-x-2 > :not([hidden]) ~ :not([hidden]) { --tw-space-x-reverse: 0; margin-right: calc(0.5rem * var(--tw-space-x-reverse)); @@ -1189,23 +1225,34 @@ video { margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); margin-bottom: calc(1rem * var(--tw-space-y-reverse)); } +.rounded-lg { + border-radius: 0.5rem; +} .rounded-md { border-radius: 0.375rem; } -.rounded-lg { - border-radius: 0.5rem; +.rounded-full { + border-radius: 9999px; } .rounded { border-radius: 0.25rem; } -.rounded-full { - border-radius: 9999px; +.border-2 { + border-width: 2px; +} +.border { + border-width: 1px; } .border-4 { border-width: 4px; } -.border-2 { - border-width: 2px; +.border-seth-blue-600 { + --tw-border-opacity: 1; + border-color: rgb(5 5 107 / var(--tw-border-opacity)); +} +.border-slate-300 { + --tw-border-opacity: 1; + border-color: rgb(203 213 225 / var(--tw-border-opacity)); } .border-slate-800 { --tw-border-opacity: 1; @@ -1219,6 +1266,10 @@ video { --tw-bg-opacity: 1; background-color: rgb(248 250 252 / var(--tw-bg-opacity)); } +.bg-slate-500 { + --tw-bg-opacity: 1; + background-color: rgb(100 116 139 / var(--tw-bg-opacity)); +} .bg-slate-800 { --tw-bg-opacity: 1; background-color: rgb(30 41 59 / var(--tw-bg-opacity)); @@ -1243,12 +1294,27 @@ video { -o-object-fit: cover; object-fit: cover; } +.p-10 { + padding: 2.5rem; +} .p-8 { padding: 2rem; } .p-2 { padding: 0.5rem; } +.px-3 { + padding-left: 0.75rem; + padding-right: 0.75rem; +} +.py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} +.px-5 { + padding-left: 1.25rem; + padding-right: 1.25rem; +} .px-2 { padding-left: 0.5rem; padding-right: 0.5rem; @@ -1261,14 +1327,6 @@ video { padding-left: 1rem; padding-right: 1rem; } -.px-5 { - padding-left: 1.25rem; - padding-right: 1.25rem; -} -.py-2 { - padding-top: 0.5rem; - padding-bottom: 0.5rem; -} .px-8 { padding-left: 2rem; padding-right: 2rem; @@ -1297,6 +1355,10 @@ video { .text-center { text-align: center; } +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} .text-3xl { font-size: 1.875rem; line-height: 2.25rem; @@ -1317,26 +1379,41 @@ video { font-size: 1.25rem; line-height: 1.75rem; } +.font-medium { + font-weight: 500; +} +.font-semibold { + font-weight: 600; +} .font-bold { font-weight: 700; } .font-black { font-weight: 900; } -.font-semibold { - font-weight: 600; -} .leading-5 { line-height: 1.25rem; } -.text-slate-800 { +.text-slate-900 { --tw-text-opacity: 1; - color: rgb(30 41 59 / var(--tw-text-opacity)); + color: rgb(15 23 42 / var(--tw-text-opacity)); +} +.text-slate-700 { + --tw-text-opacity: 1; + color: rgb(51 65 85 / var(--tw-text-opacity)); +} +.text-pink-600 { + --tw-text-opacity: 1; + color: rgb(219 39 119 / var(--tw-text-opacity)); } .text-slate-50 { --tw-text-opacity: 1; color: rgb(248 250 252 / var(--tw-text-opacity)); } +.text-slate-800 { + --tw-text-opacity: 1; + color: rgb(30 41 59 / var(--tw-text-opacity)); +} .text-seth-blue-400 { --tw-text-opacity: 1; color: rgb(5 56 107 / var(--tw-text-opacity)); @@ -1349,6 +1426,19 @@ video { --tw-text-opacity: 1; color: rgb(226 232 240 / var(--tw-text-opacity)); } +.placeholder-slate-400::-moz-placeholder { + --tw-placeholder-opacity: 1; + color: rgb(148 163 184 / var(--tw-placeholder-opacity)); +} +.placeholder-slate-400::placeholder { + --tw-placeholder-opacity: 1; + color: rgb(148 163 184 / var(--tw-placeholder-opacity)); +} +.shadow-sm { + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} .shadow-2xl { --tw-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25); --tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color); @@ -1366,6 +1456,14 @@ video { .ring-offset-4 { --tw-ring-offset-width: 4px; } +.invalid\:border-pink-500:invalid { + --tw-border-opacity: 1; + border-color: rgb(236 72 153 / var(--tw-border-opacity)); +} +.invalid\:text-pink-600:invalid { + --tw-text-opacity: 1; + color: rgb(219 39 119 / var(--tw-text-opacity)); +} .hover\:bg-slate-600:hover { --tw-bg-opacity: 1; background-color: rgb(71 85 105 / var(--tw-bg-opacity)); @@ -1395,6 +1493,56 @@ video { transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; } +.focus\:outline-none:focus { + outline: 2px solid transparent; + outline-offset: 2px; +} +.focus\:ring-1:focus { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} +.focus\:ring:focus { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} +.focus\:ring-pink-300:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(249 168 212 / var(--tw-ring-opacity)); +} +.focus\:invalid\:border-pink-500:invalid:focus { + --tw-border-opacity: 1; + border-color: rgb(236 72 153 / var(--tw-border-opacity)); +} +.focus\:invalid\:ring-pink-500:invalid:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(236 72 153 / var(--tw-ring-opacity)); +} +.active\:bg-slate-700:active { + --tw-bg-opacity: 1; + background-color: rgb(51 65 85 / var(--tw-bg-opacity)); +} +.disabled\:border-slate-200:disabled { + --tw-border-opacity: 1; + border-color: rgb(226 232 240 / var(--tw-border-opacity)); +} +.disabled\:bg-slate-50:disabled { + --tw-bg-opacity: 1; + background-color: rgb(248 250 252 / var(--tw-bg-opacity)); +} +.disabled\:text-slate-500:disabled { + --tw-text-opacity: 1; + color: rgb(100 116 139 / var(--tw-text-opacity)); +} +.disabled\:shadow-none:disabled { + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} +.peer:invalid ~ .peer-invalid\:visible { + visibility: visible; +} @media (prefers-color-scheme: dark) { .dark\:prose-invert { @@ -1480,6 +1628,13 @@ video { color: rgb(148 163 184 / var(--tw-text-opacity)); } } +@media (min-width: 640px) { + + .sm\:text-sm { + font-size: 0.875rem; + line-height: 1.25rem; + } +} @media (min-width: 768px) { .md\:grid-cols-3 { @@ -1504,6 +1659,10 @@ video { height: min-content; } + .lg\:w-3\/6 { + width: 50%; + } + .lg\:w-3\/5 { width: 60%; } From b807f51ccc4e36cd0792482bab374dcbc0c8b3b8 Mon Sep 17 00:00:00 2001 From: SethAngell Date: Mon, 22 Aug 2022 16:50:13 -0400 Subject: [PATCH 2/4] Fixed Error Message Flow on new user signup --- app/accounts/forms.py | 8 ++ .../templates/accounts/accept_invite.html | 101 ++++++++++++++++++ app/accounts/urls.py | 1 + app/accounts/views.py | 75 ++++++++++++- 4 files changed, 182 insertions(+), 3 deletions(-) create mode 100644 app/accounts/templates/accounts/accept_invite.html diff --git a/app/accounts/forms.py b/app/accounts/forms.py index 06a74c6..bcc5f57 100644 --- a/app/accounts/forms.py +++ b/app/accounts/forms.py @@ -26,3 +26,11 @@ class NewInvitationForm(forms.ModelForm): class Meta: model = Invitation fields = ("linked_domain", "email") + + +class NewUserFromInviteForm(forms.Form): + email = forms.EmailField() + name = forms.CharField(max_length=128) + invite_code = forms.CharField(max_length=128) + password1 = forms.CharField(widget=forms.PasswordInput()) + password2 = forms.CharField(widget=forms.PasswordInput()) diff --git a/app/accounts/templates/accounts/accept_invite.html b/app/accounts/templates/accounts/accept_invite.html new file mode 100644 index 0000000..983341b --- /dev/null +++ b/app/accounts/templates/accounts/accept_invite.html @@ -0,0 +1,101 @@ +{% extends "_base.html" %} +{% load static %} +{% load timeline_helper %} +{% block meta %} + + + +{% endblock meta %} + +{% block links %} + +{% endblock %} +{% block style %} + h1, h2, a { + font-family: 'Rubik', sans-serif; + } + + p { + font-family: 'Karla', sans-serif; + } + +{% endblock %} +{% block font %} + + +{% endblock %} +{% block title %}Accept Invite{% endblock title %} +{% block content %} +{% if errors %} + +{% endif %} +
+
+
+

🤫 Shhhhhhh....What's the secret password?

+
+ +
+ {% csrf_token %} +
+ + + + + + +
+
+
+
+ + +{% endblock content %} \ No newline at end of file diff --git a/app/accounts/urls.py b/app/accounts/urls.py index 6833588..5cc8466 100644 --- a/app/accounts/urls.py +++ b/app/accounts/urls.py @@ -4,4 +4,5 @@ urlpatterns = [ path("new_invite/", views.create_new_invite, name="new_user_invitation"), + path("accept_invite/", views.validate_invited_user, name="accept_invitation"), ] diff --git a/app/accounts/views.py b/app/accounts/views.py index c841f9f..e63cd97 100644 --- a/app/accounts/views.py +++ b/app/accounts/views.py @@ -1,13 +1,14 @@ from xml import dom +from django.contrib.auth import authenticate, login, logout from django.core.mail import EmailMultiAlternatives from django.http import HttpResponseRedirect -from django.shortcuts import render +from django.shortcuts import redirect, render from django.template import Context from django.template.loader import get_template -from .forms import NewInvitationForm -from .models import Domain, Invitation +from .forms import CustomUserCreationForm, NewInvitationForm, NewUserFromInviteForm +from .models import CustomUser, Domain, Invitation def create_new_invite(request): @@ -36,6 +37,52 @@ def create_new_invite(request): return render(request, "accounts/invite_user.html", context) +def validate_invited_user(request): + errors = "" + if request.method == "POST": + form = NewUserFromInviteForm(request.POST) + print("POST REQUEST") + if form.is_valid(): + print("VALID FORM") + user_email = form.cleaned_data["email"] + invite_code = form.cleaned_data["invite_code"] + + try: + invitation = Invitation.objects.get(email=user_email) + print("VALID EMAIL") + except Invitation.DoesNotExist: + print("INVALID EMAIL") + invitation = None + + errors = "Invalid Email: No email address found, make sure you use the same email that your invitation was sent to" + context = {"form": form, "errors": errors} + return render(request, "accounts/accept_invite.html", context) + + if invitation is not None and invite_code != invitation.invitation_code: + print("INVALID EMAIL CODE") + errors = "Invalid Invite Code: Make sure you copied that code properly!" + elif invitation is not None and invite_code == invitation.invitation_code: + print("VALID EMAIL CODE") + NewUser = _create_user(request) + if NewUser is not None: + invitation.linked_domain.user = NewUser + invitation.linked_domain.save() + else: + errors = "Invalid User: Failed to create new user. Go bug seth" + + if len(errors) == 0: + print("You got yourself a new user!") + redirect("home") + + else: + errors = form.errors + + form = NewUserFromInviteForm() + context = {"form": form, "errors": errors} + + return render(request, "accounts/accept_invite.html", context) + + def send_invite_email(new_invitation): print("email sent!") plaintext = get_template("accounts/emails/plaintext-invite.txt") @@ -49,3 +96,25 @@ def send_invite_email(new_invitation): msg = EmailMultiAlternatives(subject, text_content, from_email, [to]) msg.attach_alternative(html_content, "text/html") msg.send() + + +# ====================================================================================== # +# Helper Functions + + +def _create_user(request): + NewUserForm = CustomUserCreationForm(request.POST) + + if NewUserForm.is_valid(): + user = NewUserForm.save() + raw_password = NewUserForm.cleaned_data.get("password1") + user = authenticate(request, email=user.email, password=raw_password) + + if user is not None: + login(request, user) + else: + print("user is not authenticated") + + return user + else: + return None From 283bb0acf96e41327bf9d62131e77c2d6e311016 Mon Sep 17 00:00:00 2001 From: SethAngell Date: Mon, 22 Aug 2022 21:15:13 -0400 Subject: [PATCH 3/4] User Creation flow nearing completion --- app/accounts/admin.py | 7 ++- .../templates/accounts/accept_invite.html | 10 ++-- app/accounts/views.py | 47 +++++++++++-------- 3 files changed, 39 insertions(+), 25 deletions(-) diff --git a/app/accounts/admin.py b/app/accounts/admin.py index c344989..18bbc2d 100644 --- a/app/accounts/admin.py +++ b/app/accounts/admin.py @@ -2,7 +2,7 @@ from django.contrib.auth.admin import UserAdmin from .forms import CustomUserChangeForm, CustomUserCreationForm -from .models import CustomUser, Domain +from .models import CustomUser, Domain, Invitation class CustomUserAdmin(UserAdmin): @@ -49,5 +49,10 @@ class DomainAdmin(admin.ModelAdmin): pass +class InvitationAdmin(admin.ModelAdmin): + pass + + admin.site.register(CustomUser, CustomUserAdmin) admin.site.register(Domain, DomainAdmin) +admin.site.register(Invitation, InvitationAdmin) diff --git a/app/accounts/templates/accounts/accept_invite.html b/app/accounts/templates/accounts/accept_invite.html index 983341b..27c8137 100644 --- a/app/accounts/templates/accounts/accept_invite.html +++ b/app/accounts/templates/accounts/accept_invite.html @@ -38,7 +38,7 @@

🤫 Shhhhhhh....What's the secret password?

{% csrf_token %}
-