Merge pull request #14803 from guerler/add_user_ratings

Add avatar to published item components
This commit is contained in:
Aysam Guerler
2022-11-04 14:13:38 -04:00
committed by GitHub
19 changed files with 78 additions and 250 deletions
+11 -7
View File
@@ -11,8 +11,9 @@
<div class="m-3">
<div v-if="modelTitle">
<h1 class="h-sm">About this {{ modelTitle }}</h1>
<h2 class="h-md">{{ details.title || details.name }}</h2>
<StatelessTags v-if="details.tags" class="tags mt-2" :value="details.tags" :disabled="true" />
<h2 class="h-md">{{ item.title || item.name }}</h2>
<img class="float-right" :src="gravatarSource" alt="user avatar" />
<StatelessTags v-if="item.tags" class="tags mt-2" :value="item.tags" :disabled="true" />
<br />
<h2 class="h-sm">Author</h2>
<div>{{ owner }}</div>
@@ -25,7 +26,7 @@
<router-link :to="publishedByUser"> Published {{ plural }} by {{ owner }}. </router-link>
</div>
</div>
<LoadingSpan v-else message="Loading details" />
<LoadingSpan v-else message="Loading item details" />
</div>
</div>
</div>
@@ -40,21 +41,24 @@ export default {
StatelessTags,
},
props: {
details: {
item: {
type: Object,
required: true,
},
},
computed: {
gravatarSource() {
return `https://secure.gravatar.com/avatar/${this.item.email_hash}?d=identicon`;
},
modelTitle() {
const modelClass = this.details ? this.details.model_class : "Item";
const modelClass = this.item ? this.item.model_class : "Item";
if (modelClass == "StoredWorkflow") {
return "Workflow";
}
return modelClass;
},
owner() {
return this.details.owner || this.details.username || "Unavailable";
return this.item.owner || this.item.username || "Unavailable";
},
plural() {
if (this.modelTitle == "History") {
@@ -66,7 +70,7 @@ export default {
return this.plural.toLowerCase();
},
publishedByUser() {
return `/${this.pluralPath}/list_published?f-username=${this.details.username}`;
return `/${this.pluralPath}/list_published?f-username=${this.item.username}`;
},
urlAll() {
return `/${this.pluralPath}/list_published`;
@@ -1,5 +1,5 @@
<template>
<Published :details="history">
<Published :item="history">
<template v-slot>
<HistoryView :id="id" />
</template>
@@ -1,6 +1,6 @@
<template>
<config-provider v-slot="{ config, loading }">
<Published :details="page">
<Published :item="page">
<template v-slot>
<div v-if="!loading">
<markdown
@@ -57,7 +57,7 @@ export default {
},
created() {
urlData({ url: this.dataUrl }).then((data) => {
this.page = { ...data };
this.page = data;
});
},
methods: {
@@ -1,5 +1,5 @@
<template>
<Published :details="visualization">
<Published :item="visualization">
<template v-slot>
<CenterFrame :src="getUrl" />
</template>
@@ -1,5 +1,5 @@
<template>
<Published :details="workflow">
<Published :item="workflow">
<template v-slot>
<WorkflowDisplay :args="{ workflow_id: id }" :workflow="workflow" :expanded="true" />
</template>
+1
View File
@@ -440,6 +440,7 @@ class HistorySerializer(sharable.SharableModelSerializer, deletable.PurgableSeri
"detailed",
[
"contents_url",
"email_hash",
"empty",
"size",
"user_id",
+18 -1
View File
@@ -45,6 +45,7 @@ from galaxy.schema.schema import (
)
from galaxy.structured_app import MinimalManagerApp
from galaxy.util import ready_name_for_url
from galaxy.util.hash_util import md5_hash_str
if TYPE_CHECKING:
from sqlalchemy.orm import Query
@@ -383,7 +384,17 @@ class SharableModelSerializer(
def __init__(self, app, **kwargs):
super().__init__(app, **kwargs)
self.add_view(
"sharing", ["id", "title", "importable", "published", "username", "username_and_slug", "users_shared_with"]
"sharing",
[
"id",
"title",
"email_hash",
"importable",
"published",
"username",
"username_and_slug",
"users_shared_with",
],
)
def add_serializers(self):
@@ -398,11 +409,17 @@ class SharableModelSerializer(
"username": self.serialize_username,
"username_and_slug": self.serialize_username_and_slug,
"users_shared_with": self.serialize_users_shared_with,
"email_hash": self.serialize_email_hash,
}
)
# these use the default serializer but must still be white-listed
self.serializable_keyset.update(["importable", "published", "slug"])
def serialize_email_hash(self, item, key, **context):
if not (item.user and item.user.email):
return None
return md5_hash_str(item.user.email)
def serialize_title(self, item, key, **context):
if hasattr(item, "title"):
return item.title
+2
View File
@@ -69,6 +69,7 @@ from galaxy.tools.parameters.basic import (
RuntimeValue,
workflow_building_modes,
)
from galaxy.util.hash_util import md5_hash_str
from galaxy.util.json import (
safe_dumps,
safe_loads,
@@ -1480,6 +1481,7 @@ class WorkflowContentsManager(UsesAnnotations):
item["name"] = workflow.name
item["url"] = url_for("workflow", id=item["id"])
item["owner"] = stored.user.username
item["email_hash"] = md5_hash_str(stored.user.email)
item["slug"] = stored.slug
inputs = {}
for step in workflow.input_steps:
+12 -3
View File
@@ -146,7 +146,10 @@ from galaxy.util.form_builder import (
WorkflowField,
WorkflowMappingField,
)
from galaxy.util.hash_util import new_insecure_hash
from galaxy.util.hash_util import (
md5_hash_str,
new_insecure_hash,
)
from galaxy.util.json import safe_loads
from galaxy.util.sanitize_html import sanitize_html
@@ -8836,7 +8839,7 @@ class CloudAuthz(Base, _HasTable):
)
class Page(Base, Dictifiable, RepresentById):
class Page(Base, HasTags, Dictifiable, RepresentById):
__tablename__ = "page"
__table_args__ = (Index("ix_page_slug", "slug", mysql_length=200),)
@@ -8892,6 +8895,7 @@ class Page(Base, Dictifiable, RepresentById):
"importable",
"deleted",
"username",
"email_hash",
]
def to_dict(self, view="element"):
@@ -8907,6 +8911,11 @@ class Page(Base, Dictifiable, RepresentById):
def username(self):
return self.user.username
# email needed for hash generation
@property
def email_hash(self):
return md5_hash_str(self.user.email)
class PageRevision(Base, Dictifiable, RepresentById):
__tablename__ = "page_revision"
@@ -8942,7 +8951,7 @@ class PageUserShareAssociation(Base, UserShareAssociation):
page = relationship("Page", back_populates="users_shared_with")
class Visualization(Base, RepresentById):
class Visualization(Base, HasTags, RepresentById):
__tablename__ = "visualization"
__table_args__ = (
Index("ix_visualization_dbkey", "dbkey", mysql_length=200),
+15 -5
View File
@@ -3014,11 +3014,6 @@ class SharingStatus(Model):
title="Importable",
description="Whether this resource can be published using a link.",
)
username: Optional[str] = Field(
None,
title="Username",
description="The owner's username.",
)
published: bool = Field(
...,
title="Published",
@@ -3029,6 +3024,16 @@ class SharingStatus(Model):
title="Users shared with",
description="The list of encoded ids for users the resource has been shared.",
)
email_hash: Optional[str] = Field(
None,
title="Encoded Email",
description="Encoded owner email.",
)
username: Optional[str] = Field(
None,
title="Username",
description="The owner's username.",
)
username_and_slug: Optional[str] = Field(
None,
title="Username and slug",
@@ -3199,6 +3204,11 @@ class PageSummary(PageSummaryBase):
title="Username",
description="The name of the user owning this Page.",
)
email_hash: str = Field(
..., # Required
title="Encoded email",
description="The encoded email of the user",
)
published: bool = Field(
..., # Required
title="Published",
+9
View File
@@ -90,6 +90,15 @@ def md5_hash_file(path: Union[str, os.PathLike]) -> Optional[str]:
return None
def md5_hash_str(s):
"""
Return hex encoded md5 hash of string s
"""
m = hashlib.md5()
m.update(smart_str(s))
return m.hexdigest()
def new_secure_hash_v2(text_type: Union[bytes, str]) -> str:
"""More modern version of new_secure_hash.
+1 -15
View File
@@ -14,11 +14,7 @@ from babel import default_locale
from babel.dates import format_timedelta
from routes import url_for
from galaxy.util import (
hash_util,
smart_str,
unicodify,
)
from galaxy.util import unicodify
from galaxy.util.json import safe_dumps as dumps # noqa: F401
from .tags import (
javascript_link,
@@ -102,16 +98,6 @@ def dist_js(*args):
return js_helper("static/dist/", *args)
# Hashes
def md5(s):
"""
Return hex encoded md5 hash of string s
"""
m = hash_util.md5()
m.update(smart_str(s))
return m.hexdigest()
# Unicode help
def to_unicode(a_string):
"""
@@ -28,6 +28,7 @@ from galaxy.schema.schema import (
ShareWithStatus,
SharingStatus,
)
from galaxy.util.hash_util import md5_hash_str
from galaxy.web import expose_api
from galaxy.webapps.base.controller import UsesVisualizationMixin
from galaxy.webapps.base.webapp import GalaxyWebTransaction
@@ -191,6 +192,8 @@ class VisualizationsController(BaseGalaxyAPIController, UsesVisualizationMixin,
slug=visualization.slug,
)
dictionary["username"] = visualization.user.username
dictionary["email_hash"] = md5_hash_str(visualization.user.email)
dictionary["tags"] = visualization.make_tag_string_list()
dictionary["annotation"] = self.get_item_annotation_str(trans.sa_session, trans.user, visualization)
# need to encode ids in revisions as well
encoded_revisions = []
@@ -555,23 +555,6 @@ class DatasetInterface(BaseUIController, UsesAnnotations, UsesItemRatings, UsesE
"""Does nothing because datasets do not have an importable/accessible attribute. This method could potentially set another attribute."""
return
@web.expose
@web.require_login("rate items")
@web.json
def rate_async(self, trans, id, rating):
"""Rate a dataset asynchronously and return updated community data."""
decoded_id = self.decode_id(id)
dataset = self.hda_manager.get_accessible(decoded_id, trans.user)
dataset = self.hda_manager.error_if_uploading(dataset)
if not dataset:
return trans.show_error_message("The specified dataset does not exist.")
# Rate dataset.
self.rate_item(trans.sa_session, trans.get_user(), dataset, rating)
return self.get_ave_item_rating_data(trans.sa_session, dataset)
@web.expose
def display_by_username_and_slug(self, trans, username, slug, filename=None, preview=True):
"""Display dataset by username and slug; because datasets do not yet have slugs, the slug is the dataset's id."""
@@ -600,24 +583,11 @@ class DatasetInterface(BaseUIController, UsesAnnotations, UsesItemRatings, UsesE
trans.response.set_content_type(dataset.get_mime())
return open(dataset.file_name, "rb")
else:
# Get rating data.
user_item_rating = 0
if trans.get_user():
user_item_rating = self.get_user_item_rating(trans.sa_session, trans.get_user(), dataset)
if user_item_rating:
user_item_rating = user_item_rating.rating
else:
user_item_rating = 0
ave_item_rating, num_ratings = self.get_ave_item_rating_data(trans.sa_session, dataset)
return trans.fill_template_mako(
"/dataset/display.mako",
item=dataset,
item_data=dataset_data,
truncated=truncated,
user_item_rating=user_item_rating,
ave_item_rating=ave_item_rating,
num_ratings=num_ratings,
first_chunk=first_chunk,
)
@@ -571,16 +571,6 @@ class HistoryController(BaseUIController, SharableMixin, UsesAnnotations, UsesIt
# Security check raises error if user cannot access history.
self.history_manager.error_unless_accessible(history, trans.user, current_history=trans.history)
# Get rating data.
user_item_rating = 0
if trans.get_user():
user_item_rating = self.get_user_item_rating(trans.sa_session, trans.get_user(), history)
if user_item_rating:
user_item_rating = user_item_rating.rating
else:
user_item_rating = 0
ave_item_rating, num_ratings = self.get_ave_item_rating_data(trans.sa_session, history)
# Encode history id.
history_id = trans.security.encode_id(history.id)
@@ -590,9 +580,6 @@ class HistoryController(BaseUIController, SharableMixin, UsesAnnotations, UsesIt
controller="published",
action="history",
id=history_id,
user_item_rating=user_item_rating,
ave_item_rating=ave_item_rating,
num_ratings=num_ratings,
)
)
@@ -741,19 +728,6 @@ class HistoryController(BaseUIController, SharableMixin, UsesAnnotations, UsesIt
return trans.show_ok_message("Your jobs have been resumed.", refresh_frames=refresh_frames)
# TODO: used in index.mako
@web.expose
@web.require_login("rate items")
@web.json
def rate_async(self, trans, id, rating):
"""Rate a history asynchronously and return updated community data."""
history = self.history_manager.get_accessible(self.decode_id(id), trans.user, current_history=trans.history)
if not history:
return trans.show_error_message("The specified history does not exist.")
# Rate history.
self.rate_item(trans.sa_session, trans.get_user(), history, rating)
return self.get_ave_item_rating_data(trans.sa_session, history)
# TODO: used in display_base.mako
@web.expose
@web.json
@web.require_login("get history name and link")
@@ -549,25 +549,12 @@ class PageController(BaseUIController, SharableMixin, UsesStoredWorkflowMixin, U
# Encode page identifier.
page_id = trans.security.encode_id(page.id)
# Get rating data.
user_item_rating = 0
if trans.get_user():
user_item_rating = self.get_user_item_rating(trans.sa_session, trans.get_user(), page)
if user_item_rating:
user_item_rating = user_item_rating.rating
else:
user_item_rating = 0
ave_item_rating, num_ratings = self.get_ave_item_rating_data(trans.sa_session, page)
# Redirect to client.
return trans.response.send_redirect(
web.url_for(
controller="published",
action="page",
id=page_id,
user_item_rating=user_item_rating,
ave_item_rating=ave_item_rating,
num_ratings=num_ratings,
)
)
@@ -587,21 +574,6 @@ class PageController(BaseUIController, SharableMixin, UsesStoredWorkflowMixin, U
trans.sa_session.flush()
return
@web.expose
@web.require_login("rate items")
@web.json
def rate_async(self, trans, id, rating):
"""Rate a page asynchronously and return updated community data."""
page = self.get_page(trans, id, check_ownership=False, check_accessible=True)
if not page:
return trans.show_error_message("The specified page does not exist.")
# Rate page.
self.rate_item(trans.sa_session, trans.get_user(), page, rating)
return self.get_ave_item_rating_data(trans.sa_session, page)
@web.expose
def get_embed_html_async(self, trans, id):
"""Returns HTML for embedding a workflow in a page."""
@@ -435,21 +435,6 @@ class VisualizationController(
return
@web.expose
@web.require_login("rate items")
@web.json
def rate_async(self, trans, id, rating):
"""Rate a visualization asynchronously and return updated community data."""
visualization = self.get_visualization(trans, id, check_ownership=False, check_accessible=True)
if not visualization:
return trans.show_error_message("The specified visualization does not exist.")
# Rate visualization.
self.rate_item(trans.sa_session, trans.get_user(), visualization, rating)
return self.get_ave_item_rating_data(trans.sa_session, visualization)
@web.expose
@web.require_login("share Galaxy visualizations")
def imp(self, trans, id):
@@ -511,25 +496,12 @@ class VisualizationController(
# Encode page identifier.
visualization_id = trans.security.encode_id(visualization.id)
# Get rating data.
user_item_rating = 0
if trans.get_user():
user_item_rating = self.get_user_item_rating(trans.sa_session, trans.get_user(), visualization)
if user_item_rating:
user_item_rating = user_item_rating.rating
else:
user_item_rating = 0
ave_item_rating, num_ratings = self.get_ave_item_rating_data(trans.sa_session, visualization)
# Redirect to client.
return trans.response.send_redirect(
web.url_for(
controller="published",
action="visualization",
id=visualization_id,
user_item_rating=user_item_rating,
ave_item_rating=ave_item_rating,
num_ratings=num_ratings,
)
)
@@ -268,16 +268,6 @@ class WorkflowController(BaseUIController, SharableMixin, UsesStoredWorkflowMixi
for step in stored_workflow.latest_workflow.steps:
step.annotation = self.get_item_annotation_str(trans.sa_session, stored_workflow.user, step)
# Get rating data.
user_item_rating = 0
if trans.get_user():
user_item_rating = self.get_user_item_rating(trans.sa_session, trans.get_user(), stored_workflow)
if user_item_rating:
user_item_rating = user_item_rating.rating
else:
user_item_rating = 0
ave_item_rating, num_ratings = self.get_ave_item_rating_data(trans.sa_session, stored_workflow)
# Encode page identifier.
workflow_id = trans.security.encode_id(stored_workflow.id)
@@ -287,9 +277,6 @@ class WorkflowController(BaseUIController, SharableMixin, UsesStoredWorkflowMixi
controller="published",
action="workflow",
id=workflow_id,
user_item_rating=user_item_rating,
ave_item_rating=ave_item_rating,
num_ratings=num_ratings,
)
)
@@ -346,21 +333,6 @@ class WorkflowController(BaseUIController, SharableMixin, UsesStoredWorkflowMixi
trans.sa_session.flush()
return new_annotation
@web.expose
@web.require_login("rate items")
@web.json
def rate_async(self, trans, id, rating):
"""Rate a workflow asynchronously and return updated community data."""
stored = self.get_stored_workflow(trans, id, check_ownership=False, check_accessible=True)
if not stored:
return trans.show_error_message("The specified workflow does not exist.")
# Rate workflow.
self.rate_item(trans.sa_session, trans.get_user(), stored, rating)
return self.get_ave_item_rating_data(trans.sa_session, stored)
@web.expose
def get_embed_html_async(self, trans, id):
"""Returns HTML for embedding a workflow in a page."""
+1 -74
View File
@@ -138,80 +138,7 @@
<p>
<a href="${href_to_all_items}">All published ${item_plural.lower()}</a><br>
<a href="${href_to_user_items}">Published ${item_plural.lower()} by ${item.user.username | h}</a>
## Rating.
<h4>Rating</h4>
<%
label = "ratings"
if num_ratings == 1:
label = "rating"
%>
<div style="padding-bottom: 0.75em; float: left">
Community<br>
<span style="font-size:80%">
(<span id="num_ratings">${num_ratings}</span> ${label},
<span id="ave_rating">${"%.1f" % ave_item_rating}</span> average)
<span>
</div>
<div style="float: right">
<input name="star1" type="radio" class="community_rating_star star" disabled="disabled" value="1"
%if ave_item_rating > 0 and ave_item_rating <= 1.5:
checked="checked"
%endif
/>
<input name="star1" type="radio" class="community_rating_star star" disabled="disabled" value="2"
%if ave_item_rating > 1.5 and ave_item_rating <= 2.5:
checked="checked"
%endif
/>
<input name="star1" type="radio" class="community_rating_star star" disabled="disabled" value="3"
%if ave_item_rating > 2.5 and ave_item_rating <= 3.5:
checked="checked"
%endif
/>
<input name="star1" type="radio" class="community_rating_star star" disabled="disabled" value="4"
%if ave_item_rating > 3.5 and ave_item_rating <= 4.5:
checked="checked"
%endif
/>
<input name="star1" type="radio" class="community_rating_star star" disabled="disabled" value="5"
%if ave_item_rating > 4.5:
checked="checked"
%endif
/>
</div>
<div style="clear: both;"></div>
%if trans.get_user():
<div style="float: left">
Yours<br><span id="rating_feedback" style="font-size:80%; display: none">(thanks!)</span>
</div>
<div style="float: right">
<input name="star2" type="radio" class="user_rating_star" value="1"
%if user_item_rating == 1:
checked="checked"
%endif
/>
<input name="star2" type="radio" class="user_rating_star" value="2"
%if user_item_rating == 2:
checked="checked"
%endif
/>
<input name="star2" type="radio" class="user_rating_star" value="3"
%if user_item_rating == 3:
checked="checked"
%endif
/>
<input name="star2" type="radio" class="user_rating_star" value="4"
%if user_item_rating == 4:
checked="checked"
%endif
/>
<input name="star2" type="radio" class="user_rating_star" value="5"
%if user_item_rating == 5:
checked="checked"
%endif
/>
</div>
%endif
</p>
<div style="clear: both;"></div>
## Tags.
<h4>Tags</h4>