Skip to content

Commit

Permalink
Very basic "create_site" method, with tests
Browse files Browse the repository at this point in the history
  • Loading branch information
audiodude committed Nov 5, 2023
1 parent 684a383 commit ae5d792
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 27 deletions.
2 changes: 1 addition & 1 deletion rainfall-frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import NavBar from './components/NavBar.vue';
<NavBar />
</header>

<RouterView class="container max-w-screen-xl mx-auto grow shrink-0" />
<RouterView class="container max-w-screen-xl mx-auto grow shrink-0 p-4" />

<footer class="border-gray-200 shadow bg-gray-800 shrink">
<div class="w-full mx-auto max-w-screen-xl p-4 md:flex md:items-center md:justify-between">
Expand Down
66 changes: 66 additions & 0 deletions rainfall-frontend/src/components/NewSite.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<script lang="ts">
export default {
data() {
return {
createError: false,
name: '',
};
},
methods: {
async createSite() {
this.createError = false;
const resp = await fetch('/api/v1/site', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
site: {
name: this.name || 'My Cool Site',
},
}),
});
if (!resp.ok) {
setTimeout(() => {
this.createError = true;
}, 250);
}
this.$emit('site-created');
},
},
};
</script>

<template>
<div>
<div class="md:max-w-screen-md">
<div>
<label for="first_name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
>Site name</label
>
<input
v-model="name"
type="text"
class="max-w-xl bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="My Cool Site"
maxlength="255"
required
/>
</div>
<p class="text-xs italic">
The title of your site is for your reference only and doesn't affect the output
</p>
<button
@click="createSite"
class="cursor-pointer mt-4 disabled:cursor-auto bg-transparent hover:bg-blue-500 disabled:hover:bg-transparent font-semibold hover:text-white disabled:hover:dark:text-gray-300 py-2 px-4 border border-blue-500 hover:border-transparent disabled:hover:border-blue-500 rounded"
>
Create site
</button>
<p v-if="createError" class="text-sm text-red-500">
Something went wrong while creating your site.
</p>
</div>
</div>
</template>

<style></style>
26 changes: 5 additions & 21 deletions rainfall-frontend/src/views/SitesView.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<script lang="ts">
import { mapStores } from 'pinia';
import { useUserStore } from '../stores/user';
import NewSite from '../components/NewSite.vue';
export default {
components: { NewSite },
data() {
return {
createError: false,
Expand All @@ -24,22 +26,14 @@ export default {
...mapStores(useUserStore),
},
methods: {
async createSite() {
this.createError = false;
const resp = await fetch('/api/v1/site/create');
if (!resp.ok) {
setTimeout(() => {
this.createError = true;
}, 250);
}
},
reloadSites() {},
},
};
</script>

<template>
<div>
<div class="md:max-w-screen-md p-4">
<div class="md:max-w-screen-md">
<p class="mt-4">
Rainfall uses <a href="https://simonrepp.com/faircamp/">Faircamp</a> to generate your music
website. The advantage of using Rainfall versus using Faircamp directly is that you can
Expand All @@ -49,18 +43,8 @@ export default {
<p v-if="!site" class="mt-4">
When you're ready, click "Create site" to start working on your website.
</p>
<div class="mt-4">
<button
@click="createSite"
class="cursor-pointer disabled:cursor-auto bg-transparent hover:bg-blue-500 disabled:hover:bg-transparent font-semibold hover:text-white disabled:hover:dark:text-gray-300 py-2 px-4 border border-blue-500 hover:border-transparent disabled:hover:border-blue-500 rounded"
>
Create site
</button>
<p v-if="createError" class="text-sm text-red-500">
Something went wrong while creating your site.
</p>
</div>
</div>
<NewSite @site-created="reloadSites()" class="mt-4" />
</div>
</template>

Expand Down
4 changes: 2 additions & 2 deletions rainfall-frontend/src/views/WelcomeView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default {
const resp = await fetch('/api/v1/user/welcome', { method: 'POST' });
if (resp.ok) {
await this.userStore.loadUser(/* force */ true);
this.$router.push('/new');
this.$router.push('//sites');
}
},
},
Expand All @@ -36,7 +36,7 @@ export default {

<template>
<div>
<div class="md:max-w-screen-md p-4">
<div class="md:max-w-screen-md pl-4 mt-4">
<h1 class="text-3xl mt-4">Welcome!</h1>
<p class="mt-4">
<em>Rainfall</em> is an app that let's you create a website for your music. If you're an
Expand Down
13 changes: 11 additions & 2 deletions rainfall/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,23 @@ def welcome(user):

return '', 204

@app.route('/api/v1/site/create', methods=['POST'])
@app.route('/api/v1/site', methods=['POST'])
@with_current_user
def create_site(user):
if not user.is_welcomed:
return flask.jsonify(status=400,
error='User has not yet been welcomed'), 400

user.sites.append(Site())
data = flask.request.get_json()
if data is None:
return flask.jsonify(status=400, error='No JSON provided'), 400
site_data = data.get('site')
if site_data is None:
return flask.jsonify(status=400, error='Missing site data'), 400
if site_data.get('name') is None:
return flask.jsonify(status=400, error='Site name is required'), 400

user.sites.append(Site(**site_data))
db.session.add(user)
db.session.commit()

Expand Down
58 changes: 58 additions & 0 deletions rainfall/main_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ def basic_user(app):
return basic_user


@pytest.fixture
def welcomed_user(app, basic_user):
with app.app_context():
basic_user.is_welcomed = True
db.session.add(basic_user)
db.session.commit()

return basic_user


class MainTest:

def test_get_user(self, app, basic_user):
Expand Down Expand Up @@ -138,3 +148,51 @@ def test_welcome_no_user(self, app, basic_user):
rv = client.post('/api/v1/user/welcome')
assert rv.status == '404 NOT FOUND'
assert rv.json == {'status': 404, 'error': 'No signed in user'}

def test_create_site(self, app, welcomed_user):
with app.test_client() as client:
with client.session_transaction() as sess:
sess['user_id'] = BASIC_USER_ID

rv = client.post('/api/v1/site', json={'site': {'name': 'Some site'}})
assert rv.status == '204 NO CONTENT'

with app.app_context():
user = db.session.get(User, BASIC_USER_ID)
sites = user.sites
assert len(sites) == 1
assert sites[0].name == 'Some site'

def test_create_site_no_user(self, app):
with app.test_client() as client:
rv = client.post('/api/v1/site', json={'site': {'name': 'Some site'}})
assert rv.status == '404 NOT FOUND'

def test_create_site_no_user_in_session(self, app, welcomed_user):
with app.test_client() as client:
rv = client.post('/api/v1/site', json={'site': {'name': 'Some site'}})
assert rv.status == '404 NOT FOUND'

def test_create_site_no_json(self, app, welcomed_user):
with app.test_client() as client:
with client.session_transaction() as sess:
sess['user_id'] = BASIC_USER_ID

rv = client.post('/api/v1/site')
assert rv.status == '415 UNSUPPORTED MEDIA TYPE'

def test_create_site_missing_site(self, app, welcomed_user):
with app.test_client() as client:
with client.session_transaction() as sess:
sess['user_id'] = BASIC_USER_ID

rv = client.post('/api/v1/site', json={'foo': 'bar'})
assert rv.status == '400 BAD REQUEST'

def test_create_site_missing_name(self, app, welcomed_user):
with app.test_client() as client:
with client.session_transaction() as sess:
sess['user_id'] = BASIC_USER_ID

rv = client.post('/api/v1/site', json={'site': {}})
assert rv.status == '400 BAD REQUEST'
3 changes: 2 additions & 1 deletion rainfall/models/site.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.types import Uuid
from sqlalchemy.types import Uuid, String
from uuid_extensions import uuid7

from rainfall.db import db
Expand All @@ -16,6 +16,7 @@ class Site(db.Model):
id: Mapped[bytes] = mapped_column(Uuid, primary_key=True, default=uuid7)
user_id: Mapped[bytes] = mapped_column(ForeignKey("users.id"))
user: Mapped["User"] = relationship(back_populates="sites")
name: Mapped[str] = mapped_column(String(255))

def __repr__(self) -> str:
return f'Site(id={self.id!r}, user_id={self.google_id!r})'

0 comments on commit ae5d792

Please sign in to comment.