From d7a642bb9fdcd6b0de45238c857c25846fa64772 Mon Sep 17 00:00:00 2001 From: Pavle Jonoski Date: Thu, 6 Feb 2020 13:39:37 +0100 Subject: [PATCH 01/15] User profile model and infrastructure. --- ckanext/knowledgehub/cli/db.py | 3 +- ckanext/knowledgehub/logic/action/create.py | 4 ++ ckanext/knowledgehub/model/__init__.py | 4 +- ckanext/knowledgehub/model/user_profile.py | 49 +++++++++++++++++++ .../user/profile/profile_page_base.html | 0 .../user/profile/snippets/profile_read.html | 5 ++ 6 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 ckanext/knowledgehub/model/user_profile.py create mode 100644 ckanext/knowledgehub/templates/user/profile/profile_page_base.html create mode 100644 ckanext/knowledgehub/templates/user/profile/snippets/profile_read.html diff --git a/ckanext/knowledgehub/cli/db.py b/ckanext/knowledgehub/cli/db.py index f001e524..e9f5aeb0 100644 --- a/ckanext/knowledgehub/cli/db.py +++ b/ckanext/knowledgehub/cli/db.py @@ -23,7 +23,7 @@ from ckanext.knowledgehub.model.resource_validate import ( setup as resource_validate_setup ) - +from ckanext.knowledgehub.model.user_profile import setup as user_profile_setup log = logging.getLogger(__name__) @@ -51,6 +51,7 @@ def init(): ml_db_setup() data_quality_setup() resource_validate_setup() + user_profile_setup() except Exception as e: error_shout(e) else: diff --git a/ckanext/knowledgehub/logic/action/create.py b/ckanext/knowledgehub/logic/action/create.py index 383ad6e7..4a1ada48 100644 --- a/ckanext/knowledgehub/logic/action/create.py +++ b/ckanext/knowledgehub/logic/action/create.py @@ -915,3 +915,7 @@ def tag_create(context, data_dict): log.debug("Created tag '%s' " % tag) return model_dictize.tag_dictize(tag, context) + + +def user_profile_create(context, data_dict): + pass \ No newline at end of file diff --git a/ckanext/knowledgehub/model/__init__.py b/ckanext/knowledgehub/model/__init__.py index 58618c0e..d1263f91 100644 --- a/ckanext/knowledgehub/model/__init__.py +++ b/ckanext/knowledgehub/model/__init__.py @@ -11,6 +11,7 @@ from ckanext.knowledgehub.model.query import UserQuery, UserQueryResult from ckanext.knowledgehub.model.data_quality import DataQualityMetrics from ckanext.knowledgehub.model.resource_validate import ResourceValidate +from ckanext.knowledgehub.model.user_profile import UserProfile __all__ = [ @@ -29,5 +30,6 @@ 'Visualization', 'UserQuery', 'UserQueryResult', - 'ResourceValidate' + 'ResourceValidate', + 'UserProfile', ] diff --git a/ckanext/knowledgehub/model/user_profile.py b/ckanext/knowledgehub/model/user_profile.py new file mode 100644 index 00000000..d4ab99fa --- /dev/null +++ b/ckanext/knowledgehub/model/user_profile.py @@ -0,0 +1,49 @@ +# encoding: utf-8 +import datetime + +from sqlalchemy import ( + types, + Column, + Table, + or_, +) + +from ckan.common import _ + +from ckan.model.meta import ( + metadata, + mapper, + Session, + engine + ) +from ckan.model.types import make_uuid +from ckan.model.domain_object import DomainObject + + +user_profile_table = Table( + 'user_profile', metadata, + Column('id', types.UnicodeText, + primary_key=True, default=make_uuid), + Column('user_id', types.UnicodeText, + nullable=False, unique=True), + Column('interests', types.JSON), + Column('user_notified', types.Boolean, + default=False) +) + + +class UserProfile(DomainObject): + + @classmethod + def by_user_id(cls, user_id): + query = Session.query(cls) + query.filter_by(user_profile_table.c.user_id == user_id) + + return query.first() + + +mapper(UserProfile, user_profile_table) + + +def setup(): + metadata.create_all(engine) \ No newline at end of file diff --git a/ckanext/knowledgehub/templates/user/profile/profile_page_base.html b/ckanext/knowledgehub/templates/user/profile/profile_page_base.html new file mode 100644 index 00000000..e69de29b diff --git a/ckanext/knowledgehub/templates/user/profile/snippets/profile_read.html b/ckanext/knowledgehub/templates/user/profile/snippets/profile_read.html new file mode 100644 index 00000000..fffdad25 --- /dev/null +++ b/ckanext/knowledgehub/templates/user/profile/snippets/profile_read.html @@ -0,0 +1,5 @@ +
+
+ +
+
\ No newline at end of file From f9f25dd0d1e75575a2408a09b161c74e5839df1e Mon Sep 17 00:00:00 2001 From: Pavle Jonoski Date: Fri, 7 Feb 2020 16:27:43 +0100 Subject: [PATCH 02/15] API actions for user profile management. --- ckanext/knowledgehub/logic/action/create.py | 29 +++++++++++++-- ckanext/knowledgehub/logic/action/get.py | 40 ++++++++++++++++++++- ckanext/knowledgehub/logic/action/update.py | 24 +++++++++++++ ckanext/knowledgehub/logic/auth/create.py | 6 +++- ckanext/knowledgehub/logic/auth/get.py | 20 ++++++++++- ckanext/knowledgehub/logic/auth/update.py | 6 +++- ckanext/knowledgehub/model/user_profile.py | 21 +++++++++-- 7 files changed, 138 insertions(+), 8 deletions(-) diff --git a/ckanext/knowledgehub/logic/action/create.py b/ckanext/knowledgehub/logic/action/create.py index c43c0aaa..e41c4d83 100644 --- a/ckanext/knowledgehub/logic/action/create.py +++ b/ckanext/knowledgehub/logic/action/create.py @@ -9,7 +9,7 @@ from werkzeug.datastructures import FileStorage as FlaskFileStorage from ckan import logic -from ckan.common import _ +from ckan.common import _, g from ckan.plugins import toolkit from ckan import lib from ckan import model @@ -37,6 +37,7 @@ from ckanext.knowledgehub.model import UserQuery from ckanext.knowledgehub.model import UserQueryResult from ckanext.knowledgehub.model import Keyword +from ckanext.knowledgehub.model import UserProfile from ckanext.knowledgehub.backend.factory import get_backend from ckanext.knowledgehub.lib.writer import WriterService from ckanext.knowledgehub import helpers as plugin_helpers @@ -964,4 +965,28 @@ def keyword_create(context, data_dict): def user_profile_create(context, data_dict): - pass + check_access('user_profile_create', context) + + user = toolkit.get_action('user_show')({ + 'ignore_auth': True, + }, { + 'id': g.user, + }) + + profile = UserProfile.by_user_id(user['id']) + if profile: + raise ValidationError({ + 'user_id': _('Profile already created.') + }) + + profile = UserProfile(user_id=user['id'], user_notified=False, interests={}) + + for interest_type in ['research_questions', 'tags', 'keywords']: + if data_dict.get(interest_type): + profile.interests[interest_type] = data_dict[interest_type] + + profile.save() + model.Session.flush() + + profile_dict = _table_dictize(profile, context) + return profile_dict diff --git a/ckanext/knowledgehub/logic/action/get.py b/ckanext/knowledgehub/logic/action/get.py index 221bb35d..68b943bd 100644 --- a/ckanext/knowledgehub/logic/action/get.py +++ b/ckanext/knowledgehub/logic/action/get.py @@ -23,6 +23,7 @@ from ckanext.knowledgehub.model import UserQuery from ckanext.knowledgehub.model import UserQueryResult, DataQualityMetrics from ckanext.knowledgehub.model import Keyword +from ckanext.knowledgehub.model import UserProfile from ckanext.knowledgehub import helpers as kh_helpers from ckanext.knowledgehub.rnn import helpers as rnn_helpers from ckanext.knowledgehub.lib.solr import ckan_params_to_solr_args @@ -1357,4 +1358,41 @@ def keyword_list(context, data_dict): 'id': keyword.id, })) - return results \ No newline at end of file + return results + + +def _show_user_profile(context, user_id): + user = UserProfile.by_user_id(user_id) + if not user: + raise logic.NotFound(_('No such user profile')) + + return _table_dictize(user, context) + + +@toolkit.side_effect_free +def user_profile_show(context, data_dict): + check_access('user_profile_show', context) + + user = context.get('auth_user_obj') + if user.sysadmin: + if data_dict.get('user_id'): + return _show_user_profile(context, data_dict['user_id']) + + return _show_user_profile(context, user.id) + + +def user_profile_list(context, data_dict): + check_access('user_profile_list') + page = data_dict.get('page', 1) + limit = data_dict.get('limit', 20) + + order_by = data_dict.get('order') + + profiles = UserProfile.get_list(page, limit, order_by) + + results = [] + + for profile in profiles: + results.append(_table_dictize(profile, context)) + + return results diff --git a/ckanext/knowledgehub/logic/action/update.py b/ckanext/knowledgehub/logic/action/update.py index 20ea528a..a64cea42 100644 --- a/ckanext/knowledgehub/logic/action/update.py +++ b/ckanext/knowledgehub/logic/action/update.py @@ -26,6 +26,7 @@ from ckanext.knowledgehub.model import Visualization from ckanext.knowledgehub.model import UserIntents, DataQualityMetrics from ckanext.knowledgehub.model import Keyword +from ckanext.knowledgehub.model import UserProfile from ckanext.knowledgehub.backend.factory import get_backend from ckanext.knowledgehub.lib.writer import WriterService from ckanext.knowledgehub import helpers as plugin_helpers @@ -854,3 +855,26 @@ def keyword_update(context, data_dict): kwd_dict['tags'].append(tag_dict) return kwd_dict + + +def user_profile_update(context, data_dict): + check_access('user_profile_update', context, data_dict) + user = context.get('auth_user_obj') + user_id = user.id + + if user.sysadmin and data_dict.get('user_id'): + user_id = data_dict['user_id'] + + profile = UserProfile.by_user_id(user_id) + if not profile: + profile = UserProfile(user_id=user_id, user_notified=True) + profile.interests = {} + + for interest_type in ['research_questions', 'keywords', 'tags']: + if data_dict.get(interest_type): + profile.interests[interest_type] = data_dict[interest_type] + + profile.save() + model.Session.flush() + + return _table_dictize(profile, context) \ No newline at end of file diff --git a/ckanext/knowledgehub/logic/auth/create.py b/ckanext/knowledgehub/logic/auth/create.py index 0a0a647f..33633208 100644 --- a/ckanext/knowledgehub/logic/auth/create.py +++ b/ckanext/knowledgehub/logic/auth/create.py @@ -126,4 +126,8 @@ def keyword_create(context, data_dict): Authorization check for creation of a keyword. Sysadmin only. ''' # sysadmins only - return {'success': False} \ No newline at end of file + return {'success': False} + + +def user_profile_create(context, data_dict=None): + return {'success': True} \ No newline at end of file diff --git a/ckanext/knowledgehub/logic/auth/get.py b/ckanext/knowledgehub/logic/auth/get.py index 14d90c26..dcf8b4f1 100644 --- a/ckanext/knowledgehub/logic/auth/get.py +++ b/ckanext/knowledgehub/logic/auth/get.py @@ -106,4 +106,22 @@ def keyword_list(context, data_dict): ''' Authorization check for getting the list of keywords. Sysadmin only. ''' - return {'success': True} \ No newline at end of file + return {'success': True} + + +def user_profile_show(context, data_dict): + user = context.get('user_auth_obj') + if not user: + return {'success': False} + if data_dict.get('user_id'): + if user.sysadmin: + # Sysadmin can read all profiles + return {'success': True} + # Must be sysadmin to see all profiles + return {'suceess': False} + # User can view its own profile + return {'success': True} + + +def user_profile_list(context, data_dict=None): + return {'success': False} diff --git a/ckanext/knowledgehub/logic/auth/update.py b/ckanext/knowledgehub/logic/auth/update.py index 8a058751..938f4b18 100644 --- a/ckanext/knowledgehub/logic/auth/update.py +++ b/ckanext/knowledgehub/logic/auth/update.py @@ -90,4 +90,8 @@ def keyword_update(context, data_dict=None): ''' Authorization check for updating a keyword. Sysadmin only. ''' - return {'success': False} \ No newline at end of file + return {'success': False} + + +def user_profile_update(context, data_dict): + return {'success': True} \ No newline at end of file diff --git a/ckanext/knowledgehub/model/user_profile.py b/ckanext/knowledgehub/model/user_profile.py index d4ab99fa..5419bfdd 100644 --- a/ckanext/knowledgehub/model/user_profile.py +++ b/ckanext/knowledgehub/model/user_profile.py @@ -37,10 +37,27 @@ class UserProfile(DomainObject): @classmethod def by_user_id(cls, user_id): query = Session.query(cls) - query.filter_by(user_profile_table.c.user_id == user_id) - + query = query.filter(user_profile_table.c.user_id == user_id) return query.first() + @classmethod + def get_list(cls, page=1, limit=20, order_by=None): + offset = None + if page and limit: + offset = (page - 1) * limit + + query = Session.query(cls).autoflush(False) + + if order_by: + query = query.order_by(order_by) + + if limit: + query = query.limit(limit) + + if offset: + query = query.offset(offset) + + return query.all() mapper(UserProfile, user_profile_table) From b52c62b424cd9ed8574bcb42da92bb67805aa06a Mon Sep 17 00:00:00 2001 From: Pavle Jonoski Date: Fri, 7 Feb 2020 16:59:38 +0100 Subject: [PATCH 03/15] User profile templates and CKAN JS module. --- .../javascript/modules/user_profile.js | 10 ++++++++ .../knowledgehub/fanstatic/resource.config | 4 ++++ .../user/profile/profile_page_base.html | 0 .../user/profile/snippets/interests.html | 19 +++++++++++++++ .../user/profile/snippets/profile_read.html | 5 ---- .../templates/user/profile/user_profile.html | 10 ++++++++ ckanext/knowledgehub/views/user.py | 23 +++++++++++++++++++ 7 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 ckanext/knowledgehub/fanstatic/javascript/modules/user_profile.js delete mode 100644 ckanext/knowledgehub/templates/user/profile/profile_page_base.html create mode 100644 ckanext/knowledgehub/templates/user/profile/snippets/interests.html delete mode 100644 ckanext/knowledgehub/templates/user/profile/snippets/profile_read.html create mode 100644 ckanext/knowledgehub/templates/user/profile/user_profile.html diff --git a/ckanext/knowledgehub/fanstatic/javascript/modules/user_profile.js b/ckanext/knowledgehub/fanstatic/javascript/modules/user_profile.js new file mode 100644 index 00000000..cfd460d7 --- /dev/null +++ b/ckanext/knowledgehub/fanstatic/javascript/modules/user_profile.js @@ -0,0 +1,10 @@ +ckan.module('user-profile', function($){ + 'use strict'; + + console.log('User profile load...') + return { + initialize: function(){ + console.log('User Profile init...'); + } + } +}); \ No newline at end of file diff --git a/ckanext/knowledgehub/fanstatic/resource.config b/ckanext/knowledgehub/fanstatic/resource.config index adf0c6fc..d95ecb5f 100644 --- a/ckanext/knowledgehub/fanstatic/resource.config +++ b/ckanext/knowledgehub/fanstatic/resource.config @@ -29,3 +29,7 @@ map = javascript/leaflet-download.js vendor/leaflet/topojson.js javascript/modules/map.js + + +user_profile = + javascript/modules/user_profile.js \ No newline at end of file diff --git a/ckanext/knowledgehub/templates/user/profile/profile_page_base.html b/ckanext/knowledgehub/templates/user/profile/profile_page_base.html deleted file mode 100644 index e69de29b..00000000 diff --git a/ckanext/knowledgehub/templates/user/profile/snippets/interests.html b/ckanext/knowledgehub/templates/user/profile/snippets/interests.html new file mode 100644 index 00000000..99809408 --- /dev/null +++ b/ckanext/knowledgehub/templates/user/profile/snippets/interests.html @@ -0,0 +1,19 @@ +{% block user_interests %} +
+
+ {% block interests_research_questions %} + + {% endblock %} +
+
+ {% block interests_keywords %} + + {% endblock %} +
+
+ {% block interests_tags %} + + {% endblock %} +
+
+{% endblock%} \ No newline at end of file diff --git a/ckanext/knowledgehub/templates/user/profile/snippets/profile_read.html b/ckanext/knowledgehub/templates/user/profile/snippets/profile_read.html deleted file mode 100644 index fffdad25..00000000 --- a/ckanext/knowledgehub/templates/user/profile/snippets/profile_read.html +++ /dev/null @@ -1,5 +0,0 @@ -
-
- -
-
\ No newline at end of file diff --git a/ckanext/knowledgehub/templates/user/profile/user_profile.html b/ckanext/knowledgehub/templates/user/profile/user_profile.html new file mode 100644 index 00000000..8d0f9265 --- /dev/null +++ b/ckanext/knowledgehub/templates/user/profile/user_profile.html @@ -0,0 +1,10 @@ +{% resource 'knowledgehub/user_profile' %} + +{% extends "user/read_base.html" %} + +{% block primary_content_inner %} +

Tags

+
+ {% snippet 'user/profile/snippets/interests.html' %} +
+{% endblock %} diff --git a/ckanext/knowledgehub/views/user.py b/ckanext/knowledgehub/views/user.py index 14aeeeea..8b11a0c7 100644 --- a/ckanext/knowledgehub/views/user.py +++ b/ckanext/knowledgehub/views/user.py @@ -345,6 +345,28 @@ def tags(id): return base.render(u'user/tags.html', extra_vars) +def profile(): + context = { + u'model': model, + u'session': model.Session, + u'user': g.user, + u'auth_user_obj': g.userobj, + u'for_view': True + } + data_dict = { + u'user_obj': g.userobj, + u'include_num_followers': True + } + try: + logic.check_access(u'user_profile_show', context) + except logic.NotAuthorized: + base.abort(403, _(u'Not authorized to see this page')) + + extra_vars = _extra_template_variables(context, data_dict) + + return base.render('user/profile/user_profile.html', extra_vars) + + kwh_user.add_url_rule(u'/intents/', view_func=intents) kwh_user.add_url_rule(u'/keywords', view_func=keywords) kwh_user.add_url_rule(u'/keywords/delete/', methods=['GET', 'POST'], @@ -359,3 +381,4 @@ def tags(id): view_func=keyword_create_save) kwh_user.add_url_rule(u'/keywords/', view_func=keyword_read) kwh_user.add_url_rule(u'/tags/', view_func=tags) +kwh_user.add_url_rule(u'/profile', view_func=profile) From e10063d5cfd13bbad265db1c456e6c108b61572a Mon Sep 17 00:00:00 2001 From: Pavle Jonoski Date: Mon, 10 Feb 2020 15:17:12 +0100 Subject: [PATCH 04/15] Fixes server error on update keyword name. --- ckanext/knowledgehub/logic/action/update.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/ckanext/knowledgehub/logic/action/update.py b/ckanext/knowledgehub/logic/action/update.py index a64cea42..3f18c919 100644 --- a/ckanext/knowledgehub/logic/action/update.py +++ b/ckanext/knowledgehub/logic/action/update.py @@ -822,13 +822,17 @@ def keyword_update(context, data_dict): :returns: `dict`, the updated keyword. ''' check_access('keyword_update', context) - if 'name' not in data_dict: - raise ValidationError({'name': _('Missing Value')}) - - existing = Keyword.by_name(data_dict['name']) + if 'id' not in data_dict: + raise ValidationError({'id': _('Missing Value')}) + existing = Keyword.get(data_dict['id']) + if not existing: + existing = Keyword.by_name(data_dict['id']) if not existing: raise logic.NotFound(_('Not found')) + if data_dict.get('name', '').strip(): + existing.name = data_dict['name'].strip() + existing.modified_at = datetime.datetime.utcnow() existing.save() From 8dd81859d47a522a3f40c1bea2c94d9dc0851127 Mon Sep 17 00:00:00 2001 From: Pavle Jonoski Date: Mon, 10 Feb 2020 19:32:49 +0100 Subject: [PATCH 05/15] Javascript user profile module. --- .../javascript/modules/user_profile.js | 184 +++++++++++++++++- ckanext/knowledgehub/logic/action/get.py | 28 ++- ckanext/knowledgehub/logic/action/update.py | 9 +- .../user/profile/snippets/interests.html | 43 ++++ .../templates/user/profile/user_profile.html | 2 +- .../templates/user/read_base.html | 1 + ckanext/knowledgehub/views/user.py | 1 - 7 files changed, 258 insertions(+), 10 deletions(-) diff --git a/ckanext/knowledgehub/fanstatic/javascript/modules/user_profile.js b/ckanext/knowledgehub/fanstatic/javascript/modules/user_profile.js index cfd460d7..c825d8a3 100644 --- a/ckanext/knowledgehub/fanstatic/javascript/modules/user_profile.js +++ b/ckanext/knowledgehub/fanstatic/javascript/modules/user_profile.js @@ -2,9 +2,187 @@ ckan.module('user-profile', function($){ 'use strict'; console.log('User profile load...') - return { - initialize: function(){ - console.log('User Profile init...'); + + var elipsis = function(str, size){ + if (str) { + if (str.length > size){ + return str.substring(0, size) + '...'; + } + } + return str; + } + + var API = function(baseUrl){ + this.baseUrl = baseUrl; + } + + API.prototype.getUrl = function(path, query){ + var url = this.baseUrl + '/' + path; + if (query){ + url += '?'; + var prams = []; + $.each(query, function(param, value){ + prams.push(param + '=' + value); + }); + url += prams.join('&'); } + return url; + } + + API.prototype.call = function(method, path, query, data){ + console.log('API CALL') + return $.ajax({ + url: this.getUrl(path, query), + data: data ? JSON.stringify(data) : data, + dataType: 'json', + method: method, + contentType: 'application/json', + }); + } + + API.prototype.post = function(path, query, data){ + return this.call('POST', path, query, data); + } + + API.prototype.get = function(path, query){ + return this.call('GET', path, query); + } + + + var api = new API('/api/3/action'); + + var Component = function(template){ + var tmpl = $(template)[0]; + this.el = $(tmpl).clone(); } + + $.extend(Component.prototype, { + on: function(event, callback){ + this.el.on(event, callback); + }, + trigger: function(event, data){ + this.el.trigger(event, data); + }, + applyData: function(data) { + var el = this.el; + $.each(data, function(selector, value){ + $(selector, el).text(value); + }); + }, + remove: function(){ + $(this.el).remove(); + } + }); + + var ResearchQuestion = function(template, data){ + Component.prototype.constructor.apply(this, [template]); + this.applyData(data); + $('.research-question-remove', this.el).on('click', function(){ + new DeleteModal(elipsis(data.title, 30), function(){ + this.trigger('delete', data); + }.bind(this)) + }.bind(this)); + } + + $.extend(ResearchQuestion.prototype, Component.prototype); + $.extend(ResearchQuestion.prototype, { + applyData: function(data){ + var title = data.title || ''; + if (title.length > 30){ + title = title.substring(0, 30) + '...'; + } + var rq_data = { + '.research-question-title': title, + '.research-question-content': data.description, + } + Component.prototype.applyData.call(this, rq_data); + $('.research-question-title', this.el) + .attr('href', '/research-question/' + data.name) + .attr('title', data.title); + $('.research-question-image', this.el).attr('src', data.image_url) + }, + }); + + var UserInterests = function(){ + api.get('user_profile_show').done(function(data){ + console.log('User Profile ->', data); + this.profile = data.result; + this.init(); + }.bind(this)).fail(function(err){ + console.error(err); + }) + } + + $.extend(UserInterests.prototype, { + init: function(){ + this.components = { + 'research_questions': {}, + 'keywords': {}, + 'tags': {}, + } + var interests = this.profile.interests || { + 'research_questions': [], + 'keywords': [], + 'tags': [], + } + $.each(interests.research_questions, function(i, researchQuestion){ + var rq = new ResearchQuestion('.research-question-template', researchQuestion); + $(rq.el).appendTo($('.research-question-list')); + this.components.research_questions[researchQuestion.id] = rq; + rq.on('delete', function(_, data){ + this.deleteInterest('research_questions', data); + }.bind(this)); + }.bind(this)); + }, + updateInterests: function(newProfile){ + return api.post('user_profile_update', undefined, newProfile); + }, + deleteInterest: function(interest, data){ + console.log('DeleteInterest:', interest, data) + var profile = {}; + profile[interest] = [] + $.each(this.profile.interests[interest], function(_, entry){ + if (data.id == entry.id){ + return; + } + profile[interest].push(entry.id); + }) + this.updateInterests(profile).done(function(){ + var interest_entries = this.profile.interests[interest]; + var updated_entries = [] + $.each(interest_entries, function(_, entry){ + if (entry.id == data.id){ + return; + } + updated_entries.push(entry); + }.bind(this)); + this.profile.interests[interest] = updated_entries; + this.components[interest][data.id].remove(); + delete this.components[interest][data.id]; + this.flash('Successfuly deleted.'); + + }.bind(this)).fail(function(){ + this.flashError('Unable to delete this interest. Please try again later.'); + }.bind(this)); + }, + flash: function(message, error){ + + }, + flashError: function(message){ + this.flash(message, true); + } + }); + + + var DeleteModal = function(item, onYes){ + this.modal = $('#modal-delete-interest').modal({ + show: true, + }); + $('.delete-user-interes', this.modal).text(item); + $('#btnYes', this.modal).on('click', onYes); + } + + $(function(){ + var userInterests = new UserInterests(); + }) }); \ No newline at end of file diff --git a/ckanext/knowledgehub/logic/action/get.py b/ckanext/knowledgehub/logic/action/get.py index 68b943bd..5affd035 100644 --- a/ckanext/knowledgehub/logic/action/get.py +++ b/ckanext/knowledgehub/logic/action/get.py @@ -1362,11 +1362,33 @@ def keyword_list(context, data_dict): def _show_user_profile(context, user_id): - user = UserProfile.by_user_id(user_id) - if not user: + profile = UserProfile.by_user_id(user_id) + if not profile: raise logic.NotFound(_('No such user profile')) - return _table_dictize(user, context) + interests = { + 'research_questions': [], + 'keywords': [], + 'tags': [], + } + for interest, show_action in { + 'research_questions': 'research_question_show', + 'keywords': 'keyword_show', + 'tags': 'tag_show', + }.items(): + for value in (profile.interests or {}).get(interest, []): + print 'VALUE ->', interest, show_action, value + try: + entity = toolkit.get_action(show_action)(context, { + 'id': value, + }) + interests[interest].append(entity) + except logic.NotFound: + log.debug('Not found "%s" with id %s', interest, value) + + profile_dict = _table_dictize(profile, context) + profile_dict['interests'] = interests + return profile_dict @toolkit.side_effect_free diff --git a/ckanext/knowledgehub/logic/action/update.py b/ckanext/knowledgehub/logic/action/update.py index 3f18c919..267b699f 100644 --- a/ckanext/knowledgehub/logic/action/update.py +++ b/ckanext/knowledgehub/logic/action/update.py @@ -32,6 +32,8 @@ from ckanext.knowledgehub import helpers as plugin_helpers from ckanext.knowledgehub.logic.jobs import schedule_data_quality_check +from sqlalchemy.orm.attributes import flag_modified + log = logging.getLogger(__name__) @@ -873,11 +875,14 @@ def user_profile_update(context, data_dict): if not profile: profile = UserProfile(user_id=user_id, user_notified=True) profile.interests = {} - + for interest_type in ['research_questions', 'keywords', 'tags']: if data_dict.get(interest_type): profile.interests[interest_type] = data_dict[interest_type] - + + if profile.interests: + flag_modified(profile, 'interests') + profile.save() model.Session.flush() diff --git a/ckanext/knowledgehub/templates/user/profile/snippets/interests.html b/ckanext/knowledgehub/templates/user/profile/snippets/interests.html index 99809408..9e4206a5 100644 --- a/ckanext/knowledgehub/templates/user/profile/snippets/interests.html +++ b/ckanext/knowledgehub/templates/user/profile/snippets/interests.html @@ -1,19 +1,62 @@ {% block user_interests %}
+
+

{{ _('Research Questions') }}

+
{% block interests_research_questions %} +
+
{% endblock %}
+
+

{{ _('Keywords') }}

+
{% block interests_keywords %} {% endblock %}
+
+

{{ _('Tags') }}

+
{% block interests_tags %} {% endblock %}
+
+
+ +
+ +
+
+
+ {% endblock%} \ No newline at end of file diff --git a/ckanext/knowledgehub/templates/user/profile/user_profile.html b/ckanext/knowledgehub/templates/user/profile/user_profile.html index 8d0f9265..3406282f 100644 --- a/ckanext/knowledgehub/templates/user/profile/user_profile.html +++ b/ckanext/knowledgehub/templates/user/profile/user_profile.html @@ -3,7 +3,7 @@ {% extends "user/read_base.html" %} {% block primary_content_inner %} -

Tags

+

{{ _('Interests') }}

{% snippet 'user/profile/snippets/interests.html' %}
diff --git a/ckanext/knowledgehub/templates/user/read_base.html b/ckanext/knowledgehub/templates/user/read_base.html index 68d71336..bfe7996e 100644 --- a/ckanext/knowledgehub/templates/user/read_base.html +++ b/ckanext/knowledgehub/templates/user/read_base.html @@ -9,6 +9,7 @@ {{ h.build_nav_icon('kwh_user.intents', _('Intents'), id=user.name) }} {{ h.build_nav_icon('kwh_user.keywords', _('Keywords')) }} {{ h.build_nav_icon('kwh_user.tags', _('Tags'), id=user.name) }} + {{ h.build_nav_icon('kwh_user.profile', _('Profile')) }} {% endif %} {{ h.build_nav_icon('user.activity', _('Activity Stream'), id=user.name, offset=0) }} diff --git a/ckanext/knowledgehub/views/user.py b/ckanext/knowledgehub/views/user.py index 8b11a0c7..a35db7de 100644 --- a/ckanext/knowledgehub/views/user.py +++ b/ckanext/knowledgehub/views/user.py @@ -151,7 +151,6 @@ def keyword_create_update(show, create, id=None, data_dict=None): }) keyword['tags'] = data_dict.get('tags', '') - try: keyword['name'] = data_dict['name'] keyword = _save_keyword(context, keyword) From 7761be7437a898a0e58777aa2d69de177f90af6f Mon Sep 17 00:00:00 2001 From: Pavle Jonoski Date: Wed, 12 Feb 2020 18:09:46 +0100 Subject: [PATCH 06/15] Managing user interests. --- .../javascript/modules/user_profile.js | 203 +++++++++++++++++- ckanext/knowledgehub/logic/action/get.py | 16 +- ckanext/knowledgehub/logic/action/update.py | 5 +- ckanext/knowledgehub/model/keyword.py | 9 +- .../user/profile/snippets/interests.html | 144 +++++++++---- 5 files changed, 318 insertions(+), 59 deletions(-) diff --git a/ckanext/knowledgehub/fanstatic/javascript/modules/user_profile.js b/ckanext/knowledgehub/fanstatic/javascript/modules/user_profile.js index c825d8a3..412ee1ec 100644 --- a/ckanext/knowledgehub/fanstatic/javascript/modules/user_profile.js +++ b/ckanext/knowledgehub/fanstatic/javascript/modules/user_profile.js @@ -75,7 +75,7 @@ ckan.module('user-profile', function($){ }); var ResearchQuestion = function(template, data){ - Component.prototype.constructor.apply(this, [template]); + Component.prototype.constructor.call(this, template); this.applyData(data); $('.research-question-remove', this.el).on('click', function(){ new DeleteModal(elipsis(data.title, 30), function(){ @@ -101,9 +101,74 @@ ckan.module('user-profile', function($){ .attr('title', data.title); $('.research-question-image', this.el).attr('src', data.image_url) }, + }); + var Keyword = function(template, data){ + Component.prototype.constructor.call(this, template); + this.tagTemplate = $('.keyword-tag-template', this.el).clone() + $('.keyword-tag-template', this.el).remove(); + this.applyData(data); + $('.keyword-remove', this.el).on('click', function(){ + new DeleteModal(elipsis(data.name, 30), function(){ + this.trigger('delete', data); + }.bind(this)); + }.bind(this)); + } + + $.extend(Keyword.prototype, Component.prototype); + $.extend(Keyword.prototype, { + applyData: function(data){ + console.log('Keyword data:', data) + $('.keyword-name', this.el).html(data.name); + $.each(data.tags, function(_, tag){ + var tagEl = this.tagTemplate.clone() + console.log('TAg', tagEl, tag.name) + $('.keyword-tag-name', tagEl).html(tag.name); + $('.keyword-tag-list', this.el).append(tagEl); + }.bind(this)); + } + }); + + var Tag = function(template, data){ + Component.prototype.constructor.call(this, template); + this.applyData(data); + $('.tag-remove', this.el).on('click', function(){ + new DeleteModal(elipsis(data.name, 30), function(){ + this.trigger('delete', data); + }.bind(this)); + }.bind(this)); + } + + $.extend(Tag.prototype, Component.prototype); + $.extend(Tag.prototype, { + applyData: function(data){ + console.log('Tag data ->', data) + $('.tag-name', this.el).html(data.name); + } + }); + + var UserInterests = function(){ + + this.interestTypes = { + 'research_questions': { + template: '.research-question-template', + listSection: '.research-question-list', + component: ResearchQuestion, + }, + 'keywords': { + template: '.keyword-template', + listSection: '.keywords-list', + component: Keyword, + }, + 'tags': { + template: '.tag-template', + listSection: '.tags-list', + component: Tag, + }, + } + api.get('user_profile_show').done(function(data){ console.log('User Profile ->', data); this.profile = data.result; @@ -111,6 +176,7 @@ ckan.module('user-profile', function($){ }.bind(this)).fail(function(err){ console.error(err); }) + this.setupSelect() } $.extend(UserInterests.prototype, { @@ -125,12 +191,16 @@ ckan.module('user-profile', function($){ 'keywords': [], 'tags': [], } - $.each(interests.research_questions, function(i, researchQuestion){ - var rq = new ResearchQuestion('.research-question-template', researchQuestion); - $(rq.el).appendTo($('.research-question-list')); - this.components.research_questions[researchQuestion.id] = rq; - rq.on('delete', function(_, data){ - this.deleteInterest('research_questions', data); + + $.each(interests, function(interestType, values){ + $.each(values, function(_, data){ + var config = this.interestTypes[interestType]; + var component = this.newComponent(interestType, data); + $(component.el).prependTo(config.listSection); + this.components[interestType][data.id] = component; + component.on('delete', function(_, data){ + this.deleteInterest(interestType, data); + }.bind(this)); }.bind(this)); }.bind(this)); }, @@ -139,13 +209,15 @@ ckan.module('user-profile', function($){ }, deleteInterest: function(interest, data){ console.log('DeleteInterest:', interest, data) - var profile = {}; - profile[interest] = [] + var profile = { + interests: {} + }; + profile.interests[interest] = [] $.each(this.profile.interests[interest], function(_, entry){ if (data.id == entry.id){ return; } - profile[interest].push(entry.id); + profile.interests[interest].push(entry.id); }) this.updateInterests(profile).done(function(){ var interest_entries = this.profile.interests[interest]; @@ -165,11 +237,122 @@ ckan.module('user-profile', function($){ this.flashError('Unable to delete this interest. Please try again later.'); }.bind(this)); }, + addInterest: function(interest, data){ + if (this.components[interest] && this.components[interest][data.id]){ + return + } + + // update the profile with API + var profile = this.getProfile(); + profile.interests[interest].push(data.id) + api.post('user_profile_update', undefined, profile) + .done(function(){ + // add component + var component = this.newComponent(interest, data); + this.components[interest] = component; + component.on('delete', function(_, data){ + this.deleteInterest(interest, data) + }.bind(this)); + $(component.el).appendTo(this.interestTypes[interest].listSection); + }.bind(this)) + .fail(function(err){ + this.flashError('Failed to update interests.') + }.bind(this)); + + }, flash: function(message, error){ }, flashError: function(message){ this.flash(message, true); + }, + newComponent: function(interestType, data){ + var config = this.interestTypes[interestType]; + return new config.component(config.template, data); + }, + getProfile: function(){ + var profile = { + interests: {}, + }; + + $.each(this.profile.interests, function(interest, entries){ + if (!profile.interests[interest]){ + profile.interests[interest] = [] + } + $.each(entries, function(_, entry){ + profile.interests[interest].push(entry.id); + }) + }.bind(this)); + + console.log('Profile -> ', profile) + return profile; + }, + setupSelect: function(){ + this._setupSelect({ + selector: '.research-questions-select', + listAction: 'research_question_list', + interestType: 'research_questions', + formatResult: function(data){ + var option = $('.research-question-dropdown-option').clone(); + $('.rq-title', option).html(data.text) + $('.rq-img', option).attr('src', data.image_url) + return option; + }, + processResults: function(data){ + var results = [] + $.each(data.result.data, function(_, rq){ + rq.text = rq.title; + results.push(rq) + }); + return { + results: results + } + } + }); + + this._setupSelect({ + selector: '.keywords-select', + interestType: 'keywords', + listAction: 'keyword_list', + }); + + this._setupSelect({ + selector: '.tags-select', + interestType: 'tags', + listAction: 'tag_list_search', + }); + }, + _setupSelect: function(config){ + $(config.selector).select2({ + ajax: { + url: api.getUrl(config.listAction), + dataType: 'json', + type: config.type || 'GET', + data: config.queryData || function (term, page) { + return { + q: term, // search term + }; + }, + processResults: config.processResults || function(data){ + var results = [] + $.each(data.result, function(_, result){ + result.text = result.name; + results.push(result) + }); + return { + results: results + } + } + }, + formatResult: config.formatResult + }) + $(config.selector).click('select2:select', function(){ + var value = $(config.selector).select2("data"); + if (value){ + this.addInterest(config.interestType, value); + $(config.selector).select2("data", null); + } + }.bind(this)); } }); diff --git a/ckanext/knowledgehub/logic/action/get.py b/ckanext/knowledgehub/logic/action/get.py index 5affd035..171b21a0 100644 --- a/ckanext/knowledgehub/logic/action/get.py +++ b/ckanext/knowledgehub/logic/action/get.py @@ -1343,6 +1343,7 @@ def keyword_show(context, data_dict): return keyword_dict +@toolkit.side_effect_free def keyword_list(context, data_dict): '''Returns all keywords defined for this system. ''' @@ -1350,10 +1351,11 @@ def keyword_list(context, data_dict): page = data_dict.get('page') limit = data_dict.get('limit') + search = data_dict.get('q') results = [] - for keyword in Keyword.get_list(page, limit): + for keyword in Keyword.get_list(page, limit, search=search): results.append(toolkit.get_action('keyword_show')(context, { 'id': keyword.id, })) @@ -1418,3 +1420,15 @@ def user_profile_list(context, data_dict): results.append(_table_dictize(profile, context)) return results + + +@toolkit.side_effect_free +def tag_list_search(context, data_dict): + results = toolkit.get_action('tag_list')(context, data_dict) + tags = [] + for tag_name in results: + tags.append( + toolkit.get_action('tag_show')(context, {'id': tag_name}) + ) + + return tags \ No newline at end of file diff --git a/ckanext/knowledgehub/logic/action/update.py b/ckanext/knowledgehub/logic/action/update.py index 267b699f..a634a5cf 100644 --- a/ckanext/knowledgehub/logic/action/update.py +++ b/ckanext/knowledgehub/logic/action/update.py @@ -876,9 +876,10 @@ def user_profile_update(context, data_dict): profile = UserProfile(user_id=user_id, user_notified=True) profile.interests = {} + interests = data_dict.get('interests', {}) for interest_type in ['research_questions', 'keywords', 'tags']: - if data_dict.get(interest_type): - profile.interests[interest_type] = data_dict[interest_type] + if interests.get(interest_type) is not None: + profile.interests[interest_type] = interests[interest_type] if profile.interests: flag_modified(profile, 'interests') diff --git a/ckanext/knowledgehub/model/keyword.py b/ckanext/knowledgehub/model/keyword.py index f4d16b43..5ac31659 100644 --- a/ckanext/knowledgehub/model/keyword.py +++ b/ckanext/knowledgehub/model/keyword.py @@ -48,13 +48,20 @@ def get_tags(cls, keyword_id): return query.all() @classmethod - def get_list(cls, page=None, limit=None, order_by='created_at desc'): + def get_list(cls, + page=None, + limit=None, + search=None, + order_by='created_at desc'): offset = None if page and limit: offset = (page - 1) * limit query = Session.query(cls).autoflush(False) + if search: + query = query.filter(keyword_table.c.name.like('%{}%'.format(search))) + if order_by: query = query.order_by(order_by) diff --git a/ckanext/knowledgehub/templates/user/profile/snippets/interests.html b/ckanext/knowledgehub/templates/user/profile/snippets/interests.html index 9e4206a5..51fcf9e1 100644 --- a/ckanext/knowledgehub/templates/user/profile/snippets/interests.html +++ b/ckanext/knowledgehub/templates/user/profile/snippets/interests.html @@ -1,62 +1,116 @@ {% block user_interests %}
-
-

{{ _('Research Questions') }}

+
+
+
+

{{ _('Keywords') }}

+
+ + +
+
+
+
+
+
- {% block interests_research_questions %} -
- -
- {% endblock %}
+
-
-

{{ _('Keywords') }}

+
+
+
+

{{ _('Tags') }}

+
+ + +
+
+
+
+
+
- {% block interests_keywords %} - - {% endblock %}
+
-
-

{{ _('Tags') }}

+
+
+
+

{{ _('Research Questions') }}

+
+ + +
+
+
+
+
+
- {% block interests_tags %} - - {% endblock %}
-
-
-
- Research Question - +
+
+
+ +
+ +
+
+
+ + - +
{% endblock%} \ No newline at end of file From ee9e6c0a2526ead04571cb831a839d7c546ac908 Mon Sep 17 00:00:00 2001 From: Pavle Jonoski Date: Thu, 13 Feb 2020 15:52:53 +0100 Subject: [PATCH 07/15] Templates for keyword, research question and tag. --- .../user/profile/snippets/interests.html | 66 ++++++++++++------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/ckanext/knowledgehub/templates/user/profile/snippets/interests.html b/ckanext/knowledgehub/templates/user/profile/snippets/interests.html index 51fcf9e1..cf965264 100644 --- a/ckanext/knowledgehub/templates/user/profile/snippets/interests.html +++ b/ckanext/knowledgehub/templates/user/profile/snippets/interests.html @@ -1,13 +1,19 @@ {% block user_interests %}
+
+
+ {{ _('Please select your interests bellow.') }} +
+

{{ _('Keywords') }}

- - + +
@@ -23,8 +29,9 @@

{{ _('Keywords') }}

{{ _('Tags') }}

- - + +
@@ -40,9 +47,9 @@

{{ _('Tags') }}

{{ _('Research Questions') }}

- + + data-placeholder="{{ _('Select a Research Question to add as interest.') }}'">
@@ -53,17 +60,6 @@

{{ _('Research Questions') }}

-
-
- -
- -
-
-
+
+
+
+ +
+
+
+
+ +
+ +
+
+
+
@@ -93,21 +109,23 @@