Remove file extension from Python and Web APIs. Resolves #17.
authorAdam Dullage <redacted>
Fri, 29 Jul 2022 11:39:14 +0000 (12:39 +0100)
committerAdam Dullage <redacted>
Fri, 29 Jul 2022 11:39:14 +0000 (12:39 +0100)
flatnotes/error_responses.py
flatnotes/flatnotes.py
flatnotes/helpers.py
flatnotes/main.py
flatnotes/models.py
flatnotes/src/classes.js
flatnotes/src/components/App.js
flatnotes/src/components/App.vue
flatnotes/src/components/RecentlyModified.vue
flatnotes/src/constants.js

index bcf452df79e973c9bbba47e2c0feb84411ea7df3..b5c4314886afea6d0345d5905ab571b106077423 100644 (file)
@@ -1,17 +1,15 @@
 from fastapi.responses import JSONResponse
 
-file_exists_response = JSONResponse(
-    content={"message": "The specified filename already exists."},
+title_exists_response = JSONResponse(
+    content={"message": "The specified title already exists."},
     status_code=409,
 )
 
-invalid_filename_response = JSONResponse(
-    content={
-        "message": "The specified filename contains invalid characters."
-    },
+invalid_title_response = JSONResponse(
+    content={"message": "The specified title contains invalid characters."},
     status_code=400,
 )
 
-file_not_found_response = JSONResponse(
-    content={"message": "The specified file cannot be found."}, status_code=404
+note_not_found_response = JSONResponse(
+    content={"message": "The specified note cannot be found."}, status_code=404
 )
index 9e8ca2121992760c0c8ab6fed049d09f6072f0ac..e0db69cc63014c85c4f6c17a5a8aaa74d73ac802 100644 (file)
@@ -11,9 +11,13 @@ from whoosh.index import Index
 from whoosh.qparser import MultifieldParser
 from whoosh.searching import Hit
 
+from helpers import strip_ext
 
-class InvalidFilenameError(Exception):
-    def __init__(self, message="The specified filename is invalid"):
+MARKDOWN_EXT = ".md"
+
+
+class InvalidTitleError(Exception):
+    def __init__(self, message="The specified title is invalid"):
         self.message = message
         super().__init__(self.message)
 
@@ -27,12 +31,12 @@ class IndexSchema(SchemaClass):
 
 class Note:
     def __init__(
-        self, flatnotes: "Flatnotes", filename: str, new: bool = False
+        self, flatnotes: "Flatnotes", title: str, new: bool = False
     ) -> None:
-        if not self._is_valid_filename(filename):
-            raise InvalidFilenameError
         self._flatnotes = flatnotes
-        self._filename = filename
+        self._title = title.strip()
+        if not self._is_valid_title(self._title):
+            raise InvalidTitleError
         if new and os.path.exists(self.filepath):
             raise FileExistsError
         elif new:
@@ -40,11 +44,11 @@ class Note:
 
     @property
     def filepath(self):
-        return os.path.join(self._flatnotes.dir, self._filename)
+        return os.path.join(self._flatnotes.dir, self.filename)
 
     @property
-    def title(self):
-        return os.path.splitext(self._filename)[0]
+    def filename(self):
+        return self._title + MARKDOWN_EXT
 
     @property
     def last_modified(self):
@@ -52,16 +56,18 @@ class Note:
 
     # Editable Properties
     @property
-    def filename(self):
-        return self._filename
-
-    @filename.setter
-    def filename(self, new_filename):
-        if not self._is_valid_filename(new_filename):
-            raise InvalidFilenameError
-        new_filepath = os.path.join(self._flatnotes.dir, new_filename)
+    def title(self):
+        return self._title
+
+    @title.setter
+    def title(self, new_title):
+        if not self._is_valid_title(new_title):
+            raise InvalidTitleError
+        new_filepath = os.path.join(
+            self._flatnotes.dir, new_title + MARKDOWN_EXT
+        )
         os.rename(self.filepath, new_filepath)
-        self._filename = new_filename
+        self._title = new_title
 
     @property
     def content(self):
@@ -79,18 +85,16 @@ class Note:
         os.remove(self.filepath)
 
     # Functions
-    def _is_valid_filename(self, filename: str) -> bool:
-        r"""Return False if the declared filename contains any of the following
+    def _is_valid_title(self, title: str) -> bool:
+        r"""Return False if the declared title contains any of the following
         characters: <>:"/\|?*"""
         invalid_chars = r'<>:"/\|?*'
-        return not any(
-            invalid_char in filename for invalid_char in invalid_chars
-        )
+        return not any(invalid_char in title for invalid_char in invalid_chars)
 
 
 class NoteHit(Note):
     def __init__(self, flatnotes: "Flatnotes", hit: Hit) -> None:
-        super().__init__(flatnotes, hit["filename"])
+        super().__init__(flatnotes, strip_ext(hit["filename"]))
         self.title_highlights = hit.highlights("title", text=self.title)
         self.content_highlights = hit.highlights(
             "content",
@@ -127,7 +131,8 @@ class Flatnotes(object):
         self, writer: writing.IndexWriter, note: Note
     ) -> None:
         """Add a Note object to the index using the given writer. If the
-        filepath already exists in the index an update will be performed instead."""
+        filename already exists in the index an update will be performed
+        instead."""
         writer.update_document(
             filename=note.filename,
             last_modified=note.last_modified,
@@ -136,10 +141,13 @@ class Flatnotes(object):
         )
 
     def get_notes(self) -> List[Note]:
-        """Return a list containing a Note object for every file in the notes directory."""
+        """Return a list containing a Note object for every file in the notes
+        directory."""
         return [
-            Note(self, os.path.split(filepath)[1])
-            for filepath in glob.glob(os.path.join(self.dir, "*.md"))
+            Note(self, strip_ext(os.path.split(filepath)[1]))
+            for filepath in glob.glob(
+                os.path.join(self.dir, "*" + MARKDOWN_EXT)
+            )
         ]
 
     def update_index(self, clean: bool = False) -> None:
@@ -162,7 +170,9 @@ class Flatnotes(object):
                     os.path.getmtime(idx_filepath) != idx_note["last_modified"]
                 ):
                     logging.info(f"'{idx_filename}' updated")
-                    self._add_note_to_index(writer, Note(self, idx_filename))
+                    self._add_note_to_index(
+                        writer, Note(self, strip_ext(idx_filename))
+                    )
                     indexed.add(idx_filename)
                 # Ignore already indexed
                 else:
@@ -176,7 +186,8 @@ class Flatnotes(object):
         self.last_index_update = datetime.now()
 
     def update_index_debounced(self, clean: bool = False) -> None:
-        """Run update_index() but only if it hasn't been run in the last 10 seconds."""
+        """Run update_index() but only if it hasn't been run in the last 10
+        seconds."""
         if (
             self.last_index_update is None
             or (datetime.now() - self.last_index_update).seconds > 10
index 28b6fbf39a3aedb19d40997892ff564f19471208..577b25f58ad0f90be5ebd8aace7293ad87e3b375 100644 (file)
@@ -1,6 +1,12 @@
+import os
+
 from pydantic import BaseModel
 
 
+def strip_ext(filename):
+    return os.path.splitext(filename)[0]
+
+
 def camel_case(snake_case_str: str) -> str:
     """Return the declared snake_case string in camelCase."""
     parts = [part for part in snake_case_str.split("_") if part != ""]
index c1b627c154592ae0fc04e78d708c4f070d09b33b..f04fbeb25d5798caec6cad17470e9ca68ec2a947 100644 (file)
@@ -9,16 +9,16 @@ from auth import (
     validate_token,
 )
 from error_responses import (
-    file_exists_response,
-    file_not_found_response,
-    invalid_filename_response,
+    title_exists_response,
+    note_not_found_response,
+    invalid_title_response,
 )
 from fastapi import Depends, FastAPI, HTTPException
 from fastapi.responses import HTMLResponse
 from fastapi.staticfiles import StaticFiles
 from models import LoginModel, NoteHitModel, NoteModel, NotePatchModel
 
-from flatnotes import Flatnotes, InvalidFilenameError, Note
+from flatnotes import Flatnotes, InvalidTitleError, Note
 
 logging.basicConfig(
     format="%(asctime)s [%(levelname)s]: %(message)s",
@@ -47,8 +47,8 @@ async def token(data: LoginModel):
 @app.get("/")
 @app.get("/login")
 @app.get("/search")
-@app.get("/note/{filename}")
-async def root(filename: str = ""):
+@app.get("/note/{title}")
+async def root(title: str = ""):
     with open("flatnotes/dist/index.html", "r", encoding="utf-8") as f:
         html = f.read()
     return HTMLResponse(content=html)
@@ -58,7 +58,7 @@ async def root(filename: str = ""):
 async def get_notes(
     start: int = 0,
     limit: int = None,
-    sort: Literal["filename", "lastModified"] = "filename",
+    sort: Literal["title", "lastModified"] = "title",
     order: Literal["asc", "desc"] = "asc",
     include_content: bool = False,
     _: str = Depends(validate_token),
@@ -68,7 +68,7 @@ async def get_notes(
     notes.sort(
         key=lambda note: note.last_modified
         if sort == "lastModified"
-        else note.filename,
+        else note.title,
         reverse=order == "desc",
     )
     return [
@@ -81,59 +81,59 @@ async def get_notes(
 async def post_note(data: NoteModel, _: str = Depends(validate_token)):
     """Create a new note."""
     try:
-        note = Note(flatnotes, data.filename, new=True)
+        note = Note(flatnotes, data.title, new=True)
         note.content = data.content
         return NoteModel.dump(note, include_content=True)
-    except InvalidFilenameError:
-        return invalid_filename_response
+    except InvalidTitleError:
+        return invalid_title_response
     except FileExistsError:
-        return file_exists_response
+        return title_exists_response
 
 
-@app.get("/api/notes/{filename}", response_model=NoteModel)
+@app.get("/api/notes/{title}", response_model=NoteModel)
 async def get_note(
-    filename: str,
+    title: str,
     include_content: bool = True,
     _: str = Depends(validate_token),
 ):
     """Get a specific note."""
     try:
-        note = Note(flatnotes, filename)
+        note = Note(flatnotes, title)
         return NoteModel.dump(note, include_content=include_content)
-    except InvalidFilenameError:
-        return invalid_filename_response
+    except InvalidTitleError:
+        return invalid_title_response
     except FileNotFoundError:
-        return file_not_found_response
+        return note_not_found_response
 
 
-@app.patch("/api/notes/{filename}", response_model=NoteModel)
+@app.patch("/api/notes/{title}", response_model=NoteModel)
 async def patch_note(
-    filename: str, new_data: NotePatchModel, _: str = Depends(validate_token)
+    title: str, new_data: NotePatchModel, _: str = Depends(validate_token)
 ):
     try:
-        note = Note(flatnotes, filename)
-        if new_data.new_filename is not None:
-            note.filename = new_data.new_filename
+        note = Note(flatnotes, title)
+        if new_data.new_title is not None:
+            note.title = new_data.new_title
         if new_data.new_content is not None:
             note.content = new_data.new_content
         return NoteModel.dump(note, include_content=True)
-    except InvalidFilenameError:
-        return invalid_filename_response
+    except InvalidTitleError:
+        return invalid_title_response
     except FileExistsError:
-        return file_exists_response
+        return title_exists_response
     except FileNotFoundError:
-        return file_not_found_response
+        return note_not_found_response
 
 
-@app.delete("/api/notes/{filename}")
-async def delete_note(filename: str, _: str = Depends(validate_token)):
+@app.delete("/api/notes/{title}")
+async def delete_note(title: str, _: str = Depends(validate_token)):
     try:
-        note = Note(flatnotes, filename)
+        note = Note(flatnotes, title)
         note.delete()
-    except InvalidFilenameError:
-        return invalid_filename_response
+    except InvalidTitleError:
+        return invalid_title_response
     except FileNotFoundError:
-        return file_not_found_response
+        return note_not_found_response
 
 
 @app.get("/api/search", response_model=List[NoteHitModel])
index 969dd9b9fb8f9d2918601f0116b4343e7a5fe9cd..e2988be72645998f12dd8bcb42b0a4101f57ef0d 100644 (file)
@@ -11,26 +11,26 @@ class LoginModel(CamelCaseBaseModel):
 
 
 class NoteModel(CamelCaseBaseModel):
-    filename: str
+    title: str
     last_modified: Optional[int]
     content: Optional[str]
 
     @classmethod
     def dump(cls, note: Note, include_content: bool = True) -> Dict:
         return {
-            "filename": note.filename,
+            "title": note.title,
             "lastModified": note.last_modified,
             "content": note.content if include_content else None,
         }
 
 
 class NotePatchModel(CamelCaseBaseModel):
-    new_filename: Optional[str]
+    new_title: Optional[str]
     new_content: Optional[str]
 
 
 class NoteHitModel(CamelCaseBaseModel):
-    filename: str
+    title: str
     last_modified: int
     title_highlights: Optional[str]
     content_highlights: Optional[str]
@@ -38,7 +38,7 @@ class NoteHitModel(CamelCaseBaseModel):
     @classmethod
     def dump(self, note_hit: NoteHit) -> Dict:
         return {
-            "filename": note_hit.filename,
+            "title": note_hit.title,
             "lastModified": note_hit.last_modified,
             "titleHighlights": note_hit.title_highlights,
             "contentHighlights": note_hit.content_highlights,
index 29b06044cf6d0af8fe6dfa779aa3a7f05461936a..7a58597952bc7990b23ae68a2651ca667e831f70 100644 (file)
@@ -1,32 +1,20 @@
 import * as constants from "./constants";
 
 class Note {
-  constructor(filename, lastModified, content) {
-    this.filename = filename;
+  constructor(title, lastModified, content) {
+    this.title = title;
     this.lastModified = lastModified;
     this.content = content;
   }
 
-  get title() {
-    if (this.filename) {
-      return this.filename.substring(0, this.filename.lastIndexOf("."));
-    } else {
-      return null;
-    }
-  }
-
-  get ext() {
-    return this.filename.substring(this.filename.lastIndexOf(".") + 1);
-  }
-
   get href() {
     return `/${constants.basePaths.note}/${this.title}`;
   }
 }
 
 class SearchResult extends Note {
-  constructor(filename, lastModified, titleHighlights, contentHighlights) {
-    super(filename, lastModified);
+  constructor(title, lastModified, titleHighlights, contentHighlights) {
+    super(title, lastModified);
     this.titleHighlights = titleHighlights;
     this.contentHighlights = contentHighlights;
   }
index dd955527be7e678c293b690b60e51a9e4d145de0..028ba302be08043bfed30bf3561911f3ff069a65 100644 (file)
@@ -145,7 +145,7 @@ export default {
           response.data.forEach(function(result) {
             parent.searchResults.push(
               new SearchResult(
-                result.filename,
+                result.title,
                 result.lastModified,
                 result.titleHighlights,
                 result.contentHighlights
@@ -162,25 +162,25 @@ export default {
     },
 
     getContentForEditor: function() {
-      let draftContent = localStorage.getItem(this.currentNote.filename);
+      let draftContent = localStorage.getItem(this.currentNote.title);
       if (draftContent) {
         if (confirm("Do you want to resume the saved draft?")) {
           return draftContent;
         } else {
-          localStorage.removeItem(this.currentNote.filename);
+          localStorage.removeItem(this.currentNote.title);
         }
       }
       return this.currentNote.content;
     },
 
-    loadNote: function(filename) {
+    loadNote: function(title) {
       let parent = this;
       this.noteLoadFailed = false;
       api
-        .get(`/api/notes/${filename}.${constants.markdownExt}`)
+        .get(`/api/notes/${title}`)
         .then(function(response) {
           parent.currentNote = new Note(
-            response.data.filename,
+            response.data.title,
             response.data.lastModified,
             response.data.content
           );
@@ -208,7 +208,7 @@ export default {
       // To Edit Mode
       if (this.editMode == false) {
         this.titleInput = this.currentNote.title;
-        let draftContent = localStorage.getItem(this.currentNote.filename);
+        let draftContent = localStorage.getItem(this.currentNote.title);
 
         if (draftContent) {
           this.$bvModal
@@ -227,7 +227,7 @@ export default {
                 parent.initialContent = draftContent;
               } else {
                 parent.initialContent = parent.currentNote.content;
-                localStorage.removeItem(parent.currentNote.filename);
+                localStorage.removeItem(parent.currentNote.title);
               }
               parent.editMode = !parent.editMode;
             });
@@ -266,10 +266,10 @@ export default {
     },
 
     saveDraft: function() {
-      localStorage.setItem(this.currentNote.filename, this.getEditorContent());
+      localStorage.setItem(this.currentNote.title, this.getEditorContent());
     },
 
-    existingFilenameToast: function() {
+    existingTitleToast: function() {
       this.$bvToast.toast(
         "A note with this title already exists. Please try again with a new title.",
         {
@@ -300,7 +300,7 @@ export default {
       if (this.currentNote.lastModified == null) {
         api
           .post(`/api/notes`, {
-            filename: `${this.titleInput}.${constants.markdownExt}`,
+            title: this.titleInput,
             content: newContent,
           })
           .then(this.saveNoteResponseHandler)
@@ -311,7 +311,7 @@ export default {
               typeof error.response !== "undefined" &&
               error.response.status == 409
             ) {
-              parent.existingFilenameToast();
+              parent.existingTitleToast();
             } else {
               parent.unhandledServerErrorToast();
             }
@@ -324,8 +324,8 @@ export default {
         this.titleInput != this.currentNote.title
       ) {
         api
-          .patch(`/api/notes/${this.currentNote.filename}`, {
-            newFilename: `${this.titleInput}.${this.currentNote.ext}`,
+          .patch(`/api/notes/${this.currentNote.title}`, {
+            newTitle: this.titleInput,
             newContent: newContent,
           })
           .then(this.saveNoteResponseHandler)
@@ -336,7 +336,7 @@ export default {
               typeof error.response !== "undefined" &&
               error.response.status == 409
             ) {
-              parent.existingFilenameToast();
+              parent.existingTitleToast();
             } else {
               parent.unhandledServerErrorToast();
             }
@@ -351,9 +351,9 @@ export default {
     },
 
     saveNoteResponseHandler: function(response) {
-      localStorage.removeItem(this.currentNote.filename);
+      localStorage.removeItem(this.currentNote.title);
       this.currentNote = new Note(
-        response.data.filename,
+        response.data.title,
         response.data.lastModified,
         response.data.content
       );
@@ -371,7 +371,7 @@ export default {
     },
 
     cancelNote: function() {
-      localStorage.removeItem(this.currentNote.filename);
+      localStorage.removeItem(this.currentNote.title);
       if (this.currentNote.lastModified == null) {
         // Cancelling a new note
         this.currentNote = null;
@@ -395,7 +395,7 @@ export default {
         .then(function(response) {
           if (response == true) {
             api
-              .delete(`/api/notes/${parent.currentNote.filename}`)
+              .delete(`/api/notes/${parent.currentNote.title}`)
               .then(function() {
                 parent.navigate("/");
                 parent.$bvToast.toast("Note deleted ✓", {
index 141befe31be1b7c52b16452c4e2862b1a792e94e..54867e1dfad7cb4d39b9855ed3162c7b888b96b2 100644 (file)
 
         <!-- Search Results Loaded -->
         <div v-else>
-          <div
-            v-for="result in searchResults"
-            :key="result.filename"
-            class="mb-5"
-          >
+          <div v-for="result in searchResults" :key="result.title" class="mb-5">
             <p class="h5 text-center clickable-link">
               <a
                 v-html="result.titleHighlightsOrTitle"
index 17d1be8e3c00e67a9059a16484e7187a65b36133..86c5a66d27e0f9240d55257175f5e6417d9c442f 100644 (file)
@@ -4,7 +4,7 @@
 
     <!-- Loading -->
     <div v-if="notes == null">
-      <loading-indicator :failed="loadingFailed"/>
+      <loading-indicator :failed="loadingFailed" />
     </div>
 
     <!-- No Notes -->
@@ -17,7 +17,7 @@
       <p
         v-for="note in notes"
         class="text-center clickable-link mb-2"
-        :key="note.filename"
+        :key="note.title"
       >
         <a :href="note.href" @click.prevent="openNote(note.href, $event)">{{
           note.title
@@ -46,7 +46,7 @@ export default {
   },
 
   methods: {
-    getNotes: function (limit = null, sort = "filename", order = "asc") {
+    getNotes: function (limit = null, sort = "title", order = "asc") {
       let parent = this;
       api
         .get("/api/notes", {
@@ -55,7 +55,7 @@ export default {
         .then(function (response) {
           parent.notes = [];
           response.data.forEach(function (note) {
-            parent.notes.push(new Note(note.filename, note.lastModified));
+            parent.notes.push(new Note(note.title, note.lastModified));
           });
         })
         .catch(function (error) {
index 4a53c8b4bd5477284610ced1752955639d176393..8f754229ca99c6ad6c3f1f751753eda8f8546f77 100644 (file)
@@ -1,7 +1,5 @@
 import codeSyntaxHighlight from "@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight-all.js";
 
-export const markdownExt = "md";
-
 // Base Paths
 export const basePaths = { login: "login", note: "note", search: "search" };
 
git clone https://git.99rst.org/PROJECT