commit b928176bbd9081021cd412484ed689b35b3f3982
Author: Aleksandrs Korņijenko <aleksandrs.kornienko@protonmail.com>
Date:   Sun Apr 6 12:20:10 2025 +0300

    Initial commit

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..231d96b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,178 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+#   For a library or package, you might want to ignore these files since the code is
+#   intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# UV
+#   Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
+#   This is especially recommended for binary packages to ensure reproducibility, and is more
+#   commonly ignored for libraries.
+#uv.lock
+
+# poetry
+#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+#   This is especially recommended for binary packages to ensure reproducibility, and is more
+#   commonly ignored for libraries.
+#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+#   in version control.
+#   https://pdm.fming.dev/latest/usage/project/#working-with-version-control
+.pdm.toml
+.pdm-python
+.pdm-build/
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+#  and can be added to the global gitignore or merged into this file.  For a more nuclear
+#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+# Ruff stuff:
+.ruff_cache/
+
+# PyPI configuration file
+.pypirc
+
+
+# custom 
+db.json
\ No newline at end of file
diff --git a/Specifikācijas_piemērs.pdf b/Specifikācijas_piemērs.pdf
new file mode 100644
index 0000000..ab304a2
Binary files /dev/null and b/Specifikācijas_piemērs.pdf differ
diff --git a/app.py b/app.py
new file mode 100644
index 0000000..4f8a2c2
--- /dev/null
+++ b/app.py
@@ -0,0 +1,139 @@
+from flask import Flask, render_template, request, redirect, url_for
+from jsondb import JSONDB
+from utils import get_now_datetime
+
+app = Flask(__name__)
+
+db = JSONDB("db.json")
+db.ensure("lists", "object")
+
+
+@app.route("/")
+def index():
+    lists = db.get("lists")
+    return render_template("index.html", lists=lists.keys())
+
+
+@app.route("/lists/add", methods=["GET", "POST"])
+def add_list():
+    if request.method == "GET":
+        return render_template("add_list.html")
+    else:
+        name = request.form.get("name")
+
+        if not name:
+            return "Nosaukums nav definēts!"
+
+        lists = db.get("lists")
+
+        if name in lists.keys():
+            return "Šāds piezīmju saraksts jau eksistē!"
+
+        lists.add(name, [])
+        return redirect(url_for("index"))
+
+
+@app.route("/lists/<name>/delete")
+def delete_list(name):
+    lists = db.get("lists")
+    
+    if name not in lists.keys():
+        return "Šads piezīmju saraksts neeksistē!"
+    
+    lists.remove(name)
+    return redirect(url_for("index"))
+
+
+@app.route("/lists/<name>")
+def edit_list(name):
+    lists = db.get("lists")
+
+    if name not in lists.keys():
+        return "Šads piezīmju saraksts neeksistē!"
+    
+    note_list = lists.get(name)
+
+    # Get all tags
+    tags = []
+    for note in note_list._array:
+        tags += note["tags"]
+    tags = list(set(tags))
+
+    # Get search and tag filter
+    search = request.args.get('s')
+    tag = request.args.get('t')
+
+    if search:
+        notes = note_list.get_by_occurence_in_string(["name", "content"], search)
+    elif tag:
+        notes = note_list.get_by_occurence_in_array("tags", tag)
+    else:
+        notes = note_list.all()
+
+    return render_template("edit_list.html", note_list=notes, note_list_name=name, s=search, t=tag, tags=tags)
+
+
+@app.route("/lists/<list_name>/add", methods=["GET", "POST"])
+@app.route("/lists/<list_name>/notes/<note_name>/edit", methods=["GET", "POST"])
+def add_note(list_name, note_name=None):
+    edit = not note_name == None
+
+    lists = db.get("lists")
+
+    if list_name not in lists.keys():
+        return "Šads piezīmju saraksts neeksistē!"
+    
+    note_list = lists.get(list_name)
+
+    note = {}
+    if edit:
+        note_id, note = note_list.get_by_key("name", note_name)[0]
+    
+    if request.method == "GET":
+        return render_template("add_note.html", list_name=list_name, note=note, edit=edit)
+    else:
+        name = request.form.get("name")
+        tags_str = request.form.get("tags")
+        content = request.form.get("content")
+
+        if not name or not content:
+            return "Piezīmei nav satura vai nosaukuma!"
+        
+        if not edit:
+            same_name_notes = note_list.get_by_key("name", name)
+            if len(same_name_notes) > 0:
+                return "Piezīme ar šādu nosaukumu jau eksistē!"
+        
+        tags = [] if not tags_str else tags_str.split("|")
+
+        new_note = {
+            "name": name,
+            "datetime": get_now_datetime(),
+            "tags": tags,
+            "content": content
+        }
+
+        if edit:
+            note_list.change_by_index(note_id, new_note)
+        else:
+            note_list.add(new_note)
+        
+        return redirect(url_for("edit_list", name=list_name))
+
+
+@app.route("/list/<list_name>/notes/<note_name>/delete")
+def delete_note(list_name, note_name):
+    lists = db.get("lists")
+
+    if list_name not in lists.keys():
+        return "Šads piezīmju saraksts neeksistē!"
+    
+    note_list = lists.get(list_name)
+    notes = note_list.get_by_key("name", note_name)
+
+    if len(notes) == 0:
+        return "Piezīme ar šādu nosaukumu neeksistē!"
+    
+    note_index = notes[0][0]
+    note_list.remove_by_index(note_index)
+    return redirect(url_for("edit_list", name=list_name))
diff --git a/jsondb.py b/jsondb.py
new file mode 100644
index 0000000..b60ec73
--- /dev/null
+++ b/jsondb.py
@@ -0,0 +1,161 @@
+from typing import Any
+from json import loads, dumps, decoder
+
+class JSONDBArray:
+    def __init__(self, array: list, db):
+        self._array = array
+        self._db = db
+    
+    def all(self) -> list:
+        return [(a[0], a[1]) for a in enumerate(self._array)]
+    
+    def get_by_key(self, key: str, value):
+        results = []
+
+        for i, obj in enumerate(self._array):
+            if not isinstance(obj, dict):
+                continue
+
+            if not key in obj:
+                continue
+
+            if obj[key] == value:
+                results.append((i, obj))
+        
+        return results
+    
+    def get_by_occurence_in_array(self, key: str, value) -> list:
+        results = []
+
+        for i, obj in enumerate(self._array):
+            if not isinstance(obj, dict):
+                continue
+
+            if not key in obj:
+                continue
+
+            if value in obj[key]:
+                results.append((i, obj))
+        
+        return results
+    
+    def get_by_occurence_in_string(self, keys: list[str], value: str) -> list:
+        results = []
+
+        for i, obj in enumerate(self._array):
+            if not isinstance(obj, dict):
+                continue
+            
+            for key in keys:
+                if not key in obj:
+                    continue
+
+                if value.lower() in obj[key].lower():
+                    results.append((i, obj))
+                    break
+        
+        return results
+    
+    def change_by_index(self, index: int, new):
+        self._array[index] = new
+        self._db._write()
+    
+    def add(self, element):
+        self._array.append(element)
+        self._db._write()
+    
+    def remove_by_index(self, index: int):
+        self._array.pop(index)
+        self._db._write()
+
+
+class JSONDBObject:
+    def __init__(self, obj: dict, db):
+        self._obj = obj
+        self._db = db
+    
+    def get(self, key: str) -> JSONDBArray | Any:
+        val = self._obj.get(key)
+
+        if isinstance(val, list):
+            return JSONDBArray(val, self._db)
+        else:
+            return val
+    
+    def keys(self) -> list:
+        return list(self._obj.keys())
+    
+    def add(self, key: str, value):
+        self._obj[key] = value
+        self._db._write()
+    
+    def remove(self, key: str):
+        del self._obj[key]
+        self._db._write()
+
+
+class JSONDB:
+    """
+    JSONDB galvenā klase.
+    """
+
+    def __init__(self, filename: str) -> None:
+        self._filename = filename
+        self._db = {}
+
+        # Create a db file if does not exist
+        f = open(self._filename, "a")
+        f.close()
+
+        self._read()
+
+    def _read(self) -> None:
+        f = open(self._filename, "r+", encoding="utf-8")
+        fc = f.read()
+        f.close()
+        
+        try:
+            self._db = loads(fc)
+        except decoder.JSONDecodeError:
+            if len(fc) == 0:
+                self._db = {}
+            else:
+                raise Exception("DB file is not correct")
+            
+
+    def _write(self) -> None:
+        fc = dumps(self._db)
+
+        f = open(self._filename, "w", encoding="utf-8")
+        f.write(fc)
+        f.close()
+
+    def ensure(self, key: str, type: str) -> bool:
+        """
+        Pārliecinās, ka attiecīgā sadaļa eksistē JSON datubāzē.
+        """
+        if key in self._db:
+            return True
+
+        if type == "array":
+            self._db[key] = []
+        elif type == "object":
+            self._db[key] = {}
+        else:
+            raise Exception("Unknown ensure type")
+        
+        self._write()
+
+    def get(self, key: str) -> JSONDBObject | JSONDBArray | None:
+        """
+        Lasa JSON datubāzes sadaļu
+        """
+        val = self._db.get(key)
+
+        if val == None:
+            return None
+
+        if isinstance(val, list):
+            return JSONDBArray(val, self)
+        elif isinstance(val, object):
+            return JSONDBObject(val, self)
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..da3d9cb
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,17 @@
+# Notes
+
+Piezīmju lietotne, kas ir realizēta pēc [piemēra specifikācijas](Specifikācijas_piemērs.pdf).
+
+## Lietotnes palaišana
+
+1. Klonējiet repozitoriju savā izvēlētajā mapē.
+2. Atveriet konsoli repozitorija mapē.
+3. Instalējiet nepieciešamas bibliotēkas:
+    ```sh
+    pip install -r requirements.txt
+    ```
+4. Palaidiet lietotni:
+    ```sh
+    flask --app app.py run
+    ```
+
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..1912dec
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+flask==3.1.0
+jinja2==3.1.5
\ No newline at end of file
diff --git a/static/scripts.js b/static/scripts.js
new file mode 100644
index 0000000..4ab437d
--- /dev/null
+++ b/static/scripts.js
@@ -0,0 +1,5 @@
+function deletePrompt(text, url) {
+    if (confirm(text)) {
+        location.href = url
+    }
+}
\ No newline at end of file
diff --git a/templates/add_list.html b/templates/add_list.html
new file mode 100644
index 0000000..4b98f26
--- /dev/null
+++ b/templates/add_list.html
@@ -0,0 +1,15 @@
+{% extends "base.html" %}
+
+{% block title %}Pievienot piezīmju sarakstu{% endblock %}
+
+{% block content %}
+<h1 class="mb-4">Pievienot piezīmju sarakstu</h1>
+<form method="post">
+    <div class="mb-3">
+        <label for="noteListName" class="form-label">Nosaukums</label>
+        <input type="text" class="form-control" id="noteListName" name="name" required>
+    </div>
+    <button type="submit" class="btn btn-primary">Pievienot</button>
+    <a href="{{ url_for('index') }}" class="btn btn-secondary">Atpakaļ</a>
+</form>
+{% endblock %}
diff --git a/templates/add_note.html b/templates/add_note.html
new file mode 100644
index 0000000..24e30fc
--- /dev/null
+++ b/templates/add_note.html
@@ -0,0 +1,23 @@
+{% extends "base.html" %}
+
+{% block title %}{% if edit %}Rediģēt{% else %}Pievienot{% endif %} jaunu piezīmi{% endblock %}
+
+{% block content %}
+<h1 class="mb-4">{% if edit %}Rediģēt{% else %}Pievienot{% endif %} jaunu piezīmi</h1>
+<form method="post">
+    <div class="mb-3">
+        <label for="noteTitle" class="form-label">Nosaukums</label>
+        <input type="text" class="form-control" value="{{ note.name }}" name="name" id="noteTitle" required>
+    </div>
+    <div class="mb-3">
+        <label for="noteTags" class="form-label">Birkas</label>
+        <input type="text" class="form-control" value="{{ '|'.join(note.tags) }}" id="noteTags" name="tags" placeholder="Birkas raksta, atdalot tās ar simbolu '|'!">
+    </div>
+    <div class="mb-3">
+        <label for="noteContent" class="form-label">Saturs</label>
+        <textarea class="form-control" id="noteContent" name="content" rows="5" required>{{ note.content }}</textarea>
+    </div>
+    <button type="submit" class="btn btn-success">{% if edit %}Rediģēt{% else %}Pievienot{% endif %} piezīmi</button>
+    <a href="{{ url_for('edit_list', name=list_name) }}" class="btn btn-secondary">Atcelt</a>
+</form>
+{% endblock %}
\ No newline at end of file
diff --git a/templates/base.html b/templates/base.html
new file mode 100644
index 0000000..1dfa08f
--- /dev/null
+++ b/templates/base.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<html lang="lv">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>{% block title %}Piezīmju lietotne{% endblock %}</title>
+    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/css/bootstrap.min.css" rel="stylesheet">
+    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/js/bootstrap.bundle.min.js"></script>
+    <script src="{{ url_for('static', filename='scripts.js') }}"></script>
+</head>
+<body>
+    <div class="container mt-5">
+        {% block content %}
+        {% endblock %}
+    </div>
+</body>
+</html>
\ No newline at end of file
diff --git a/templates/edit_list.html b/templates/edit_list.html
new file mode 100644
index 0000000..f70d6c8
--- /dev/null
+++ b/templates/edit_list.html
@@ -0,0 +1,66 @@
+{% extends "base.html" %}
+
+{% block title %}{{ note_list_name }}{% endblock %}
+
+{% block content %}
+<h1 class="mb-4">Piezīmju saraksts "{{ note_list_name }}"</h1>
+<a href="{{ url_for('index') }}">Atpakaļ uz piezīmju sarakstiem</a>
+
+<hr>
+
+<!-- Birku filtrēšana -->
+<h2 class="mt-4">Filtrēt pēc birkām</h2>
+<div class="mb-3">
+    {% for tag in tags %}
+        <a 
+            class="btn {% if tag == t %}btn-outline-primary{% else %}btn-outline-secondary{% endif %} btn-sm"
+            href="{{ url_for('edit_list', name=note_list_name) }}?t={{ tag }}"
+        >{{ tag }}</a>
+    {% endfor %}
+    {% if t %}
+    <a class="btn btn-danger btn-sm" href="{{ url_for('edit_list', name=note_list_name) }}">Atcelt filtru pēc birkas</a>
+    {% endif %}
+</div>
+
+<!-- Meklēšanas forma -->
+<h2 class="mt-4">Meklēt piezīmes</h2>
+<form class="mb-3" method="get">
+    <div class="input-group">
+        <input type="text" class="form-control" name="s" placeholder="Ievadiet meklējamo tekstu" value="{{ s|default('', true) }}">
+        <button class="btn btn-primary" type="submit">Meklēt</button>
+    </div>
+</form>
+
+<hr />
+
+<!-- Piezīmju saraksts -->
+<h2 class="mt-4">Piezīmes</h2>
+<a class="btn btn-success mb-3" href="{{ url_for('add_note', list_name=note_list_name) }}">Pievienot piezīmi</a>
+<ul class="list-group">
+    {% for index, note in note_list %}
+        <li class="list-group-item d-flex justify-content-between align-items-center">
+            <div>
+                <h5>{{ note.name }}</h5>
+                <small>
+                    {{ note.datetime.replace('T', ' ') }} 
+                    {% if note.tags %}•{% endif %}
+                    {% for tag in note.tags  %}
+                        <span class="badge bg-secondary">{{ tag }}</span>
+                    {% endfor %}
+                </small>
+                <p>{{ note.content.replace('\n', '<br>')|safe }}</p>
+            </div>
+            <div>
+                <a 
+                    class="btn btn-warning btn-sm"
+                    href="{{ url_for('add_note', list_name=note_list_name, note_name=note.name) }}"
+                >Rediģēt</a>
+                <button 
+                    onclick="deletePrompt('Vai Jūs tiešām gribat dzēst šo piezīmi?', `{{ url_for('delete_note', list_name=note_list_name, note_name=note.name) }}`)" 
+                    class="btn btn-danger btn-sm"
+                >Dzēst</button>
+            </div>
+        </li>
+    {% endfor %}
+</ul>
+{% endblock %}
diff --git a/templates/index.html b/templates/index.html
new file mode 100644
index 0000000..1e22d14
--- /dev/null
+++ b/templates/index.html
@@ -0,0 +1,21 @@
+{% extends "base.html" %}
+
+{% block content %}
+<h1 class="mb-4">Piezīmju saraksti</h1>
+
+<a class="btn btn-primary mb-3" href="{{ url_for('add_list') }}">Pievienot piezīmju sarakstu</a>
+
+<ul class="list-group">
+    {% for list in lists %}
+    <li class="list-group-item d-flex justify-content-between align-items-center">
+        <a href="{{ url_for('edit_list', name=list) }}">{{ list }}</a>
+        <div>
+            <button 
+                class="btn btn-danger btn-sm" 
+                onclick="deletePrompt('Vai Jūs tiešām gribat dzēst šo piezīmju sarakstu?', `{{ url_for('delete_list', name=list) }}`)"
+            >Dzēst</button>
+        </div>
+    </li>
+    {% endfor %}
+</ul>
+{% endblock %}
diff --git a/utils.py b/utils.py
new file mode 100644
index 0000000..530d098
--- /dev/null
+++ b/utils.py
@@ -0,0 +1,6 @@
+from datetime import datetime
+
+def get_now_datetime():
+    dt = datetime.now()
+    dt = dt.replace(microsecond=0)
+    return dt.isoformat()
\ No newline at end of file