Skip to content

Commit

Permalink
Merge pull request #4381 from hotosm/develop
Browse files Browse the repository at this point in the history
v4.4.1 release
  • Loading branch information
willemarcel authored Mar 16, 2021
2 parents 9ff5e20 + 9680462 commit ba2def2
Show file tree
Hide file tree
Showing 89 changed files with 1,751 additions and 804 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.8-alpine as base
FROM quay.io/hotosm/base-python-image as base
LABEL version=0.1
LABEL maintainer="HOT Sysadmin <sysadmin@hotosm.org>"
LABEL description="Builds backend docker image"
Expand Down
9 changes: 4 additions & 5 deletions backend/api/organisations/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,12 +323,11 @@ def patch(self, organisation_id):
try:
organisation_dto = UpdateOrganisationDTO(request.get_json())
organisation_dto.organisation_id = organisation_id
# Don't update organisation type if user is not admin
# Don't update organisation type and subscription_tier if request user is not an admin
if User.get_by_id(token_auth.current_user()).role != 1:
org_type = OrganisationService.get_organisation_by_id(
organisation_id
).type
organisation_dto.type = OrganisationType(org_type).name
org = OrganisationService.get_organisation_by_id(organisation_id)
organisation_dto.type = OrganisationType(org.type).name
organisation_dto.subscription_tier = org.subscription_tier
organisation_dto.validate()
except DataError as e:
current_app.logger.error(f"error validating request: {str(e)}")
Expand Down
6 changes: 4 additions & 2 deletions backend/api/users/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def get(self, user_id):
parameters:
- in: header
name: Authorization
description: Base64 encoded sesesion token
description: Base64 encoded session token
required: true
type: string
default: Token sessionTokenHere==
Expand All @@ -41,7 +41,9 @@ def get(self, user_id):
description: Internal Server Error
"""
try:
user_dto = UserService.get_user_dto_by_id(user_id)
user_dto = UserService.get_user_dto_by_id(
user_id, token_auth.current_user()
)
return user_dto.to_primitive(), 200
except NotFound:
return {"Error": "User not found"}, 404
Expand Down
1 change: 1 addition & 0 deletions backend/models/dtos/mapping_dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ class TaskDTO(Model):
last_updated = UTCDateTimeType(
serialized_name="lastUpdated", serialize_when_none=False
)
comments_number = IntType(serialized_name="numberOfComments")


class TaskDTOs(Model):
Expand Down
2 changes: 2 additions & 0 deletions backend/models/dtos/organisation_dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class OrganisationDTO(Model):
teams = ListType(ModelType(OrganisationTeamsDTO))
campaigns = ListType(ListType(StringType))
type = StringType(validators=[is_known_organisation_type])
subscription_tier = IntType(serialized_name="subscriptionTier")


class ListOrganisationsDTO(Model):
Expand All @@ -77,6 +78,7 @@ class NewOrganisationDTO(Model):
description = StringType()
url = StringType()
type = StringType(validators=[is_known_organisation_type])
subscription_tier = IntType(serialized_name="subscriptionTier")


class UpdateOrganisationDTO(OrganisationDTO):
Expand Down
1 change: 1 addition & 0 deletions backend/models/dtos/team_dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ def __init__(self):
team_id = IntType(serialized_name="teamId")
organisation_id = IntType(required=True)
organisation = StringType(required=True)
organisation_slug = StringType(serialized_name="organisationSlug")
name = StringType(required=True)
logo = StringType()
description = StringType()
Expand Down
3 changes: 3 additions & 0 deletions backend/models/postgis/organisation.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class Organisation(db.Model):
description = db.Column(db.String)
url = db.Column(db.String)
type = db.Column(db.Integer, default=OrganisationType.FREE.value, nullable=False)
subscription_tier = db.Column(db.Integer)

managers = db.relationship(
User,
Expand All @@ -67,6 +68,7 @@ def create_from_dto(cls, new_organisation_dto: NewOrganisationDTO):
new_org.description = new_organisation_dto.description
new_org.url = new_organisation_dto.url
new_org.type = OrganisationType[new_organisation_dto.type].value
new_org.subscription_tier = new_organisation_dto.subscription_tier

for manager in new_organisation_dto.managers:
user = User.get_by_username(manager)
Expand Down Expand Up @@ -174,6 +176,7 @@ def as_dto(self, omit_managers=False):
organisation_dto.url = self.url
organisation_dto.managers = []
organisation_dto.type = OrganisationType(self.type).name
organisation_dto.subscription_tier = self.subscription_tier

if omit_managers:
return organisation_dto
Expand Down
2 changes: 2 additions & 0 deletions backend/models/postgis/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,7 @@ def as_dto(
self,
task_history: List[TaskHistoryDTO] = [],
last_updated: datetime.datetime = None,
comments: int = None,
):
"""Just converts to a TaskDTO"""
task_dto = TaskDTO()
Expand All @@ -1011,6 +1012,7 @@ def as_dto(
task_dto.task_history = task_history
task_dto.last_updated = last_updated if last_updated else None
task_dto.auto_unlock_seconds = Task.auto_unlock_delta().total_seconds()
task_dto.comments_number = comments if type(comments) == int else None
return task_dto

def as_dto_with_instructions(self, preferred_locale: str = "en") -> TaskDTO:
Expand Down
12 changes: 6 additions & 6 deletions backend/models/postgis/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,16 +372,16 @@ def as_dto(self, logged_in_username: str) -> UserDTO:
user_dto.comments_notifications = self.comments_notifications
user_dto.tasks_notifications = self.tasks_notifications
user_dto.teams_notifications = self.teams_notifications
gender = None
if self.gender is not None:
gender = UserGender(self.gender).name
user_dto.gender = gender
user_dto.self_description_gender = self.self_description_gender

if self.username == logged_in_username:
# Only return email address when logged in user is looking at their own profile
# Only return email address and gender information when logged in user is looking at their own profile
user_dto.email_address = self.email_address
user_dto.is_email_verified = self.is_email_verified
gender = None
if self.gender is not None:
gender = UserGender(self.gender).name
user_dto.gender = gender
user_dto.self_description_gender = self.self_description_gender
return user_dto

def create_or_update_interests(self, interests_ids):
Expand Down
1 change: 1 addition & 0 deletions backend/services/team_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ def get_team_as_dto(
team_dto.logo = team.organisation.logo
team_dto.organisation = team.organisation.name
team_dto.organisation_id = team.organisation.id
team_dto.organisation_slug = team.organisation.slug

if user_id != 0:
if UserService.is_user_an_admin(user_id):
Expand Down
46 changes: 35 additions & 11 deletions backend/services/users/user_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,12 @@ def get_user_dto_by_username(
return requested_user.as_dto(logged_in_user.username)

@staticmethod
def get_user_dto_by_id(requested_user: int) -> UserDTO:
def get_user_dto_by_id(requested_user: int, logged_in_user: int) -> UserDTO:
"""Gets user DTO for supplied user id """
requested_user = UserService.get_user_by_id(requested_user)
logged_in_user = UserService.get_user_by_id(logged_in_user)

return requested_user.as_dto(requested_user.username)
return requested_user.as_dto(logged_in_user.username)

@staticmethod
def get_interests_stats(user_id):
Expand Down Expand Up @@ -237,9 +238,9 @@ def get_tasks_dto(
) -> UserTaskDTOs:
base_query = (
TaskHistory.query.with_entities(
TaskHistory.project_id,
TaskHistory.task_id,
func.max(TaskHistory.action_date),
TaskHistory.project_id.label("project_id"),
TaskHistory.task_id.label("task_id"),
func.max(TaskHistory.action_date).label("max"),
)
.filter(TaskHistory.user_id == user_id)
.group_by(TaskHistory.task_id, TaskHistory.project_id)
Expand All @@ -264,14 +265,37 @@ def get_tasks_dto(
user_task_dtos = UserTaskDTOs()
task_id_list = base_query.subquery()

comments_query = (
TaskHistory.query.with_entities(
TaskHistory.project_id,
TaskHistory.task_id,
func.count(TaskHistory.action).label("count"),
)
.filter(TaskHistory.action == "COMMENT")
.group_by(TaskHistory.task_id, TaskHistory.project_id)
).subquery()

sq = (
db.session.query(
func.coalesce(comments_query.c.count, 0).label("comments"), task_id_list
)
.select_from(task_id_list)
.outerjoin(
comments_query,
(comments_query.c.task_id == task_id_list.c.task_id)
& (comments_query.c.project_id == task_id_list.c.project_id),
)
.subquery()
)

tasks = Task.query.join(
task_id_list,
sq,
and_(
Task.id == task_id_list.c.task_id,
Task.project_id == task_id_list.c.project_id,
Task.id == sq.c.task_id,
Task.project_id == sq.c.project_id,
),
)
tasks = tasks.add_column("max_1")
tasks = tasks.add_columns("max", "comments")

if project_status:
tasks = tasks.filter(
Expand All @@ -286,8 +310,8 @@ def get_tasks_dto(

task_list = []

for task, action_date in results.items:
task_list.append(task.as_dto(last_updated=action_date))
for task, action_date, comments in results.items:
task_list.append(task.as_dto(last_updated=action_date, comments=comments))

user_task_dtos.user_tasks = task_list
user_task_dtos.pagination = Pagination(results)
Expand Down
36 changes: 18 additions & 18 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
"license": "BSD-2-Clause",
"private": false,
"dependencies": {
"@formatjs/intl-locale": "^2.4.19",
"@formatjs/intl-pluralrules": "^4.0.11",
"@formatjs/intl-relativetimeformat": "^8.1.2",
"@formatjs/intl-locale": "^2.4.20",
"@formatjs/intl-pluralrules": "^4.0.12",
"@formatjs/intl-relativetimeformat": "^8.1.3",
"@formatjs/macro": "^0.2.8",
"@hotosm/id": "^2.19.5",
"@hotosm/id": "^2.19.6",
"@hotosm/iso-countries-languages": "^1.1.0",
"@lokibai/react-use-copy-clipboard": "^1.0.4",
"@mapbox/geo-viewport": "^0.4.1",
Expand All @@ -17,8 +17,8 @@
"@mapbox/mapbox-gl-language": "^0.10.1",
"@mapbox/togeojson": "^0.16.0",
"@reach/router": "^1.3.4",
"@sentry/react": "^6.2.1",
"@sentry/tracing": "^6.2.1",
"@sentry/react": "^6.2.2",
"@sentry/tracing": "^6.2.2",
"@turf/area": "^6.3.0",
"@turf/bbox": "^6.3.0",
"@turf/bbox-polygon": "^6.3.0",
Expand All @@ -30,10 +30,10 @@
"@webscopeio/react-textarea-autocomplete": "^4.7.3",
"axios": "^0.21.1",
"chart.js": "^2.9.4",
"date-fns": "^2.18.0",
"dompurify": "^2.2.6",
"date-fns": "^2.19.0",
"dompurify": "^2.2.7",
"downshift-hooks": "^0.8.1",
"final-form": "^4.20.1",
"final-form": "^4.20.2",
"fromentries": "^1.3.2",
"humanize-duration": "^3.25.1",
"immutable": "^4.0.0-rc.12",
Expand All @@ -56,7 +56,7 @@
"react-placeholder": "^4.1.0",
"react-redux": "^7.2.2",
"react-scripts": "^4.0.3",
"react-select": "^4.1.0",
"react-select": "^4.2.1",
"react-tooltip": "^4.2.13",
"reactjs-popup": "^1.5.0",
"redux": "^4.0.5",
Expand All @@ -68,12 +68,12 @@
"tachyons": "^4.12.0",
"use-query-params": "^1.2.0",
"webfontloader": "^1.6.28",
"workbox-core": "^6.1.1",
"workbox-expiration": "^6.1.1",
"workbox-precaching": "^6.1.1",
"workbox-recipes": "^6.1.1",
"workbox-routing": "^6.1.1",
"workbox-strategies": "^6.1.1"
"workbox-core": "^6.1.2",
"workbox-expiration": "^6.1.2",
"workbox-precaching": "^6.1.2",
"workbox-recipes": "^6.1.2",
"workbox-routing": "^6.1.2",
"workbox-strategies": "^6.1.2"
},
"scripts": {
"build-locales": "combine-messages -i './src/**/messages.js' -o './src/locales/en.json'",
Expand Down Expand Up @@ -107,10 +107,10 @@
"@testing-library/jest-dom": "^5.11.9",
"@testing-library/react": "^11.2.4",
"@testing-library/react-hooks": "^5.1.0",
"@testing-library/user-event": "^12.8.1",
"@testing-library/user-event": "^12.8.3",
"combine-react-intl-messages": "^4.0.0",
"jest-canvas-mock": "^2.3.1",
"msw": "^0.27.0",
"msw": "^0.27.1",
"prettier": "^2.2.1",
"react-select-event": "^5.2.0",
"react-test-renderer": "^17.0.1",
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/components/comments/commentInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const CommentInputField = ({ comment, setComment, enableHashtagPaste = fa
return (
<div {...getRootProps()}>
<UserFetchTextarea
{...getInputProps()}
inputProps={getInputProps}
value={comment}
setValueFn={(e) => setComment(e.target.value)}
token={token}
Expand All @@ -40,7 +40,7 @@ export const CommentInputField = ({ comment, setComment, enableHashtagPaste = fa
);
};

export const UserFetchTextarea = ({ value, setValueFn, token }) => {
export const UserFetchTextarea = ({ value, setValueFn, token, inputProps }) => {
const fetchUsers = async (user) => {
const url = `users/queries/filter/${user}/`;
let userItems;
Expand All @@ -59,6 +59,7 @@ export const UserFetchTextarea = ({ value, setValueFn, token }) => {

return (
<ReactTextareaAutocomplete
{...inputProps}
value={value}
listClassName="list ma0 pa0 ba b--grey-light bg-blue-grey overflow-y-scroll base-font f5 relative z-5"
listStyle={{ maxHeight: '16rem' }}
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/components/contributions/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,8 @@ export default defineMessages({
id: 'mytasks.tasks.button.retry',
defaultMessage: 'Retry',
},
commentsNumber: {
id: 'mytasks.tasks.comments.number',
defaultMessage: '{number, plural, one {# comment} other {# comments}}',
},
});
17 changes: 15 additions & 2 deletions frontend/src/components/contributions/taskCard.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React, { useState } from 'react';
import { Link } from '@reach/router';
import Popup from 'reactjs-popup';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage, useIntl } from 'react-intl';

import messages from './messages';
import { RelativeTimeWithUnit } from '../../utils/formattedRelativeTime';
import { ListIcon, ResumeIcon, ClockIcon } from '../svgIcons';
import { ListIcon, ResumeIcon, ClockIcon, CommentIcon } from '../svgIcons';
import { TaskStatus } from '../taskSelection/taskList';
import { TaskActivity } from '../taskSelection/taskActivity';

Expand All @@ -21,9 +21,11 @@ export function TaskCard({
autoUnlockSeconds,
lastUpdated,
lastUpdatedBy,
numberOfComments,
}: Object) {
const [isHovered, setHovered] = useState(false);
const taskLink = `/projects/${projectId}/tasks?search=${taskId}`;
const intl = useIntl();

const timeToAutoUnlock =
lastUpdated &&
Expand Down Expand Up @@ -79,6 +81,17 @@ export function TaskCard({
</div>
</div>
<div className="w-third-ns w-100 fr">
{numberOfComments ? (
<span
className="w-auto tr fl mv1 pv2 f6 blue-grey"
title={intl.formatMessage(messages.commentsNumber, { number: numberOfComments })}
>
<CommentIcon className="pr2 v-mid" height="19px" width="13px" />
{numberOfComments}
</span>
) : (
''
)}
<Popup
trigger={
<ListIcon className="pointer fr h1 w1 mv1 pv2 v-mid pr3 blue-light hover-blue-grey" />
Expand Down
Loading

0 comments on commit ba2def2

Please sign in to comment.