From 4baa832f9599d0255a31748b353e57656fff93f1 Mon Sep 17 00:00:00 2001 From: Amirhossein <66219826+amirhssn@users.noreply.github.com> Date: Tue, 27 Oct 2020 14:43:06 +0330 Subject: [PATCH 01/13] Add an option to select the program from --- .gitignore | 2 + main.py | 44 +++++++++++--- miniconf/load_site_data.py | 36 ++++++++++- miniconf/site_data.py | 8 ++- static/js/little_helpers.js | 3 + static/js/papers.js | 117 ++++++++++++++++++++++++++++++------ templates/papers.html | 75 +++++++++++++++++++---- 7 files changed, 245 insertions(+), 40 deletions(-) diff --git a/.gitignore b/.gitignore index 1a137e4..2f9f972 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,5 @@ node_modules/ # MacOS .DS_Store + +.vscode \ No newline at end of file diff --git a/main.py b/main.py index c9eec15..76b1ba6 100644 --- a/main.py +++ b/main.py @@ -186,13 +186,35 @@ def chat(): def papers_json(): return jsonify(site_data["papers"]) +@app.route("/papers_.json") +def papers_program_track(program_name): + paper: Paper + if program_name == "workshop": + papers_for_program = [] + for wsh in site_data["workshops"]: + papers_for_program.extend(wsh.papers) + else: + papers_for_program = [ + paper for paper in site_data["papers"] if paper.content.program == program_name + ] + return jsonify(papers_for_program) + -@app.route("/track_.json") -def track_json(track_name): +@app.route("/track__.json") +def track_json(program_name, track_name): paper: Paper - papers_for_track = [ - paper for paper in site_data["papers"] if paper.content.track == track_name - ] + if program_name == "workshop": + papers_for_track = None + for wsh in site_data["workshops"]: + if wsh.title == track_name: + papers_for_track = wsh.papers + break + else: + papers_for_track = [ + paper + for paper in site_data["papers"] + if paper.content.track == track_name and paper.content.program == program_name + ] return jsonify(papers_for_track) @@ -216,8 +238,16 @@ def generator(): paper: Paper for paper in site_data["papers"]: yield "paper", {"uid": paper.id} - for track in site_data["tracks"]: - yield "track_json", {"track_name": track} + # for track in site_data["tracks"]: + # yield "track_json", {"track_name": track} + for program in site_data["programs"]: + yield "papers_json", {"program_name": program} + for track in site_data["tracks"]: + yield "track__json", {"track_name": track, "program_name":program} + + yield "papers_json", {"program_name": "workshop"} + for wsh in site_data["workshops"]: + yield "track__json", {"track_name": wsh.title, "program_name":"workshop"} plenary_session: PlenarySession for _, plenary_sessions_on_date in site_data["plenary_sessions"].items(): for plenary_session in plenary_sessions_on_date: diff --git a/miniconf/load_site_data.py b/miniconf/load_site_data.py index 2afd0ad..71b5d8a 100644 --- a/miniconf/load_site_data.py +++ b/miniconf/load_site_data.py @@ -5,6 +5,7 @@ import json import os from collections import OrderedDict, defaultdict +from dataclasses import asdict from datetime import date, datetime, timedelta from itertools import chain from typing import Any, DefaultDict, Dict, List, Tuple @@ -159,6 +160,19 @@ def load_site_data( ] site_data["plenary_session_days"][0][-1] = "active" + + # Papers' progam to their data + for p in site_data["main_papers"] + site_data["cl_papers"] + site_data["tacl_papers"]: + p["program"] = "main" + + for p in site_data["demo_papers"]: + p["program"] = "demo" + + for p in site_data["srw_papers"]: + p["program"] = "srw" + + site_data["programs"] = ["main", "demo", "findings", "srw", "workshop"] + # papers.{html,json} papers = build_papers( raw_papers=site_data["main_papers"] @@ -653,6 +667,7 @@ def build_papers( paper_type=item.get("paper_type", ""), sessions=sessions_for_paper[item["UID"]], similar_paper_uids=paper_recs.get(item["UID"], [item["UID"]]), + program=item["program"] ), ) for item in raw_papers @@ -712,6 +727,11 @@ def build_workshops( workshop_schedules: Dict[str, List[Dict[str, Any]]], ) -> List[Workshop]: + def workshop_title(workshop_id): + for wsh in raw_workshops: + if wsh["UID"] == workshop_id: + return wsh["title"] + workshop_papers: DefaultDict[str, List[WorkshopPaper]] = defaultdict(list) for workshop_id, papers in raw_workshop_papers.items(): for item in papers: @@ -720,7 +740,21 @@ def build_workshops( id=item["UID"], title=item["title"], speakers=item["speakers"], - presentation_id=item.get("presentation_id", ""), + presentation_id=item.get("presentation_id", None), + content=PaperContent( + title=item["title"], + authors=[item["speakers"]], + track=workshop_title(workshop_id), + paper_type=None, + abstract=None, + tldr=None, + keywords=[], + pdf_url=None, + demo_url=None, + sessions=[], + similar_paper_uids=[], + program="workshop", + ) ) ) diff --git a/miniconf/site_data.py b/miniconf/site_data.py index 797111f..26b0279 100644 --- a/miniconf/site_data.py +++ b/miniconf/site_data.py @@ -63,16 +63,19 @@ class PaperContent: demo_url: Optional[str] sessions: List[SessionInfo] similar_paper_uids: List[str] + program: str def __post_init__(self): - assert self.track, self + if self.program != "workshop": + assert self.track, self if self.pdf_url: assert self.pdf_url.startswith("https://"), self.pdf_url if self.demo_url: assert self.demo_url.startswith("https://") or self.demo_url.startswith( "http://" ), self.demo_url - assert self.paper_type[0].isupper(), self + if self.program != "workshop": + assert self.paper_type[0].isupper(), self @dataclass(frozen=True) @@ -178,6 +181,7 @@ class WorkshopPaper: title: str speakers: str presentation_id: Optional[str] + content: PaperContent @dataclass(frozen=True) diff --git a/static/js/little_helpers.js b/static/js/little_helpers.js index 7782ea5..427a6ac 100644 --- a/static/js/little_helpers.js +++ b/static/js/little_helpers.js @@ -81,15 +81,18 @@ let calcAllKeys = function (allPapers, allKeys) { const collectAuthors = new Set(); const collectKeywords = new Set(); const collectSessions = new Set(); + const collectTracks = new Set(); allPapers.forEach( d => { d.content.authors.forEach(a => collectAuthors.add(a)); d.content.keywords.forEach(a => collectKeywords.add(a)); d.content.sessions.forEach(a => collectSessions.add(a)); + collectTracks.add(d.content.track); allKeys.titles.push(d.content.title); }); allKeys.authors = Array.from(collectAuthors); allKeys.keywords = Array.from(collectKeywords); allKeys.sessions = Array.from(collectSessions); + allKeys.tracks = Array.from(collectTracks).sort(); }; diff --git a/static/js/papers.js b/static/js/papers.js index 4fab76c..34754fb 100644 --- a/static/js/papers.js +++ b/static/js/papers.js @@ -118,6 +118,17 @@ const setUpKeyBindings = () => { const persistor = new Persistor('Mini-Conf-Papers'); const favPersistor = new Persistor('Mini-Conf-Favorite-Papers'); +const updateTrackList = (tracks, default_track) => { + default_track = default_track || "All tracks"; + + tracks = Array.from([default_track]).concat(tracks); + let optionsHtml = tracks.map(track_html); + + $('#track_selector').html(optionsHtml); + $('#track_selector').selectpicker('refresh'); + $('#track_selector').selectpicker('val', default_track); +} + const updateCards = (papers) => { const storedPapers = persistor.getAll(); const favPapers = favPersistor.getAll(); @@ -207,8 +218,22 @@ const openQuickviewModal = (paper) => { $('#quickviewModal').modal('show') } +const maybe_update = (element_ids, value, callback) => { + if (value && ( + (typeof value == "string" && value !== "") || + (Array.isArray(value) && value.length > 0 ))) + element_ids.forEach(x => $(x).show()); + else + element_ids.forEach(x => $(x).hide()); + + callback(value); +} + const updateModalData = (paper) => { - $('#modalTitle').text(paper.content.title); + + let program = paper.content.program; + $('#modalTitle').html( + `${paper.content.title}   ${program}`); let isVisited = persistor.get(paper.id) || false if (isVisited) @@ -217,23 +242,56 @@ const updateModalData = (paper) => { $('#modalTitle').removeClass('card-title-visited'); let authorsHtml = paper.content.authors.map(author_html).join(', '); - $('#modalAuthors').html(authorsHtml); - - $('#modalPaperType').text(paper.content.paper_type); - $('#modalPaperTrack').text(paper.content.track); + maybe_update( + ["#modalAuthors"], + authorsHtml, + x => $('#modalAuthors').html(x)); - $('#modalAbstract').text(paper.content.abstract); - - $('#modalChatUrl').attr('href', `https://${chat_server}/channel/paper-${paper.id.replace('.', '-')}`); - $('#modalPresUrl').attr('href', `https://slideslive.com/${paper.presentation_id}`); - $('#modalPaperUrl').attr('href', paper.content.pdf_url); - $('#modalPaperPage').attr('href', `paper_${paper.id}.html`); + + maybe_update( + ['#modalPaperType'], + paper.content.paper_type, + x => $('#modalPaperType').text(x)); + maybe_update( + ['#modalPaperTrack'], + paper.content.track, + x => $('#modalPaperTrack').text(x)); + + maybe_update( + ['#modalAbstract', '#modalAbstractHeader'], + paper.content.abstract, + x => $('#modalAbstract').text(x)); + + if (program != "workshop"){ + $('#modalChatUrl').attr('href', `https://${chat_server}/channel/paper-${paper.id.replace('.', '-')}`); + $('#modalPaperPage').attr('href', `paper_${paper.id}.html`); + } else { + $('#modalChatUrl').hide(); + $('#modalPaperPage').hide(); + } + + maybe_update( + ['#modalPresUrl'], + paper.presentation_id, + x => $('#modalPresUrl').attr('href', `https://slideslive.com/${x}`)); + maybe_update( + ['#modalPaperUrl'], + paper.content.pdf_url, + x => $('#modalPaperUrl').attr('href', x)); + let keywordsHtml = paper.content.keywords.map(modal_keyword).join('\n'); - $('#modalKeywords').html(keywordsHtml); + maybe_update( + ['#modalKeywords', '#modalKeywordsHeader'], + paper.content.keywords, + x => $('#modalKeywords').html(keywordsHtml)); let sessionsHtml = paper.content.sessions.map(s => modal_session_html(s, paper)).join('\n'); $('#modalSessions').html(sessionsHtml); + maybe_update( + ['#modalSessions', '#modalSessionsHeader'], + paper.content.sessions, + x => $('#modalSessions').html(sessionsHtml)); $('#modalPaperPage').unbind( "click" ); $('#modalPaperPage').click(() => { @@ -391,18 +449,29 @@ const updateSession = () => { /** * START here and load JSON. */ -const start = (track) => { +const start = () => { // const urlFilter = getUrlParameter("filter") || 'keywords'; const urlFilter = getUrlParameter("filter") || 'titles'; + const program = getUrlParameter("program") || 'main' + let track = "" + if (program !== "workshop") { + track = getUrlParameter("track") || 'All tracks' + } else { + track = getUrlParameter("track") || 'All workshops' + } setQueryStringParameter("filter", urlFilter); + setQueryStringParameter("program", program); updateFilterSelectionBtn(urlFilter); + // $('.program_option').find(`input[name=program][value=${program}]`).checked = true + document.querySelector(`input[name=program][value=${program}]`).checked = true; + var path_to_papers_json; - if (track === "All tracks") { - path_to_papers_json = "papers.json"; + if (track === "All tracks" || track === "All workshops") { + path_to_papers_json = `papers_${program}.json`; } else { - path_to_papers_json = "track_" + track + ".json"; + path_to_papers_json = `track_${program}_${track}.json`; } d3.json(path_to_papers_json).then(papers => { @@ -413,13 +482,17 @@ const start = (track) => { $('#progressBar').hide(); calcAllKeys(allPapers, allKeys); + let default_track = program == "workshop"? "All workshops" : "All tracks"; + if (path_to_papers_json.startsWith("papers_")) + updateTrackList(allKeys.tracks, default_track); + setTypeAhead(urlFilter, allKeys, filters, render); // updateCards(allPapers); render(); - }).catch(e => console.error(e)) + }).catch(e => console.error(e)); }; @@ -453,6 +526,14 @@ d3.selectAll('.render_option input').on('click', function () { render(); }); +d3.selectAll('.program_option input').on('click', function () { + const me = d3.select(this); + let program = me.property('value'); + setQueryStringParameter("program", program); + + start(); +}); + d3.select('.visited').on('click', () => { sortSelectedFirst(allPapers); @@ -471,6 +552,8 @@ d3.select('.reshuffle').on('click', () => { * CARDS */ +const track_html = track => ``; + const keyword = kw => `${kw.toLowerCase()}`; diff --git a/templates/papers.html b/templates/papers.html index 0954ca0..5c9e75f 100644 --- a/templates/papers.html +++ b/templates/papers.html @@ -101,10 +101,7 @@
@@ -113,6 +110,51 @@
+ +
+
+ + + + + +
+
+
- -   + +
@@ -213,13 +255,13 @@
@@ -257,18 +299,25 @@
Sessions
$(document).ready(function () { tippy("[data-tippy-content]", { trigger: "mouseenter focus" }); - const track = getUrlParameter("track") || "All tracks"; + const program = getUrlParameter("program") || 'main' + let track = "" + if (program !== "workshop") { + track = getUrlParameter("track") || 'All tracks' + } else { + track = getUrlParameter("track") || 'All workshops' + } updateTabs(); - // start(track); - $('#track_selector').selectpicker('val', track); + start(); + // $('#track_selector').selectpicker('val', track); }); $('#track_selector').on('changed.bs.select', function (e, clickedIndex, isSelected, previousValue) { let track = e.target.value; + if (track === previousValue) return; setQueryStringParameter("track", track); - start(track); + start(); }); $('#tabBrowse').click(()=>{ From 9983ea938c8b617cee742d38b3f14ea1cf0d6339 Mon Sep 17 00:00:00 2001 From: Amirhossein <66219826+amirhssn@users.noreply.github.com> Date: Tue, 27 Oct 2020 14:47:01 +0330 Subject: [PATCH 02/13] Fix formatting --- main.py | 16 ++++++++++------ miniconf/load_site_data.py | 14 +++++++------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/main.py b/main.py index 76b1ba6..b9d5a0e 100644 --- a/main.py +++ b/main.py @@ -186,6 +186,7 @@ def chat(): def papers_json(): return jsonify(site_data["papers"]) + @app.route("/papers_.json") def papers_program_track(program_name): paper: Paper @@ -195,7 +196,9 @@ def papers_program_track(program_name): papers_for_program.extend(wsh.papers) else: papers_for_program = [ - paper for paper in site_data["papers"] if paper.content.program == program_name + paper + for paper in site_data["papers"] + if paper.content.program == program_name ] return jsonify(papers_for_program) @@ -211,9 +214,10 @@ def track_json(program_name, track_name): break else: papers_for_track = [ - paper - for paper in site_data["papers"] - if paper.content.track == track_name and paper.content.program == program_name + paper + for paper in site_data["papers"] + if paper.content.track == track_name + and paper.content.program == program_name ] return jsonify(papers_for_track) @@ -243,11 +247,11 @@ def generator(): for program in site_data["programs"]: yield "papers_json", {"program_name": program} for track in site_data["tracks"]: - yield "track__json", {"track_name": track, "program_name":program} + yield "track__json", {"track_name": track, "program_name": program} yield "papers_json", {"program_name": "workshop"} for wsh in site_data["workshops"]: - yield "track__json", {"track_name": wsh.title, "program_name":"workshop"} + yield "track__json", {"track_name": wsh.title, "program_name": "workshop"} plenary_session: PlenarySession for _, plenary_sessions_on_date in site_data["plenary_sessions"].items(): for plenary_session in plenary_sessions_on_date: diff --git a/miniconf/load_site_data.py b/miniconf/load_site_data.py index 71b5d8a..fc166ae 100644 --- a/miniconf/load_site_data.py +++ b/miniconf/load_site_data.py @@ -160,14 +160,15 @@ def load_site_data( ] site_data["plenary_session_days"][0][-1] = "active" - # Papers' progam to their data - for p in site_data["main_papers"] + site_data["cl_papers"] + site_data["tacl_papers"]: + for p in ( + site_data["main_papers"] + site_data["cl_papers"] + site_data["tacl_papers"] + ): p["program"] = "main" - + for p in site_data["demo_papers"]: p["program"] = "demo" - + for p in site_data["srw_papers"]: p["program"] = "srw" @@ -667,7 +668,7 @@ def build_papers( paper_type=item.get("paper_type", ""), sessions=sessions_for_paper[item["UID"]], similar_paper_uids=paper_recs.get(item["UID"], [item["UID"]]), - program=item["program"] + program=item["program"], ), ) for item in raw_papers @@ -726,7 +727,6 @@ def build_workshops( raw_workshop_papers: Dict[str, List[Dict[str, Any]]], workshop_schedules: Dict[str, List[Dict[str, Any]]], ) -> List[Workshop]: - def workshop_title(workshop_id): for wsh in raw_workshops: if wsh["UID"] == workshop_id: @@ -754,7 +754,7 @@ def workshop_title(workshop_id): sessions=[], similar_paper_uids=[], program="workshop", - ) + ), ) ) From ac52f5fb2955f8411db453167c82403f2614d5e8 Mon Sep 17 00:00:00 2001 From: Amirhossein <66219826+amirhssn@users.noreply.github.com> Date: Tue, 27 Oct 2020 14:51:39 +0330 Subject: [PATCH 03/13] Fix formatting --- miniconf/load_site_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miniconf/load_site_data.py b/miniconf/load_site_data.py index fc166ae..ef8eef5 100644 --- a/miniconf/load_site_data.py +++ b/miniconf/load_site_data.py @@ -5,7 +5,6 @@ import json import os from collections import OrderedDict, defaultdict -from dataclasses import asdict from datetime import date, datetime, timedelta from itertools import chain from typing import Any, DefaultDict, Dict, List, Tuple @@ -731,6 +730,7 @@ def workshop_title(workshop_id): for wsh in raw_workshops: if wsh["UID"] == workshop_id: return wsh["title"] + return "" workshop_papers: DefaultDict[str, List[WorkshopPaper]] = defaultdict(list) for workshop_id, papers in raw_workshop_papers.items(): From dc42fddc2bada56317f592fe8bf9975414475278 Mon Sep 17 00:00:00 2001 From: Amirhossein <66219826+amirhssn@users.noreply.github.com> Date: Tue, 27 Oct 2020 17:08:35 +0330 Subject: [PATCH 04/13] Fix incorrect Flask freezer command --- main.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/main.py b/main.py index b9d5a0e..407b362 100644 --- a/main.py +++ b/main.py @@ -187,10 +187,10 @@ def papers_json(): return jsonify(site_data["papers"]) -@app.route("/papers_.json") -def papers_program_track(program_name): +@app.route("/papers_.json") +def papers_program(program): paper: Paper - if program_name == "workshop": + if program == "workshop": papers_for_program = [] for wsh in site_data["workshops"]: papers_for_program.extend(wsh.papers) @@ -198,7 +198,7 @@ def papers_program_track(program_name): papers_for_program = [ paper for paper in site_data["papers"] - if paper.content.program == program_name + if paper.content.program == program ] return jsonify(papers_for_program) @@ -242,16 +242,14 @@ def generator(): paper: Paper for paper in site_data["papers"]: yield "paper", {"uid": paper.id} - # for track in site_data["tracks"]: - # yield "track_json", {"track_name": track} for program in site_data["programs"]: - yield "papers_json", {"program_name": program} + yield "papers_program", {"program": program} for track in site_data["tracks"]: - yield "track__json", {"track_name": track, "program_name": program} + yield "track_json", {"track_name": track, "program_name": program} - yield "papers_json", {"program_name": "workshop"} + yield "papers_program", {"program": "workshop"} for wsh in site_data["workshops"]: - yield "track__json", {"track_name": wsh.title, "program_name": "workshop"} + yield "track_json", {"track_name": wsh.title, "program_name": "workshop"} plenary_session: PlenarySession for _, plenary_sessions_on_date in site_data["plenary_sessions"].items(): for plenary_session in plenary_sessions_on_date: From 5439d318ee5b4c35b5674bb5c0c01322dc66af0b Mon Sep 17 00:00:00 2001 From: Amirhossein <66219826+amirhssn@users.noreply.github.com> Date: Tue, 27 Oct 2020 17:13:25 +0330 Subject: [PATCH 05/13] Fix formating --- main.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/main.py b/main.py index 407b362..1789554 100644 --- a/main.py +++ b/main.py @@ -196,9 +196,7 @@ def papers_program(program): papers_for_program.extend(wsh.papers) else: papers_for_program = [ - paper - for paper in site_data["papers"] - if paper.content.program == program + paper for paper in site_data["papers"] if paper.content.program == program ] return jsonify(papers_for_program) From c622e5da84a2f280f938090fe730dfb890aee708 Mon Sep 17 00:00:00 2001 From: Amirhossein <66219826+amirhssn@users.noreply.github.com> Date: Wed, 28 Oct 2020 20:07:12 +0330 Subject: [PATCH 06/13] Fix bug and hide track list on demo and findings --- static/css/main.css | 19 +++++++++++++++++++ static/js/papers.js | 42 ++++++++++++++++++++++++++++-------------- templates/papers.html | 10 +++++----- 3 files changed, 52 insertions(+), 19 deletions(-) diff --git a/static/css/main.css b/static/css/main.css index 87b5e26..cea04b2 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -585,3 +585,22 @@ div#papers_options > div { .card-link-warning { color: #ffc107 !important; } + +.button-box { + display: flex; +} + +.bottom-bar { + margin-bottom: 0em; +} + +@media (max-width: 768px) { + .button-box { + display: block; + } + + .bottom-bar { + margin-bottom: 1em; + } +} + diff --git a/static/js/papers.js b/static/js/papers.js index 34754fb..882e3e5 100644 --- a/static/js/papers.js +++ b/static/js/papers.js @@ -446,29 +446,43 @@ const updateSession = () => { } } +const updateToolboxUI = (program, urlFilter, track) =>{ + updateFilterSelectionBtn(urlFilter); + + // Update program selector UI + document.querySelector(`input[name=program][value=${program}]`).checked = true; + + if (["main", "workshop", "all"].includes(program)) { + $("#track_selector").selectpicker('show'); + $("#track_selector_placeholder").removeClass("d-lg-block"); + } else{ + $("#track_selector").selectpicker('hide'); + $("#track_selector_placeholder").addClass("d-lg-block"); + } +} + /** * START here and load JSON. */ -const start = () => { - // const urlFilter = getUrlParameter("filter") || 'keywords'; +const start = (reset_track) => { + + reset_track = reset_track || false; + const urlFilter = getUrlParameter("filter") || 'titles'; const program = getUrlParameter("program") || 'main' - let track = "" - if (program !== "workshop") { - track = getUrlParameter("track") || 'All tracks' - } else { - track = getUrlParameter("track") || 'All workshops' - } + let default_track = program == "workshop"? "All workshops" : "All tracks"; + + let track = getUrlParameter("track") || default_track; + if (reset_track) + track = default_track; setQueryStringParameter("filter", urlFilter); setQueryStringParameter("program", program); - updateFilterSelectionBtn(urlFilter); - // $('.program_option').find(`input[name=program][value=${program}]`).checked = true - document.querySelector(`input[name=program][value=${program}]`).checked = true; + updateToolboxUI(program, urlFilter, track) - var path_to_papers_json; - if (track === "All tracks" || track === "All workshops") { + let path_to_papers_json; + if (track === default_track) { path_to_papers_json = `papers_${program}.json`; } else { path_to_papers_json = `track_${program}_${track}.json`; @@ -531,7 +545,7 @@ d3.selectAll('.program_option input').on('click', function () { let program = me.property('value'); setQueryStringParameter("program", program); - start(); + start(reset_track=true); }); d3.select('.visited').on('click', () => { diff --git a/templates/papers.html b/templates/papers.html index 5c9e75f..48d7334 100644 --- a/templates/papers.html +++ b/templates/papers.html @@ -101,19 +101,20 @@
+
+
-
+