Skip to content
This repository was archived by the owner on Feb 21, 2025. It is now read-only.

Commit 778262d

Browse files
authored
Issue #46: Done in PR #47
Реліз
2 parents c93876a + 1d7a7c7 commit 778262d

File tree

115 files changed

+3332
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

115 files changed

+3332
-0
lines changed

.env.example

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
PROJECT_NAME=binova-personal-assistant
2+
3+
DJANGO_SECRET=
4+
DJANGO_ALLOWED_HOSTS=
5+
DJANGO_DEBUG=
6+
7+
DJANGO_EMAIL_HOST=
8+
DJANGO_EMAIL_PORT=
9+
DJANGO_EMAIL_USER=
10+
DJANGO_EMAIL_PASSWORD=
11+
12+
DATABASE_TAG=16.4-alpine3.20
13+
DATABASE_HOST=
14+
DATABASE_PORT=
15+
DATABASE_ENGINE=
16+
DATABASE_OPTIONS=
17+
DATABASE_NAME=
18+
DATABASE_USER=
19+
DATABASE_PASSWORD=
20+
21+
CLOUDINARY_CLOUD_NAME=
22+
CLOUDINARY_API_KEY=
23+
CLOUDINARY_API_SECRET=

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.env
2+
.venv
3+
poetry.lock
4+
db.sqlite3
5+
__pycache__
6+
personal_assistant/static

README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,69 @@
1+
```
2+
____ _ _ _ _ _
3+
| _ \ ___ _ __ ___ ___ _ __ __ _| | / \ ___ ___(_)___| |_ __ _ _ __ | |_
4+
| |_) / _ \ '__/ __|/ _ \| '_ \ / _` | | / _ \ / __/ __| / __| __/ _` | '_ \| __|
5+
| __/ __/ | \__ \ (_) | | | | (_| | | / ___ \\__ \__ \ \__ \ || (_| | | | | |_
6+
|_| \___|_| |___/\___/|_| |_|\__,_|_| /_/ \_\___/___/_|___/\__\__,_|_| |_|\__|
7+
```
8+
19
# Personal Assistant
10+
11+
This web application offers a comprehensive suite of features for managing
12+
contacts, notes, and files, all integrated with a powerful tagging system for
13+
enhanced organization and search capabilities. It also provides up-to-date news
14+
and currency exchange rates, ensuring users have access to real-time
15+
information. With robust user authentication and personalized data access, the
16+
application ensures a secure and customized experience, while utilizing cloud
17+
storage and caching for optimal performance.
18+
19+
20+
## Features
21+
22+
- **Contacts:** Allows users to manage their contact list with CRUD operations,
23+
including organizing by birthdays and searching by name.
24+
25+
- **Tags:** Enables users to create and delete tags, with restrictions on
26+
deletion if the tag is in use, and shared across all users.
27+
28+
- **Notes:** Provides a simple note-taking feature with title and description,
29+
enhanced by tag-based categorization and advanced search functionality.
30+
31+
- **Files:** Allows file uploads and viewing, securely storing content in the
32+
cloud and enabling filtering and sorting via a tag-like system.
33+
34+
- **News:** Displays global news and currency exchange rates, with real-time
35+
data and a caching system for faster load times, including a
36+
superuser-controlled data sync.
37+
38+
- **Users:** Manages user authentication, including registration, login, and
39+
password recovery, ensuring personalized access to data and features while
40+
protecting against spam accounts.
41+
42+
43+
## Installation
44+
45+
**Note:** The Docker command is optional since the project can work with
46+
`SQLite` when environment variables for `PostgreSQL` are not defined.
47+
48+
```bash
49+
$ git clone https://github.com/BIN0VA/Personal-Assistant.git
50+
$ cd Personal-Assistant
51+
$ docker compose up -d
52+
$ poetry shell
53+
$ poetry install
54+
$ cd personal_assistant
55+
$ python manage.py migrate
56+
$ python manage.py createsuperuser
57+
```
58+
59+
60+
## Usage
61+
62+
```bash
63+
$ docker compose up -d
64+
$ poetry shell
65+
$ cd personal_assistant
66+
$ python manage.py runserver
67+
```
68+
69+
Go to http://localhost:8000.

compose.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
services:
2+
db:
3+
image: "postgres:${DATABASE_TAG}"
4+
container_name: "${PROJECT_NAME}-database"
5+
environment:
6+
POSTGRES_DB: $DATABASE_NAME
7+
POSTGRES_USER: $DATABASE_USER
8+
POSTGRES_PASSWORD: $DATABASE_PASSWORD
9+
ports:
10+
- "${DATABASE_PORT}:5432"

personal_assistant/manage.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/usr/bin/env python
2+
"""Django's command-line utility for administrative tasks."""
3+
from os import environ
4+
from sys import argv
5+
6+
7+
def main() -> None:
8+
"""Run administrative tasks."""
9+
environ.setdefault('DJANGO_SETTINGS_MODULE', 'personal_assistant.settings')
10+
11+
try:
12+
from django.core.management import execute_from_command_line
13+
except ImportError as exc:
14+
raise ImportError(
15+
"Couldn't import Django. Are you sure it's installed and "
16+
"available on your PYTHONPATH environment variable? Did you "
17+
"forget to activate a virtual environment?"
18+
) from exc
19+
20+
execute_from_command_line(argv)
21+
22+
23+
if __name__ == '__main__':
24+
main()

personal_assistant/pa_contacts/__init__.py

Whitespace-only changes.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from django.contrib import admin
2+
from .models import Contact
3+
4+
5+
admin.site.register(Contact)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from django.apps import AppConfig
2+
3+
4+
class ContactsConfig(AppConfig):
5+
default_auto_field = 'django.db.models.BigAutoField'
6+
name = 'pa_contacts'
7+
verbose_name = 'Contacts'
8+
description = '''Allows users to manage their contact list with CRUD operations, including organizing by birthdays and searching by name.
9+
This web application is designed to manage contacts and perform full CRUD (Create, Read, Update, Delete) operations on them. Each contact entry contains several fields: name, physical address, email address, phone number, and date of birth. A key feature of the application is a dedicated section that displays upcoming birthdays based on the date of birth field. Users can view birthday notifications for contacts over three selectable timeframes: the next week, the next month, or the next three months.
10+
Additionally, the application provides a search functionality, allowing users to quickly locate specific contacts by searching for their names. This feature ensures efficient navigation through potentially large contact lists and enhances user experience by streamlining access to contact information.'''
11+
icon = 'person-vcard-fill'
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
from datetime import datetime
2+
from re import sub
3+
4+
from django.forms import ModelForm, CharField, EmailField, TextInput, \
5+
DateField, DateInput
6+
from django.core.exceptions import ValidationError
7+
from phonenumbers import parse, is_valid_number, NumberParseException, \
8+
format_number, PhoneNumberFormat
9+
10+
from .models import Contact
11+
12+
13+
PHONE = '+380776665544'
14+
15+
16+
class ContactsForm(ModelForm):
17+
current_year = datetime.now().year
18+
19+
name = CharField(
20+
min_length=1,
21+
max_length=50,
22+
required=True,
23+
widget=TextInput({'class': 'form-control'}),
24+
)
25+
26+
address = CharField(
27+
min_length=10,
28+
max_length=150,
29+
required=False,
30+
widget=TextInput({'class': 'form-control'}),
31+
)
32+
33+
phone = CharField(
34+
min_length=10,
35+
max_length=20,
36+
required=True,
37+
widget=TextInput({'class': 'form-control', 'placeholder': PHONE}),
38+
)
39+
40+
email = EmailField(
41+
min_length=5,
42+
max_length=50,
43+
required=False,
44+
widget=TextInput({
45+
'class': 'form-control',
46+
'type': 'email',
47+
'id': 'email',
48+
'name': 'email',
49+
'placeholder': 'example@example.com',
50+
}),
51+
)
52+
53+
birthday = DateField(
54+
required=False,
55+
widget=DateInput({'class': 'form-control', 'type': 'date'})
56+
)
57+
58+
class Meta:
59+
model = Contact
60+
fields = ('name', 'address', 'phone', 'email', 'birthday')
61+
62+
def __init__(self, *args, **kwargs):
63+
self.user = kwargs.pop('user', None)
64+
65+
super().__init__(*args, **kwargs)
66+
67+
for field in self.fields:
68+
if self.errors.get(field):
69+
self.fields[field].widget.attrs['class'] += ' is-invalid'
70+
else:
71+
self.fields[field].widget.attrs['class'] += ' form-control'
72+
73+
def clean_phone(self):
74+
if phone := self.cleaned_data.get('phone'):
75+
phone = sub(r'(?<!^)\D+', '', phone)
76+
77+
incorrect = ValidationError(
78+
'Please enter your phone number in the international format '
79+
f'(e.g., {PHONE}).',
80+
)
81+
82+
try:
83+
if (
84+
Contact.objects
85+
.filter(user=self.user, phone=phone)
86+
.exclude(id=self.instance.id if self.instance else None)
87+
.exists()
88+
):
89+
raise ValidationError(
90+
'A contact with this phone number already exists.',
91+
)
92+
93+
parsed = parse(phone, None if phone.startswith('+') else 'UA')
94+
95+
if not is_valid_number(parsed):
96+
raise incorrect
97+
98+
return format_number(parsed, PhoneNumberFormat.E164)
99+
100+
except NumberParseException:
101+
raise incorrect
102+
103+
return phone
104+
105+
def clean_email(self):
106+
if (
107+
(email := self.cleaned_data.get('email')) and
108+
109+
Contact.objects
110+
.filter(user=self.user, email=email)
111+
.exclude(id=self.instance.id if self.instance else None)
112+
.exists()
113+
):
114+
raise ValidationError('A contact with this e-mail already exists.')
115+
116+
return email
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Generated by Django 5.1.1 on 2024-10-10 13:06
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
initial = True
9+
10+
dependencies = [
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name='Contact',
16+
fields=[
17+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18+
('name', models.CharField(max_length=50)),
19+
('address', models.CharField(max_length=150, null=True)),
20+
('phone', models.CharField(max_length=12, unique=True)),
21+
('email', models.EmailField(blank=True, max_length=50, null=True, unique=True)),
22+
('birthday', models.DateField(null=True)),
23+
],
24+
),
25+
]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.1.2 on 2024-10-11 16:03
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('pa_contacts', '0001_initial'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='contact',
15+
name='phone',
16+
field=models.CharField(max_length=20, unique=True),
17+
),
18+
]
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Generated by Django 5.1.2 on 2024-10-12 12:51
2+
3+
import django.db.models.deletion
4+
from django.conf import settings
5+
from django.db import migrations, models
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('pa_contacts', '0002_alter_contact_phone'),
12+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
13+
]
14+
15+
operations = [
16+
migrations.AddField(
17+
model_name='contact',
18+
name='user',
19+
field=models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, related_name='contacts', to=settings.AUTH_USER_MODEL),
20+
preserve_default=False,
21+
),
22+
migrations.AlterField(
23+
model_name='contact',
24+
name='email',
25+
field=models.EmailField(blank=True, max_length=50, null=True),
26+
),
27+
migrations.AlterField(
28+
model_name='contact',
29+
name='phone',
30+
field=models.CharField(max_length=20),
31+
),
32+
migrations.AddConstraint(
33+
model_name='contact',
34+
constraint=models.UniqueConstraint(fields=('user', 'phone'), name='unique_phone_per_user'),
35+
),
36+
migrations.AddConstraint(
37+
model_name='contact',
38+
constraint=models.UniqueConstraint(fields=('user', 'email'), name='unique_email_per_user'),
39+
),
40+
]

personal_assistant/pa_contacts/migrations/__init__.py

Whitespace-only changes.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from django.contrib.auth.models import User
2+
from django.db.models import (
3+
CASCADE,
4+
CharField,
5+
DateField,
6+
EmailField,
7+
ForeignKey,
8+
Model,
9+
UniqueConstraint,
10+
)
11+
12+
13+
class Contact(Model):
14+
name = CharField(max_length=50, null=False)
15+
address = CharField(max_length=150, null=True)
16+
phone = CharField(max_length=20, null=False)
17+
email = EmailField(max_length=50, null=True, blank=True)
18+
birthday = DateField(null=True)
19+
20+
user = ForeignKey(User, CASCADE, related_name='contacts')
21+
22+
class Meta:
23+
constraints = [
24+
UniqueConstraint(
25+
fields=['user', 'phone'],
26+
name='unique_phone_per_user',
27+
),
28+
UniqueConstraint(
29+
fields=['user', 'email'],
30+
name='unique_email_per_user',
31+
),
32+
]
33+
34+
def save(self, *args, **kwargs):
35+
if self.email == '':
36+
self.email = None
37+
38+
super().save(*args, **kwargs)
39+
40+
def __str__(self):
41+
return self.name

0 commit comments

Comments
 (0)