Merge branch 'master' into dillo
# Conflicts: # pillar/api/nodes/__init__.py
This commit is contained in:
commit
38e4c7c937
@ -579,6 +579,8 @@ projects_schema = {
|
|||||||
'picture_square': _file_embedded_schema,
|
'picture_square': _file_embedded_schema,
|
||||||
# Header
|
# Header
|
||||||
'picture_header': _file_embedded_schema,
|
'picture_header': _file_embedded_schema,
|
||||||
|
# Picture with a 16:9 aspect ratio (for Open Graph)
|
||||||
|
'picture_16_9': _file_embedded_schema,
|
||||||
'header_node': dict(
|
'header_node': dict(
|
||||||
nullable=True,
|
nullable=True,
|
||||||
**_node_embedded_schema
|
**_node_embedded_schema
|
||||||
|
@ -149,6 +149,12 @@ def post_node_comment(parent_id: bson.ObjectId, markdown_msg: str, attachments:
|
|||||||
rating_positive=0,
|
rating_positive=0,
|
||||||
rating_negative=0,
|
rating_negative=0,
|
||||||
attachments=attachments,
|
attachments=attachments,
|
||||||
|
),
|
||||||
|
permissions=dict(
|
||||||
|
users=[dict(
|
||||||
|
user=current_user.objectid,
|
||||||
|
methods=['PUT'])
|
||||||
|
]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
r, _, _, status = current_app.post_internal('nodes', comment)
|
r, _, _, status = current_app.post_internal('nodes', comment)
|
||||||
|
@ -7,7 +7,6 @@ from bson import ObjectId
|
|||||||
from werkzeug import exceptions as wz_exceptions
|
from werkzeug import exceptions as wz_exceptions
|
||||||
|
|
||||||
from pillar import current_app
|
from pillar import current_app
|
||||||
import pillar.markdown
|
|
||||||
from pillar.api.activities import activity_subscribe, activity_object_add
|
from pillar.api.activities import activity_subscribe, activity_object_add
|
||||||
from pillar.api.file_storage_backends.gcs import update_file_name
|
from pillar.api.file_storage_backends.gcs import update_file_name
|
||||||
from pillar.api.node_types import PILLAR_NAMED_NODE_TYPES
|
from pillar.api.node_types import PILLAR_NAMED_NODE_TYPES
|
||||||
@ -122,6 +121,7 @@ def before_inserting_nodes(items):
|
|||||||
# Default the 'user' property to the current user.
|
# Default the 'user' property to the current user.
|
||||||
item.setdefault('user', current_user.user_id)
|
item.setdefault('user', current_user.user_id)
|
||||||
|
|
||||||
|
|
||||||
def get_comment_verb_and_context_object_id(comment):
|
def get_comment_verb_and_context_object_id(comment):
|
||||||
nodes_collection = current_app.data.driver.db['nodes']
|
nodes_collection = current_app.data.driver.db['nodes']
|
||||||
verb = 'commented'
|
verb = 'commented'
|
||||||
@ -152,7 +152,6 @@ def after_inserting_nodes(items):
|
|||||||
# Subscribe to the parent of the parent comment (post or group)
|
# Subscribe to the parent of the parent comment (post or group)
|
||||||
activity_subscribe(item['user'], 'node', context_object_id)
|
activity_subscribe(item['user'], 'node', context_object_id)
|
||||||
|
|
||||||
|
|
||||||
if context_object_id and item['node_type'] in PILLAR_NAMED_NODE_TYPES:
|
if context_object_id and item['node_type'] in PILLAR_NAMED_NODE_TYPES:
|
||||||
# * Skip activity for first level items (since the context is not a
|
# * Skip activity for first level items (since the context is not a
|
||||||
# node, but a project).
|
# node, but a project).
|
||||||
|
@ -30,6 +30,7 @@ class ProjectForm(FlaskForm):
|
|||||||
('deleted', 'Deleted')])
|
('deleted', 'Deleted')])
|
||||||
picture_header = FileSelectField('Picture header', file_format='image')
|
picture_header = FileSelectField('Picture header', file_format='image')
|
||||||
picture_square = FileSelectField('Picture square', file_format='image')
|
picture_square = FileSelectField('Picture square', file_format='image')
|
||||||
|
picture_16_9 = FileSelectField('Picture 16:9', file_format='image')
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
rv = FlaskForm.validate(self)
|
rv = FlaskForm.validate(self)
|
||||||
|
@ -349,8 +349,7 @@ def project_navigation_links(project: typing.Type[Project], api) -> list:
|
|||||||
|
|
||||||
|
|
||||||
def render_project(project, api, extra_context=None, template_name=None):
|
def render_project(project, api, extra_context=None, template_name=None):
|
||||||
project.picture_square = utils.get_file(project.picture_square, api=api)
|
utils.attach_project_pictures(project, api)
|
||||||
project.picture_header = utils.get_file(project.picture_header, api=api)
|
|
||||||
|
|
||||||
def load_latest(list_of_ids, node_type=None):
|
def load_latest(list_of_ids, node_type=None):
|
||||||
"""Loads a list of IDs in reversed order."""
|
"""Loads a list of IDs in reversed order."""
|
||||||
@ -424,7 +423,7 @@ def render_project(project, api, extra_context=None, template_name=None):
|
|||||||
node=None,
|
node=None,
|
||||||
show_node=False,
|
show_node=False,
|
||||||
show_project=True,
|
show_project=True,
|
||||||
og_picture=project.picture_header,
|
og_picture=project.picture_16_9,
|
||||||
activity_stream=activity_stream,
|
activity_stream=activity_stream,
|
||||||
navigation_links=navigation_links,
|
navigation_links=navigation_links,
|
||||||
extension_sidebar_links=extension_sidebar_links,
|
extension_sidebar_links=extension_sidebar_links,
|
||||||
@ -492,9 +491,9 @@ def view_node(project_url, node_id):
|
|||||||
extension_sidebar_links = ''
|
extension_sidebar_links = ''
|
||||||
og_picture = node.picture = utils.get_file(node.picture, api=api)
|
og_picture = node.picture = utils.get_file(node.picture, api=api)
|
||||||
if project:
|
if project:
|
||||||
|
utils.attach_project_pictures(project, api)
|
||||||
if not node.picture:
|
if not node.picture:
|
||||||
og_picture = utils.get_file(project.picture_header, api=api)
|
og_picture = project.picture_16_9
|
||||||
project.picture_square = utils.get_file(project.picture_square, api=api)
|
|
||||||
navigation_links = project_navigation_links(project, api)
|
navigation_links = project_navigation_links(project, api)
|
||||||
extension_sidebar_links = current_app.extension_sidebar_links(project)
|
extension_sidebar_links = current_app.extension_sidebar_links(project)
|
||||||
|
|
||||||
@ -541,8 +540,7 @@ def search(project_url):
|
|||||||
"""Search into a project"""
|
"""Search into a project"""
|
||||||
api = system_util.pillar_api()
|
api = system_util.pillar_api()
|
||||||
project = find_project_or_404(project_url, api=api)
|
project = find_project_or_404(project_url, api=api)
|
||||||
project.picture_square = utils.get_file(project.picture_square, api=api)
|
utils.attach_project_pictures(project, api)
|
||||||
project.picture_header = utils.get_file(project.picture_header, api=api)
|
|
||||||
|
|
||||||
return render_template('nodes/search.html',
|
return render_template('nodes/search.html',
|
||||||
project=project,
|
project=project,
|
||||||
@ -583,6 +581,8 @@ def edit(project_url):
|
|||||||
project.picture_square = form.picture_square.data
|
project.picture_square = form.picture_square.data
|
||||||
if form.picture_header.data:
|
if form.picture_header.data:
|
||||||
project.picture_header = form.picture_header.data
|
project.picture_header = form.picture_header.data
|
||||||
|
if form.picture_16_9.data:
|
||||||
|
project.picture_16_9 = form.picture_16_9.data
|
||||||
|
|
||||||
# Update world permissions from is_private checkbox
|
# Update world permissions from is_private checkbox
|
||||||
if form.is_private.data:
|
if form.is_private.data:
|
||||||
@ -598,6 +598,8 @@ def edit(project_url):
|
|||||||
form.picture_square.data = project.picture_square._id
|
form.picture_square.data = project.picture_square._id
|
||||||
if project.picture_header:
|
if project.picture_header:
|
||||||
form.picture_header.data = project.picture_header._id
|
form.picture_header.data = project.picture_header._id
|
||||||
|
if project.picture_16_9:
|
||||||
|
form.picture_16_9.data = project.picture_16_9._id
|
||||||
|
|
||||||
# List of fields from the form that should be hidden to regular users
|
# List of fields from the form that should be hidden to regular users
|
||||||
if current_user.has_role('admin'):
|
if current_user.has_role('admin'):
|
||||||
|
@ -45,6 +45,7 @@ def attach_project_pictures(project, api):
|
|||||||
|
|
||||||
project.picture_square = get_file(project.picture_square, api=api)
|
project.picture_square = get_file(project.picture_square, api=api)
|
||||||
project.picture_header = get_file(project.picture_header, api=api)
|
project.picture_header = get_file(project.picture_header, api=api)
|
||||||
|
project.picture_16_9 = get_file(project.picture_16_9, api=api)
|
||||||
|
|
||||||
|
|
||||||
def mass_attach_project_pictures(projects: typing.Iterable[pillarsdk.Project], *,
|
def mass_attach_project_pictures(projects: typing.Iterable[pillarsdk.Project], *,
|
||||||
|
@ -52,7 +52,7 @@ html5lib==1.0.1
|
|||||||
idna==2.5
|
idna==2.5
|
||||||
ipaddress==1.0.22
|
ipaddress==1.0.22
|
||||||
itsdangerous==0.24
|
itsdangerous==0.24
|
||||||
Jinja2==2.10
|
Jinja2==2.10.1
|
||||||
kombu==4.2.1
|
kombu==4.2.1
|
||||||
oauth2client==4.1.2
|
oauth2client==4.1.2
|
||||||
oauthlib==2.1.0
|
oauthlib==2.1.0
|
||||||
|
@ -689,6 +689,12 @@
|
|||||||
.pointer-events-none
|
.pointer-events-none
|
||||||
pointer-events: none
|
pointer-events: none
|
||||||
|
|
||||||
|
.column-count-2
|
||||||
|
column-count: 2
|
||||||
|
|
||||||
|
.column-count-3
|
||||||
|
column-count: 3
|
||||||
|
|
||||||
// Bootstrap has .img-fluid, a class to limit the width of an image to 100%.
|
// Bootstrap has .img-fluid, a class to limit the width of an image to 100%.
|
||||||
// .imgs-fluid below is to be applied on a parent container when we can't add
|
// .imgs-fluid below is to be applied on a parent container when we can't add
|
||||||
// classes to the images themselves. e.g. the blog.
|
// classes to the images themselves. e.g. the blog.
|
||||||
|
@ -33,6 +33,9 @@
|
|||||||
@import "../../node_modules/bootstrap/scss/utilities"
|
@import "../../node_modules/bootstrap/scss/utilities"
|
||||||
|
|
||||||
// Pillar components.
|
// Pillar components.
|
||||||
|
$pillar-font-path: "../../../../static/pillar/assets/font"
|
||||||
|
@import "../../../pillar/src/styles/font-pillar"
|
||||||
|
|
||||||
@import "apps_base"
|
@import "apps_base"
|
||||||
@import "components/base"
|
@import "components/base"
|
||||||
|
|
||||||
|
@ -155,14 +155,14 @@ $card-progress-height: 5px
|
|||||||
|
|
||||||
/* Tiny label for cards. e.g. 'WATCHED' on videos. */
|
/* Tiny label for cards. e.g. 'WATCHED' on videos. */
|
||||||
.card-label
|
.card-label
|
||||||
background-color: rgba($black, .5)
|
@extend .font-weight-bold
|
||||||
border-radius: 3px
|
@extend .position-absolute
|
||||||
color: $white
|
@extend .rounded
|
||||||
display: block
|
@extend .text-white
|
||||||
|
@extend .bg-dark
|
||||||
|
bottom: $card-progress-height + 3px // enough to be above the progress-bar
|
||||||
font-size: $font-size-xxs
|
font-size: $font-size-xxs
|
||||||
left: 5px
|
left: 5px
|
||||||
bottom: $card-progress-height + 3px // enough to be above the progress-bar
|
|
||||||
position: absolute
|
|
||||||
padding: 1px 5px
|
padding: 1px 5px
|
||||||
z-index: 1
|
z-index: 1
|
||||||
|
|
||||||
|
@ -15,10 +15,9 @@
|
|||||||
&-title // .group-title
|
&-title // .group-title
|
||||||
@extend .border-bottom
|
@extend .border-bottom
|
||||||
@extend .bg-white
|
@extend .bg-white
|
||||||
@extend .text-uppercase
|
|
||||||
@extend .font-weight-bold
|
|
||||||
a
|
a
|
||||||
color: $color-text
|
color: $color-text-dark-hint
|
||||||
|
|
||||||
.node-details-description
|
.node-details-description
|
||||||
font:
|
font:
|
||||||
|
@ -24,9 +24,9 @@
|
|||||||
|
|
||||||
@import "../../node_modules/bootstrap/scss/utilities"
|
@import "../../node_modules/bootstrap/scss/utilities"
|
||||||
|
|
||||||
|
|
||||||
// Pillar components.
|
// Pillar components.
|
||||||
@import "font-pillar"
|
$pillar-font-path: "../../../../static/pillar/assets/font"
|
||||||
|
@import "../../../pillar/src/styles/font-pillar"
|
||||||
|
|
||||||
@import "apps_base"
|
@import "apps_base"
|
||||||
@import "components/navbar"
|
@import "components/navbar"
|
||||||
|
@ -49,16 +49,16 @@ a.card.asset.card-image-fade.mb-2(
|
|||||||
|
|
||||||
ul.card-text.list-unstyled.d-flex.text-black-50.mt-auto.mb-0.text-truncate
|
ul.card-text.list-unstyled.d-flex.text-black-50.mt-auto.mb-0.text-truncate
|
||||||
| {% if node_type %}
|
| {% if node_type %}
|
||||||
li.pr-2.font-weight-bold {{ node_type | undertitle }}
|
li.item-type.pr-2.font-weight-bold {{ node_type | undertitle }}
|
||||||
| {% endif %}
|
| {% endif %}
|
||||||
| {% if asset.project.name %}
|
| {% if asset.project.name %}
|
||||||
li.pr-2.text-truncate {{ asset.project.name }}
|
li.item-name.pr-2.text-truncate {{ asset.project.name }}
|
||||||
| {% endif %}
|
| {% endif %}
|
||||||
| {% if asset.user.full_name %}
|
| {% if asset.user.full_name %}
|
||||||
li.pr-2.text-truncate {{ asset.user.full_name }}
|
li.item-full_name.pr-2.text-truncate {{ asset.user.full_name }}
|
||||||
| {% endif %}
|
| {% endif %}
|
||||||
| {% if asset._created %}
|
| {% if asset._created %}
|
||||||
li.text-truncate {{ asset._created | pretty_date }}
|
li.item-date.text-truncate {{ asset._created | pretty_date }}
|
||||||
| {% endif %}
|
| {% endif %}
|
||||||
|
|
||||||
| {% endmacro %}
|
| {% endmacro %}
|
||||||
|
@ -70,6 +70,22 @@ class CommentEditTest(AbstractPillarTest):
|
|||||||
|
|
||||||
self.user_uid = self.create_user(24 * 'b', groups=[ctd.EXAMPLE_ADMIN_GROUP_ID],
|
self.user_uid = self.create_user(24 * 'b', groups=[ctd.EXAMPLE_ADMIN_GROUP_ID],
|
||||||
token='user-token')
|
token='user-token')
|
||||||
|
self.other_user_uid = self.create_user(24 * 'c',token='other-user-token')
|
||||||
|
|
||||||
|
# Add world POST permission to comments for the project
|
||||||
|
# This allows any user to post a comment
|
||||||
|
for node_type in self.project['node_types']:
|
||||||
|
if node_type['name'] != 'comment':
|
||||||
|
continue
|
||||||
|
node_type['permissions'] = {'world': ['POST']}
|
||||||
|
|
||||||
|
with self.app.app_context():
|
||||||
|
proj_coll = self.app.db('projects')
|
||||||
|
proj_coll.update(
|
||||||
|
{'_id': self.pid},
|
||||||
|
{'$set': {
|
||||||
|
'node_types': self.project['node_types'],
|
||||||
|
}})
|
||||||
|
|
||||||
def test_edit_comment(self):
|
def test_edit_comment(self):
|
||||||
# Create the comment
|
# Create the comment
|
||||||
@ -86,7 +102,50 @@ class CommentEditTest(AbstractPillarTest):
|
|||||||
payload = json.loads(resp.data)
|
payload = json.loads(resp.data)
|
||||||
comment_id = payload['id']
|
comment_id = payload['id']
|
||||||
|
|
||||||
comment_url = flask.url_for('nodes_api.patch_node_comment', node_path=str(self.node_id), comment_path=comment_id)
|
comment_url = flask.url_for('nodes_api.patch_node_comment', node_path=str(self.node_id),
|
||||||
|
comment_path=comment_id)
|
||||||
|
# Edit the comment
|
||||||
|
resp = self.patch(
|
||||||
|
comment_url,
|
||||||
|
json={
|
||||||
|
'msg': 'Edited comment',
|
||||||
|
},
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(200, resp.status_code)
|
||||||
|
payload = json.loads(resp.data)
|
||||||
|
self.assertEqual('Edited comment', payload['msg_markdown'])
|
||||||
|
self.assertEqual('<p>Edited comment</p>\n', payload['msg_html'])
|
||||||
|
|
||||||
|
def test_edit_comment_non_admin(self):
|
||||||
|
"""Verify that a comment can be edited by a regular user."""
|
||||||
|
# Create the comment
|
||||||
|
with self.login_as(self.other_user_uid):
|
||||||
|
comment_url = flask.url_for('nodes_api.post_node_comment', node_path=str(self.node_id))
|
||||||
|
resp = self.post(
|
||||||
|
comment_url,
|
||||||
|
json={
|
||||||
|
'msg': 'There is no place like [home](https://cloud.blender.org/)',
|
||||||
|
},
|
||||||
|
expected_status=201,
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = json.loads(resp.data)
|
||||||
|
|
||||||
|
# Check that the comment has edit (PUT) permission for the current user
|
||||||
|
with self.app.app_context():
|
||||||
|
nodes_coll = self.app.db('nodes')
|
||||||
|
db_node = nodes_coll.find_one(ObjectId(payload['id']))
|
||||||
|
expected_permissions = {'users': [{
|
||||||
|
'user': self.other_user_uid,
|
||||||
|
'methods': ['PUT']
|
||||||
|
}]}
|
||||||
|
self.assertEqual(db_node['permissions'], expected_permissions)
|
||||||
|
|
||||||
|
comment_id = payload['id']
|
||||||
|
comment_url = flask.url_for('nodes_api.patch_node_comment', node_path=str(self.node_id),
|
||||||
|
comment_path=comment_id)
|
||||||
# Edit the comment
|
# Edit the comment
|
||||||
resp = self.patch(
|
resp = self.patch(
|
||||||
comment_url,
|
comment_url,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user