forked from tangled.org/core
this repo has no description

group profile timeline by month

Changed files
+401 -90
appview
+48 -18
appview/db/issues.go
··· 12 12 OwnerDid string 13 13 IssueId int 14 14 IssueAt string 15 - Created *time.Time 15 + Created time.Time 16 16 Title string 17 17 Body string 18 18 Open bool 19 + 20 + // optionally, populate this when querying for reverse mappings 21 + // like comment counts, parent repo etc. 19 22 Metadata *IssueMetadata 20 23 } 21 24 22 25 type IssueMetadata struct { 23 26 CommentCount int 27 + Repo *Repo 24 28 // labels, assignee etc. 25 29 } 26 30 ··· 143 147 if err != nil { 144 148 return nil, err 145 149 } 146 - issue.Created = &createdTime 150 + issue.Created = createdTime 147 151 issue.Metadata = &metadata 148 152 149 153 issues = append(issues, issue) ··· 156 160 return issues, nil 157 161 } 158 162 159 - func GetIssuesByOwnerDid(e Execer, ownerDid string) ([]Issue, error) { 163 + // timeframe here is directly passed into the sql query filter, and any 164 + // timeframe in the past should be negative; e.g.: "-3 months" 165 + func GetIssuesByOwnerDid(e Execer, ownerDid string, timeframe string) ([]Issue, error) { 160 166 var issues []Issue 161 167 162 168 rows, err := e.Query( ··· 168 174 i.title, 169 175 i.body, 170 176 i.open, 171 - count(c.id) 177 + r.did, 178 + r.name, 179 + r.knot, 180 + r.rkey, 181 + r.created 172 182 from 173 183 issues i 174 - left join 175 - comments c on i.repo_at = c.repo_at and i.issue_id = c.issue_id 184 + join 185 + repos r on i.repo_at = r.at_uri 176 186 where 177 - i.owner_did = ? 178 - group by 179 - i.id, i.owner_did, i.repo_at, i.issue_id, i.created, i.title, i.body, i.open 187 + i.owner_did = ? and i.created >= date ('now', ?) 180 188 order by 181 189 i.created desc`, 182 - ownerDid) 190 + ownerDid, timeframe) 183 191 if err != nil { 184 192 return nil, err 185 193 } ··· 187 195 188 196 for rows.Next() { 189 197 var issue Issue 190 - var createdAt string 191 - var metadata IssueMetadata 192 - err := rows.Scan(&issue.OwnerDid, &issue.RepoAt, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open, &metadata.CommentCount) 198 + var issueCreatedAt, repoCreatedAt string 199 + var repo Repo 200 + err := rows.Scan( 201 + &issue.OwnerDid, 202 + &issue.RepoAt, 203 + &issue.IssueId, 204 + &issueCreatedAt, 205 + &issue.Title, 206 + &issue.Body, 207 + &issue.Open, 208 + &repo.Did, 209 + &repo.Name, 210 + &repo.Knot, 211 + &repo.Rkey, 212 + &repoCreatedAt, 213 + ) 193 214 if err != nil { 194 215 return nil, err 195 216 } 196 217 197 - createdTime, err := time.Parse(time.RFC3339, createdAt) 218 + issueCreatedTime, err := time.Parse(time.RFC3339, issueCreatedAt) 219 + if err != nil { 220 + return nil, err 221 + } 222 + issue.Created = issueCreatedTime 223 + 224 + repoCreatedTime, err := time.Parse(time.RFC3339, repoCreatedAt) 198 225 if err != nil { 199 226 return nil, err 200 227 } 201 - issue.Created = &createdTime 202 - issue.Metadata = &metadata 228 + repo.Created = repoCreatedTime 229 + 230 + issue.Metadata = &IssueMetadata{ 231 + Repo: &repo, 232 + } 203 233 204 234 issues = append(issues, issue) 205 235 } ··· 226 256 if err != nil { 227 257 return nil, err 228 258 } 229 - issue.Created = &createdTime 259 + issue.Created = createdTime 230 260 231 261 return &issue, nil 232 262 } ··· 246 276 if err != nil { 247 277 return nil, nil, err 248 278 } 249 - issue.Created = &createdTime 279 + issue.Created = createdTime 250 280 251 281 comments, err := GetComments(e, repoAt, issueId) 252 282 if err != nil {
+117 -48
appview/db/profile.go
··· 1 1 package db 2 2 3 3 import ( 4 + "encoding/json" 4 5 "fmt" 5 - "sort" 6 6 "time" 7 7 ) 8 8 9 - type ProfileTimelineEvent struct { 10 - EventAt time.Time 11 - Type string 12 - *Issue 13 - *Pull 14 - *Repo 9 + type RepoEvent struct { 10 + Repo *Repo 11 + Source *Repo 12 + } 13 + 14 + type ProfileTimeline struct { 15 + ByMonth []ByMonth 16 + } 17 + 18 + type ByMonth struct { 19 + RepoEvents []RepoEvent 20 + IssueEvents IssueEvents 21 + PullEvents PullEvents 22 + } 23 + 24 + type IssueEvents struct { 25 + Items []*Issue 26 + } 27 + 28 + type IssueEventStats struct { 29 + Open int 30 + Closed int 31 + } 32 + 33 + func (i IssueEvents) Stats() IssueEventStats { 34 + var open, closed int 35 + for _, issue := range i.Items { 36 + if issue.Open { 37 + open += 1 38 + } else { 39 + closed += 1 40 + } 41 + } 42 + 43 + return IssueEventStats{ 44 + Open: open, 45 + Closed: closed, 46 + } 47 + } 48 + 49 + type PullEvents struct { 50 + Items []*Pull 51 + } 52 + 53 + func (p PullEvents) Stats() PullEventStats { 54 + var open, merged, closed int 55 + for _, pull := range p.Items { 56 + switch pull.State { 57 + case PullOpen: 58 + open += 1 59 + case PullMerged: 60 + merged += 1 61 + case PullClosed: 62 + closed += 1 63 + } 64 + } 65 + 66 + return PullEventStats{ 67 + Open: open, 68 + Merged: merged, 69 + Closed: closed, 70 + } 71 + } 15 72 16 - // optional: populate only if Repo is a fork 17 - Source *Repo 73 + type PullEventStats struct { 74 + Closed int 75 + Open int 76 + Merged int 18 77 } 19 78 20 - func MakeProfileTimeline(e Execer, forDid string) ([]ProfileTimelineEvent, error) { 21 - timeline := []ProfileTimelineEvent{} 22 - limit := 30 79 + const TimeframeMonths = 3 80 + 81 + func MakeProfileTimeline(e Execer, forDid string) (*ProfileTimeline, error) { 82 + timeline := ProfileTimeline{ 83 + ByMonth: make([]ByMonth, TimeframeMonths), 84 + } 85 + currentMonth := time.Now().Month() 86 + timeframe := fmt.Sprintf("-%d months", TimeframeMonths) 23 87 24 - pulls, err := GetPullsByOwnerDid(e, forDid) 88 + pulls, err := GetPullsByOwnerDid(e, forDid, timeframe) 25 89 if err != nil { 26 - return timeline, fmt.Errorf("error getting pulls by owner did: %w", err) 90 + return nil, fmt.Errorf("error getting pulls by owner did: %w", err) 27 91 } 28 92 93 + // group pulls by month 29 94 for _, pull := range pulls { 30 - repo, err := GetRepoByAtUri(e, string(pull.RepoAt)) 31 - if err != nil { 32 - return timeline, fmt.Errorf("error getting repo by at uri: %w", err) 95 + pullMonth := pull.Created.Month() 96 + 97 + if currentMonth-pullMonth > TimeframeMonths { 98 + // shouldn't happen; but times are weird 99 + continue 33 100 } 34 101 35 - timeline = append(timeline, ProfileTimelineEvent{ 36 - EventAt: pull.Created, 37 - Type: "pull", 38 - Pull: &pull, 39 - Repo: repo, 40 - }) 102 + idx := currentMonth - pullMonth 103 + items := &timeline.ByMonth[idx].PullEvents.Items 104 + 105 + *items = append(*items, &pull) 41 106 } 42 107 43 - issues, err := GetIssuesByOwnerDid(e, forDid) 108 + issues, err := GetIssuesByOwnerDid(e, forDid, timeframe) 44 109 if err != nil { 45 - return timeline, fmt.Errorf("error getting issues by owner did: %w", err) 110 + return nil, fmt.Errorf("error getting issues by owner did: %w", err) 46 111 } 47 112 48 113 for _, issue := range issues { 49 - repo, err := GetRepoByAtUri(e, string(issue.RepoAt)) 50 - if err != nil { 51 - return timeline, fmt.Errorf("error getting repo by at uri: %w", err) 114 + issueMonth := issue.Created.Month() 115 + 116 + if currentMonth-issueMonth > TimeframeMonths { 117 + // shouldn't happen; but times are weird 118 + continue 52 119 } 53 120 54 - timeline = append(timeline, ProfileTimelineEvent{ 55 - EventAt: *issue.Created, 56 - Type: "issue", 57 - Issue: &issue, 58 - Repo: repo, 59 - }) 121 + idx := currentMonth - issueMonth 122 + items := &timeline.ByMonth[idx].IssueEvents.Items 123 + 124 + *items = append(*items, &issue) 60 125 } 61 126 62 127 repos, err := GetAllReposByDid(e, forDid) 63 128 if err != nil { 64 - return timeline, fmt.Errorf("error getting all repos by did: %w", err) 129 + return nil, fmt.Errorf("error getting all repos by did: %w", err) 65 130 } 66 131 67 132 for _, repo := range repos { 133 + // TODO: get this in the original query; requires COALESCE because nullable 68 134 var sourceRepo *Repo 69 135 if repo.Source != "" { 70 136 sourceRepo, err = GetRepoByAtUri(e, repo.Source) ··· 73 139 } 74 140 } 75 141 76 - timeline = append(timeline, ProfileTimelineEvent{ 77 - EventAt: repo.Created, 78 - Type: "repo", 79 - Repo: &repo, 80 - Source: sourceRepo, 142 + repoMonth := repo.Created.Month() 143 + 144 + if currentMonth-repoMonth > TimeframeMonths { 145 + // shouldn't happen; but times are weird 146 + continue 147 + } 148 + 149 + idx := currentMonth - repoMonth 150 + 151 + items := &timeline.ByMonth[idx].RepoEvents 152 + *items = append(*items, RepoEvent{ 153 + Repo: &repo, 154 + Source: sourceRepo, 81 155 }) 82 156 } 83 157 84 - sort.Slice(timeline, func(i, j int) bool { 85 - return timeline[i].EventAt.After(timeline[j].EventAt) 86 - }) 87 - 88 - if len(timeline) > limit { 89 - timeline = timeline[:limit] 90 - } 158 + x, _ := json.MarshalIndent(timeline, "", "\t") 159 + fmt.Println(string(x)) 91 160 92 - return timeline, nil 161 + return &timeline, nil 93 162 }
+40 -14
appview/db/pulls.go
··· 64 64 // meta 65 65 Created time.Time 66 66 PullSource *PullSource 67 + 68 + // optionally, populate this when querying for reverse mappings 69 + Repo *Repo 67 70 } 68 71 69 72 type PullSource struct { ··· 522 525 return &pull, nil 523 526 } 524 527 525 - func GetPullsByOwnerDid(e Execer, did string) ([]Pull, error) { 528 + // timeframe here is directly passed into the sql query filter, and any 529 + // timeframe in the past should be negative; e.g.: "-3 months" 530 + func GetPullsByOwnerDid(e Execer, did, timeframe string) ([]Pull, error) { 526 531 var pulls []Pull 527 532 528 533 rows, err := e.Query(` 529 534 select 530 - owner_did, 531 - repo_at, 532 - pull_id, 533 - created, 534 - title, 535 - state 535 + p.owner_did, 536 + p.repo_at, 537 + p.pull_id, 538 + p.created, 539 + p.title, 540 + p.state, 541 + r.did, 542 + r.name, 543 + r.knot, 544 + r.rkey, 545 + r.created 536 546 from 537 - pulls 547 + pulls p 548 + join 549 + repos r on p.repo_at = r.at_uri 538 550 where 539 - owner_did = ? 551 + p.owner_did = ? and p.created >= date ('now', ?) 540 552 order by 541 - created desc`, did) 553 + p.created desc`, did, timeframe) 542 554 if err != nil { 543 555 return nil, err 544 556 } ··· 546 558 547 559 for rows.Next() { 548 560 var pull Pull 549 - var createdAt string 561 + var repo Repo 562 + var pullCreatedAt, repoCreatedAt string 550 563 err := rows.Scan( 551 564 &pull.OwnerDid, 552 565 &pull.RepoAt, 553 566 &pull.PullId, 554 - &createdAt, 567 + &pullCreatedAt, 555 568 &pull.Title, 556 569 &pull.State, 570 + &repo.Did, 571 + &repo.Name, 572 + &repo.Knot, 573 + &repo.Rkey, 574 + &repoCreatedAt, 557 575 ) 558 576 if err != nil { 559 577 return nil, err 560 578 } 561 579 562 - createdTime, err := time.Parse(time.RFC3339, createdAt) 580 + pullCreatedTime, err := time.Parse(time.RFC3339, pullCreatedAt) 563 581 if err != nil { 564 582 return nil, err 565 583 } 566 - pull.Created = createdTime 584 + pull.Created = pullCreatedTime 585 + 586 + repoCreatedTime, err := time.Parse(time.RFC3339, repoCreatedAt) 587 + if err != nil { 588 + return nil, err 589 + } 590 + repo.Created = repoCreatedTime 591 + 592 + pull.Repo = &repo 567 593 568 594 pulls = append(pulls, pull) 569 595 }
+3 -1
appview/db/repos.go
··· 77 77 where 78 78 r.did = ? 79 79 group by 80 - r.at_uri`, did) 80 + r.at_uri 81 + order by r.created desc`, 82 + did) 81 83 if err != nil { 82 84 return nil, err 83 85 }
+3 -2
appview/pages/pages.go
··· 178 178 CollaboratingRepos []db.Repo 179 179 ProfileStats ProfileStats 180 180 FollowStatus db.FollowStatus 181 - DidHandleMap map[string]string 182 181 AvatarUri string 183 - ProfileTimeline []db.ProfileTimelineEvent 182 + ProfileTimeline *db.ProfileTimeline 183 + 184 + DidHandleMap map[string]string 184 185 } 185 186 186 187 type ProfileStats struct {
+179 -2
appview/pages/templates/user/profile.html
··· 9 9 {{ block "ownRepos" . }}{{ end }} 10 10 {{ block "collaboratingRepos" . }}{{ end }} 11 11 </div> 12 - 13 12 <div class="md:col-span-2 order-3 md:order-3"> 14 - {{ block "profileTimeline" . }}{{ end }} 13 + {{ block "profileTimeline2" . }}{{ end }} 15 14 </div> 16 15 </div> 17 16 {{ end }} 18 17 18 + {{ define "profileTimeline2" }} 19 + <p class="text-sm font-bold py-2 dark:text-white">ACTIVITY</p> 20 + <div class="flex flex-col gap-6 relative"> 21 + {{ with .ProfileTimeline }} 22 + {{ range $idx, $byMonth := .ByMonth }} 23 + {{ with $byMonth }} 24 + <div> 25 + {{ if eq $idx 0 }} 26 + <p class="text-sm font-bold py-2 dark:text-white">This month</p> 27 + {{ else }} 28 + {{ $s := "s" }} 29 + {{ if eq $idx 1 }} 30 + {{ $s = "" }} 31 + {{ end }} 32 + <p class="text-sm font-bold py-2 dark:text-white">{{$idx}} month{{$s}} ago</p> 33 + {{ end }} 34 + 35 + <div class="flex flex-col gap-4"> 36 + {{ block "repoEvents" (list .RepoEvents $.DidHandleMap) }} {{ end }} 37 + {{ block "issueEvents" (list .IssueEvents $.DidHandleMap) }} {{ end }} 38 + {{ block "pullEvents" (list .PullEvents $.DidHandleMap) }} {{ end }} 39 + </div> 40 + </div> 41 + 42 + {{ end }} 43 + {{ else }} 44 + <p class="dark:text-white">This user does not have any activity yet.</p> 45 + {{ end }} 46 + {{ end }} 47 + </div> 48 + {{ end }} 49 + 50 + {{ define "repoEvents" }} 51 + {{ $items := index . 0 }} 52 + {{ $handleMap := index . 1 }} 53 + 54 + {{ if gt (len $items) 0 }} 55 + <details open> 56 + <summary class="list-none cursor-pointer"> 57 + <div class="flex items-center gap-2"> 58 + {{ i "unfold-vertical" "w-4 h-4" }} 59 + created {{ len $items }} repositories 60 + </div> 61 + </summary> 62 + <div class="p-2 pl-8 text-sm flex flex-col gap-3"> 63 + {{ range $items }} 64 + <div class="flex flex-wrap items-center gap-2"> 65 + <span class="text-gray-500 dark:text-gray-400"> 66 + {{ if .Source }} 67 + {{ i "git-fork" "w-4 h-4" }} 68 + {{ else }} 69 + {{ i "book-plus" "w-4 h-4" }} 70 + {{ end }} 71 + </span> 72 + <a href="/{{ index $handleMap .Repo.Did }}/{{ .Repo.Name }}" class="no-underline hover:underline"> 73 + {{- .Repo.Name -}} 74 + </a> 75 + <time class="text-gray-700 dark:text-gray-400 text-xs ml-2">{{ .Repo.Created | shortTimeFmt }}</time> 76 + </div> 77 + {{ end }} 78 + </div> 79 + </details> 80 + {{ end }} 81 + {{ end }} 82 + 83 + {{ define "issueEvents" }} 84 + {{ $i := index . 0 }} 85 + {{ $items := $i.Items }} 86 + {{ $stats := $i.Stats }} 87 + {{ $handleMap := index . 1 }} 88 + 89 + {{ if gt (len $items) 0 }} 90 + <details open> 91 + <summary class="list-none cursor-pointer"> 92 + <div class="flex items-center gap-2"> 93 + {{ i "unfold-vertical" "w-4 h-4" }} 94 + <span> 95 + created {{ len $items }} issues 96 + </span> 97 + <span class="px-2 py-1/2 text-sm rounded-sm text-white bg-green-600 dark:bg-green-700"> 98 + {{$stats.Open}} open 99 + </span> 100 + <span class="px-2 py-1/2 text-sm rounded-sm text-white bg-gray-800 dark:bg-gray-700"> 101 + {{$stats.Closed}} closed 102 + </span> 103 + </div> 104 + </summary> 105 + <div class="p-2 pl-8 text-sm flex flex-col gap-3"> 106 + {{ range $items }} 107 + {{ $repoOwner := index $handleMap .Metadata.Repo.Did }} 108 + {{ $repoName := .Metadata.Repo.Name }} 109 + {{ $repoUrl := printf "%s/%s" $repoOwner $repoName }} 110 + 111 + <div class="flex flex-wrap items-center gap-2 text-gray-600 dark:text-gray-300"> 112 + {{ if .Open }} 113 + <span class="text-green-600 dark:text-green-500"> 114 + {{ i "circle-dot" "w-4 h-4" }} 115 + </span> 116 + {{ else }} 117 + <span class="text-gray-500 dark:text-gray-400"> 118 + {{ i "ban" "w-4 h-4" }} 119 + </span> 120 + {{ end }} 121 + <a href="/{{$repoUrl}}/issues/{{ .IssueId }}" class="no-underline hover:underline"> 122 + <span class="text-gray-500 dark:text-gray-400">#{{ .IssueId }}</span> 123 + {{ .Title -}} 124 + </a> 125 + on 126 + <a href="/{{$repoUrl}}" class="no-underline hover:underline"> 127 + {{$repoUrl}} 128 + </a> 129 + <time class="text-gray-700 dark:text-gray-400 text-xs ml-2">{{ .Created | shortTimeFmt }}</time> 130 + </p> 131 + {{ end }} 132 + </div> 133 + </details> 134 + {{ end }} 135 + {{ end }} 136 + 137 + {{ define "pullEvents" }} 138 + {{ $i := index . 0 }} 139 + {{ $items := $i.Items }} 140 + {{ $stats := $i.Stats }} 141 + {{ $handleMap := index . 1 }} 142 + {{ if gt (len $items) 0 }} 143 + <details open> 144 + <summary class="list-none cursor-pointer"> 145 + <div class="flex items-center gap-2"> 146 + {{ i "unfold-vertical" "w-4 h-4" }} 147 + <span> 148 + created {{ len $items }} pull requests 149 + </span> 150 + <span class="px-2 py-1/2 text-sm rounded-sm text-white bg-green-600 dark:bg-green-700"> 151 + {{$stats.Open}} open 152 + </span> 153 + <span class="px-2 py-1/2 text-sm rounded-sm text-white bg-purple-600 dark:bg-purple-700"> 154 + {{$stats.Merged}} merged 155 + </span> 156 + <span class="px-2 py-1/2 text-sm rounded-sm text-black dark:text-white bg-gray-50 dark:bg-gray-700 "> 157 + {{$stats.Closed}} closed 158 + </span> 159 + </div> 160 + </summary> 161 + <div class="p-2 pl-8 text-sm flex flex-col gap-3"> 162 + {{ range $items }} 163 + {{ $repoOwner := index $handleMap .Repo.Did }} 164 + {{ $repoName := .Repo.Name }} 165 + {{ $repoUrl := printf "%s/%s" $repoOwner $repoName }} 166 + 167 + <div class="flex flex-wrap items-center gap-2 text-gray-600 dark:text-gray-300"> 168 + {{ if .State.IsOpen }} 169 + <span class="text-green-600 dark:text-green-500"> 170 + {{ i "git-pull-request" "w-4 h-4" }} 171 + </span> 172 + {{ else if .State.IsMerged }} 173 + <span class="text-purple-600 dark:text-purple-500"> 174 + {{ i "git-merge" "w-4 h-4" }} 175 + </span> 176 + {{ else }} 177 + <span class="text-gray-600 dark:text-gray-300"> 178 + {{ i "git-pull-request-closed" "w-4 h-4" }} 179 + </span> 180 + {{ end }} 181 + <a href="/{{$repoUrl}}/pulls/{{ .PullId }}" class="no-underline hover:underline"> 182 + <span class="text-gray-500 dark:text-gray-400">#{{ .PullId }}</span> 183 + {{ .Title -}} 184 + </a> 185 + on 186 + <a href="/{{$repoUrl}}" class="no-underline hover:underline"> 187 + {{$repoUrl}} 188 + </a> 189 + <time class="text-gray-700 dark:text-gray-400 text-xs ml-2">{{ .Created | shortTimeFmt }}</time> 190 + </p> 191 + {{ end }} 192 + </div> 193 + </details> 194 + {{ end }} 195 + {{ end }} 19 196 20 197 {{ define "profileTimeline" }} 21 198 <div class="flex flex-col gap-3 relative">
+11 -5
appview/state/profile.go
··· 43 43 for _, r := range collaboratingRepos { 44 44 didsToResolve = append(didsToResolve, r.Did) 45 45 } 46 - for _, evt := range timeline { 47 - if evt.Repo != nil { 48 - if evt.Repo.Source != "" { 49 - didsToResolve = append(didsToResolve, evt.Source.Did) 46 + for _, byMonth := range timeline.ByMonth { 47 + for _, pe := range byMonth.PullEvents.Items { 48 + didsToResolve = append(didsToResolve, pe.Repo.Did) 49 + } 50 + for _, ie := range byMonth.IssueEvents.Items { 51 + didsToResolve = append(didsToResolve, ie.Metadata.Repo.Did) 52 + } 53 + for _, re := range byMonth.RepoEvents { 54 + didsToResolve = append(didsToResolve, re.Repo.Did) 55 + if re.Source != nil { 56 + didsToResolve = append(didsToResolve, re.Source.Did) 50 57 } 51 58 } 52 - didsToResolve = append(didsToResolve, evt.Repo.Did) 53 59 } 54 60 55 61 resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve)