Skip to content

Commit

Permalink
content authorship wip
Browse files Browse the repository at this point in the history
  • Loading branch information
iximiuz committed Mar 17, 2024
1 parent 475ff21 commit 550b2e6
Show file tree
Hide file tree
Showing 8 changed files with 347 additions and 41 deletions.
41 changes: 40 additions & 1 deletion cmd/content/content.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,60 @@
package content

import (
"fmt"

"github.com/spf13/cobra"
"github.com/spf13/pflag"

"github.com/iximiuz/labctl/internal/labcli"
)

func NewCommand(cli labcli.CLI) *cobra.Command {
cmd := &cobra.Command{
Use: "content <create|pull|files|sync|rm> <content-name> [flags]",
Use: "content <create|list|pull|files|sync|rm> <content-name> [flags]",
Aliases: []string{"c", "contents"},
Short: "Authoring and managing content (challenge, tutorial, course, etc.)",
}

cmd.AddCommand(
newCreateCommand(cli),
newListCommand(cli),
newSyncCommand(cli),
newRemoveCommand(cli),
)

return cmd
}

type ContentKind string

var _ pflag.Value = (*ContentKind)(nil)

const (
KindChallenge ContentKind = "challenge"
KindTutorial ContentKind = "tutorial"
KindCourse ContentKind = "course"
)

func (k *ContentKind) Set(v string) error {
switch string(v) {
case string(KindChallenge):
*k = KindChallenge
case string(KindTutorial):
*k = KindTutorial
case string(KindCourse):
*k = KindCourse
default:
return fmt.Errorf("unknown content kind: %s", v)
}

return nil
}

func (k *ContentKind) String() string {
return string(*k)
}

func (k *ContentKind) Type() string {
return "content-kind"
}
42 changes: 6 additions & 36 deletions cmd/content/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,6 @@ import (
"github.com/iximiuz/labctl/internal/labcli"
)

type ContentKind string

const (
KindChallenge ContentKind = "challenge"
KindTutorial ContentKind = "tutorial"
KindCourse ContentKind = "course"
)

func (k *ContentKind) UnmarshalText(text []byte) error {
switch string(text) {
case "challenge":
*k = KindChallenge
case "tutorial":
*k = KindTutorial
case "course":
*k = KindCourse
default:
return fmt.Errorf("unknown content kind: %s", text)
}

return nil
}

type createOptions struct {
kind ContentKind
name string
Expand All @@ -56,7 +33,7 @@ func newCreateCommand(cli labcli.CLI) *cobra.Command {
Short: "Create a new piece of content (visible only to the author)",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
if err := opts.kind.UnmarshalText([]byte(args[0])); err != nil {
if err := opts.kind.Set(args[0]); err != nil {
return labcli.WrapStatusError(err)
}
opts.name = args[1]
Expand Down Expand Up @@ -152,7 +129,7 @@ func createChallenge(ctx context.Context, cli labcli.CLI, opts *createOptions) e
Kind: "challenge",
Name: opts.name,
Content: `---
title: Sample Challenge
title: Sample Challenge 444
description: |
This is a sample challenge.
Expand Down Expand Up @@ -213,17 +190,10 @@ func hasAuthorProfile(ctx context.Context, cli labcli.CLI) (bool, error) {
}

func maybeCreateAuthorProfile(ctx context.Context, cli labcli.CLI) error {
confirm := true
if err := huh.NewConfirm().
Title("You don't have an author profile yet. Would you like to create one now?").
Affirmative("Yes!").
Negative("No.").
Value(&confirm).
Run(); err != nil {
return err
}

if !confirm {
if !cli.Confirm(
"You don't have an author profile yet. Would you like to create one now?",
"Yes", "No",
) {
return labcli.NewStatusError(0, "See you later!")
}

Expand Down
73 changes: 73 additions & 0 deletions cmd/content/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package content

import (
"context"
"fmt"

"github.com/spf13/cobra"
"gopkg.in/yaml.v2"

"github.com/iximiuz/labctl/internal/api"
"github.com/iximiuz/labctl/internal/labcli"
)

type listOptions struct {
kind ContentKind
}

func newListCommand(cli labcli.CLI) *cobra.Command {
var opts listOptions

cmd := &cobra.Command{
Use: "list [--kind challenge|tutorial|course]",
Short: "List authored content, possibly filtered by kind.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return labcli.WrapStatusError(runListContent(cmd.Context(), cli, &opts))
},
}

flags := cmd.Flags()

flags.Var(
&opts.kind,
"kind",
`Content kind to filter by - one of challenge, tutorial, course (an empty string means all)`,
)

return cmd
}

type AuthoredContent struct {
Challenges []api.Challenge `json:"challenges" yaml:"challenges"`
Tutorials []api.Tutorial `json:"tutorials" yaml:"tutorials"`
// Courses []api.Course `json:"courses" yaml:"courses"`
}

func runListContent(ctx context.Context, cli labcli.CLI, opts *listOptions) error {
var content AuthoredContent

if opts.kind == "" || opts.kind == KindChallenge {
challenges, err := cli.Client().ListAuthoredChallenges(ctx)
if err != nil {
return fmt.Errorf("cannot list authored challenges: %w", err)
}

content.Challenges = challenges
}

if opts.kind == "" || opts.kind == KindTutorial {
tutorials, err := cli.Client().ListAuthoredTutorials(ctx)
if err != nil {
return fmt.Errorf("cannot list authored tutorials: %w", err)
}

content.Tutorials = tutorials
}

if err := yaml.NewEncoder(cli.OutputStream()).Encode(content); err != nil {
return err
}

return nil
}
63 changes: 63 additions & 0 deletions cmd/content/remove.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package content

import (
"context"
"fmt"

"github.com/spf13/cobra"

"github.com/iximiuz/labctl/internal/labcli"
)

type removeOptions struct {
kind ContentKind
name string
force bool
}

func newRemoveCommand(cli labcli.CLI) *cobra.Command {
var opts removeOptions

cmd := &cobra.Command{
Use: "remove [flags] <challenge|tutorial|course> <name>",
Short: "Remove a piece of content you authored.",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
if err := opts.kind.Set(args[0]); err != nil {
return labcli.WrapStatusError(err)
}
opts.name = args[1]

return labcli.WrapStatusError(runRemoveContent(cmd.Context(), cli, &opts))
},
}

return cmd
}

func runRemoveContent(ctx context.Context, cli labcli.CLI, opts *removeOptions) error {
cli.PrintAux("Removing %s %s...\n", opts.kind, opts.name)

if !opts.force {
if !cli.Confirm(
"This action is irreversible. Are you sure?",
"Yes", "No",
) {
return labcli.NewStatusError(0, "Glad you changed your mind!")
}
}

switch opts.kind {
case KindChallenge:
return cli.Client().DeleteChallenge(ctx, opts.name)

case KindTutorial:
return cli.Client().DeleteTutorial(ctx, opts.name)

case KindCourse:
return fmt.Errorf("removing courses is not supported yet")

default:
return fmt.Errorf("unknown content kind %q", opts.kind)
}
}
67 changes: 67 additions & 0 deletions cmd/content/sync.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package content

import (
"context"
"fmt"
"os"
"path/filepath"
"time"

"github.com/spf13/cobra"

"github.com/iximiuz/labctl/internal/api"
"github.com/iximiuz/labctl/internal/labcli"
)

type syncOptions struct {
dir string
}

func newSyncCommand(cli labcli.CLI) *cobra.Command {
var opts syncOptions

cmd := &cobra.Command{
Use: "sync [flags] [content-dir]",
Short: "Sync a local directory to remote content storage - the main content authoring routine.",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 1 {
opts.dir = args[0]
} else {
if cwd, err := os.Getwd(); err != nil {
return labcli.WrapStatusError(fmt.Errorf("couldn't get the current working directory: %w", err))
} else {
opts.dir = cwd
}
}

return labcli.WrapStatusError(runSyncContent(cmd.Context(), cli, &opts))
},
}

return cmd
}

func runSyncContent(ctx context.Context, cli labcli.CLI, opts *syncOptions) error {
cli.PrintAux("Starting content sync in %s...\n", opts.dir)

for ctx.Err() == nil {
data, err := os.ReadFile(filepath.Join(opts.dir, "index.md"))
if err != nil {
return fmt.Errorf("couldn't read index.md: %w", err)
}

if err := cli.Client().PutMarkdown(ctx, api.PutMarkdownRequest{
Kind: "challenge",
Name: "foobar-qux",
Content: string(data),
}); err != nil {
return fmt.Errorf("couldn't update content: %w", err)
}

cli.PrintAux("Synced content...\n")
time.Sleep(5 * time.Second)
}

return nil
}
35 changes: 31 additions & 4 deletions internal/api/challenges.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import (
)

type Challenge struct {
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
CreatedAt string `json:"createdAt" yaml:"createdAt"`
UpdatedAt string `json:"updatedAt" yaml:"updatedAt"`

Name string `json:"name"`
Name string `json:"name" yaml:"name"`

PageURL string `json:"pageUrl" yaml:"pageUrl"`

PageURL string `json:"pageUrl"`
AttemptCount int `json:"attemptCount" yaml:"attemptCount"`
CompletionCount int `json:"completionCount" yaml:"completionCount"`
}

type CreateChallengeRequest struct {
Expand All @@ -26,3 +29,27 @@ func (c *Client) CreateChallenge(ctx context.Context, req CreateChallengeRequest
var ch Challenge
return &ch, c.PostInto(ctx, "/challenges", nil, nil, body, &ch)
}

func (c *Client) GetChallenge(ctx context.Context, name string) (*Challenge, error) {
var ch Challenge
return &ch, c.GetInto(ctx, "/challenges/"+name, nil, nil, &ch)
}

func (c *Client) ListChallenges(ctx context.Context) ([]Challenge, error) {
var challenges []Challenge
return challenges, c.GetInto(ctx, "/challenges", nil, nil, &challenges)
}

func (c *Client) ListAuthoredChallenges(ctx context.Context) ([]Challenge, error) {
var challenges []Challenge
return challenges, c.GetInto(ctx, "/challenges/authored", nil, nil, &challenges)
}

func (c *Client) DeleteChallenge(ctx context.Context, name string) error {
resp, err := c.Delete(ctx, "/challenges/"+name, nil, nil)
if err != nil {
return err
}
resp.Body.Close()
return nil
}
Loading

0 comments on commit 550b2e6

Please sign in to comment.