Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a form to move notes to other folders. #231

Merged
merged 10 commits into from
May 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions archivy/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import subprocess
import os
from pathlib import Path
from shutil import rmtree
import shutil

import frontmatter
from flask import current_app
Expand Down Expand Up @@ -131,10 +131,27 @@ def get_item(dataobj_id):
data = frontmatter.load(file)
data["fullpath"] = str(file)
data["dir"] = str(file.parent.relative_to(get_data_dir()))
# replace . for root items to ''
if data["dir"] == ".":
data["dir"] = ""
return data
return None


def move_item(dataobj_id, new_path):
"""Move dataobj of given id to new_path"""
file = get_by_id(dataobj_id)
data_dir = get_data_dir()
out_dir = data_dir / new_path
if not file:
raise FileNotFoundError
if (out_dir / file.parts[-1]).exists():
raise FileExistsError
elif is_relative_to(out_dir, data_dir) and out_dir.exists(): # check file isn't
return shutil.move(str(file), f"{get_data_dir()}/{new_path}/")
return False


def delete_item(dataobj_id):
"""Delete dataobj of given id"""
file = get_by_id(dataobj_id)
Expand Down Expand Up @@ -236,8 +253,6 @@ def get_dirs():
if dir_path.is_dir()
]

# append name for root dir
dirnames.append("not classified")
return dirnames


Expand All @@ -258,7 +273,7 @@ def delete_dir(name):
if not is_relative_to(target_dir, root_dir) or target_dir == root_dir:
return False
try:
rmtree(target_dir)
shutil.rmtree(target_dir)
return True
except FileNotFoundError:
return False
Expand Down
5 changes: 5 additions & 0 deletions archivy/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ class NewFolderForm(FlaskForm):
submit = SubmitField("Create sub directory")


class MoveDataForm(FlaskForm):
path = SelectField("Move to")
submit = SubmitField("✓")


class DeleteDataForm(FlaskForm):
submit = SubmitField("Delete")

Expand Down
53 changes: 45 additions & 8 deletions archivy/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,12 @@ def index():
@app.route("/bookmarks/new", methods=["GET", "POST"])
def new_bookmark():
form = forms.NewBookmarkForm()
form.path.choices = [(pathname, pathname) for pathname in data.get_dirs()]
default_dir = "root directory"
form.path.choices = [("", default_dir)] + [
(pathname, pathname) for pathname in data.get_dirs()
]
if form.validate_on_submit():
path = form.path.data if form.path.data != "not classified" else ""
path = form.path.data
tags = form.tags.data.split(",") if form.tags.data != "" else []
bookmark = DataObj(url=form.url.data, tags=tags, path=path, type="bookmark")
bookmark.process_bookmark_url()
Expand All @@ -74,27 +77,30 @@ def new_bookmark():
return redirect(f"/dataobj/{bookmark_id}")
# for bookmarklet
form.url.data = request.args.get("url", "")
path = request.args.get("path", "not classified").strip("/")
path = request.args.get("path", default_dir).strip("/")
# handle empty argument
form.path.data = path if path != "" else "not classified"
form.path.data = path
return render_template("dataobjs/new.html", title="New Bookmark", form=form)


@app.route("/notes/new", methods=["GET", "POST"])
def new_note():
form = forms.NewNoteForm()
form.path.choices = [(pathname, pathname) for pathname in data.get_dirs()]
default_dir = "root directory"
form.path.choices = [("", default_dir)] + [
(pathname, pathname) for pathname in data.get_dirs()
]
if form.validate_on_submit():
path = form.path.data if form.path.data != "not classified" else ""
path = form.path.data
tags = form.tags.data.split(",") if form.tags.data != "" else []
note = DataObj(title=form.title.data, tags=tags, path=path, type="note")
note_id = note.insert()
if note_id:
flash("Note Saved!", "success")
return redirect(f"/dataobj/{note_id}")
path = request.args.get("path", "not classified").strip("/")
path = request.args.get("path", default_dir).strip("/")
# handle empty argument
form.path.data = path if path != "" else "not classified"
form.path.data = path
return render_template("/dataobjs/new.html", title="New Note", form=form)


Expand All @@ -121,6 +127,12 @@ def show_dataobj(dataobj_id):
if hit["id"] != dataobj_id:
backlinks.append({"title": hit["title"], "id": hit["id"]})

# Form for moving data into another folder
move_form = forms.MoveDataForm()
move_form.path.choices = [("", "root directory")] + [
(pathname, pathname) for pathname in data.get_dirs()
]

post_title_form = forms.TitleForm()
post_title_form.title.data = dataobj["title"]

Expand All @@ -134,9 +146,34 @@ def show_dataobj(dataobj_id):
view_only=0,
search_enabled=app.config["SEARCH_CONF"]["enabled"],
post_title_form=post_title_form,
move_form=move_form,
)


@app.route("/dataobj/move/<dataobj_id>", methods=["POST"])
def move_data(dataobj_id):
form = forms.MoveDataForm()
out_dir = form.path.data if form.path.data != "" else "root directory"
if form.path.data == None:
flash("No path specified.")
return redirect(f"/dataobj/{dataobj_id}")
try:
if data.move_item(dataobj_id, form.path.data):
print("c")
flash(f"Data successfully moved to {out_dir}.", "success")
return redirect(f"/dataobj/{dataobj_id}")
else:
print("k")
flash(f"Data could not be moved to {out_dir}.", "error")
return redirect(f"/dataobj/{dataobj_id}")
except FileNotFoundError:
flash("Data not found.", "error")
return redirect("/")
except FileExistsError:
flash("Data already in target directory.", "error")
return redirect(f"/dataobj/{dataobj_id}")


@app.route("/dataobj/delete/<dataobj_id>", methods=["DELETE", "GET"])
def delete_data(dataobj_id):
try:
Expand Down
9 changes: 9 additions & 0 deletions archivy/static/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,15 @@ form {
.bidirBtn::before {
content: "Link to a note";
}

#post-move-form {
margin-left: 0em;
}

#post-move-form input[type="submit"] {
margin: 0;
}

/* mobile adjustements */
@media only screen and (max-width: 500px) {
.content {
Expand Down
41 changes: 25 additions & 16 deletions archivy/templates/dataobjs/show.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,23 +47,32 @@ <h2 id="post-title">
<br>
<!-- Edit / Delete buttons -->
{% if not view_only %}
<div class="d-flex" id="post-btns">
<button onclick="toggleEditor(this)" class="btn">
<svg class="octicon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M11.013 1.427a1.75 1.75 0 012.474 0l1.086 1.086a1.75 1.75 0 010 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 01-.927-.928l.929-3.25a1.75 1.75 0 01.445-.758l8.61-8.61zm1.414 1.06a.25.25 0 00-.354 0L10.811 3.75l1.439 1.44 1.263-1.263a.25.25 0 000-.354l-1.086-1.086zM11.189 6.25L9.75 4.81l-6.286 6.287a.25.25 0 00-.064.108l-.558 1.953 1.953-.558a.249.249 0 00.108-.064l6.286-6.286z"></path></svg>
<span>Edit</span>
</button>
<form action="/dataobj/delete/{{ dataobj['id'] }}" method="delete" onsubmit="return confirm('Delete this item permanently?')" novalidate>
{{ form.hidden_tag() }}
<button class="btn btn-delete">
<svg class="octicon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M6.5 1.75a.25.25 0 01.25-.25h2.5a.25.25 0 01.25.25V3h-3V1.75zm4.5 0V3h2.25a.75.75 0 010 1.5H2.75a.75.75 0 010-1.5H5V1.75C5 .784 5.784 0 6.75 0h2.5C10.216 0 11 .784 11 1.75zM4.496 6.675a.75.75 0 10-1.492.15l.66 6.6A1.75 1.75 0 005.405 15h5.19c.9 0 1.652-.681 1.741-1.576l.66-6.6a.75.75 0 00-1.492-.149l-.66 6.6a.25.25 0 01-.249.225h-5.19a.25.25 0 01-.249-.225l-.66-6.6z"></path></svg>
<span>Delete</span>
<div class="d-flex" id="post-btns">
<button onclick="toggleEditor(this)" class="btn">
<svg class="octicon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M11.013 1.427a1.75 1.75 0 012.474 0l1.086 1.086a1.75 1.75 0 010 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 01-.927-.928l.929-3.25a1.75 1.75 0 01.445-.758l8.61-8.61zm1.414 1.06a.25.25 0 00-.354 0L10.811 3.75l1.439 1.44 1.263-1.263a.25.25 0 000-.354l-1.086-1.086zM11.189 6.25L9.75 4.81l-6.286 6.287a.25.25 0 00-.064.108l-.558 1.953 1.953-.558a.249.249 0 00.108-.064l6.286-6.286z"></path></svg>
<span>Edit</span>
</button>
</form>
<label class="sr-only">
<input type="checkbox" id="sr-checkbox">
screenreader mode
</label>
</div>
<form action="/dataobj/delete/{{ dataobj['id'] }}" method="delete" onsubmit="return confirm('Delete this item permanently?')" novalidate>
{{ form.hidden_tag() }}
<button class="btn btn-delete">
<svg class="octicon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M6.5 1.75a.25.25 0 01.25-.25h2.5a.25.25 0 01.25.25V3h-3V1.75zm4.5 0V3h2.25a.75.75 0 010 1.5H2.75a.75.75 0 010-1.5H5V1.75C5 .784 5.784 0 6.75 0h2.5C10.216 0 11 .784 11 1.75zM4.496 6.675a.75.75 0 10-1.492.15l.66 6.6A1.75 1.75 0 005.405 15h5.19c.9 0 1.652-.681 1.741-1.576l.66-6.6a.75.75 0 00-1.492-.149l-.66 6.6a.25.25 0 01-.249.225h-5.19a.25.25 0 01-.249-.225l-.66-6.6z"></path></svg>
<span>Delete</span>
</button>
</form>

<form action="/dataobj/move/{{ dataobj['id'] }}" method="post" novalidate id="post-move-form">
{{ move_form.hidden_tag() }}
<button class="btn">
Move to
</button>
{{ move_form.path(placeholder=move_form.path.name) }}
</form>
<label class="sr-only">
<input type="checkbox" id="sr-checkbox">
screenreader mode
</label>
</div>

{% endif %}
</div>
<div id="content-cont" class="markdown-body">
Expand Down
44 changes: 41 additions & 3 deletions tests/functional/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from werkzeug.security import generate_password_hash

from archivy.helpers import get_max_id, get_db
from archivy.data import get_dirs, create_dir
from archivy.data import get_dirs, create_dir, get_item


def test_get_index(test_app, client: FlaskClient):
Expand Down Expand Up @@ -67,7 +67,7 @@ def test_create_new_bookmark(
bookmark_data = {
"url": "https://example.com",
"tags": "testing,bookmark",
"path": "not classified",
"path": "",
"submit": "true",
}

Expand All @@ -88,7 +88,7 @@ def test_create_note(test_app, client: FlaskClient):
note_data = {
"title": "Testing the create route",
"tags": "testing,note",
"path": "not classified",
"path": "",
"submit": "true",
}

Expand Down Expand Up @@ -242,3 +242,41 @@ def test_bookmark_with_long_title_gets_truncated(test_app, client, mocked_respon

resp = client.post("/bookmarks/new", data=bookmark_data)
assert resp.status_code == 200


def test_move_data(test_app, note_fixture, client):
create_dir("random")

resp = client.post(
"/dataobj/move/1",
data={"path": "random", "submit": "true"},
follow_redirects=True,
)
assert resp.status_code == 200
assert "Data successfully moved to random."

assert get_item(1)["dir"] == "random"


def test_invalid_inputs_fail_move_data(test_app, note_fixture, client):

resp = client.post("/dataobj/move/1", follow_redirects=True)
assert b"No path specified." in resp.data

resp = client.post(
"/dataobj/move/2", data={"path": "aaa", "submit": "true"}, follow_redirects=True
)
assert b"Data not found" in resp.data

resp = client.post(
"/dataobj/move/1", data={"path": "", "submit": "true"}, follow_redirects=True
)
assert b"Data already in target directory" in resp.data

faulty_paths = ["../adarnad", "~/adasd", "ssss"]
for p in faulty_paths:
print(p)
resp = client.post(
"/dataobj/move/1", data={"path": p, "submit": "true"}, follow_redirects=True
)
assert b"Data could not be moved to " + bytes(p, "utf-8") in resp.data