Merge branch 'master' into dillo

# Conflicts:
#	pillar/api/nodes/__init__.py
This commit is contained in:
Francesco Siddi 2019-04-20 22:26:51 +02:00
commit 38e4c7c937
15 changed files with 107 additions and 29 deletions

View File

@ -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

View File

@ -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)

View File

@ -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).

View File

@ -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)

View File

@ -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'):

View File

@ -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], *,

View File

@ -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

View File

@ -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.

View File

@ -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"

View File

@ -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

View File

@ -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:

View File

@ -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"

View File

@ -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 %}

View File

@ -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,