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>