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