Skip to content

Commit

Permalink
Issue #46: Done in PR #47
Browse files Browse the repository at this point in the history
Реліз
  • Loading branch information
lexhouk authored Oct 15, 2024
2 parents c93876a + 1d7a7c7 commit 778262d
Show file tree
Hide file tree
Showing 115 changed files with 3,332 additions and 0 deletions.
23 changes: 23 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
PROJECT_NAME=binova-personal-assistant

DJANGO_SECRET=
DJANGO_ALLOWED_HOSTS=
DJANGO_DEBUG=

DJANGO_EMAIL_HOST=
DJANGO_EMAIL_PORT=
DJANGO_EMAIL_USER=
DJANGO_EMAIL_PASSWORD=

DATABASE_TAG=16.4-alpine3.20
DATABASE_HOST=
DATABASE_PORT=
DATABASE_ENGINE=
DATABASE_OPTIONS=
DATABASE_NAME=
DATABASE_USER=
DATABASE_PASSWORD=

CLOUDINARY_CLOUD_NAME=
CLOUDINARY_API_KEY=
CLOUDINARY_API_SECRET=
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.env
.venv
poetry.lock
db.sqlite3
__pycache__
personal_assistant/static
68 changes: 68 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,69 @@
```
____ _ _ _ _ _
| _ \ ___ _ __ ___ ___ _ __ __ _| | / \ ___ ___(_)___| |_ __ _ _ __ | |_
| |_) / _ \ '__/ __|/ _ \| '_ \ / _` | | / _ \ / __/ __| / __| __/ _` | '_ \| __|
| __/ __/ | \__ \ (_) | | | | (_| | | / ___ \\__ \__ \ \__ \ || (_| | | | | |_
|_| \___|_| |___/\___/|_| |_|\__,_|_| /_/ \_\___/___/_|___/\__\__,_|_| |_|\__|
```

# Personal Assistant

This web application offers a comprehensive suite of features for managing
contacts, notes, and files, all integrated with a powerful tagging system for
enhanced organization and search capabilities. It also provides up-to-date news
and currency exchange rates, ensuring users have access to real-time
information. With robust user authentication and personalized data access, the
application ensures a secure and customized experience, while utilizing cloud
storage and caching for optimal performance.


## Features

- **Contacts:** Allows users to manage their contact list with CRUD operations,
including organizing by birthdays and searching by name.

- **Tags:** Enables users to create and delete tags, with restrictions on
deletion if the tag is in use, and shared across all users.

- **Notes:** Provides a simple note-taking feature with title and description,
enhanced by tag-based categorization and advanced search functionality.

- **Files:** Allows file uploads and viewing, securely storing content in the
cloud and enabling filtering and sorting via a tag-like system.

- **News:** Displays global news and currency exchange rates, with real-time
data and a caching system for faster load times, including a
superuser-controlled data sync.

- **Users:** Manages user authentication, including registration, login, and
password recovery, ensuring personalized access to data and features while
protecting against spam accounts.


## Installation

**Note:** The Docker command is optional since the project can work with
`SQLite` when environment variables for `PostgreSQL` are not defined.

```bash
$ git clone https://github.com/BIN0VA/Personal-Assistant.git
$ cd Personal-Assistant
$ docker compose up -d
$ poetry shell
$ poetry install
$ cd personal_assistant
$ python manage.py migrate
$ python manage.py createsuperuser
```


## Usage

```bash
$ docker compose up -d
$ poetry shell
$ cd personal_assistant
$ python manage.py runserver
```

Go to http://localhost:8000.
10 changes: 10 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
services:
db:
image: "postgres:${DATABASE_TAG}"
container_name: "${PROJECT_NAME}-database"
environment:
POSTGRES_DB: $DATABASE_NAME
POSTGRES_USER: $DATABASE_USER
POSTGRES_PASSWORD: $DATABASE_PASSWORD
ports:
- "${DATABASE_PORT}:5432"
24 changes: 24 additions & 0 deletions personal_assistant/manage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
from os import environ
from sys import argv


def main() -> None:
"""Run administrative tasks."""
environ.setdefault('DJANGO_SETTINGS_MODULE', 'personal_assistant.settings')

try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc

execute_from_command_line(argv)


if __name__ == '__main__':
main()
Empty file.
5 changes: 5 additions & 0 deletions personal_assistant/pa_contacts/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.contrib import admin
from .models import Contact


admin.site.register(Contact)
11 changes: 11 additions & 0 deletions personal_assistant/pa_contacts/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.apps import AppConfig


class ContactsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'pa_contacts'
verbose_name = 'Contacts'
description = '''Allows users to manage their contact list with CRUD operations, including organizing by birthdays and searching by name.
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.
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.'''
icon = 'person-vcard-fill'
116 changes: 116 additions & 0 deletions personal_assistant/pa_contacts/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
from datetime import datetime
from re import sub

from django.forms import ModelForm, CharField, EmailField, TextInput, \
DateField, DateInput
from django.core.exceptions import ValidationError
from phonenumbers import parse, is_valid_number, NumberParseException, \
format_number, PhoneNumberFormat

from .models import Contact


PHONE = '+380776665544'


class ContactsForm(ModelForm):
current_year = datetime.now().year

name = CharField(
min_length=1,
max_length=50,
required=True,
widget=TextInput({'class': 'form-control'}),
)

address = CharField(
min_length=10,
max_length=150,
required=False,
widget=TextInput({'class': 'form-control'}),
)

phone = CharField(
min_length=10,
max_length=20,
required=True,
widget=TextInput({'class': 'form-control', 'placeholder': PHONE}),
)

email = EmailField(
min_length=5,
max_length=50,
required=False,
widget=TextInput({
'class': 'form-control',
'type': 'email',
'id': 'email',
'name': 'email',
'placeholder': 'example@example.com',
}),
)

birthday = DateField(
required=False,
widget=DateInput({'class': 'form-control', 'type': 'date'})
)

class Meta:
model = Contact
fields = ('name', 'address', 'phone', 'email', 'birthday')

def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None)

super().__init__(*args, **kwargs)

for field in self.fields:
if self.errors.get(field):
self.fields[field].widget.attrs['class'] += ' is-invalid'
else:
self.fields[field].widget.attrs['class'] += ' form-control'

def clean_phone(self):
if phone := self.cleaned_data.get('phone'):
phone = sub(r'(?<!^)\D+', '', phone)

incorrect = ValidationError(
'Please enter your phone number in the international format '
f'(e.g., {PHONE}).',
)

try:
if (
Contact.objects
.filter(user=self.user, phone=phone)
.exclude(id=self.instance.id if self.instance else None)
.exists()
):
raise ValidationError(
'A contact with this phone number already exists.',
)

parsed = parse(phone, None if phone.startswith('+') else 'UA')

if not is_valid_number(parsed):
raise incorrect

return format_number(parsed, PhoneNumberFormat.E164)

except NumberParseException:
raise incorrect

return phone

def clean_email(self):
if (
(email := self.cleaned_data.get('email')) and

Contact.objects
.filter(user=self.user, email=email)
.exclude(id=self.instance.id if self.instance else None)
.exists()
):
raise ValidationError('A contact with this e-mail already exists.')

return email
25 changes: 25 additions & 0 deletions personal_assistant/pa_contacts/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 5.1.1 on 2024-10-10 13:06

from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='Contact',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('address', models.CharField(max_length=150, null=True)),
('phone', models.CharField(max_length=12, unique=True)),
('email', models.EmailField(blank=True, max_length=50, null=True, unique=True)),
('birthday', models.DateField(null=True)),
],
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2024-10-11 16:03

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('pa_contacts', '0001_initial'),
]

operations = [
migrations.AlterField(
model_name='contact',
name='phone',
field=models.CharField(max_length=20, unique=True),
),
]
40 changes: 40 additions & 0 deletions personal_assistant/pa_contacts/migrations/0003_contact_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Generated by Django 5.1.2 on 2024-10-12 12:51

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('pa_contacts', '0002_alter_contact_phone'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.AddField(
model_name='contact',
name='user',
field=models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, related_name='contacts', to=settings.AUTH_USER_MODEL),
preserve_default=False,
),
migrations.AlterField(
model_name='contact',
name='email',
field=models.EmailField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='contact',
name='phone',
field=models.CharField(max_length=20),
),
migrations.AddConstraint(
model_name='contact',
constraint=models.UniqueConstraint(fields=('user', 'phone'), name='unique_phone_per_user'),
),
migrations.AddConstraint(
model_name='contact',
constraint=models.UniqueConstraint(fields=('user', 'email'), name='unique_email_per_user'),
),
]
Empty file.
41 changes: 41 additions & 0 deletions personal_assistant/pa_contacts/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from django.contrib.auth.models import User
from django.db.models import (
CASCADE,
CharField,
DateField,
EmailField,
ForeignKey,
Model,
UniqueConstraint,
)


class Contact(Model):
name = CharField(max_length=50, null=False)
address = CharField(max_length=150, null=True)
phone = CharField(max_length=20, null=False)
email = EmailField(max_length=50, null=True, blank=True)
birthday = DateField(null=True)

user = ForeignKey(User, CASCADE, related_name='contacts')

class Meta:
constraints = [
UniqueConstraint(
fields=['user', 'phone'],
name='unique_phone_per_user',
),
UniqueConstraint(
fields=['user', 'email'],
name='unique_email_per_user',
),
]

def save(self, *args, **kwargs):
if self.email == '':
self.email = None

super().save(*args, **kwargs)

def __str__(self):
return self.name
Loading

0 comments on commit 778262d

Please sign in to comment.