Skip to content

Commit f02266e

Browse files
authored
Add issues and projects pages (#1)
1 parent d8dddc6 commit f02266e

File tree

12 files changed

+788
-146
lines changed

12 files changed

+788
-146
lines changed

.pre-commit-config.yaml

+10-1
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,18 @@ repos:
1616
hooks:
1717
- id: ruff
1818
args:
19-
- "--fix"
19+
- '--fix'
2020
- id: ruff-format
2121

22+
- repo: https://github.com/djlint/djLint
23+
rev: v1.35.2
24+
hooks:
25+
- id: djlint-reformat-django
26+
files: 'templates/.*.html'
27+
entry: djlint --reformat
28+
types:
29+
- html
30+
2231
- repo: https://github.com/pycqa/isort
2332
rev: 5.13.2
2433
hooks:

backend/apps/github/index/issue.py

+2
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ class IssueIndex(AlgoliaIndex):
2121
"idx_author_name",
2222
"idx_comments_count",
2323
"idx_created_at",
24+
"idx_hint",
2425
"idx_labels",
2526
"idx_project_description",
2627
"idx_project_level",
2728
"idx_project_name",
2829
"idx_project_tags",
30+
"idx_project_url",
2931
"idx_repository_contributors_count",
3032
"idx_repository_description",
3133
"idx_repository_forks_count",

backend/apps/github/models/mixins/issue.py

+10
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ def idx_created_at(self):
3434
"""Return created at for indexing."""
3535
return self.created_at
3636

37+
@property
38+
def idx_hint(self):
39+
"""Return hint for indexing."""
40+
return self.hint if self.hint else None
41+
3742
@property
3843
def idx_labels(self):
3944
"""Return labels for indexing."""
@@ -64,6 +69,11 @@ def idx_project_name(self):
6469
"""Return project name for indexing."""
6570
return self.project.idx_name if self.project else ""
6671

72+
@property
73+
def idx_project_url(self):
74+
"""Return project URL for indexing."""
75+
return self.project.idx_url if self.project else None
76+
6777
@property
6878
def idx_repository_contributors_count(self):
6979
"""Return repository contributors count for indexing."""

backend/apps/owasp/api/search/issue.py

+6
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,17 @@ def get_issues(query, distinct=False, limit=25):
1010
"""Return issues relevant to a search query."""
1111
params = {
1212
"attributesToRetrieve": [
13+
"idx_comments_count",
1314
"idx_created_at",
15+
"idx_hint",
16+
"idx_labels",
1417
"idx_project_name",
18+
"idx_project_url",
1519
"idx_repository_languages",
20+
"idx_repository_topics",
1621
"idx_summary",
1722
"idx_title",
23+
"idx_updated_at",
1824
"idx_url",
1925
],
2026
"hitsPerPage": limit,

backend/apps/owasp/api/search/project.py

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def get_projects(query, limit=25):
1313
"idx_contributors_count",
1414
"idx_created_at",
1515
"idx_forks_count",
16+
"idx_leaders",
1617
"idx_level",
1718
"idx_name",
1819
"idx_stars_count",
+236-73
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,240 @@
1+
{% extends "base.html" %}
12
{% load static %}
3+
{% block content %}
4+
{{ block.super }}
5+
<div id="app">
6+
<div class="container">
7+
<div class="row mb-4">
8+
<div class="col-lg-6 mx-auto">
9+
<div class="input-group">
10+
<input v-model="search"
11+
type="text"
12+
@input="handleInput"
13+
class="form-control"
14+
placeholder="Search for a project to contribute to...">
15+
<button class="btn btn-outline-secondary"
16+
@click="search = ''"
17+
type="button"
18+
id="button-addon2">
19+
<i class="fa-solid fa-xmark"></i>
20+
</button>
21+
</div>
22+
</div>
23+
</div>
24+
<div v-for="(issue, i) in issues" :key="`issue-${i}`" class="card m-4">
25+
<div class="card-body px-4">
26+
<div class="row" id="idx_metadata">
27+
<div class="сol-2 position-relative;">
28+
<div class="position-absolute top-0 end-0">
29+
<div class="d-flex flex-row text-muted">
30+
<div data-bs-toggle="tooltip"
31+
data-bs-placement="top"
32+
title="Issue created"
33+
class="d-flex flex-column align-items-center justify-content-center border border-light pt-2"
34+
style="width: 120px">
35+
<div class="px-2">
36+
<i class="fa-regular fa-clock"></i>
37+
</div>
38+
<div class="px-2">${issue.idx_created_at} ago</div>
39+
</div>
40+
<div data-bs-toggle="tooltip"
41+
data-bs-placement="top"
42+
title="Issue updated"
43+
class="d-flex flex-column align-items-center justify-content-center border border-light pt-2"
44+
style="width: 120px"
45+
v-if="issue.idx_updated_at !== issue.idx_created_at">
46+
<div class="px-2">
47+
<i class="fa-solid fa-arrows-rotate"></i>
48+
</div>
49+
<div class="px-2">${issue.idx_updated_at} ago</div>
50+
</div>
51+
<div data-bs-toggle="tooltip"
52+
data-bs-placement="top"
53+
title="Number of comments"
54+
class="d-flex flex-column align-items-center justify-content-center border border-light pt-2"
55+
style="width: 100px"
56+
v-if="issue.idx_comments_count">
57+
<div class="px-2">
58+
<i class="fa-regular fa-comment"></i>
59+
</div>
60+
<div class="px-2">${issue.idx_comments_count}</div>
61+
</div>
62+
</div>
63+
</div>
64+
</div>
65+
<div class="col-8">
66+
<div id="idx_title">
67+
<h4>
68+
<a :href="`${issue.idx_url}`" target="_blank">${issue.idx_title}</a>
69+
</h4>
70+
</div>
71+
</div>
72+
</div>
73+
<div id="idx_project_name">
74+
<h6>
75+
<a :href="`${issue.idx_project_url}`" target="_blank">${issue.idx_project_name}</a>
76+
</h6>
77+
</div>
78+
<div id="idx_summary" class="mb-1">
79+
<div class="text-3" v-html="issue.idx_summary_md"></div>
80+
<button v-if="issue.idx_summary || issue.idx_hint"
81+
type="button"
82+
@click="showIssueDetails(issue)"
83+
data-bs-toggle="modal"
84+
data-bs-target="#detailsModal"
85+
class="mt-3 btn btn-outline-primary btn-sm inline-block float-end"
86+
style="text-decoration: none">Read more</button>
87+
</div>
88+
<div class="row"></div>
89+
<div id="idx_languages">
90+
<div>
91+
<div role="button"
92+
data-bs-toggle="tooltip"
93+
data-bs-placement="bottom"
94+
title="Click to search by"
95+
@click="clickSearch(lang)"
96+
class="badge bg-secondary mx-1 mt-2"
97+
v-for="lang in issue.idx_repository_language">${lang}</div>
98+
</div>
99+
<div id="idx_topics">
100+
<div>
101+
<div role="button"
102+
data-bs-toggle="tooltip"
103+
data-bs-placement="bottom"
104+
title="Click to search by"
105+
@click="clickSearch(label)"
106+
class="badge bg-light-gray mx-1 mt-2"
107+
v-for="label in issue.idx_labels">${label}</div>
108+
</div>
109+
</div>
110+
</div>
111+
</div>
112+
<div class="modal fade"
113+
id="detailsModal"
114+
tabindex="-1"
115+
aria-labelledby="detailsModalLabel"
116+
aria-hidden="true">
117+
<div class="modal-dialog modal-lg">
118+
<div class="modal-content">
119+
<div class="modal-header d-block">
120+
<div class="d-flex">
121+
<h4 class="modal-title" id="exampleModalLabel">${selectedIssue.idx_title}</h4>
122+
<button type="button"
123+
class="btn-close"
124+
data-bs-dismiss="modal"
125+
aria-label="Close"></button>
126+
</div>
127+
<small class="pt-2 text-muted">
128+
The issue summary and the recommended steps to address it have been generated by AI
129+
</small>
130+
</div>
131+
<div class="modal-body p-4">
132+
<div v-if="selectedIssue.idx_summary" class="mb-3">
133+
<h5>Issue summary</h5>
134+
<div v-html="selectedIssue.idx_summary_md"></div>
135+
</div>
136+
<div v-if="selectedIssue.idx_hint">
137+
<h5>How to tackle it</h5>
138+
<div v-html="selectedIssue.idx_hint"></div>
139+
</div>
140+
</div>
141+
<div class="modal-footer">
142+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
143+
</div>
144+
</div>
145+
</div>
146+
</div>
147+
</div>
148+
</div>
149+
</div>
150+
<script>
151+
const {
152+
createApp
153+
} = Vue;
154+
createApp({
155+
delimiters: ['${', '}'],
156+
data() {
157+
return {
158+
issues: [],
159+
selectedIssue: {},
160+
search: '',
161+
isManual: true,
162+
};
163+
},
164+
watch: {
165+
search() {
166+
if (this.isManual) {
167+
this.handleInput();
168+
} else {
169+
this.getIssues();
170+
}
171+
}
172+
},
173+
methods: {
174+
async getIssues() {
175+
const response = await fetch(`/api/v1/owasp/search/issue?q=${this.search}`)
176+
.then(res => res.json())
177+
.then(json => {
178+
json.forEach(issue => {
179+
issue.idx_hint = marked.parse(issue.idx_hint || '');
180+
issue.idx_title_md = marked.parse(issue.idx_title || '');
181+
issue.idx_summary_md = marked.parse(issue.idx_summary || '');
182+
issue.idx_created_at = dayjs.unix(issue.idx_created_at || '').fromNow(true);
183+
issue.idx_updated_at = dayjs.unix(issue.idx_updated_at || '').fromNow(true);
184+
issue.idx_labels = issue.idx_labels.length ? issue.idx_labels.slice(0, 10) : [];
185+
issue.idx_repository_language = issue.idx_repository_languages.length ? issue.idx_repository_languages.slice(0, 10) : [];
186+
});
187+
this.issues = json;
188+
})
189+
.catch((err) => console.error("There was an error! ", err));
190+
},
191+
showIssueDetails(issue) {
192+
this.selectedIssue = issue;
193+
},
194+
handleInput(event) {
195+
clearTimeout(this.timeout);
196+
this.timeout = setTimeout(this.getIssues, 1000);
197+
},
198+
clickSearch(search) {
199+
this.isManual = false;
200+
this.search = search;
201+
document.body.scrollTop = 0;
202+
document.documentElement.scrollTop = 0;
203+
}
204+
},
205+
mounted() {
206+
dayjs.extend(dayjs_plugin_relativeTime);
207+
this.getIssues();
208+
const url = new URL(window.location.href);
209+
const params = new URLSearchParams(url.search);
210+
const searchQuery = params.get('q');
211+
if (searchQuery) {
212+
this.isManual = false;
213+
this.search = searchQuery;
214+
}
215+
}
216+
}).mount('#app');
217+
</script>
218+
<style scoped lang="scss">
219+
.text-3 {
220+
display: -webkit-box;
221+
-webkit-box-orient: vertical;
222+
-webkit-line-clamp: 3;
223+
overflow: hidden;
224+
text-overflow: ellipsis;
225+
}
2226

3-
<head>
4-
<meta charset="UTF-8" />
5-
<link
6-
rel="stylesheet"
7-
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"
8-
/>
9-
</head>
227+
a {
228+
color: #1d7bd7;
229+
text-decoration: none;
10230

11-
<script src="{% static 'js/htmx.min.js' %}"></script>
231+
:hover {
232+
text-decoration: underline;
233+
}
234+
}
12235

13-
<h3>Find an issue to work on</h3>
14-
<input
15-
autocomplete="off"
16-
class="form-control"
17-
id="query"
18-
name="q"
19-
placeholder="Type To Search..."
20-
type="search"
21-
hx-get="{% url 'api-search-project-issues' %}"
22-
hx-indicator=".htmx-indicator"
23-
hx-swap="none"
24-
hx-target="#search-results"
25-
hx-trigger="load, input changed delay:1000ms, search"
26-
/>
27-
<span class="htmx-indicator"> Searching... </span>
28-
29-
<script>
30-
document
31-
.getElementById('query')
32-
.addEventListener('htmx:afterRequest', function (event) {
33-
var jsonResponse = event.detail.xhr.response;
34-
var hits = JSON.parse(jsonResponse);
35-
36-
const resultsContainer = document.getElementById('search-results');
37-
resultsContainer.innerHTML = '';
38-
39-
hits.forEach((hit) => {
40-
const highlightedTitle = hit._highlightResult.idx_title.value;
41-
const languages = hit.idx_repository_languages;
42-
const projectName = hit.idx_project_name;
43-
const createdAt = new Date(hit.idx_created_at * 1000);
44-
45-
const languageIcons = {
46-
Python: 'fab fa-python',
47-
JavaScript: 'fab fa-js',
48-
Java: 'fab fa-java',
49-
PHP: 'fab fa-php',
50-
};
51-
52-
const container = document.createElement('div');
53-
languages.forEach((language) => {
54-
const iconClass = languageIcons[language];
55-
if (iconClass) {
56-
const languageSpan = document.createElement('span');
57-
languageSpan.innerHTML = `<i class="${iconClass}"></i>`;
58-
container.appendChild(languageSpan);
59-
}
60-
});
61-
62-
const url = hit.idx_url;
63-
64-
const resultItem = document.createElement('div');
65-
resultItem.innerHTML = `
66-
<h2><a href="${url}" target="_blank">${highlightedTitle}</a></h2>
67-
<p>Project: ${projectName}. Created at: ${createdAt}</p>
68-
<p>${container.innerHTML}</p>
69-
<p></p>
70-
`;
71-
72-
resultsContainer.appendChild(resultItem);
73-
});
74-
});
75-
</script>
76-
77-
<div id="search-results"></div>
236+
.bg-light-gray {
237+
background-color: #868E96;
238+
}
239+
</style>
240+
{% endblock content %}

0 commit comments

Comments
 (0)