first commit

This commit is contained in:
ayxan 2022-08-24 17:37:03 +04:00
commit f9930c2476
53 changed files with 785 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
.venv
__pycache__
migrations
db.sqlite3
src/config/settings/static
.env

0
README.md Normal file
View File

4
src/.env.example Normal file
View File

@ -0,0 +1,4 @@
SECRET_KEY=
DB_NAME=
DB_USER=
DB_PASSWORD=

View File

@ -0,0 +1 @@
from .get_request import APIClient

View File

@ -0,0 +1,33 @@
from requests import get
from django.core.exceptions import ValidationError
class APIClient:
def __init__(self, api_key: str) -> None:
self.api_key = api_key
def __get_data(self, link: str) -> dict:
raw_data = get(link)
if raw_data.status_code == 200:
data = raw_data.json()
if data['errorMessage'] == 'Invalid API Key': raise ValidationError('Invalid API Key')
elif data['errorMessage']: raise ValidationError('id is not correct.')
return data
raise ValidationError('Could not created. Please try again later.')
def key_control(self) -> None:
self.__get_data(f"https://imdb-api.com/en/API/Title/{self.api_key}/tt7939766")
def get_seasons(self, series_id: str) -> list:
data = self.__get_data(f"https://imdb-api.com/en/API/Title/{self.api_key}/{series_id}")
if not data['tvSeriesInfo']:
raise ValidationError('This is not a TV series id')
return data['tvSeriesInfo']['seasons']
def get_episodes_count(self, season: int, series_id: str) -> int:
data = self.__get_data(
f"https://imdb-api.com/en/API/SeasonEpisodes/{self.api_key}/{series_id}/{season}")
return len(data['episodes'])

0
src/account/__init__.py Normal file
View File

13
src/account/admin.py Normal file
View File

@ -0,0 +1,13 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User
@admin.register(User)
class CustomAdmin(UserAdmin):
list_display = ('username', 'email')
fieldsets = UserAdmin.fieldsets + (
('IMDB API Key Field', {
'fields': ['imdb_api_key']
}),
)

6
src/account/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class AccountConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'account'

View File

@ -0,0 +1,2 @@
from .register import RegisterForm
from .profile_editing import ProfileEditingForm

View File

@ -0,0 +1,10 @@
from django.contrib.auth.forms import UserChangeForm
from account.models import User
class ProfileEditingForm(UserChangeForm):
password = None
class Meta:
model = User
fields = ('email', 'username', 'imdb_api_key')

View File

@ -0,0 +1,14 @@
from django.contrib.auth.forms import UserCreationForm
from account.models import User
class RegisterForm(UserCreationForm):
password = None
class Meta:
model = User
fields = ('username',
'email',
'password1',
'password2',
'imdb_api_key')

25
src/account/models.py Normal file
View File

@ -0,0 +1,25 @@
from django.db import models
from django.contrib.auth.models import AbstractUser
from API_client import APIClient
from django.core.exceptions import ValidationError
class User(AbstractUser):
imdb_api_key = models.CharField(max_length=15, blank=False, null=False)
def clean(self) -> None:
client = APIClient(self.imdb_api_key)
client.key_control()
# raw_data = get(f"https://imdb-api.com/en/API/Title/{self.imdb_api_key}/tt0110413")
# if raw_data.status_code == 200: data = raw_data.json()
# else: raise ValidationError('Account not created. Please try again later')
# if not data['errorMessage']: raise ValidationError(data['errorMessage'])
class Meta:
db_table = 'user'
verbose_name = 'User'
verbose_name_plural = 'Users'
def __str__(self):
return self.username

View File

@ -0,0 +1,23 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block title %} Change Password {% endblock title %}
{% block content %}
<form enctype="multipart/form-data" method="POST">
{% csrf_token %}
{{form.media}}
{{form|crispy}}
<style>
.btn:hover {
background: #fff;
color: #0069D9;
}
</style>
<input type="submit" value="Change Password" class="btn btn-primary mt-3 mb-5">
</form>
{% endblock content %}

View File

@ -0,0 +1,24 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block title %} Login {% endblock title %}
{% block content %}
{% include 'components/message.html' %}
<form method="POST">
{% csrf_token %}
{{form|crispy}}
<style>
.btn:hover {
background: #fff;
color: #0069D9;
}
</style>
<input type="submit" value="Login" class="btn btn-primary mt-3 mb-3">
<p class="mb-5">No account? <a href="{% url 'register' %}">Signup</a></p>
</form>
{% endblock content %}

View File

@ -0,0 +1,24 @@
{% extends 'base.html' %}
{% load static %}
{% block title %} Profile {% endblock title %}
{% block content %}
<div class="card mb-3">
<div class="card-body">
<h5>Username: <small>{{profile.username}}</small></h5>
<h5>Email: <small>{{profile.email|default:""}}</small></h5>
<h5>IMDB API Key: <small>{{profile.imdb_api_key}}</small></h5>
</div>
</div>
<a href="/account/profile-editing" style="color: inherit; text-decoration: none;">
<button type="button" class="btn btn-primary">Edit Profile</button>
</a>
<a href="/account/change-password" style="color: inherit; text-decoration: none;">
<button type="button" class="btn btn-primary">Change Password</button>
</a>
{% endblock content %}

View File

@ -0,0 +1,23 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block title %} Profil Güncəllə {% endblock title %}
{% block content %}
<form enctype="multipart/form-data" method="POST">
{% csrf_token %}
{{form.media}}
{{form|crispy}}
<style>
.btn:hover {
background: #fff;
color: #0069D9;
}
</style>
<input type="submit" value="Update Profile" class="btn btn-primary mt-3 mb-5">
</form>
{% endblock content %}

View File

@ -0,0 +1,25 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block title %} Signup {% endblock title %}
{% block content %}
<form method="POST">
{% csrf_token %}
{{form.media}}
{{form|crispy}}
<a href="https://imdb-api.com/Identity/Account/Register">Get API Key</a><br>
<style>
.btn:hover {
background: #fff;
color: #0069D9;
}
</style>
<input type="submit" value="Signup" class="btn btn-primary mt-3 mb-3">
<p class="mb-5">Already have an account? <a href="{% url 'login' %}">Login</a></p>
</form>
{% endblock content %}

3
src/account/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

15
src/account/urls.py Normal file
View File

@ -0,0 +1,15 @@
from django.urls import path
from account import views
from django.contrib.auth import views as auth_views
urlpatterns = [
path('login', auth_views.LoginView.as_view(
template_name = 'login.html'
), name='login'),
path('register', views.register_view, name='register'),
path('logout', views.logout_view, name='logout'),
path('change-password', views.change_password_view, name='change-password'),
path('profile-editing', views.profile_editing_view, name='profile-editing'),
path('profile', views.ProfileDetailView.as_view(), name='profile')
]

View File

@ -0,0 +1,5 @@
from .register import register_view
from .logout import logout_view
from .change_password import change_password_view
from .profile_editing import profile_editing_view
from .profile_detail import ProfileDetailView

View File

@ -0,0 +1,22 @@
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth import update_session_auth_hash
from django.contrib import messages
@login_required(login_url='/')
def change_password_view(request):
if request.method == 'POST':
form = PasswordChangeForm(request.user, request.POST)
if form.is_valid():
user = form.save()
update_session_auth_hash(request, user)
messages.success(request, 'Password changed')
return redirect('homepage')
else:
form = PasswordChangeForm(request.user)
return render(request, 'change_password.html', context={'form': form})

View File

@ -0,0 +1,12 @@
from django.contrib.auth import logout
from django.shortcuts import redirect
from django.contrib import messages
from django.contrib.auth.decorators import login_required
@login_required(login_url='/account/login')
def logout_view(request):
logout(request)
messages.success(request, 'Logged Out')
return redirect('login')

View File

@ -0,0 +1,16 @@
from django.views.generic import DetailView
from account.models import User
from django.urls import reverse, reverse_lazy
from django.shortcuts import get_object_or_404
from django.contrib.auth.mixins import LoginRequiredMixin
class ProfileDetailView(LoginRequiredMixin, DetailView):
login_url = reverse_lazy('login')
template_name = 'profile_detail.html'
context_object_name = 'profile'
def get_object(self):
return get_object_or_404(
User, id=self.request.user.id
)

View File

@ -0,0 +1,20 @@
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from account.forms import ProfileEditingForm
@login_required(login_url='/')
def profile_editing_view(request):
if request.method == 'POST':
form = ProfileEditingForm(request.POST, instance=request.user)
if form.is_valid():
form.save()
messages.success(request, 'Profile Updated')
return redirect('homepage')
else:
form = ProfileEditingForm(instance=request.user)
return render(request, 'profile_editing.html', context={"form": form})

View File

@ -0,0 +1,35 @@
from django.shortcuts import render, redirect
from django.contrib import messages
from account.forms import RegisterForm
from django.contrib.auth import login, authenticate
from django.core.exceptions import ValidationError
from requests import get
def register_view(request):
if request.method == 'POST':
form = RegisterForm(request.POST)
if form.is_valid():
# raw_data = get(f"https://imdb-api.com/en/API/Title/{request.POST['imdb_api_key']}/tt0110413")
# if raw_data.status_code == 200: data = raw_data.json()
# else:
# messages.info(request, 'Account not created. Please try again later')
# return redirect('register')
# if data['errorMessage'] == 'Invalid API Key':
# messages.info(request, 'Invalid API Key')
# return redirect('register')
form.save()
username = form.cleaned_data.get('username')
password = form.cleaned_data.get('password1')
user = authenticate(username=username, password=password)
login(request, user)
messages.success(request, 'Registration Successful')
return redirect('homepage')
else:
form = RegisterForm()
return render(request, 'register.html', context={"form": form})

0
src/config/__init__.py Normal file
View File

16
src/config/asgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
ASGI config for config project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
application = get_asgi_application()

View File

View File

@ -0,0 +1,84 @@
import os
import environ
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent.parent
env = environ.Env()
environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
SECRET_KEY = env("SECRET_KEY")
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'account',
'series',
'crispy_forms',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'config.urls'
CRISPY_TEMPLATE_PACK = 'bootstrap4'
LOGIN_REDIRECT_URL = '/'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'config.wsgi.application'
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
AUTH_USER_MODEL = 'account.User'
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

View File

@ -0,0 +1,21 @@
from .base import *
DEBUG = True
ALLOWED_HOSTS = []
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
STATIC_URL = '/static/'
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
STATIC_ROOT = os.path.join(PROJECT_DIR, 'static')
STATICFILES_DIRS = [
BASE_DIR / 'static'
]

View File

@ -0,0 +1,20 @@
from .base import *
DEBUG = True
ALLOWED_HOSTS = []
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': env('DB_NAME'),
'USER': env('DB_USER'),
'PASSWORD': env('DB_PASSWORD'),
'HOST': 'databasepostgresql',
'PORT': '5432',
}
}
STATIC_URL = '/django_static/'
STATIC_ROOT = os.path.join(os.path.dirname(os.path.dirname(BASE_DIR)), 'opt/services/djangoapp/django_static')

9
src/config/urls.py Normal file
View File

@ -0,0 +1,9 @@
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('account/', include('account.urls')),
path('', include('series.urls')),
]

16
src/config/wsgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
WSGI config for config project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
application = get_wsgi_application()

22
src/manage.py Executable file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.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(sys.argv)
if __name__ == '__main__':
main()

0
src/series/__init__.py Normal file
View File

5
src/series/admin.py Normal file
View File

@ -0,0 +1,5 @@
from django.contrib import admin
from .models import SeriesModel
admin.site.register(SeriesModel)

6
src/series/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class SeriesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'series'

42
src/series/models.py Normal file
View File

@ -0,0 +1,42 @@
from django.db import models
from django.core.exceptions import ValidationError
from API_client import APIClient
from django.core.validators import (MaxValueValidator,
MinValueValidator,
MinLengthValidator)
class SeriesModel(models.Model):
user = models.ForeignKey('account.User', on_delete=models.CASCADE, related_name='series')
title = models.CharField(max_length=35, blank=False, null=False)
imdb_id = models.CharField(max_length=10, validators=[MinLengthValidator(9)],
blank=False, null=False)
last_season = models.IntegerField(validators=[
MaxValueValidator(30),
MinValueValidator(1)
], blank=False, null=False)
last_episode = models.IntegerField(validators=[
MaxValueValidator(60),
MinValueValidator(1)
], blank=False, null=False)
show = models.BooleanField(default=True)
def clean(self) -> None:
if len(self.imdb_id) < 9:
raise ValidationError(f'Ensure this value has at least 9 characters (it has {len(self.imdb_id)}).')
if len(self.imdb_id) > 10:
raise ValidationError(f'Make sure this value is no more than 10 characters (it has {len(self.imdb_id)}).')
client = APIClient(self.user.imdb_api_key)
seasons = client.get_seasons(self.imdb_id)
if str(self.last_season) not in seasons:
raise ValidationError('The season number you entered is not correct.')
episodes_count = client.get_episodes_count(self.last_season, self.imdb_id)
if self.last_episode > episodes_count:
raise ValidationError('The episode number you entered is not correct.')
def __str__(self) -> str:
return f"{self.user.username} - {self.title}"

View File

@ -0,0 +1,9 @@
{% extends 'base.html' %}
{% block title %} Home Page {% endblock title %}
{% block content %}
{% include 'components/message.html' %}
{% endblock content %}

3
src/series/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

8
src/series/urls.py Normal file
View File

@ -0,0 +1,8 @@
from django.urls import path
from series import views
urlpatterns = [
path('', views.homepage_view, name='homepage'),
# path('series/create', views.homepage_view, name='homepage'),
]

View File

@ -0,0 +1 @@
from .homepage import homepage_view

View File

@ -0,0 +1,7 @@
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
@login_required(login_url='/account/login')
def homepage_view(request):
return render(request, 'homepage.html')

7
src/static/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

35
src/static/css/footer.css Normal file
View File

@ -0,0 +1,35 @@
footer {
position: fixed;
bottom: 0;
width: 100%;
height: 65px;
background: black;
text-align: center;
}
.div-left {
margin-top: 1.5rem;
margin-left: 1.3rem;
}
.div-right {
margin-top: 1rem;
margin-right: 1.5rem;
}
footer div span a {
color: inherit;
text-decoration: none;
}
.contact-links{
border: 0;
padding-top: 4px;
padding-bottom: 4px;
padding-left: 8px;
padding-right: 8px;
}
.contact-icons{
font-size: 23px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

7
src/static/js/bootstrap.bundle.min.js vendored Normal file

File diff suppressed because one or more lines are too long

7
src/static/js/bootstrap.min.js vendored Normal file

File diff suppressed because one or more lines are too long

6
src/static/js/popper.min.js vendored Normal file

File diff suppressed because one or more lines are too long

30
src/templates/base.html Normal file
View File

@ -0,0 +1,30 @@
{% load static %}
<!-- neye bakıyon lan html var css var neye bakıyon -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
<title>{% block title %}{% endblock title %}</title>
</head>
<body>
{% include "components/navbar.html" %}
<div class="container mt-5">
{% block content %}{% endblock content %}
</div>
{% include "components/footer.html" %}
<!-- Bootstrap JS -->
<script src="{% static 'js/bootstrap.bundle.min.js' %}"></script>
</body>
</html>

View File

@ -0,0 +1,24 @@
{% load static %}
<link rel="stylesheet" href="{% static 'css/footer.css' %}">
<footer>
<div class="div-left" style="float: left;">
<span class="text-muted">
<a href="mailto:ayxan.shahsuvarov1@gmail.com">© 2022 Aykhan Shahsuvarov</a>
</span>
</div>
<div class="div-right" style="float: right;">
<a class="btn btn-outline-light btn-floating m-1 contact-links" target="_blank" href="https://github.com/Ayxan-z" role="button">
<i class="fab fa-github contact-icons"></i>
</a>
<a class="btn btn-outline-light btn-floating m-1 contact-links" target="_blank" href="https://www.linkedin.com/in/ayxan-shahsuvarov-59a314187" role="button">
<i class="fab fa-linkedin-in contact-icons"></i>
</a>
<a class="btn btn-outline-light btn-floating m-1 contact-links" target="_blank" href="mailto:ayxan.shahsuvarov1@gmail.com" role="button">
<i class="fa fa-envelope contact-icons"></i>
</a>
</div>
</footer>

View File

@ -0,0 +1,9 @@
{% if messages %}
{% for message in messages %}
{% if message.tags == "info" %}
<div class="alert alert-danger">{{ message }}</div>
{% else %}
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
{% endif %}
{% endfor %}
{% endif %}

View File

@ -0,0 +1,30 @@
{% load static %}
<nav class="navbar navbar-expand-lg bg-dark navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/" style="padding-top: 2px; padding-bottom: 2px;">
<img src="{% static 'icons/retro-tv.png' %}" width="55px" height="55px">
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="me-auto"></ul>
{% if request.user.is_authenticated == True %}
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="{% url 'profile' %}" style="font-size: 18px;">
Profile
</a>
</li>
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="{% url 'logout' %}" style="font-size: 18px;">
Logout
</a>
</li>
</ul>
{% endif %}
</div>
</div>
</nav>