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,
|
||||
# Header
|
||||
'picture_header': _file_embedded_schema,
|
||||
# Picture with a 16:9 aspect ratio (for Open Graph)
|
||||
'picture_16_9': _file_embedded_schema,
|
||||
'header_node': dict(
|
||||
nullable=True,
|
||||
**_node_embedded_schema
|
||||
|
@ -149,6 +149,12 @@ def post_node_comment(parent_id: bson.ObjectId, markdown_msg: str, attachments:
|
||||
rating_positive=0,
|
||||
rating_negative=0,
|
||||
attachments=attachments,
|
||||
),
|
||||
permissions=dict(
|
||||
users=[dict(
|
||||
user=current_user.objectid,
|
||||
methods=['PUT'])
|
||||
]
|
||||
)
|
||||
)
|
||||
r, _, _, status = current_app.post_internal('nodes', comment)
|
||||
|
@ -7,7 +7,6 @@ from bson import ObjectId
|
||||
from werkzeug import exceptions as wz_exceptions
|
||||
|
||||
from pillar import current_app
|
||||
import pillar.markdown
|
||||
from pillar.api.activities import activity_subscribe, activity_object_add
|
||||
from pillar.api.file_storage_backends.gcs import update_file_name
|
||||
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.
|
||||
item.setdefault('user', current_user.user_id)
|
||||
|
||||
|
||||
def get_comment_verb_and_context_object_id(comment):
|
||||
nodes_collection = current_app.data.driver.db['nodes']
|
||||
verb = 'commented'
|
||||
@ -152,7 +152,6 @@ def after_inserting_nodes(items):
|
||||
# Subscribe to the parent of the parent comment (post or group)
|
||||
activity_subscribe(item['user'], 'node', context_object_id)
|
||||
|
||||
|
||||
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
|
||||
# node, but a project).
|
||||
|
@ -73,9 +73,9 @@ EXAMPLE_PROJECT = {
|
||||
'nodes_featured': [],
|
||||
'nodes_latest': [],
|
||||
'permissions': {'groups': [{'group': EXAMPLE_ADMIN_GROUP_ID,
|
||||
'methods': ['GET', 'POST', 'PUT', 'DELETE']}],
|
||||
'users': [],
|
||||
'world': ['GET']},
|
||||
'methods': ['GET', 'POST', 'PUT', 'DELETE']}],
|
||||
'users': [],
|
||||
'world': ['GET']},
|
||||
'picture_header': ObjectId('5673f260c379cf0007b31bc4'),
|
||||
'picture_square': ObjectId('5673f256c379cf0007b31bc3'),
|
||||
'status': 'published',
|
||||
|
@ -30,6 +30,7 @@ class ProjectForm(FlaskForm):
|
||||
('deleted', 'Deleted')])
|
||||
picture_header = FileSelectField('Picture header', file_format='image')
|
||||
picture_square = FileSelectField('Picture square', file_format='image')
|
||||
picture_16_9 = FileSelectField('Picture 16:9', file_format='image')
|
||||
|
||||
def 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):
|
||||
project.picture_square = utils.get_file(project.picture_square, api=api)
|
||||
project.picture_header = utils.get_file(project.picture_header, api=api)
|
||||
utils.attach_project_pictures(project, api)
|
||||
|
||||
def load_latest(list_of_ids, node_type=None):
|
||||
"""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,
|
||||
show_node=False,
|
||||
show_project=True,
|
||||
og_picture=project.picture_header,
|
||||
og_picture=project.picture_16_9,
|
||||
activity_stream=activity_stream,
|
||||
navigation_links=navigation_links,
|
||||
extension_sidebar_links=extension_sidebar_links,
|
||||
@ -492,9 +491,9 @@ def view_node(project_url, node_id):
|
||||
extension_sidebar_links = ''
|
||||
og_picture = node.picture = utils.get_file(node.picture, api=api)
|
||||
if project:
|
||||
utils.attach_project_pictures(project, api)
|
||||
if not node.picture:
|
||||
og_picture = utils.get_file(project.picture_header, api=api)
|
||||
project.picture_square = utils.get_file(project.picture_square, api=api)
|
||||
og_picture = project.picture_16_9
|
||||
navigation_links = project_navigation_links(project, api)
|
||||
extension_sidebar_links = current_app.extension_sidebar_links(project)
|
||||
|
||||
@ -541,8 +540,7 @@ def search(project_url):
|
||||
"""Search into a project"""
|
||||
api = system_util.pillar_api()
|
||||
project = find_project_or_404(project_url, api=api)
|
||||
project.picture_square = utils.get_file(project.picture_square, api=api)
|
||||
project.picture_header = utils.get_file(project.picture_header, api=api)
|
||||
utils.attach_project_pictures(project, api)
|
||||
|
||||
return render_template('nodes/search.html',
|
||||
project=project,
|
||||
@ -583,6 +581,8 @@ def edit(project_url):
|
||||
project.picture_square = form.picture_square.data
|
||||
if 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
|
||||
if form.is_private.data:
|
||||
@ -598,6 +598,8 @@ def edit(project_url):
|
||||
form.picture_square.data = project.picture_square._id
|
||||
if project.picture_header:
|
||||
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
|
||||
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_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], *,
|
||||
|
@ -52,7 +52,7 @@ html5lib==1.0.1
|
||||
idna==2.5
|
||||
ipaddress==1.0.22
|
||||
itsdangerous==0.24
|
||||
Jinja2==2.10
|
||||
Jinja2==2.10.1
|
||||
kombu==4.2.1
|
||||
oauth2client==4.1.2
|
||||
oauthlib==2.1.0
|
||||
|
@ -689,6 +689,12 @@
|
||||
.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%.
|
||||
// .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.
|
||||
|
@ -33,6 +33,9 @@
|
||||
@import "../../node_modules/bootstrap/scss/utilities"
|
||||
|
||||
// Pillar components.
|
||||
$pillar-font-path: "../../../../static/pillar/assets/font"
|
||||
@import "../../../pillar/src/styles/font-pillar"
|
||||
|
||||
@import "apps_base"
|
||||
@import "components/base"
|
||||
|
||||
|
@ -155,14 +155,14 @@ $card-progress-height: 5px
|
||||
|
||||
/* Tiny label for cards. e.g. 'WATCHED' on videos. */
|
||||
.card-label
|
||||
background-color: rgba($black, .5)
|
||||
border-radius: 3px
|
||||
color: $white
|
||||
display: block
|
||||
@extend .font-weight-bold
|
||||
@extend .position-absolute
|
||||
@extend .rounded
|
||||
@extend .text-white
|
||||
@extend .bg-dark
|
||||
bottom: $card-progress-height + 3px // enough to be above the progress-bar
|
||||
font-size: $font-size-xxs
|
||||
left: 5px
|
||||
bottom: $card-progress-height + 3px // enough to be above the progress-bar
|
||||
position: absolute
|
||||
padding: 1px 5px
|
||||
z-index: 1
|
||||
|
||||
|
@ -15,10 +15,9 @@
|
||||
&-title // .group-title
|
||||
@extend .border-bottom
|
||||
@extend .bg-white
|
||||
@extend .text-uppercase
|
||||
@extend .font-weight-bold
|
||||
|
||||
a
|
||||
color: $color-text
|
||||
color: $color-text-dark-hint
|
||||
|
||||
.node-details-description
|
||||
font:
|
||||
|
@ -24,9 +24,9 @@
|
||||
|
||||
@import "../../node_modules/bootstrap/scss/utilities"
|
||||
|
||||
|
||||
// Pillar components.
|
||||
@import "font-pillar"
|
||||
$pillar-font-path: "../../../../static/pillar/assets/font"
|
||||
@import "../../../pillar/src/styles/font-pillar"
|
||||
|
||||
@import "apps_base"
|
||||
@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
|
||||
| {% if node_type %}
|
||||
li.pr-2.font-weight-bold {{ node_type | undertitle }}
|
||||
li.item-type.pr-2.font-weight-bold {{ node_type | undertitle }}
|
||||
| {% endif %}
|
||||
| {% if asset.project.name %}
|
||||
li.pr-2.text-truncate {{ asset.project.name }}
|
||||
li.item-name.pr-2.text-truncate {{ asset.project.name }}
|
||||
| {% endif %}
|
||||
| {% 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 %}
|
||||
| {% if asset._created %}
|
||||
li.text-truncate {{ asset._created | pretty_date }}
|
||||
li.item-date.text-truncate {{ asset._created | pretty_date }}
|
||||
| {% endif %}
|
||||
|
||||
| {% endmacro %}
|
||||
|
@ -70,6 +70,22 @@ class CommentEditTest(AbstractPillarTest):
|
||||
|
||||
self.user_uid = self.create_user(24 * 'b', groups=[ctd.EXAMPLE_ADMIN_GROUP_ID],
|
||||
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):
|
||||
# Create the comment
|
||||
@ -86,7 +102,50 @@ class CommentEditTest(AbstractPillarTest):
|
||||
payload = json.loads(resp.data)
|
||||
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
|
||||
resp = self.patch(
|
||||
comment_url,
|
||||
|
Loading…
x
Reference in New Issue
Block a user