medvednikov

/

plz Public
0 commits 0 issues 0 pull requests 31 contributor Discussions Projects CI

committed 56 years ago · View patch
3 files changed +241 -0
org.v new file +86 -0
@@ -0,0 +1,86 @@
1 +
// Copyright (c) 2019-2026 Alexander Medvednikov. All rights reserved.
2 +
// Use of this source code is governed by a GPL license that can be found in the LICENSE file.
3 +
module main
4 +
5 +
import time
6 +
7 +
struct Org {
8 +
	id            int    @[primary; sql: serial]
9 +
	name          string @[unique]
10 +
	contact_email string
11 +
	kind          string
12 +
	created_at    time.Time
13 +
	created_by    int
14 +
}
15 +
16 +
struct OrgMember {
17 +
	id      int @[primary; sql: serial]
18 +
	org_id  int @[unique: 'org_member']
19 +
	user_id int @[unique: 'org_member']
20 +
	role    string
21 +
}
22 +
23 +
pub fn (mut app App) add_org(name string, contact_email string, kind string, created_by int) !int {
24 +
	new_org := Org{
25 +
		name:          name
26 +
		contact_email: contact_email
27 +
		kind:          kind
28 +
		created_at:    time.now()
29 +
		created_by:    created_by
30 +
	}
31 +
	sql app.db {
32 +
		insert new_org into Org
33 +
	}!
34 +
	row := app.get_org_by_name(name) or { return error('failed to load newly created org') }
35 +
	return row.id
36 +
}
37 +
38 +
pub fn (app App) get_org_by_name(name string) ?Org {
39 +
	rows := sql app.db {
40 +
		select from Org where name == name limit 1
41 +
	} or { [] }
42 +
	if rows.len == 0 {
43 +
		return none
44 +
	}
45 +
	return rows.first()
46 +
}
47 +
48 +
pub fn (app App) get_org_by_id(id int) ?Org {
49 +
	rows := sql app.db {
50 +
		select from Org where id == id limit 1
51 +
	} or { [] }
52 +
	if rows.len == 0 {
53 +
		return none
54 +
	}
55 +
	return rows.first()
56 +
}
57 +
58 +
pub fn (mut app App) add_org_member(org_id int, user_id int, role string) ! {
59 +
	member := OrgMember{
60 +
		org_id:  org_id
61 +
		user_id: user_id
62 +
		role:    role
63 +
	}
64 +
	sql app.db {
65 +
		insert member into OrgMember
66 +
	}!
67 +
}
68 +
69 +
pub fn (app App) find_orgs_for_user(user_id int) []Org {
70 +
	members := sql app.db {
71 +
		select from OrgMember where user_id == user_id
72 +
	} or { [] }
73 +
	mut orgs := []Org{cap: members.len}
74 +
	for m in members {
75 +
		org := app.get_org_by_id(m.org_id) or { continue }
76 +
		orgs << org
77 +
	}
78 +
	return orgs
79 +
}
80 +
81 +
pub fn (app App) is_org_member(org_id int, user_id int) bool {
82 +
	count := sql app.db {
83 +
		select count from OrgMember where org_id == org_id && user_id == user_id
84 +
	} or { 0 }
85 +
	return count > 0
86 +
}
org_routes.v new file +68 -0
@@ -0,0 +1,68 @@
1 +
// Copyright (c) 2019-2026 Alexander Medvednikov. All rights reserved.
2 +
// Use of this source code is governed by a GPL license that can be found in the LICENSE file.
3 +
module main
4 +
5 +
import veb
6 +
import validation
7 +
8 +
@['/organizations/new']
9 +
pub fn (mut app App) new_org(mut ctx Context) veb.Result {
10 +
	if !ctx.logged_in {
11 +
		return ctx.redirect_to_login()
12 +
	}
13 +
	return $veb.html('templates/new/org.html')
14 +
}
15 +
16 +
@['/organizations/new'; post]
17 +
pub fn (mut app App) handle_new_org(mut ctx Context) veb.Result {
18 +
	if !ctx.logged_in {
19 +
		return ctx.redirect_to_login()
20 +
	}
21 +
	org_name := ctx.form['org_name']
22 +
	contact_email := ctx.form['contact_email']
23 +
	org_kind := ctx.form['org_kind']
24 +
	accept_terms := ctx.form['accept_terms'] == '1'
25 +
26 +
	if validation.is_string_empty(org_name) {
27 +
		ctx.error('Organization name is required')
28 +
		return app.new_org(mut ctx)
29 +
	}
30 +
	if org_name.len > max_username_len {
31 +
		ctx.error('The organization name is too long (should be fewer than ${max_username_len} characters)')
32 +
		return app.new_org(mut ctx)
33 +
	}
34 +
	if org_name.contains(' ') {
35 +
		ctx.error('Organization name cannot contain spaces')
36 +
		return app.new_org(mut ctx)
37 +
	}
38 +
	if validation.is_string_empty(contact_email) {
39 +
		ctx.error('Contact email is required')
40 +
		return app.new_org(mut ctx)
41 +
	}
42 +
	if org_kind != 'personal' && org_kind != 'business' {
43 +
		ctx.error('Please select who this organization belongs to')
44 +
		return app.new_org(mut ctx)
45 +
	}
46 +
	if !accept_terms {
47 +
		ctx.error('You must accept the Terms of Service')
48 +
		return app.new_org(mut ctx)
49 +
	}
50 +
	if _ := app.get_user_by_username(org_name) {
51 +
		ctx.error('The name "${org_name}" is already taken')
52 +
		return app.new_org(mut ctx)
53 +
	}
54 +
	if _ := app.get_org_by_name(org_name) {
55 +
		ctx.error('The name "${org_name}" is already taken')
56 +
		return app.new_org(mut ctx)
57 +
	}
58 +
59 +
	org_id := app.add_org(org_name, contact_email, org_kind, ctx.user.id) or {
60 +
		ctx.error('Could not create organization: ${err}')
61 +
		return app.new_org(mut ctx)
62 +
	}
63 +
	app.add_org_member(org_id, ctx.user.id, 'admin') or {
64 +
		ctx.error('Could not add you as the organization owner: ${err}')
65 +
		return app.new_org(mut ctx)
66 +
	}
67 +
	return ctx.redirect('/new?owner=${org_name}')
68 +
}
templates/new/org.html new file +87 -0
@@ -0,0 +1,87 @@
1 +
<!DOCTYPE html>
2 +
<html>
3 +
	<head>
4 +
		@include '../layout/head.html'
5 +
	</head>
6 +
	<body>
7 +
		@include '../layout/header.html'
8 +
9 +
		<div class="content">
10 +
			<div class="new-org">
11 +
				<div class="new-org__header">
12 +
					<p class="new-org__subtitle">%new_org_subtitle</p>
13 +
					<h1>%new_org_title</h1>
14 +
				</div>
15 +
16 +
				@if ctx.form_error != ''
17 +
					<div class='form-error'>@ctx.form_error</div>
18 +
				@end
19 +
20 +
				<form class="new-org__form" method='post' action='/organizations/new'>
21 +
					<div class="new-org__field">
22 +
						<label class="new-org__label" for="org_name">%new_org_name <span class="new-org__req">*</span></label>
23 +
						<input id="org_name" class="new-org__input" type='text' name='org_name' required autofocus maxlength=@max_username_len>
24 +
						<p class="new-org__hint">%new_org_name_hint</p>
25 +
						<p class="new-org__hint">%new_org_url_hint https://@app.config.hostname/<span class="new-org__url-slot" id="url_slot"></span></p>
26 +
					</div>
27 +
28 +
					<div class="new-org__field">
29 +
						<label class="new-org__label" for="contact_email">%new_org_contact_email <span class="new-org__req">*</span></label>
30 +
						<input id="contact_email" class="new-org__input" type='email' name='contact_email' required>
31 +
					</div>
32 +
33 +
					<div class="new-org__field">
34 +
						<p class="new-org__label">%new_org_belongs_to</p>
35 +
						<label class="new-org__option">
36 +
							<input type="radio" name="org_kind" value="personal" required>
37 +
							<span class="new-org__option-body">
38 +
								<span class="new-org__option-title">%new_org_personal</span>
39 +
								<span class="new-org__option-desc">
40 +
								%new_org_personal_example @ctx.user.username
41 +
								@if ctx.user.full_name != ''
42 +
									(@ctx.user.full_name)
43 +
								@end
44 +
							</span>
45 +
							</span>
46 +
						</label>
47 +
						<label class="new-org__option">
48 +
							<input type="radio" name="org_kind" value="business" required>
49 +
							<span class="new-org__option-body">
50 +
								<span class="new-org__option-title">%new_org_business</span>
51 +
								<span class="new-org__option-desc">%new_org_business_example</span>
52 +
							</span>
53 +
						</label>
54 +
					</div>
55 +
56 +
					<div class="new-org__field new-org__field--inline">
57 +
						<label class="new-org__checkbox">
58 +
							<input type="checkbox" id="accept_terms" name="accept_terms" value="1" required>
59 +
							<span>%new_org_accept_terms</span>
60 +
						</label>
61 +
					</div>
62 +
63 +
					<div class="new-org__actions">
64 +
						<input id="new_org_submit" type='submit' class='new-org__submit' value='%new_org_next' disabled>
65 +
					</div>
66 +
				</form>
67 +
			</div>
68 +
		</div>
69 +
70 +
		<script>
71 +
			(function() {
72 +
				var nameInput = document.getElementById('org_name');
73 +
				var slot = document.getElementById('url_slot');
74 +
				var terms = document.getElementById('accept_terms');
75 +
				var submit = document.getElementById('new_org_submit');
76 +
				function syncUrl() { slot.textContent = nameInput.value; }
77 +
				function syncSubmit() { submit.disabled = !terms.checked; }
78 +
				nameInput.addEventListener('input', syncUrl);
79 +
				terms.addEventListener('change', syncSubmit);
80 +
				syncUrl();
81 +
				syncSubmit();
82 +
			})();
83 +
		</script>
84 +
85 +
		@include '../layout/footer.html'
86 +
	</body>
87 +
</html>