Merge branch 'dev_unstable' of github.com:vvsu-rent-project/dev_rent into dev_unstable

This commit is contained in:
balistiks 2023-05-17 11:34:58 +10:00
commit 0af62ee05d
50 changed files with 1973 additions and 245 deletions

10
.env.example Normal file
View File

@ -0,0 +1,10 @@
APP_DEBUG=false
APP_PORT=80
DB_HOST=db
DB_PORT=5432
DB_USER=root
DB_PASSWORD=
DB_DATABASE=pairent
DJANGO_KEY=

3
.gitignore vendored
View File

@ -28,7 +28,4 @@ local_settings.py
db.sqlite3 db.sqlite3
# macOS moment # macOS moment
pairent_backend/.DS_Store
# macOS moment жиза
.DS_Store .DS_Store

View File

@ -1,76 +1,8 @@
# Аренда квартир для студентов <h1 align='center'>Точка</h1>
Добро пожаловать в репозиторий проекта "точка"
В проекте есть три ветки: # Запуск локального сайта
1. Создайте `.env` файл, рекомендуем использовать для этого шаблон `.env.example`
- main — в эту ветку загружается код после положительного тестирования и ревью. 2. Сгенерируйте ключи `genkeys.sh`
- dev_stable — в эту ветку загружается стабильный код, который будет использоваться в продашкене. 3. Постройте статичные файлы `build.sh`
- dev_unstable — основная ветка для разработчиков. 3. Поднимите контейнеры `docker-compose up -d`
## ТУТ НАЗВАНЫ ПАПКИ ПОД РАЗРАБОТКУ И ФРОНТ В БЕК НЕ ЗАХОДИТ И НИЧЕГО НЕ МЕНЯЕТ!
## Как начать работать?
- Устанавливаем [python](https://www.python.org/downloads/) версия 3.11.1 и [git](https://git-scm.com/)
- [Настройте git](https://tproger.ru/curriculum/git-guide/). В основном нужно только ввести имя пользователя и почту, пароль запросит при клонировании
Если не хотите заморачиваться с терминалом. Советую использовать [GitHub Desktop](https://desktop.github.com/)
### Клонирование проекта через терминал
Команды написаны под windows. Вводить в командную строку. Проверяйте, что в терминале вы находитесь в правильной директории
- Клонируем репозиторий
```bash
git clone https://github.com/vvsu-rent-project/dev_rent.github
```
- В консоли переходим в папку проекта
- Далее переходим на ветку unstable
```bash
git switch dev_unstable
```
### Клонирование проекта через GitHub Desktop
- Заходим в настройки
![Заходим в настройки](misc/1_step.png)
- Входим в аккаунт
![Входим в аккаунт](misc/2_step.png)
- Клонируем репозиторий
![Клонируем репозиторий](misc/3_step.png)
![Клонируем репозиторий](misc/4_step.png)
### Подготовка проекта к работе
- Открываем VS Code, открываем папку проекта и запускаем терминал
- В терминале переходим в папку pairent_backend
```bash
cd pairent_backend
```
- Создаем виртуальное окружение
```bash
python -m venv venv
```
- Активируем виртуальное окружение. Активировать окружение нужно: когда запускаете сервер, устанавливаете зависимости, добавляете новое django приложение
```bash
venv\Scripts\activate
```
- После активации, устанавливаем зависимости.
```bash
pip install -r requirements.txt
```

74
build.sh Executable file
View File

@ -0,0 +1,74 @@
#!/bin/sh
# ----- Start commands -----
fatal_err() {
echo -e "\033[31mFatal error $*\033[0m"
}
# ----- End commands -----
# ----- Start safeguards -----
if [[ "$1" != '-a' ]]; then
fatal_err
echo " This script will potentially break any existing instance of Pairent."
echo " To execute this script, re-run it with option -a as first argument."
exit -1
fi
if ! [ -f .env ]; then
fatal_err
echo -e " No .env file was found."
echo -e " Please use the \033[32m.env.example\033[0m to create a dotenv file:"
echo -e " 1. \033[34mcp .env.example .env\033[0m"
echo -e " 2. Edit your \033[32m.env\033[0m in your favourite editor"
exit -1
fi
# ----- End safeguards -----
# ----- Start bootstrap front -----
echo Building frontend static files...
cd pairent_frontend_react
NPMS=('npm' 'pnpm' 'yarn')
NPM='None'
for n in ${NPMS[@]}; do
if [ -x "$(command -v "$n")" ]; then
NPM=$n
fi
done
if [[ "$NPM" == 'None' ]]; then
fatal_err
echo ' No node package manager was found in your system.'
echo ' Please install one of the following:'
for n in ${NPMS[@]}; do
echo -e " $n"
done
exit -1
fi
rm -rf node_modules
$NPM install
$NPM run build
DIST_FOLDER=$(realpath dist)
cd ..
cp -r $DIST_FOLDER/* static
cp $DIST_FOLDER/index.html pairent_backend/pairent_app/templates
cp $DIST_FOLDER/../public/* static -r
echo Done building frontend static files
# ----- End bootstrap front -----

2
conf/nginx/logs/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

49
conf/nginx/mime.types Normal file
View File

@ -0,0 +1,49 @@
types {
text/html html htm shtml;
text/css css;
text/xml xml rss;
image/gif gif;
image/jpeg jpeg jpg;
application/x-javascript js;
text/plain txt;
text/x-component htc;
text/mathml mml;
image/png png;
image/x-icon ico;
image/x-jng jng;
image/vnd.wap.wbmp wbmp;
image/svg+xml svg svgz;
application/java-archive jar war ear;
application/mac-binhex40 hqx;
application/pdf pdf;
application/x-cocoa cco;
application/x-java-archive-diff jardiff;
application/x-java-jnlp-file jnlp;
application/x-makeself run;
application/x-perl pl pm;
application/x-pilot prc pdb;
application/x-rar-compressed rar;
application/x-redhat-package-manager rpm;
application/x-sea sea;
application/x-shockwave-flash swf;
application/x-stuffit sit;
application/x-tcl tcl tk;
application/x-x509-ca-cert der pem crt;
application/x-xpinstall xpi;
application/zip zip;
application/octet-stream deb;
application/octet-stream bin exe dll;
application/octet-stream dmg;
application/octet-stream eot;
application/octet-stream iso img;
application/octet-stream msi msp msm;
audio/mpeg mp3;
audio/x-realaudio ra;
video/mpeg mpeg mpg;
video/quicktime mov;
video/x-flv flv;
video/x-msvideo avi;
video/x-ms-wmv wmv;
video/x-ms-asf asx asf;
video/x-mng mng;
}

42
conf/nginx/nginx.conf Normal file
View File

@ -0,0 +1,42 @@
user nobody nobody;
worker_processes 5;
error_log logs/error.log;
pid logs/nginx.pid;
worker_rlimit_nofile 8192;
events {
worker_connections 4096;
}
http {
include mime.types;
include /etc/nginx/proxy.conf;
index index.html index.htm;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] $status '
'"$request" $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log logs/access.log main;
sendfile on;
tcp_nopush on;
gzip on;
server_names_hash_bucket_size 128; # this seems to be required for some vhosts
server {
listen 80;
location ~ ^/(api|admin)/ {
proxy_pass http://back;
}
location / {
root /var/www/html;
try_files $uri $uri/ /;
}
location ~ ^/\. {
deny all;
}
}
}

10
conf/nginx/proxy.conf Normal file
View File

@ -0,0 +1,10 @@
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
client_max_body_size 10m;
client_body_buffer_size 128k;
proxy_connect_timeout 90;
proxy_send_timeout 90;
proxy_read_timeout 90;
proxy_buffers 32 4k;

2
data/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

46
docker-compose.yml Normal file
View File

@ -0,0 +1,46 @@
version: '3'
services:
nginx:
image: nginx:alpine
volumes:
- './conf/nginx:/etc/nginx'
- './static:/var/www/html'
restart: always
ports:
- '${APP_PORT:-80}:80'
networks:
pairent:
aliases:
- nginx
depends_on:
- back
back:
build:
context: ./pairent_backend
dockerfile: Dockerfile
networks:
pairent:
aliases:
- back
volumes:
- './pairent_backend:/opt/code'
environment:
DB_PASS: '${DB_PASSWORD}'
depends_on:
- db
db:
image: postgres
networks:
pairent:
aliases:
- db
volumes:
- './data/db:/etc/mysql'
environment:
POSTGRES_PASSWORD: '${DB_PASSWORD}'
POSTGRES_DB: 'pairent_db'
POSTGRES_USER: 'root'
networks:
pairent:
driver: bridge

58
genkeys.sh Executable file
View File

@ -0,0 +1,58 @@
#!/bin/bash
# ----- Start commands -----
fatal_err() {
echo -e "\033[31mFatal error $*\033[0m"
}
# ----- End commands -----
# ----- Start safeguards -----
if [[ "$1" != '-a' ]]; then
fatal_err
echo " This script will potentially break any existing instance of Pairent."
echo " To execute this script, re-run it with option -a as first argument."
exit -1
fi
if ! [ -f .env ]; then
fatal_err
echo -e " No .env file was found."
echo -e " Please use the \033[32m.env.example\033[0m to create a dotenv file:"
echo -e " 1. \033[34mcp .env.example .env\033[0m"
echo -e " 2. Edit your \033[32m.env\033[0m in your favourite editor"
exit -1
fi
if ! [ -x "$(command -v python3)" ]; then
fatal_err
echo -e " Python is not installed."
echo -e " Please install Python 3.11 on your system"
exit -1
fi
# ----- End safeguards -----
# ----- Start bootstrap back -----
echo Generating keys...
KEY=$(python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())')
sed -Ei "s/^DJANGO_KEY=.*$/DJANGO_KEY=/" .env
sed -Ei "s/^DJANGO_KEY=.*$/DJANGO_KEY='$KEY'/" .env
DB_PASS=$(tr -dc A-Za-z0-9 </dev/urandom | head -c $(( $RANDOM % 32 + 32 )))
sed -Ei "s/DB_PASSWORD=\w*/DB_PASSWORD=$DB_PASS/" .env
echo Using these keys:
echo -e " \033[32mDjango key: \033[0m$KEY"
echo -e " \033[32mDatabase key: \033[0m$DB_PASS"
ln -s .env pairent_backend/.env
# ----- End bootstrap back -----
exit

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,19 @@
FROM python:alpine
# mysql mariadb-connector-c-dev
RUN apk add --no-cache postgresql-libs && \
apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev && \
mkdir /opt/code && \
apk --purge del .build-deps
COPY requirements.txt /opt/code/requirements.txt
WORKDIR /opt/code
RUN pip install -r requirements.txt
USER nobody
COPY . /opt/code
CMD [ "/bin/sh", "docker-entry.sh" ]

View File

@ -0,0 +1,6 @@
#!/bin/sh
PYTHON='/usr/bin/env python3'
$PYTHON manage.py migrate
$PYTHON -m gunicorn -w 3 -b 0.0.0.0:80 pairent_backend.wsgi

View File

@ -0,0 +1 @@
templates/index.html

View File

@ -1,32 +1,22 @@
from rest_framework.request import Request
from django.http import HttpResponseBadRequest, HttpResponse, JsonResponse, HttpRequest from django.http import HttpResponseBadRequest, HttpResponse, JsonResponse, HttpRequest
def VVSUAuthProxy(req: HttpRequest): from .models import User, AuthToken
proxy = 'https://vvsu.ru/connect' + req.path[len('/api/auth/vvsu'):];
preq = requests.request(req.method, proxy, headers={
'User-Agent': 'OIDC Client / Pairent',
'Origin': 'http://pairent.vvsu.ru',
'Referer': 'http://pairent.vvsu.ru'
});
resp = HttpResponse(preq.content); import ipware as iplib, time, requests, uuid
resp.headers['Content-Type'] = preq.headers['Content-Type']; ipware = iplib.IpWare();
return resp; def client_ip(req: HttpRequest):
return ipware.get_client_ip(req.META)[0].exploded;
def register(oid, provider_id, name): def register(oid, provider_id, name):
user = User( user = User(
favorites_apartments='', favorites_apartments='',
comparison_apartments='', comparison_apartments='',
name=name, name=name,
# date_of_birth=,
about_me='', about_me='',
gender='?', gender='?',
phone='+00000',
# email=,
# telegram=,
# discord=,
# city=,
role='s', role='s',
photo_provider='VVSU', photo_provider='VVSU',
openid_addr=oid, openid_addr=oid,
@ -80,8 +70,36 @@ def verify_auth_token(key, ip):
token.delete(); token.delete();
return False; return False;
if (token.expires > time.time()): if (token.expires < time.time()):
token.delete(); token.delete();
return False; return False;
return True; return True;
def auth_required(func):
"""
Use authorization for this route.
"""
def inner(req: HttpRequest, *args, **kwargs):
if ('Authorization' not in req.headers.keys()):
return JsonResponse({'error': 'no auth token'});
if (not verify_auth_token(req.headers['Authorization'], client_ip(req))):
return JsonResponse({'error': 'auth token invalid or expired'});
func(req, *args, **kwargs);
return inner;
def rest_auth_required(func):
"""
Use authorization for this restframework view.
"""
def inner(self, req: HttpRequest, *args, **kwargs):
if ('Authorization' not in req.headers.keys()):
return JsonResponse({'error': 'no auth token'});
if (not verify_auth_token(req.headers['Authorization'], client_ip(req))):
return JsonResponse({'error': 'auth token invalid or expired'});
func(self, req, *args, **kwargs);
return inner;

View File

@ -0,0 +1,34 @@
import django.core.validators
from django.core.validators import RegexValidator, MaxValueValidator
from django.db import migrations, models
from pairent_app.models import User
class Migration(migrations.Migration):
dependencies = [
('pairent_app', '0005_user'),
]
operations = [
migrations.CreateModel(
name='PsychTestAnswers',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на первый вопрос')),
('second_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на второй вопрос')),
('third_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на третий вопрос')),
('fourth_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на четвертый вопрос')),
('fifth_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на пятый вопрос')),
('sixth_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на шестой вопрос')),
('seventh_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на седьмой вопрос')),
('eighth_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на восьмой вопрос')),
('nineth_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на девятый вопрос')),
('tenth_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на десятый вопрос')),
('eleventh_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на одиннадцатый вопрос')),
('twelfth_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на двенадцатый вопрос')),
# ('users', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pairent_app.user', verbose_name='Пользователь')),
('user', models.BigIntegerField(verbose_name='ID Пользователя')),
],
)
]

View File

@ -28,7 +28,11 @@ class PsychTestAddResultSerializer(serializers.ModelSerializer):
class PublicUserSerializer(serializers.ModelSerializer): class PublicUserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = User model = User
exclude = ('favorites_apartments', 'comparison_apartments') exclude = (
'favorites_apartments',
'comparison_apartments',
'openid_id',
)
# class PsychTestReultsSerializer(serializers.ModelSerializer): # class PsychTestReultsSerializer(serializers.ModelSerializer):
# class Meta: # class Meta:

View File

@ -20,7 +20,7 @@ from .serializer import (ApartamentListSerializer,
from .authlib import * from .authlib import *
import json, math, random, re, requests, oidc_client, base64, uuid, time, ipware as iplib import json, math, random, re, requests, base64, uuid, time, ipware as iplib
ipware = iplib.IpWare(); ipware = iplib.IpWare();
class ApartamentViewSet(viewsets.ReadOnlyModelViewSet): class ApartamentViewSet(viewsets.ReadOnlyModelViewSet):
@ -106,33 +106,21 @@ class PsychTestAddResultViewSet(viewsets.ViewSet):
return Response({'successfully': 'results post'}) return Response({'successfully': 'results post'})
# TODO: remake
# class CompatibleUsersView(viewsets.ViewSet):
# def list(self, req: Request):
# user_data = dict(req.data);
# # TODO: Verify auth
# vvsu_login = user_data['openid'];
# # Exclude already viewed users
# exclude = [];
# if ('exclude' in user_data.keys()):
# exclude = user_data['exclude'];
# try: class CompatibleUsersView(viewsets.ViewSet):
# validate_email(vvsu_login); @rest_auth_required
# except ValidationError: def list(self, req: Request):
# return Request({'error': 'bad login'}, 400); user_data = dict(req.data);
# try:
# this_user = User.objects.get(openid_addr=vvsu_login);
# except User.DoesNotExist:
# return Response({'error': 'user not found'}, 404);
# try: token = AuthToken.objects.get(key=req.headers['authorization']);
# answers_this_user = PsychTestReultsSerializer(PsychTestAnswers.objects.get(user=this_user)).dict;
# except PsychTestAnswers.DoesNotExist: this_user = User.objects.get(pk=token.user);
# return Response({'error': 'answers not found'}, 404);
answers_this_user = None;
try:
answers_this_user = PsychTestReultsSerializer(PsychTestAnswers.objects.get(user=this_user.id)).dict;
except PsychTestAnswers.DoesNotExist:
return Response({'error': 'answers not found'}, 404);
# users_answers_query = PsychTestReultsSerializer(PsychTestAnswers.objects.all(), many=True).dict # users_answers_query = PsychTestReultsSerializer(PsychTestAnswers.objects.all(), many=True).dict
@ -146,12 +134,8 @@ class PsychTestAddResultViewSet(viewsets.ViewSet):
# if score / 12 * 100 > 30: # if score / 12 * 100 > 30:
# users.append(UserSerializer(User.objects.get(pk=user_answers[0])).data) # users.append(UserSerializer(User.objects.get(pk=user_answers[0])).data)
# # for user in users_query:
# # if (abs(user.psych_test_result - score) < 20):
# # users.append(PublicUserSerializer(user).data);
# return Response(users); return Response(users);
class UserLogin(APIView): class UserLogin(APIView):
@ -243,3 +227,17 @@ class UserGet(APIView):
return JsonResponse(PublicUserSerializer(user).data); return JsonResponse(PublicUserSerializer(user).data);
def VVSUAuthProxy(req: HttpRequest):
proxy = 'https://vvsu.ru/connect' + req.path[len('/api/auth/vvsu'):];
preq = requests.request(req.method, proxy, headers={
'User-Agent': 'OIDC Client / Pairent',
'Origin': 'http://pairent.vvsu.ru',
'Referer': 'http://pairent.vvsu.ru'
});
resp = HttpResponse(preq.content);
resp.headers['Content-Type'] = preq.headers['Content-Type'];
return resp;

View File

@ -10,6 +10,7 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.1/ref/settings/ https://docs.djangoproject.com/en/4.1/ref/settings/
""" """
from dotenv import dotenv_values
from pathlib import Path from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
@ -19,11 +20,12 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! DOTENV = dotenv_values(BASE_DIR / '.env');
SECRET_KEY = 'django-insecure-=(7b+4ka=5jak-mokuh4#-6_14f58#^0kjwqsz7wyon$4i@sel'
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = DOTENV['DJANGO_KEY']
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True#DOTENV['APP_DEBUG'] == 'true'
ALLOWED_HOSTS = ['*'] ALLOWED_HOSTS = ['*']
@ -72,7 +74,9 @@ ROOT_URLCONF = 'pairent_backend.urls'
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [], 'DIRS': [
'templates/'
],
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [
@ -93,15 +97,16 @@ WSGI_APPLICATION = 'pairent_backend.wsgi.application'
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.mysql', 'ENGINE': 'django.db.backends.postgresql',
'NAME': 'pairent_db', 'NAME': 'pairent_db',
'USER': 'django', 'USER': DOTENV['DB_USER'],
'PASSWORD': 'Budnya1924$', 'PASSWORD': DOTENV['DB_PASSWORD'],
'HOST': '178.20.44.123', 'HOST': DOTENV['DB_HOST'],
'PORT': '3306', 'PORT': DOTENV['DB_PORT'],
'OPTIONS': { # 'OPTIONS': {
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'" # 'init_command': "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci",
} # 'charset': 'utf8mb4'
# }
} }
} }

View File

@ -13,10 +13,16 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path 1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.template import loader
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from django.http import HttpResponse
def root_url(req):
return HttpResponse(loader.get_template('index.html').render())
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('api/', include('pairent_app.urls')), path('api/', include('pairent_app.urls')),
path('', root_url)
] ]

View File

@ -1,7 +1,10 @@
mysqlclient psycopg2-binary
django django
djangorestframework djangorestframework
django-cors-headers django-cors-headers
Pillow Pillow
requests requests
python-ipware python-ipware
python-dotenv
gunicorn
factory-boy

View File

@ -3,6 +3,7 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"type": "module", "type": "module",
"homepage": "/static/dist",
"dependencies": { "dependencies": {
"axios": "^1.4.0", "axios": "^1.4.0",
"bootstrap": "^5.2.3", "bootstrap": "^5.2.3",

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -0,0 +1,30 @@
import { IAPIObject } from "./IAPIObject";
class APIToken extends IAPIObject {
static storage_key = 'pairent_api_key';
constructor(data) {
super();
this.user = data.user;
this.key = data.key;
this.expires = data.expires;
this.ip = data.ip;
}
/** @type {number} */
user;
/** @type {string} */
key;
/** A Unix timestamp (when the token will expire)
* @type {number}
*/
expires;
/** @type {string} */
ip;
}
export { APIToken };

View File

@ -0,0 +1,72 @@
import axios, { Axios } from "axios";
import { User } from "./User";
import { APIToken } from './APIToken';
class ClientCreateOptions {
/** Key used to access APIs, if you don't have user data
* @type {APIToken?}
*/
key;
/** Current user's data
* @type {User?}
*/
user;
}
/**
* An API client which is basically a User but with
* API token and with access to private fields.
*/
class Client extends User {
/** @type {APIToken} */
key;
/** @type {string} */
openid_id;
/** @type {string} */
favorites_apartments;
/** @type {string} */
comparison_apartments;
/**
*
* @param {ClientCreateOptions} options
*/
constructor(options) {
super();
if (options.key === undefined & options.user === undefined) {
throw new Error('Either key or user is required.');
}
this.key = options.key;
if (options.user) {
for (const key in options.user) {
this[key] = options.user[key];
}
}
}
/**
*
* @param {string} uri
* @param {string} method
* @param {import('axios').AxiosRequestConfig<any>} options
*/
fetchData(url, method, options) {
axios.request({
url,
method,
headers: {
...(options.headers ? options.headers : {}),
'Authorization': this.key.key
},
...options
});
}
}

View File

@ -0,0 +1,18 @@
import { IAPIObject } from "./IAPIObject";
import { Client } from './Client';
class Tinder extends IAPIObject {
/**
*
* @param {Client} client Client to use to create requests
*/
constructor(client) {
super();
this.client = client;
this.viewed = [];
}
getCompatible() {
}
}

View File

@ -2,6 +2,7 @@ import axios from 'axios';
import constants from '../constants'; import constants from '../constants';
import { IAPIObject } from './IAPIObject'; import { IAPIObject } from './IAPIObject';
import { APIToken } from './APIToken';
const { API_ROOT, api_path } = constants; const { API_ROOT, api_path } = constants;
@ -16,33 +17,6 @@ class UserLoginResponse {
id; id;
} }
class APIToken extends IAPIObject {
static storage_key = 'pairent_api_key';
constructor(data) {
super();
this.user = data.user;
this.key = data.key;
this.expires = data.expires;
this.ip = data.ip;
}
/** @type {number} */
user;
/** @type {string} */
key;
/** A Unix timestamp (when the token will expire)
* @type {number}
*/
expires;
/** @type {string} */
ip;
}
class User extends IAPIObject { class User extends IAPIObject {
isLoggedIn() { isLoggedIn() {

View File

@ -64,7 +64,7 @@ const Header = function () {
</a> </a>
<VerticalLine /> <VerticalLine />
<Link to='/'> <Link to='/'>
<PairentLogo>Pairent</PairentLogo> <PairentLogo>Точка.</PairentLogo>
</Link> </Link>
<Link to="/"> <Link to="/">
<LocationButton> <LocationButton>

View File

@ -1,14 +0,0 @@
import "./styles/PsychTestHeader.css";
const PsychTestHeader = () => {
return (
<div className="header">
<button type="button" className="header__btn">
Вернуться назад
</button>
<h1 className="header__title">Тест на совместимость</h1>
</div>
);
};
export default PsychTestHeader;

View File

@ -1,17 +1,90 @@
import React, { useState } from "react"; import React, { useState } from "react";
import "./styles/PsychTestQuestion.css";
import { styled } from "styled-components";
const Question = styled.div`
padding-top: 4px;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
margin: 0 auto;
width: 799px;
min-height: 151px;
border-bottom: 1px solid #cccccc;
margin-bottom: 34.5px;
& h2 {
color: #cccccc;
position: absolute;
top: 0px;
left: 0;
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
border: 2px solid #cccccc;
border-radius: 100px;
}
`;
const QuestionTitle = styled.p`
margin-bottom: 80px;
text-align: center;
width: 638px;
font-size: 16px;
margin-right: 0;
`;
const Answers = styled.div`
display: flex;
margin-bottom: 29.5px;
& p, & span {
font-size: 14px;
}
& input, & p {
margin-right: 32px;
transform: translateY(6px)
}
& label {
transform: translateY(6px)
}
`;
const Answer = styled.label`
& span {
position: absolute;
top: -17px;
left: 3.5px
}
`;
const Id = styled.div`
color: #cccccc;
position: absolute;
top: 0px;
left: 0;
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
border: 1px solid #b2b4b2;
border-radius: 100px;
`;
const PsychTestQuestion = (props) => { const PsychTestQuestion = (props) => {
const answerChangeHandler = (event) => { const answerChangeHandler = (event) => {
props.onChangeAnswer(event.target.value, event.target.name); props.onChangeAnswer(event.target.value, event.target.name);
}; };
return ( return (
<div className="question"> <Question>
<div className="question__account">{+props.account + 1}</div> <Id>{+props.account + 1}</Id>
<p className="question__title">{props.name}</p> <QuestionTitle>{props.name}</QuestionTitle>
<div className="question__answers"> <Answers>
<p>Не важно</p> <p>Не важно</p>
<label className="question__answer"> <Answer>
<input <input
name={props.account} name={props.account}
type="radio" type="radio"
@ -19,8 +92,8 @@ const PsychTestQuestion = (props) => {
onChange={answerChangeHandler} onChange={answerChangeHandler}
/> />
<span>1</span> <span>1</span>
</label> </Answer>
<label className="question__answer"> <Answer>
<input <input
name={props.account} name={props.account}
type="radio" type="radio"
@ -28,8 +101,8 @@ const PsychTestQuestion = (props) => {
onChange={answerChangeHandler} onChange={answerChangeHandler}
/> />
<span>2</span> <span>2</span>
</label> </Answer>
<label className="question__answer"> <Answer>
<input <input
name={props.account} name={props.account}
type="radio" type="radio"
@ -37,8 +110,8 @@ const PsychTestQuestion = (props) => {
onChange={answerChangeHandler} onChange={answerChangeHandler}
/> />
<span>3</span> <span>3</span>
</label> </Answer>
<label className="question__answer"> <Answer>
<input <input
name={props.account} name={props.account}
type="radio" type="radio"
@ -46,8 +119,8 @@ const PsychTestQuestion = (props) => {
onChange={answerChangeHandler} onChange={answerChangeHandler}
/> />
<span>4</span> <span>4</span>
</label> </Answer>
<label className="question__answer"> <Answer>
<input <input
name={props.account} name={props.account}
type="radio" type="radio"
@ -55,10 +128,10 @@ const PsychTestQuestion = (props) => {
onChange={answerChangeHandler} onChange={answerChangeHandler}
/> />
<span>5</span> <span>5</span>
</label> </Answer>
<p>Очень важно</p> <p>Очень важно</p>
</div> </Answers>
</div> </Question>
); );
}; };

View File

@ -7,8 +7,8 @@ const api_path = path => API_ROOT + path;
* Api root path * Api root path
* @type {string} * @type {string}
*/ */
const API_ROOT = window.location.protocol + '//127.0.0.1:8000'; const API_ROOT = window.location.protocol + '//' + window.location.host;
// ДЛЯ ПРОДА ПОСТАВИТЬ ЭТО: '//pairent.vvsu.ru' // ДЛЯ ПРОДА ПОСТАВИТЬ ЭТО: '//' + window.location.host
/** OpenID Connect Client Config /** OpenID Connect Client Config
* @type {import('oidc-client-ts').OidcClientSettings} * @type {import('oidc-client-ts').OidcClientSettings}

View File

@ -5,13 +5,15 @@ import { Filters } from "../../API/Filters";
import styled from 'styled-components'; import styled from 'styled-components';
// swiper customization
import './swiper.css';
import ApartamentService from "../../API/ApartamentService"; import ApartamentService from "../../API/ApartamentService";
import { HashLoader } from 'react-spinners'; import { HashLoader } from 'react-spinners';
import AwesomeSlider from 'react-awesome-slider'; import AwesomeSlider from 'react-awesome-slider';
import withAutoplay from 'react-awesome-slider/dist/autoplay';
import 'react-awesome-slider/dist/styles.css'; import 'react-awesome-slider/dist/styles.css';
// ras customization
import './ras.css';
const AutoplaySlider = withAutoplay(AwesomeSlider);
import constants from "../../constants"; import constants from "../../constants";
@ -376,10 +378,10 @@ export default class IndexPage extends React.Component {
return ( return (
<IndexPageRoot> <IndexPageRoot>
<SliderContainer> <SliderContainer>
<AwesomeSlider bullets={false}> <AutoplaySlider play cancelOnInteraction={false} interval={8000} bullets={false}>
<div data-src='/images/house-s.jpg' /> <a href='https://vvsu.ithub.ru/dod' data-src='/images/OpenDoorDay.jpg' />
<div data-src='/images/house-s-2.jpg' /> <div data-src='/images/Business.jpg' />
</AwesomeSlider> </AutoplaySlider>
</SliderContainer> </SliderContainer>
<Title> <Title>

View File

@ -0,0 +1,4 @@
.awssld {
--loader-bar-color: #0077aa !important;
/* --loader-bar-height: 8px !important; */
}

View File

@ -1,12 +0,0 @@
:root {
--swiper-scrollbar-drag-bg-color: #e1e3e1a0;
--swiper-scrollbar-size: 5px;
--swiper-scrollbar-bottom: 0px;
--swiper-scrollbar-border-radius: 5px;
--swiper-navigation-color: white
}
.swiper {
box-shadow: 0 2px 12px #00000030;
}

View File

@ -1,9 +1,11 @@
import PsychTestQuestion from "../PsychTestQuestion"; import PsychTestQuestion from "../../../components/PsychTestQuestion";
import React, { useState } from "react"; import React, { useState } from "react";
import PsychTestAddResult from "../../API/PsychTestAddResult"; import PsychTestAddResult from "../../../API/PsychTestAddResult";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import "./styles/PsychTestForm.css"; import "./styles/PsychTestForm.css";
import { styled } from "styled-components";
const PsychTestForm = () => { const PsychTestForm = () => {
const [isValid, setIsValid] = useState(true); const [isValid, setIsValid] = useState(true);
const navigate = useNavigate(); const navigate = useNavigate();

View File

@ -0,0 +1,37 @@
import { styled } from "styled-components";
import "./styles/PsychTestHeader.css";
const Header = styled.div`
display: flex;
padding-left: 40px;
padding-top: 14px;
align-items: center;
margin-bottom: 55px;
& h1 {
font-size: 20pt;
}
& button {
color: #bababa;
font-size: 15px;
width: 180px;
height: 36px;
background-color: white;
border: 1px solid #cccccc;
border-radius: 12px;
margin-right: 13px;
}
`;
const PsychTestHeader = () => {
return (
<Header>
<button type="button" onClick={window.history.back()}>
Вернуться назад
</button>
<h1>Тест на совместимость</h1>
</Header>
);
};
export default PsychTestHeader;

View File

@ -1,5 +1,5 @@
import PsychTestForm from "../../components/PsychTestForm"; import PsychTestForm from "./PsychTestForm";
import PsychTestHeader from "../../components/PsychTestHeader"; import PsychTestHeader from "./PsychTestHeader";
const PsychTest = () => { const PsychTest = () => {
return ( return (

View File

@ -2,6 +2,7 @@ import React from "react";
import styled, { keyframes } from 'styled-components'; import styled, { keyframes } from 'styled-components';
import SVGIcon from "../../components/UI/Icon/SVGIcon"; import SVGIcon from "../../components/UI/Icon/SVGIcon";
import Pagination from "../../components/UI/Pagination"; import Pagination from "../../components/UI/Pagination";
import { HashLoader } from 'react-spinners';
const ChatSVG = () => { const ChatSVG = () => {
return <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M8 15c4.418 0 8-3.134 8-7s-3.582-7-8-7-8 3.134-8 7c0 1.76.743 3.37 1.97 4.6-.097 1.016-.417 2.13-.771 2.966-.079.186.074.394.273.362 2.256-.37 3.597-.938 4.18-1.234A9.06 9.06 0 0 0 8 15z"/></svg>; return <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M8 15c4.418 0 8-3.134 8-7s-3.582-7-8-7-8 3.134-8 7c0 1.76.743 3.37 1.97 4.6-.097 1.016-.417 2.13-.771 2.966-.079.186.074.394.273.362 2.256-.37 3.597-.938 4.18-1.234A9.06 9.06 0 0 0 8 15z"/></svg>;
@ -101,7 +102,6 @@ const UserList = styled.div`
margin: 32px auto; margin: 32px auto;
margin-bottom: 0; margin-bottom: 0;
display: block; display: block;
width: fit-content;
`; `;
@ -214,25 +214,32 @@ const RightBadge = styled(LeftBadge)`
} }
`; `;
class Users extends React.Component { const LoadingText = styled.h2`
text-align: center;
line-height: 3.5em;
margin: 0;
padding: 100px 0;
& span {
display: inline;
margin: 0px auto
}
`;
class UserDisplay extends React.Component {
/** @type {{ value: import('../../API/User').User[] }} */
props;
constructor(props) {
super(props);
}
render() { render() {
return ( return (
<UserList> <div>123
<h2 style={{textAlign: 'center', lineHeight: '11pt', marginBottom: 32}}>
Выбери соседа
<br/>
<br/>
<span style={{ fontSize: '11pt', fontWeight: 500 }}>
Не забывай, с этим человеком<br/>
придется жить бок-о-бок!
</span>
</h2>
{ {
[...Array(11)].map((_, i) => { this.props.value.map(x => {
if (i == 5) return <br/>;
return ( return (
<UserCard> <UserCard>
<SVGIcon src='/images/icons/user.svg' width='100' height='100' /> <SVGIcon src='/images/icons/user.svg' width='100' height='100' />
@ -254,9 +261,50 @@ class Users extends React.Component {
</ContactButton> </ContactButton>
</UserCard> </UserCard>
); )
}) })
} }
</div>
);
}
}
class Users extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [{}],
loading: false
}
}
componentDidMount() {
}
render() {
return (
<UserList>
<h2 style={{textAlign: 'center', lineHeight: '11pt', marginBottom: 32}}>
Выбери соседа
<br/>
<br/>
<span style={{ fontSize: '11pt', fontWeight: 500 }}>
Не забывай, с этим человеком<br/>
придется жить бок-о-бок!
</span>
</h2>
{
this.state.loading ?
<UserDisplay value={this.state.data} /> :
<LoadingText>
Пожалуйста подождите, идет загрузка данных<br/>
<HashLoader color='#0077aa' />
</LoadingText>
}
</UserList> </UserList>
) )

View File

@ -4,6 +4,5 @@ import react from '@vitejs/plugin-react'
export default { export default {
plugins: [ plugins: [
react() react()
], ],
} }

2
static/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

20
static/index.html Normal file
View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Pairent</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<script type="module" crossorigin src="/assets/index-22e3f5a9.js"></script>
<link rel="stylesheet" href="/assets/index-b130f2c5.css">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>