IEEE.org     |     IEEE Xplore Digital Library     |     IEEE Standards     |     IEEE Spectrum     |     More Sites

Verified Commit 5e3f5ea3 authored by Emi Simpson's avatar Emi Simpson
Browse files

Added the explore page

parent a79523ec
......@@ -19,7 +19,7 @@ from hashlib import md5
from mystic.sources import SOURCE_PROCESSORS, Source
from functools import reduce
from typing import List, Optional, Dict, Tuple, Type, Union, cast
from typing import Any, List, Optional, Dict, Tuple, Type, Union, cast
from pymysql.cursors import Cursor
from pymysql.err import IntegrityError
......@@ -367,6 +367,100 @@ class Project:
project.add_owner(c, draft_owner, False)
return project
@staticmethod
def get_all_projects(c: Cursor, page: int = 0, page_size: int = 20) -> List['Project']:
"""
Produce an excerpt from the list of all projects
Results are paginated, so the provided value should be the page number. Generally
used for populating the Explore page. If you're looking for a specific project,
prefer to use a specific method.
`page_size` can also be specified to set the number of results on each page. Keep
in mind that the page size will affect where page 1 starts, so don't change it
mid-iteration.
Draft projects will not be returned.
Note that pagination starts at 0, so page 0 is the 1st page, page 1 is the 2nd,
and so on.
"""
c.execute('''
SELECT *
FROM projects
WHERE draft_owner IS NULL
LIMIT %s
OFFSET %s;
''', (page_size, page * page_size))
results: Optional[Tuple[Any, ...]] = c.fetchall()
assert results is not None
return [
Project.from_record(record)
for record in results
]
@staticmethod
def bulk_populate(c: Cursor, projects: List['Project']):
"""
Completely populates the `owners` and `data_sources` fields
This is much more efficient than using one call per project. In addition, all
fields are filled entirely, meaning every owner in the `owners` list will have all
fields loaded.
"""
project_dict = { p.project_id: p for p in projects }
pids_str = ','.join('%s' for _ in range(len(projects)))
c.execute('''
SELECT owners.project_id, users.*
FROM owners
JOIN users ON owners.user_id = users.user_id
WHERE owners.project_id IN(
''' + pids_str + '''
);
''', list(project_dict.keys()))
print(pids_str, project_dict.keys())
results = cast(Tuple[Tuple[int, int, str, str, str, str]], c.fetchall())
print(results)
for result in results:
pid = result[0]
owner_record = result[1:]
project = project_dict[pid]
owner_to_add = User.from_record(*owner_record)
if project._owners == None:
project._owners = [owner_to_add]
else:
project._owners.append(owner_to_add)
c.execute('''
SELECT *
FROM data_sources
WHERE project_id IN(
''' + pids_str + '''
);
''', list(project_dict.keys()))
results = cast(Tuple[Tuple[int, int, str, str, bool]], c.fetchall())
for result in results:
pid = result[1]
project = project_dict[pid]
if project._data_sources == None:
project._data_sources = sources = []
else:
sources = project._data_sources
sources.append(Source(result[0], project, result[2], result[3], result[4]).specialize())
# Any remaining projects with unpopulated fields must have no owners / no sources
for project in projects:
if project._owners is None:
project._owners = []
if project._data_sources is None:
project._data_sources = []
@staticmethod
def from_record(
sql_record: Tuple[int, str, str, Optional[int]]
......@@ -1074,6 +1168,34 @@ class User:
]
return self._projects
@staticmethod
def from_record(
user_id: int,
username: str,
first_name: str,
last_name: str,
email: str
) -> 'User':
"""
Constructs a user from raw parts
Can be called as `User.from_record(*sql_record)`
A properly formatted SQL record returned by a function like `SELECT * FROM
projects` can be converted into a User object using this method.
No validation whatsoever is performed on the resulting User
Raises:
KeyError: The provided record did not have the required number of fields
"""
user = User(user_id)
user._username = username
user._first_name = first_name
user._last_name = last_name
user._email = email
return user
@property
def loaded(self) -> bool:
"""
......
......@@ -135,6 +135,15 @@ class Source(NamedTuple):
def display_name() -> str:
raise NotImplementedError()
def specialize(self) -> 'Source':
"""
Automatically return this source with the methods specific to its subtype
Uses :data:`SOURCE_PROCESSORS` to lookup the source and return this source as that
type. Returns itself if it fails to find an appropriate subclass
"""
return SOURCE_PROCESSORS.get(self.source_type, Source)(*self) #type: ignore
class Git(Source):
@staticmethod
def display_name() -> str:
......
......@@ -30,7 +30,7 @@
<body>
<nav>
<a href="/" {% if selected is sameas "home" %} class='selected'{% endif %}>HOME</a>
<a href="/stats/" {% if selected is sameas "stats" %} class='selected'{% endif %}>STATS</a>
<a href="/explore/" {% if selected is sameas "explore" %} class='selected'{% endif %}>EXPLORE</a>
<div class="nav-spacer"></div>
<form method=POST id="contrast-toggle">
{% if high_contrast() %}
......
{% extends "base.html" %}
{% import "utils.html" as utils %}
{% set selected = "explore" %}
{% block title %} Explore {% endblock %}
{% block head %}
{{super()}}
<style>
#projects {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 40px;
}
#projects > .project {
background-color: #F2F4F5;
width: min(400px, 80%);
padding: 10px 0;
display: flex;
flex-direction: column;
}
.project-spacer {
flex-grow: 1;
}
.project h6 > a {
color: black;
}
.project > h6 {
margin: 10px 40px;
}
.project > * {
margin-left: 40px;
margin-right: 40px;
}
.owners {
display: grid;
grid-template-columns: 1fr 1fr;
}
.owner {
display: grid;
grid-template-columns: 0fr 1fr;
align-items: center;
gap: 5px;
font-size: 0.8em;
}
.avi {
width: 24px;
border-radius: 100px;
}
.languagebar {
display: flex;
width: 100%;
margin: 0;
}
.language {
border-top: 8px solid black;
text-align: center;
padding-top: 6px;
font-size: 0.8em;
color: #666;
}
.language.gitlab {
border-top-color: purple;
}
.language.git {
border-top-color: cornflowerblue;
}
.language.rss {
border-top-color: orangered;
}
</style>
{% endblock %}
{% block main %}
<header>
<h2>Explore</h2>
</header>
<div id=projects>
{% for project in projects %}
<div class=project>
<h6>
<a href="/{{id}}"> {{project.get_display_name(cursor)}}</a>
</h6>
<p class="small">
{{project.get_description(cursor)}}
</p>
<div class="project-spacer"></div>
<div class="owners">
{% for owner in project.get_owners(cursor) %}
<div class="owner">
<img class="avi" src={{owner.get_avatar(cursor)}}></img>
<p class="owner-name">
{{ owner.get_first_name(cursor) }}
{{ owner.get_last_name(cursor) }}
</p>
</div>
{% endfor %}
</div>
<div class="languagebar">
{% for source_type, sources in project.get_folded_data_sources(cursor).items() %}
<div
class="language {{sources[0].source_type}}"
style="flex-grow: {{sources|length}}">
{{source_type.display_name()}}
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
{% endblock %}
......@@ -104,6 +104,26 @@ def view_project(pid: str) -> str:
dashboards=dashboards
)
@bp.route("/explore/")
@bp.route("/explore/<int:page>")
def explore_paginated(page: int = 0) -> str:
'''
Render the explore page
'''
db = get_database()
with db.cursor() as cursor:
all_projects = Project.get_all_projects(cursor, page = page)
Project.bulk_populate(cursor, all_projects)
return render_template(
"explore.html",
projects=all_projects,
page=page,
login=_login_or_profile(cursor),
cursor=cursor
)
@bp.post("/")
def post_main() -> str:
"""
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment