├── .gitignore ├── .travis.yml ├── Procfile ├── README.md ├── __init__.py ├── constants.py ├── developer ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── templates │ ├── developer_create.html │ ├── developer_list.html │ └── games_by_developer.html ├── tests.py ├── urls.py └── views.py ├── examples ├── example1.png └── example2.png ├── final_project_coderslab ├── __init__.py ├── celery.py ├── settings.py ├── storage_backends.py ├── urls.py └── wsgi.py ├── game ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── templates │ ├── game_create.html │ ├── game_details.html │ └── game_list.html ├── tests.py ├── urls.py └── views.py ├── game_recommendation ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ └── __init__.py ├── models.py ├── templates │ ├── recommend_by_rated_games.html │ ├── recommend_by_rated_games_result.html │ ├── recommend_games_manually.html │ └── recommend_games_manually_result.html ├── tests.py ├── urls.py └── views.py ├── genre ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── templates │ ├── games_by_genre.html │ ├── genre_create.html │ └── genre_list.html ├── tests.py ├── urls.py └── views.py ├── locale └── pl │ └── LC_MESSAGES │ └── django.po ├── main_app ├── __init__.py ├── admin.py ├── apps.py ├── context_processor.py ├── migrations │ └── __init__.py ├── models.py ├── tasks.py ├── templates │ ├── 403.html │ ├── about.html │ └── download_games_to_csv_result.html ├── tests.py ├── urls.py └── views.py ├── manage.py ├── media └── csv │ ├── game_data.csv │ └── gaming_quotes.csv ├── news_api ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ └── __init__.py ├── models.py ├── templates │ └── news_main.html ├── tests.py ├── urls.py └── views.py ├── requirements.txt ├── rest_api ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ └── __init__.py ├── models.py ├── serializers.py ├── templates │ └── rest_api_main.html ├── tests.py ├── urls.py └── views.py ├── runtime.txt ├── settings.ini.example ├── setup.cfg ├── static ├── css │ ├── hover-min.css │ └── style.css ├── img │ ├── IGN.jpg │ ├── icon.jpg │ ├── icon.png │ ├── ign.png │ ├── logo.jpg │ ├── new_banner.jpg │ ├── polygon.png │ ├── techradar.png │ └── verge.png └── js │ ├── dropdown_hover.js │ ├── jquery-3.3.1.min.js │ ├── main.js │ └── slider.js ├── tag ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── templates │ ├── games_by_tag.html │ ├── tag_create.html │ └── tag_list.html ├── tests.py ├── urls.py └── views.py ├── templates ├── base.html ├── message_block.html ├── navbar_staff.html ├── navbar_user_authenticated.html └── navbar_user_unauthenticated.html ├── users ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── templates │ ├── user_create.html │ ├── user_list.html │ └── user_login.html ├── tests.py ├── urls.py └── views.py └── utils └── tests.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | ../venv/lib/python3.5/lib-dynload/_testbuffer.cpython-35m-x86_64-linux-gnu.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | ### JetBrains template 108 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 109 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 110 | 111 | # User-specific stuff 112 | .idea/**/workspace.xml 113 | .idea/**/tasks.xml 114 | .idea/**/usage.statistics.xml 115 | .idea/**/dictionaries 116 | .idea/**/shelf 117 | 118 | # Sensitive or high-churn files 119 | .idea/**/dataSources/ 120 | .idea/**/dataSources.ids 121 | .idea/**/dataSources.local.xml 122 | .idea/**/sqlDataSources.xml 123 | .idea/**/dynamic.xml 124 | .idea/**/uiDesigner.xml 125 | .idea/**/dbnavigator.xml 126 | 127 | # Gradle 128 | .idea/**/gradle.xml 129 | .idea/**/libraries 130 | 131 | # Gradle and Maven with auto-import 132 | # When using Gradle or Maven with auto-import, you should exclude module files, 133 | # since they will be recreated, and may cause churn. Uncomment if using 134 | # auto-import. 135 | # .idea/modules.xml 136 | # .idea/*.iml 137 | # .idea/modules 138 | 139 | # CMake 140 | cmake-build-*/ 141 | 142 | # Mongo Explorer plugin 143 | .idea/**/mongoSettings.xml 144 | 145 | # File-based project format 146 | *.iws 147 | 148 | # IntelliJ 149 | out/ 150 | 151 | # mpeltonen/sbt-idea plugin 152 | .idea_modules/ 153 | 154 | # JIRA plugin 155 | atlassian-ide-plugin.xml 156 | 157 | # Cursive Clojure plugin 158 | .idea/replstate.xml 159 | 160 | # Crashlytics plugin (for Android Studio and IntelliJ) 161 | com_crashlytics_export_strings.xml 162 | crashlytics.properties 163 | crashlytics-build.properties 164 | fabric.properties 165 | 166 | # Editor-based Rest Client 167 | .idea/httpRequests 168 | 169 | settings.ini 170 | media/images 171 | staticfiles 172 | .idea 173 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial # required for Python >= 3.7 2 | language: python 3 | python: 4 | - "3.7" 5 | 6 | services: postgresql 7 | 8 | env: 9 | - DJANGO=2.1.2 10 | 11 | before_install: 12 | - export DJANGO_SETTINGS_MODULE=final_project_coderslab.settings 13 | - export PYTHONPATH=$HOME/builds/dawidbudzynski/gameweb_python_django 14 | - export PIP_USE_MIRRORS=true 15 | 16 | install: 17 | - pip install -r requirements.txt 18 | - pip install django==$DJANGO --quiet 19 | - pip install psycopg2 --quiet 20 | 21 | before_script: 22 | - psql -c "CREATE DATABASE travis_db;" -U postgres 23 | 24 | script: 25 | - python manage.py makemigrations 26 | - python manage.py migrate 27 | - python manage.py collectstatic --noinput 28 | - coverage run --source='.' manage.py test 29 | 30 | after_success: 31 | coverage report 32 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn final_project_coderslab.wsgi 2 | worker: celery -A final_project_coderslab worker 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gameweb [![Build Status](https://travis-ci.org/dawidbudzynski/gameweb_python_django.svg?branch=master)](https://travis-ci.org/dawidbudzynski/gameweb_python_django) 2 | 3 | 4 | 5 | ## General info 6 | A web application made using Python 3, Django 2, Bootstrap and REST API. 7 | It's website about technology where user can find interesting news (displayed used API), 8 | add new games to database and find new titles (using recommendation feature). 9 | 10 | ## Main functions 11 | * displaying gaming and tech news - updated every few hours using API 12 | * adding new users and games to database 13 | * recommending new games based on user's preferences 14 | 15 | ## Technologies 16 | * Python 3.7 17 | * Django 2.1.2 18 | * Bootstrap 4 19 | * Django REST Framework 20 | * Celery 21 | * Redis 22 | * AWS S3 23 | * Travis CI 24 | 25 | ## Setup 26 | To run this project: 27 | 1. Create PostgreSQL database 28 | 2. Rename settings.ini.example to settings.ini and fill required fields. 29 | 3. Install required libraries using pip: 30 | ``` 31 | $ pip install -r requirements.txt 32 | ``` 33 | 4. Apply migrations:: 34 | ``` 35 | $ python manage.py migrate 36 | ``` 37 | 5. To run your local server use command: 38 | ``` 39 | $ python manage.py runserver 40 | ``` 41 | 42 | To run asynchronous tasks: 43 | 1. Install Redis 44 | 2. Start Redis server: 45 | ``` 46 | $ redis-server 47 | ``` 48 | 3. Open new terminal tab 49 | 4. Go to project root 50 | 5. Start Celery worker: 51 | ``` 52 | $ celery -A final_project_coderslab worker -l info 53 | ``` 54 | 55 | To run periodic tasks: 56 | 1. Open new terminal tab 57 | 2. Go to project root 58 | 3. Start Celery beat: 59 | ``` 60 | $ celery -A final_project_coderslab beat -l info 61 | ``` 62 | 63 | ## Demo 64 | ## http://dawidb.pythonanywhere.com 65 | 66 | **Gaming and tech news updated every few hours using API from sites like: Polygon, IGN, The Verge and TechRadar** 67 | 68 | ![alt text](https://raw.githubusercontent.com/dawidbudzynski/game_picker_python_django/master/examples/example1.png) 69 | 70 | 71 | **Games recommendation based on user's preferences** 72 | 73 | ![alt text](https://raw.githubusercontent.com/dawidbudzynski/game_picker_python_django/master/examples/example2.png) 74 | 75 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/__init__.py -------------------------------------------------------------------------------- /constants.py: -------------------------------------------------------------------------------- 1 | YEARS = {(x, x) for x in range(1998, 2020)} 2 | SORTED_YEARS = sorted(YEARS, key=lambda x: x[1], reverse=True) 3 | 4 | RATING = {(i, '*' * i) for i in range(1, 11)} 5 | SORTED_RATING = sorted(RATING, key=lambda x: x[0], reverse=True) 6 | 7 | NEWS_SOURCE_DATA_ALL = { 8 | 'ign': { 9 | 'api_name': 'IGN', 10 | 'image_url': 'img/ign.png' 11 | }, 12 | 'polygon': { 13 | 'api_name': 'Polygon', 14 | 'image_url': 'img/polygon.png' 15 | }, 16 | 'techradar': { 17 | 'api_name': 'TechRadar', 18 | 'image_url': 'img/techradar.png' 19 | }, 20 | 'the-verge': { 21 | 'api_name': 'The Verge', 22 | 'image_url': 'img/verge.png' 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /developer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/developer/__init__.py -------------------------------------------------------------------------------- /developer/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Developer 4 | 5 | admin.site.register(Developer) 6 | -------------------------------------------------------------------------------- /developer/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DeveloperConfig(AppConfig): 5 | name = 'developer' 6 | -------------------------------------------------------------------------------- /developer/forms.py: -------------------------------------------------------------------------------- 1 | from django.forms import (CharField, Form) 2 | from django.utils.translation import ugettext_lazy as _ 3 | 4 | 5 | class AddDeveloperForm(Form): 6 | name = CharField(max_length=64, label=_('Name')) 7 | -------------------------------------------------------------------------------- /developer/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.2 on 2019-05-18 10:16 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='Developer', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=64)), 19 | ], 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /developer/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/developer/migrations/__init__.py -------------------------------------------------------------------------------- /developer/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Developer(models.Model): 5 | name = models.CharField(max_length=64) 6 | 7 | def __str__(self): 8 | return self.name 9 | -------------------------------------------------------------------------------- /developer/templates/developer_create.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load bootstrap4 i18n staticfiles %} 3 | {% block content %} 4 | 5 | 6 | 7 | 8 | 9 | 10 |

{% trans 'Create developer' %}

11 | 12 | {% include 'message_block.html' %} 13 | 14 | 21 | 22 |
15 |
16 | {% csrf_token %} 17 | {{ form }} 18 | 19 |
20 |
23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /developer/templates/developer_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load bootstrap4 i18n staticfiles %} 3 | 4 | {% block content %} 5 | 6 | 7 | 8 | 9 | 10 | 11 |

{% trans 'Developers' %}

12 | {% if user.is_authenticated %} 13 | 14 | 15 | 16 | {% trans 'Create developer' %} 18 | 19 | 20 | 21 | {% endif %} 22 |

23 | {% include 'message_block.html' %} 24 | {% for developer in object_list %} 25 | 26 | 27 | 28 | 45 | 46 | 47 |
29 |
31 | 36 | {% if user.is_staff %} 37 | 42 | {% endif %} 43 |
44 |
48 | {% endfor %} 49 | {% endblock %} 50 | -------------------------------------------------------------------------------- /developer/templates/games_by_developer.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load bootstrap4 i18n staticfiles %} 3 | {% block content %} 4 | 5 | 6 | 7 | 8 | 9 | 10 |

{% trans 'Games made by: ' %}{{ selected_developer.name }}

11 | {% for game in all_games_with_developer %} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 28 | 31 | 32 | 33 | 34 | 36 | 37 | 38 | 39 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 54 | 55 | 56 |

{{ game.title }}

{% trans 'Genre' %}

22 | {% for genre in game.genre.all %} 23 |
24 | {{ genre.name }} 25 |
26 | {% endfor %} 27 |
29 | {% trans 'No image' %} 30 |

{% trans 'Developer' %}

35 | {{ game.developer }}

{% trans 'Tags' %}

40 | {% for tag in game.tags.all %} 41 | {{ tag.name }}
42 | {% endfor %} 43 |

{% trans 'Year' %}

{{ game.year }}
51 | {% trans 'Delete' %} 53 |
57 | {% endfor %} 58 | {% endblock %} 59 | -------------------------------------------------------------------------------- /developer/tests.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.test import TestCase 3 | from django.urls import reverse 4 | 5 | from developer.models import Developer 6 | from .forms import AddDeveloperForm 7 | 8 | 9 | class DeveloperFormTests(TestCase): 10 | 11 | def test_developer_form(self): 12 | # redirects to login screen 13 | response = self.client.get(reverse('developer:developer-create')) 14 | self.assertEqual('/users/login?next=/pl/developer/developer_create/', response['location']) 15 | self.assertEquals(response.status_code, 302) 16 | 17 | self.client.force_login(User.objects.get_or_create(username='user_1')[0]) 18 | response = self.client.get(reverse('developer:developer-create')) 19 | self.assertEquals(response.status_code, 200) 20 | 21 | form = AddDeveloperForm() 22 | self.assertFalse(form.is_valid()) 23 | 24 | data = {"name": ""} 25 | form = AddDeveloperForm(data=data) 26 | self.assertFalse(form.is_valid()) 27 | self.assertEqual(form.errors, {'name': ['To pole jest wymagane.']}) 28 | 29 | data = {"name": "dev_1"} 30 | form = AddDeveloperForm(data=data) 31 | self.assertTrue(form.is_valid()) 32 | 33 | 34 | class DeveloperListTests(TestCase): 35 | 36 | def setUp(self): 37 | Developer.objects.create(name='developer_1') 38 | Developer.objects.create(name='developer_2') 39 | 40 | def test_text_content(self): 41 | dev_object = Developer.objects.get(name='developer_1') 42 | expected_object_name = f'{dev_object.name}' 43 | self.assertEquals(expected_object_name, 'developer_1') 44 | 45 | def test_developer_list_view(self): 46 | response = self.client.get(reverse('developer:developer-list')) 47 | self.assertEqual(response.status_code, 200) 48 | self.assertContains(response, 'developer_1') 49 | self.assertEqual(len(response.context[0]['object_list']), 2) 50 | self.assertTemplateUsed(response, 'developer_list.html') 51 | -------------------------------------------------------------------------------- /developer/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = 'developer' 6 | 7 | urlpatterns = [ 8 | path('developer_create/', views.DeveloperCreateView.as_view(), 9 | name='developer-create'), 10 | path('developer_list/', views.DeveloperListView.as_view(), 11 | name='developer-list'), 12 | path('developer_delete/', views.DeveloperDeleteView.as_view(), 13 | name='developer-delete'), 14 | path('games_by_developer/', views.GamesByDeveloperView.as_view(), 15 | name='games-by-developer') 16 | ] 17 | -------------------------------------------------------------------------------- /developer/views.py: -------------------------------------------------------------------------------- 1 | from decouple import config 2 | from django.contrib import messages 3 | from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin 4 | from django.http import HttpResponseRedirect 5 | from django.shortcuts import render 6 | from django.urls import reverse 7 | from django.utils.translation import ugettext as _ 8 | from django.views import View 9 | from django.views.generic.list import ListView 10 | from game.models import Game 11 | 12 | from .forms import AddDeveloperForm 13 | from .models import Developer 14 | 15 | NEWS_API_KEY = config('NEWS_API_KEY', cast=str) 16 | 17 | 18 | class DeveloperListView(ListView): 19 | model = Developer 20 | template_name = 'developer_list.html' 21 | 22 | 23 | class DeveloperCreateView(LoginRequiredMixin, View): 24 | def get(self, request): 25 | return render( 26 | request, 27 | template_name='developer_create.html', 28 | context={'form': AddDeveloperForm().as_p()} 29 | ) 30 | 31 | def post(self, request): 32 | form = AddDeveloperForm(request.POST) 33 | if form.is_valid(): 34 | name = form.cleaned_data['name'] 35 | 36 | if Developer.objects.filter(name=name).exists(): 37 | messages.add_message(request, messages.WARNING, _('Developer with this name already exists')) 38 | return HttpResponseRedirect(reverse('developer:developer-create')) 39 | 40 | Developer.objects.create(name=name) 41 | messages.add_message(request, messages.INFO, _('Developer: {} created successfully').format(name)) 42 | return HttpResponseRedirect(reverse('developer:developer-list')) 43 | 44 | messages.add_message(request, messages.ERROR, _('Form invalid')) 45 | return HttpResponseRedirect(reverse('developer:developer-create')) 46 | 47 | 48 | class DeveloperDeleteView(PermissionRequiredMixin, View): 49 | permission_required = 'developer.delete_developer' 50 | raise_exception = True 51 | 52 | def get(self, request, developer_id): 53 | developer = Developer.objects.get(id=developer_id) 54 | developer.delete() 55 | messages.add_message(request, messages.WARNING, _('Developer: {} has been deleted').format(developer.name)) 56 | return HttpResponseRedirect(reverse('developer:developer-list')) 57 | 58 | 59 | class GamesByDeveloperView(View): 60 | """Display all games with selected developer""" 61 | 62 | def get(self, request, developer_id): 63 | selected_developer = Developer.objects.get(id=developer_id) 64 | ctx = { 65 | 'all_games_with_developer': Game.objects.filter(developer=selected_developer), 66 | 'selected_developer': selected_developer 67 | } 68 | 69 | return render( 70 | request, 71 | template_name='games_by_developer.html', 72 | context=ctx 73 | ) 74 | -------------------------------------------------------------------------------- /examples/example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/examples/example1.png -------------------------------------------------------------------------------- /examples/example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/examples/example2.png -------------------------------------------------------------------------------- /final_project_coderslab/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | # This will make sure the app is always imported when 4 | # Django starts so that shared_task will use this app. 5 | from .celery import app as celery_app 6 | -------------------------------------------------------------------------------- /final_project_coderslab/celery.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import os 4 | 5 | from celery import Celery 6 | from django.conf import settings 7 | 8 | # set the default Django settings module for the 'celery' program. 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'final_project_coderslab.settings') 10 | app = Celery('final_project_coderslab') 11 | 12 | # Using a string here means the worker will not have to 13 | # pickle the object when using Windows. 14 | app.config_from_object('django.conf:settings') 15 | app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) 16 | 17 | 18 | @app.task(bind=True) 19 | def debug_task(self): 20 | print('Request: {0!r}'.format(self.request)) 21 | -------------------------------------------------------------------------------- /final_project_coderslab/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import django_heroku 4 | from decouple import config 5 | from django.utils.translation import ugettext_lazy as _ 6 | 7 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 8 | SECRET_KEY = config('SECRET_KEY', default=False, cast=str) 9 | DEBUG = config('DEBUG', default=False, cast=bool) 10 | 11 | ALLOWED_HOSTS = ['gamewebdjango.herokuapp.com'] 12 | 13 | INSTALLED_APPS = [ 14 | 'django.contrib.admin', 15 | 'django.contrib.auth', 16 | 'django.contrib.contenttypes', 17 | 'django.contrib.sessions', 18 | 'django.contrib.messages', 19 | 'django.contrib.staticfiles', 20 | 21 | 'bootstrap4', 22 | 'debug_toolbar', 23 | 'rest_framework', 24 | 'storages', 25 | 26 | 'developer.apps.DeveloperConfig', 27 | 'game_recommendation.apps.GameRecommendationConfig', 28 | 'game.apps.GameConfig', 29 | 'genre.apps.GenreConfig', 30 | 'main_app.apps.MainAppConfig', 31 | 'news_api.apps.NewsApiConfig', 32 | 'tag.apps.TagConfig', 33 | 'users.apps.UsersConfig', 34 | 'rest_api.apps.RestApiConfig' 35 | ] 36 | 37 | MIDDLEWARE = [ 38 | 'whitenoise.middleware.WhiteNoiseMiddleware', 39 | 'django.middleware.security.SecurityMiddleware', 40 | 'django.contrib.sessions.middleware.SessionMiddleware', 41 | 'django.middleware.locale.LocaleMiddleware', 42 | 'django.middleware.common.CommonMiddleware', 43 | 'django.middleware.csrf.CsrfViewMiddleware', 44 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 45 | 'django.contrib.messages.middleware.MessageMiddleware', 46 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 47 | 'debug_toolbar.middleware.DebugToolbarMiddleware', 48 | ] 49 | 50 | ROOT_URLCONF = 'final_project_coderslab.urls' 51 | 52 | TEMPLATES = [ 53 | { 54 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 55 | 'DIRS': [os.path.join(BASE_DIR, 'templates')], 56 | 'APP_DIRS': True, 57 | 'OPTIONS': { 58 | 'context_processors': [ 59 | 'django.template.context_processors.debug', 60 | 'django.template.context_processors.request', 61 | 'django.contrib.auth.context_processors.auth', 62 | 'django.contrib.messages.context_processors.messages', 63 | 'django.template.context_processors.i18n', 64 | 'main_app.context_processor.gaming_quotes_processor' 65 | ], 66 | }, 67 | }, 68 | ] 69 | 70 | WSGI_APPLICATION = 'final_project_coderslab.wsgi.application' 71 | 72 | # DATABASE SETTINGS 73 | TRAVIS_ENVIRONMENT = 'TRAVIS' in os.environ 74 | WORK_ON_POSTGRE_SQL = config('WORK_ON_POSTGRE_SQL', default=False, cast=str) 75 | if TRAVIS_ENVIRONMENT: 76 | DATABASES = { 77 | 'default': { 78 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 79 | 'NAME': 'travis_db', 80 | 'USER': 'postgres', 81 | 'PASSWORD': '', 82 | 'HOST': 'localhost', 83 | 'PORT': '', 84 | } 85 | } 86 | elif WORK_ON_POSTGRE_SQL: 87 | DATABASES = { 88 | 'default': { 89 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 90 | 'NAME': 'game_db', 91 | 'USER': 'postgres', 92 | 'PASSWORD': 'postgres', 93 | 'HOST': 'localhost', 94 | 'PORT': '', 95 | } 96 | } 97 | else: 98 | DATABASES = { 99 | 'default': { 100 | 'ENGINE': 'django.db.backends.sqlite3', 101 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 102 | } 103 | } 104 | 105 | AUTH_PASSWORD_VALIDATORS = [ 106 | { 107 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 108 | }, 109 | { 110 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 111 | }, 112 | { 113 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 114 | }, 115 | { 116 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 117 | }, 118 | ] 119 | 120 | # Localization 121 | LANGUAGE_CODE = 'pl' 122 | LANGUAGES = ( 123 | ('en', _('English')), 124 | ('pl', _('Polish')), 125 | ) 126 | TIME_ZONE = 'Europe/Warsaw' 127 | USE_I18N = True 128 | USE_L10N = True 129 | USE_TZ = True 130 | LOCALE_PATHS = (os.path.join(BASE_DIR, 'locale'),) 131 | 132 | MEDIA_URL = '/media/' 133 | MEDIA_ROOT = (os.path.join(BASE_DIR, 'media')) 134 | TEMPLATE_DIRS = (os.path.join(BASE_DIR, 'templates'),) 135 | 136 | SESSION_EXPIRE_AT_BROWSER_CLOSE = True 137 | 138 | LOGIN_URL = '/users/login' 139 | LOGIN_REDIRECT_URL = '/users/login' 140 | 141 | # Activate Django-Heroku. 142 | django_heroku.settings(locals()) 143 | 144 | # internal_ips for django-debug-toolbar 145 | INTERNAL_IPS = '127.0.0.1' 146 | 147 | # CELERY CONFIG 148 | if DEBUG: 149 | BROKER_URL = 'redis://localhost:6379' 150 | CELERY_RESULT_BACKEND = 'redis://localhost:6379' 151 | else: 152 | BROKER_URL = config('BROKER_URL', default=False, cast=str) 153 | CELERY_RESULT_BACKEND = config('CELERY_RESULT_BACKEND', default=False, cast=str) 154 | CELERY_ACCEPT_CONTENT = ['application/json'] 155 | CELERY_TASK_SERIALIZER = 'json' 156 | CELERY_RESULT_SERIALIZER = 'json' 157 | CELERY_TIMEZONE = 'Europe/Warsaw' 158 | 159 | # AWS CONFIG 160 | STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] 161 | if TRAVIS_ENVIRONMENT: 162 | STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') 163 | STATIC_URL = '/static/' 164 | else: 165 | AWS_LOCATION = 'static' 166 | AWS_ACCESS_KEY_ID = config('AWS_ACCESS_KEY_ID', default=False, cast=str) 167 | AWS_SECRET_ACCESS_KEY = config('AWS_SECRET_ACCESS_KEY', default=False, cast=str) 168 | AWS_STORAGE_BUCKET_NAME = config('AWS_STORAGE_BUCKET_NAME', default=False, cast=str) 169 | AWS_S3_CUSTOM_DOMAIN = '%s.s3.amazonaws.com' % AWS_STORAGE_BUCKET_NAME 170 | AWS_S3_OBJECT_PARAMETERS = { 171 | 'CacheControl': 'max-age=86400', 172 | } 173 | DEFAULT_FILE_STORAGE = 'final_project_coderslab.storage_backends.MediaStorage' 174 | STATICFILES_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" 175 | STATIC_URL = 'https://%s/%s/' % (AWS_S3_CUSTOM_DOMAIN, AWS_LOCATION) 176 | ADMIN_MEDIA_PREFIX = STATIC_URL + 'admin/' 177 | STATICFILES_FINDERS = ( 178 | 'django.contrib.staticfiles.finders.FileSystemFinder', 179 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 180 | ) 181 | AWS_DEFAULT_ACL = None 182 | -------------------------------------------------------------------------------- /final_project_coderslab/storage_backends.py: -------------------------------------------------------------------------------- 1 | from storages.backends.s3boto3 import S3Boto3Storage 2 | 3 | 4 | class MediaStorage(S3Boto3Storage): 5 | location = 'media' 6 | file_overwrite = False 7 | -------------------------------------------------------------------------------- /final_project_coderslab/urls.py: -------------------------------------------------------------------------------- 1 | import debug_toolbar 2 | from django.conf import settings 3 | from django.conf.urls.i18n import i18n_patterns 4 | from django.conf.urls.static import static 5 | from django.contrib import admin 6 | from django.urls import include, path 7 | 8 | urlpatterns = [] 9 | 10 | urlpatterns += i18n_patterns( 11 | path('admin/', admin.site.urls), 12 | path('', include("news_api.urls", namespace="news_api-mainpage")), 13 | path('developer/', include("developer.urls", namespace="developer")), 14 | path('game/', include("game.urls", namespace="game")), 15 | path('genre/', include("genre.urls", namespace="genre")), 16 | path('main_app/', include("main_app.urls", namespace="main_app")), 17 | path('news_api/', include("news_api.urls", namespace="news_api")), 18 | path('recommendation/', include("game_recommendation.urls", namespace="game_recommendation")), 19 | path('tag/', include("tag.urls", namespace="tag")), 20 | path('users/', include("users.urls", namespace="users")), 21 | path('rest_api/', include("rest_api.urls", namespace="rest_api")) 22 | ) 23 | if settings.DEBUG: 24 | urlpatterns = [path('__debug__/', include(debug_toolbar.urls)), ] + urlpatterns 25 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 26 | -------------------------------------------------------------------------------- /final_project_coderslab/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for projekt_koncowy project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "final_project_coderslab.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /game/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/game/__init__.py -------------------------------------------------------------------------------- /game/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Game, GameScore 4 | 5 | admin.site.register(Game) 6 | admin.site.register(GameScore) 7 | -------------------------------------------------------------------------------- /game/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class GameConfig(AppConfig): 5 | name = 'game' 6 | -------------------------------------------------------------------------------- /game/forms.py: -------------------------------------------------------------------------------- 1 | from django.forms import (CharField, CheckboxSelectMultiple, ChoiceField, Form, ImageField, ModelChoiceField, 2 | ModelMultipleChoiceField, NullBooleanField, Select) 3 | 4 | from constants import SORTED_RATING, SORTED_YEARS 5 | from django.utils.translation import ugettext_lazy as _ 6 | from developer.models import Developer 7 | from genre.models import Genre 8 | from tag.models import Tag 9 | 10 | all_tags = Tag.objects.all() 11 | all_developers = Developer.objects.all() 12 | all_genres = Genre.objects.all() 13 | 14 | 15 | class RateGameForm(Form): 16 | score = ChoiceField(choices=SORTED_RATING) 17 | 18 | 19 | class AddGameForm(Form): 20 | title = CharField(max_length=64) 21 | year = ChoiceField(choices=SORTED_YEARS) 22 | developer = ModelChoiceField( 23 | queryset=all_developers.order_by('name'), 24 | widget=Select 25 | ) 26 | genre = ModelChoiceField( 27 | queryset=all_genres.order_by('name'), 28 | widget=Select 29 | ) 30 | tags = ModelMultipleChoiceField( 31 | label=_('Tags (Select 6)'), 32 | queryset=all_tags.order_by('name'), 33 | widget=CheckboxSelectMultiple(attrs={'class': 'checkboxmultiple'}) 34 | ) 35 | image = ImageField(required=False) 36 | to_be_rated = NullBooleanField(label=_('Should be rated?'), required=False) 37 | -------------------------------------------------------------------------------- /game/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.2 on 2019-05-18 10:16 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('tag', '0001_initial'), 13 | ('genre', '0001_initial'), 14 | ('developer', '0001_initial'), 15 | ('users', '0001_initial'), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='Game', 21 | fields=[ 22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('title', models.CharField(max_length=64)), 24 | ('year', models.IntegerField(choices=[(2019, 2019), (2018, 2018), (2017, 2017), (2016, 2016), (2015, 2015), (2014, 2014), (2013, 2013), (2012, 2012), (2011, 2011), (2010, 2010), (2009, 2009), (2008, 2008), (2007, 2007), (2006, 2006), (2005, 2005), (2004, 2004), (2003, 2003), (2002, 2002), (2001, 2001), (2000, 2000), (1999, 1999), (1998, 1998)])), 25 | ('image', models.ImageField(blank=True, upload_to='images/')), 26 | ('to_be_rated', models.NullBooleanField(default=False)), 27 | ('developer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='developer.Developer')), 28 | ('genre', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='genre.Genre')), 29 | ('tags', models.ManyToManyField(to='tag.Tag')), 30 | ], 31 | ), 32 | migrations.CreateModel( 33 | name='GameScore', 34 | fields=[ 35 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 36 | ('score', models.IntegerField(blank=True, choices=[(10, '**********'), (9, '*********'), (8, '********'), (7, '*******'), (6, '******'), (5, '*****'), (4, '****'), (3, '***'), (2, '**'), (1, '*')])), 37 | ('game', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='game.Game')), 38 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.User')), 39 | ], 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /game/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/game/migrations/__init__.py -------------------------------------------------------------------------------- /game/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from constants import SORTED_RATING, SORTED_YEARS 4 | from developer.models import Developer 5 | from genre.models import Genre 6 | from tag.models import Tag 7 | from users.models import User 8 | 9 | 10 | class Game(models.Model): 11 | title = models.CharField(max_length=64) 12 | year = models.IntegerField(choices=SORTED_YEARS) 13 | developer = models.ForeignKey(Developer, on_delete=models.CASCADE) 14 | genre = models.ForeignKey(Genre, on_delete=models.CASCADE) 15 | tags = models.ManyToManyField(Tag) 16 | image = models.ImageField(upload_to='images/', blank=True) 17 | to_be_rated = models.NullBooleanField(null=True, default=False) 18 | 19 | def __str__(self): 20 | return self.title 21 | 22 | 23 | class GameScore(models.Model): 24 | user = models.ForeignKey(User, on_delete=models.CASCADE) 25 | game = models.ForeignKey(Game, on_delete=models.CASCADE) 26 | score = models.IntegerField(choices=SORTED_RATING, blank=True) 27 | 28 | def __str__(self): 29 | return '{}-{}-{}'.format(self.user, self.game, self.score) 30 | -------------------------------------------------------------------------------- /game/templates/game_create.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load bootstrap4 i18n staticfiles %} 3 | {% block content %} 4 | 5 | 6 | 7 | 8 | 9 | 10 |

{% trans 'Add game' %}

11 | 12 | 13 | 20 | 21 |
14 |
15 | {% csrf_token %} 16 | {{ form }} 17 | 18 |
19 |
22 | 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /game/templates/game_details.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load bootstrap4 i18n staticfiles %} 3 | {% block content %} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 18 | 20 | 21 | 22 | 23 | 25 | 26 | 27 | 28 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 47 | 48 | {% if user.is_authenticated %} 49 | 50 | 51 | 58 | 59 | {% endif %} 60 | {% if user.is_staff %} 61 | 62 | 67 | {% endif %} 68 | 69 |

{{ game.title }}

{% trans 'Genre' %}

14 | {% for genre in game.genre.all %} 15 |
{{ genre.name }}
16 | {% endfor %} 17 |
19 | {% trans 'No image' %}

{% trans 'Developer' %}

24 | {{ game.developer }}

{% trans 'Tags' %}

29 | {% for tag in game.tags.all %} 30 | {{ tag.name }}
31 | {% endfor %} 32 |

{% trans 'Year' %}

{{ game.year }}

{% trans 'Rating' %}

41 | {% if gamescore %} 42 |
{{ gamescore.score }}
43 | {% else %} 44 |
{% trans 'No rating' %}
45 | {% endif %} 46 |

{% trans 'Rate game' %}

52 |
53 | {% csrf_token %} 54 | {{ form.as_p }} 55 | 56 |
57 |
63 | {% trans 'Delete' %} 65 | 66 |
70 | {% endblock %} 71 | -------------------------------------------------------------------------------- /game/templates/game_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load bootstrap4 i18n staticfiles %} 3 | {% block content %} 4 | 5 | 6 | 7 | 8 | 9 | 10 |

{% trans 'Games' %}

11 | {% if user.is_authenticated %} 12 | 13 | 14 | 15 | {% trans 'Add game' %} 17 | 18 | 19 | 20 | 21 | {% endif %} 22 |

23 | {% include 'message_block.html' %} 24 | {% for game in object_list %} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 39 | {% if game.image %} 40 | 44 | {% else %} 45 | 49 | {% endif %} 50 | 51 | 52 | 53 | 56 | 57 | 58 | 59 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 74 | 75 | {% if user.is_staff %} 76 | 77 | 81 | 82 | {% endif %} 83 | 84 |

{{ game.title }}

{% trans 'Genre' %}

35 | {% for genre in game.genre.all %} 36 |
{{ genre.name }}
37 | {% endfor %} 38 |
41 | {% trans 'No image' %} 43 | 46 | {% trans 'No image' %} 48 |

{% trans 'Developer' %}

54 | {{ game.developer }}
55 |

{% trans 'Tags' %}

60 | {% for tag in game.tags.all %} 61 | {{ tag.name }}
62 | {% endfor %} 63 |

{% trans 'Year' %}

{{ game.year }}
71 | {% trans 'Details' %} 73 |
78 | {% trans 'Delete' %} 80 |
85 | {% endfor %} 86 | {% endblock %} 87 | -------------------------------------------------------------------------------- /game/tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from django.conf import settings 4 | from django.contrib.auth.models import User as DjangoUser 5 | from django.test import TestCase 6 | from django.urls import reverse 7 | 8 | from developer.models import Developer 9 | from game.models import Game 10 | from genre.models import Genre 11 | from tag.models import Tag 12 | from users.models import User 13 | from .forms import AddGameForm 14 | 15 | 16 | class GameFormTests(TestCase, unittest.TestCase): 17 | 18 | def setUp(self): 19 | django_user = DjangoUser.objects.create( 20 | username='user_1', 21 | password='password_1', 22 | first_name='first_name_1', 23 | last_name='last_name_1', 24 | email='email_1' 25 | ) 26 | self.user = User.objects.create(user=django_user) 27 | 28 | Genre.objects.create(name='genre_1') 29 | Developer.objects.create(name='developer_1') 30 | for i in range(1, 7): 31 | Tag.objects.create(name=f'tag_{i}') 32 | 33 | game_1 = Game.objects.create( 34 | developer=Developer.objects.get(name='developer_1'), 35 | genre=Genre.objects.get(name='genre_1'), 36 | title='game_1', 37 | year=2015, 38 | to_be_rated=False 39 | ) 40 | for tag in Tag.objects.all(): 41 | game_1.tags.add(tag) 42 | game_1.save() 43 | 44 | def test_game_form(self): 45 | # redirects to login screen 46 | response = self.client.get(reverse('game:game-create')) 47 | self.assertEqual('/users/login?next=/pl/game/game_create/', response['location']) 48 | self.assertEquals(response.status_code, 302) 49 | 50 | self.client.force_login(DjangoUser.objects.get_or_create(username='user_1')[0]) 51 | response = self.client.get(reverse('game:game-create')) 52 | self.assertEquals(response.status_code, 200) 53 | 54 | form = AddGameForm() 55 | self.assertFalse(form.is_valid()) 56 | 57 | data = {"name": ""} 58 | form = AddGameForm(data=data) 59 | self.assertFalse(form.is_valid()) 60 | self.assertEqual(form.errors, { 61 | 'developer': ['To pole jest wymagane.'], 62 | 'genre': ['To pole jest wymagane.'], 63 | 'tags': ['To pole jest wymagane.'], 64 | 'title': ['To pole jest wymagane.'], 65 | 'year': ['To pole jest wymagane.'] 66 | }) 67 | 68 | data = { 69 | 'developer': Developer.objects.get(name='developer_1').id, 70 | 'genre': Genre.objects.get(name='genre_1').id, 71 | 'tags': [ 72 | Tag.objects.get(name='tag_1'), 73 | Tag.objects.get(name='tag_2'), 74 | Tag.objects.get(name='tag_3'), 75 | Tag.objects.get(name='tag_4'), 76 | Tag.objects.get(name='tag_5'), 77 | Tag.objects.get(name='tag_6') 78 | ], 79 | 'title': 'game_1', 80 | 'year': 2015, 81 | 'to_be_rated': False 82 | } 83 | form = AddGameForm(data=data) 84 | self.assertTrue(form.is_valid()) 85 | 86 | def test_created_object(self): 87 | game_object = Game.objects.get(title='game_1') 88 | developer_id = 5 if (settings.WORK_ON_POSTGRE_SQL or settings.TRAVIS_ENVIRONMENT) else 1 89 | self.assertDictContainsSubset({ 90 | 'developer_id': developer_id, 91 | 'genre_id': 1, 92 | 'id': 1, 93 | 'image': '', 94 | 'title': 'game_1', 95 | 'to_be_rated': False, 96 | 'year': 2015}, 97 | game_object.__dict__ 98 | ) 99 | 100 | def test_game_list_view(self): 101 | response = self.client.get(reverse('game:game-list')) 102 | self.assertEqual(response.status_code, 200) 103 | self.assertContains(response, 'game_1') 104 | self.assertEqual(len(response.context[0]['object_list']), 1) 105 | self.assertTemplateUsed(response, 'game_list.html') 106 | -------------------------------------------------------------------------------- /game/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = 'game' 6 | 7 | urlpatterns = [ 8 | path('game_create/', views.GameCreateView.as_view(), 9 | name='game-create'), 10 | path('game_list/', views.GameListView.as_view(), 11 | name='game-list'), 12 | path('game_details/', views.GameDetails.as_view(), 13 | name='game-details'), 14 | path('game_delete/', views.GameDeleteView.as_view(), 15 | name='game-delete'), 16 | ] 17 | -------------------------------------------------------------------------------- /game/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin 3 | from django.http import HttpResponseRedirect 4 | from django.shortcuts import render 5 | from django.urls import reverse 6 | from django.utils.translation import ugettext as _ 7 | from django.views import View 8 | from django.views.generic import ListView 9 | 10 | from users.models import User 11 | from .forms import AddGameForm, RateGameForm 12 | from .models import Game, GameScore 13 | 14 | 15 | class GameListView(ListView): 16 | model = Game 17 | template_name = 'game_list.html' 18 | 19 | 20 | class GameCreateView(LoginRequiredMixin, View): 21 | def get(self, request): 22 | return render( 23 | request, 24 | template_name='game_create.html', 25 | context={'form': AddGameForm().as_p()} 26 | ) 27 | 28 | def post(self, request): 29 | form = AddGameForm(request.POST, request.FILES) 30 | if form.is_valid(): 31 | title = form.cleaned_data['title'] 32 | year = form.cleaned_data['year'] 33 | developer = form.cleaned_data['developer'] 34 | genre = form.cleaned_data['genre'] 35 | tags = form.cleaned_data['tags'] 36 | image = form.cleaned_data['image'] 37 | to_be_rated = form.cleaned_data['to_be_rated'] 38 | 39 | if Game.objects.filter(title=title).exists(): 40 | messages.add_message(request, messages.WARNING, _('Game with this name already exists')) 41 | return HttpResponseRedirect(reverse('game:game-create')) 42 | 43 | new_game = Game.objects.create( 44 | title=title, 45 | year=year, 46 | developer=developer, 47 | genre=genre, 48 | image=image, 49 | to_be_rated=to_be_rated 50 | ) 51 | 52 | for tag in tags: 53 | new_game.tags.add(tag) 54 | 55 | messages.add_message(request, messages.INFO, _('Game: {} created successfully').format(title)) 56 | return HttpResponseRedirect(reverse('game:game-list')) 57 | 58 | messages.add_message(request, messages.ERROR, _('Form invalid')) 59 | return HttpResponseRedirect(reverse('game:game-create')) 60 | 61 | 62 | class GameDetails(View): 63 | def get(self, request, game_id): 64 | game = Game.objects.get(id=game_id) 65 | user = User.objects.get(id=request.user.id) 66 | gamescore_qs = GameScore.objects.filter(game=game, user=user) 67 | gamescore = gamescore_qs.first() if gamescore_qs.exists() else None 68 | 69 | ctx = { 70 | 'game': game, 71 | 'gamescore': gamescore, 72 | 'form': RateGameForm() 73 | } 74 | 75 | return render( 76 | request, 77 | template_name='game_details.html', 78 | context=ctx 79 | ) 80 | 81 | def post(self, request, game_id): 82 | """Get rating from form and create or update gamescore""" 83 | form = RateGameForm(request.POST) 84 | try: 85 | user = User.objects.get(user=request.user) 86 | except User.DoesNotExist: 87 | messages.add_message(request, messages.INFO, _('User {} can\'t rate games').format(request.user)) 88 | return HttpResponseRedirect(reverse('game:game-list')) 89 | game = Game.objects.get(id=game_id) 90 | try: 91 | old_gamescore = GameScore.objects.get(game=game, user=user) 92 | except GameScore.DoesNotExist: 93 | old_gamescore = None 94 | if form.is_valid(): 95 | score = form.cleaned_data['score'] 96 | if old_gamescore is not None: 97 | old_gamescore.score = score 98 | old_gamescore.save() 99 | gamescore = old_gamescore 100 | else: 101 | gamescore = GameScore.objects.create( 102 | user=user, 103 | game=game, 104 | score=score 105 | ) 106 | 107 | ctx = { 108 | 'game': game, 109 | 'form': form, 110 | 'gamescore': gamescore 111 | } 112 | 113 | return render( 114 | request, 115 | template_name='game_details.html', 116 | context=ctx 117 | ) 118 | 119 | 120 | class GameDeleteView(PermissionRequiredMixin, View): 121 | permission_required = 'game.delete_game' 122 | raise_exception = True 123 | 124 | def get(self, request, game_id): 125 | game = Game.objects.get(id=game_id) 126 | game.delete() 127 | return HttpResponseRedirect(reverse('game:game-list')) 128 | -------------------------------------------------------------------------------- /game_recommendation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/game_recommendation/__init__.py -------------------------------------------------------------------------------- /game_recommendation/admin.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/game_recommendation/admin.py -------------------------------------------------------------------------------- /game_recommendation/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class GameRecommendationConfig(AppConfig): 5 | name = 'game_recommendation' 6 | -------------------------------------------------------------------------------- /game_recommendation/forms.py: -------------------------------------------------------------------------------- 1 | from django.forms import (CheckboxSelectMultiple, Form, ModelChoiceField, ModelMultipleChoiceField, Select) 2 | from django.utils.translation import ugettext_lazy as _ 3 | 4 | from developer.models import Developer 5 | from genre.models import Genre 6 | from tag.models import Tag 7 | 8 | all_tags = Tag.objects.all() 9 | all_developers = Developer.objects.all() 10 | all_genres = Genre.objects.all() 11 | 12 | 13 | class ChooseTagsForm(Form): 14 | genre = ModelChoiceField( 15 | queryset=all_genres.order_by('name'), 16 | widget=Select 17 | ) 18 | developer = ModelChoiceField( 19 | queryset=all_developers.order_by('name'), 20 | widget=Select 21 | ) 22 | tags = ModelMultipleChoiceField( 23 | label=_('Tags (Select 6)'), 24 | queryset=all_tags.order_by('name'), 25 | widget=CheckboxSelectMultiple(attrs={'class': 'checkboxmultiple'}) 26 | ) 27 | -------------------------------------------------------------------------------- /game_recommendation/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/game_recommendation/migrations/__init__.py -------------------------------------------------------------------------------- /game_recommendation/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/game_recommendation/models.py -------------------------------------------------------------------------------- /game_recommendation/templates/recommend_by_rated_games.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load bootstrap4 i18n staticfiles %} 3 | {% block content %} 4 |
5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | {% for game in to_be_rated %} 14 | 15 | 18 | 21 | {% csrf_token %} 22 | 34 | 35 | {% endfor %} 36 | 37 | 40 | 41 | 42 |

{% trans 'Rate games' %}

9 |
16 | {% trans 'No image' %} 17 | 19 |

{{ game.title }}

20 |
23 |
24 | 28 | 32 |
33 |
38 | 39 |
43 |
44 | {% endblock %} 45 | -------------------------------------------------------------------------------- /game_recommendation/templates/recommend_by_rated_games_result.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load bootstrap4 i18n staticfiles %} 3 | {% block content %} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 27 | 32 | 33 | 34 |

{% trans 'According to your answers' %}

{% trans 'Favorite genre' %}

{% trans 'Favorite developer' %}

{% trans 'Favorite tags' %}

16 | {% for genre in user_favorites.favorite_genre %} 17 | 18 | {{ genre.name }}
19 | {% endfor %} 20 |
22 | {% for developer in user_favorites.favorite_developer %} 23 | 24 | {{ developer.name }}
25 | {% endfor %} 26 |
28 | {% for tag in user_favorites.favorite_tags_top_6 %} 29 | {{ tag.name }}
30 | {% endfor %} 31 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |

{% trans 'Games you may like' %}

43 | {% for single_game_info in sorted_all_game_information %} 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 67 | 68 | 69 | 70 | 71 | 72 | 87 | 91 | 92 | 93 | 94 | 111 | 112 | 113 | 114 | 121 | 122 | 123 | 124 | 131 | 132 | 133 | 134 | 135 | 136 | 137 |

{{ single_game_info.game.title }}

55 |
56 |

{% trans 'Score:' %} {{ single_game_info.match_score }} 57 | {% if single_game_info.match_score >= 60 %} 58 | thumb_up 59 | {% elif single_game_info.match_score > 35 %} 60 | thumbs_up_down 61 | {% else %} 62 | thumb_down 63 | {% endif %} 64 |

65 |
66 |

{% trans 'Genre' %}

73 |

74 | {{ single_game_info.game.genre.name }}

75 | {% if single_game_info.genre_match == True %} 76 | 77 | {% trans 'Match' %} 78 | sentiment_very_satisfied 79 |
80 | {% else %} 81 | 82 | {% trans 'No match' %} 83 | sentiment_very_dissatisfied 84 |
85 | {% endif %} 86 |
88 | {% trans 'No image' %} 90 |

{% trans 'Developer' %}

95 |

96 | 97 | {{ single_game_info.game.developer }} 98 |

99 | {% if single_game_info.developer_match == True %} 100 | 101 | {% trans 'Match' %} 102 | sentiment_very_satisfied 103 |
104 | {% else %} 105 | 106 | {% trans 'No match' %} 107 | sentiment_very_dissatisfied 108 |
109 | {% endif %} 110 |

{% trans 'Matching tags' %}

115 | {% for game_tag in single_game_info.matched_tags %} 116 | {{ game_tag.name }} 118 |
119 | {% endfor %} 120 |

{% trans 'Other tags' %}

125 | {% for game_tag in single_game_info.unmatched_tags %} 126 | {{ game_tag.name }} 128 |
129 | {% endfor %} 130 |

{% trans 'Year' %}

{{ single_game_info.game.year }}

138 | {% endfor %} 139 | {% endblock %} 140 | -------------------------------------------------------------------------------- /game_recommendation/templates/recommend_games_manually.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load bootstrap4 i18n staticfiles %} 3 | {% block content %} 4 | 5 | 6 | 7 | 8 | 9 | 10 |

{% trans 'Choose your preferences' %}

11 | 12 | 13 | 20 | 21 |
14 |
15 | {% csrf_token %} 16 | {{ form }} 17 | 18 |
19 |
22 | 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /game_recommendation/templates/recommend_games_manually_result.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load bootstrap4 i18n staticfiles %} 3 | {% block content %} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 21 | 26 | 27 | 28 |

{% trans 'According to your answers' %}

{% trans 'Favorite game' %}

{% trans 'Favorite developer' %}

{% trans 'Favorite tags' %}

16 | {{ user_favorites.favorite_genre }}

17 |

19 | {{ user_favorites.favorite_developer }}

20 |
22 | {% for tag in user_favorites.favorite_tags_top_6 %} 23 | {{ tag.name }}
24 | {% endfor %} 25 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |

{% trans 'Games you may like' %}

37 | {% for single_game_information in sorted_all_game_information %} 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 61 | 62 | 63 | 64 | 65 | 66 | 82 | 85 | 86 | 87 | 88 | 105 | 106 | 107 | 108 | 114 | 115 | 116 | 117 | 123 | 124 | 125 | 126 | 127 | 128 | 129 |

{{ single_game_information.game.title }}

49 |
50 |

{% trans 'Score' %}: {{ single_game_information.match_score }} 51 | {% if single_game_information.match_score >= 70 %} 52 | thumb_up 53 | {% elif single_game_information.match_score > 50 %} 54 | thumbs_up_down 55 | {% else %} 56 | thumb_down 57 | {% endif %} 58 |

59 |
60 |

{% trans 'Genre' %}

67 | {% for genre in single_game_information.game.genre.all %} 68 |

{{ genre.name }}

69 | {% endfor %} 70 | {% if single_game_information.genre_match == True %} 71 | 72 | {% trans 'Match' %} 73 | sentiment_very_satisfied 74 |
75 | {% else %} 76 | 77 | {% trans 'No match' %} 78 | sentiment_very_dissatisfied 79 |
80 | {% endif %} 81 |
83 | 84 |

{% trans 'Developer' %}

89 |

90 | {{ single_game_information.game.developer }} 91 |

92 | {% if single_game_information.developer_match == True %} 93 | 94 | {% trans 'Match' %} 95 | sentiment_very_satisfied 96 |
97 | {% else %} 98 | 99 | {% trans 'No match' %} 100 | sentiment_very_dissatisfied 101 |
102 | {% endif %} 103 | 104 |

{% trans 'Matching tags' %}

109 | {% for game_tag in single_game_information.matched_tags %} 110 | {{ game_tag.name }}
112 | {% endfor %} 113 |

{% trans 'Other tags' %}

118 | {% for game_tag in single_game_information.unmatched_tags %} 119 | {{ game_tag.name }}
121 | {% endfor %} 122 |

{% trans 'Year' %}

{{ single_game_information.game.year }}

130 | {% endfor %} 131 | {% endblock %} 132 | -------------------------------------------------------------------------------- /game_recommendation/tests.py: -------------------------------------------------------------------------------- 1 | import random 2 | import unittest 3 | 4 | from django.test import TestCase 5 | from django.urls import reverse 6 | 7 | from developer.models import Developer 8 | from genre.models import Genre 9 | from tag.models import Tag 10 | from utils.tests import populate_database 11 | from .forms import ChooseTagsForm 12 | 13 | 14 | class GameRecommendationTests(TestCase, unittest.TestCase): 15 | 16 | def setUp(self): 17 | populate_database() 18 | 19 | def test_recommendation_manually_content(self): 20 | response = self.client.get(reverse('game_recommendation:recommend-manually')) 21 | self.assertEquals(response.status_code, 200) 22 | 23 | form = ChooseTagsForm() 24 | self.assertFalse(form.is_valid()) 25 | 26 | data = {"name": ""} 27 | form = ChooseTagsForm(data=data) 28 | self.assertFalse(form.is_valid()) 29 | self.assertEqual( 30 | form.errors, 31 | {'developer': ['To pole jest wymagane.'], 32 | 'genre': ['To pole jest wymagane.'], 33 | 'tags': ['To pole jest wymagane.']} 34 | ) 35 | 36 | tag_list = [] 37 | for i in random.sample(range(1, 21), 6): 38 | tag_list.append(Tag.objects.get(name=f'tag_{i}')) 39 | 40 | data = { 41 | 'developer': Developer.objects.get(name='developer_7').id, 42 | 'genre': Genre.objects.get(name='genre_9').id, 43 | 'tags': tag_list 44 | } 45 | form = ChooseTagsForm(data=data) 46 | self.assertTrue(form.is_valid()) 47 | -------------------------------------------------------------------------------- /game_recommendation/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = 'game_recommendation' 6 | 7 | urlpatterns = [ 8 | path('recommend_game_manually', views.RecommendGamesManuallyView.as_view(), 9 | name='recommend-manually'), 10 | path('recommend_game_by_rating', views.RecommendByRatedGamesView.as_view(), 11 | name='recommend-by-rating') 12 | ] 13 | -------------------------------------------------------------------------------- /game_recommendation/views.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | from operator import itemgetter 3 | 4 | from django.contrib import messages 5 | from django.http import HttpResponseRedirect 6 | from django.shortcuts import render 7 | from django.urls import reverse 8 | from django.utils.translation import ugettext as _ 9 | from django.views import View 10 | 11 | from game.models import Game 12 | from .forms import ChooseTagsForm 13 | 14 | DEVELOPER_MATCH_SCORE = 14 15 | GENRE_MATCH_SCORE = 14 16 | TAG_MATCH_SCORE = 12 17 | 18 | 19 | class RecommendGamesManuallyView(View): 20 | def get(self, request): 21 | return render( 22 | request, 23 | template_name='recommend_games_manually.html', 24 | context={'form': ChooseTagsForm().as_p()} 25 | ) 26 | 27 | def post(self, request): 28 | all_games_qs = Game.objects.all() 29 | form = ChooseTagsForm(request.POST) 30 | if form.is_valid(): 31 | user_genre = form.cleaned_data['genre'] 32 | user_developer = form.cleaned_data['developer'] 33 | user_tags = form.cleaned_data['tags'] 34 | 35 | all_games_info = [] 36 | 37 | for game in self._assign_matching_tags_to_game(user_tags, all_games_qs): 38 | game_object = game['game_object'] 39 | game_info = { 40 | 'game': game_object, 41 | 'matched_tags': [], 42 | 'unmatched_tags': [] 43 | } 44 | match_score = 0 45 | 46 | if game_object.developer == user_developer: 47 | game_info['developer_match'] = True 48 | match_score += DEVELOPER_MATCH_SCORE 49 | else: 50 | game_info['developer_match'] = False 51 | 52 | if game_object.genre == user_genre: 53 | game_info['genre_match'] = True 54 | match_score += GENRE_MATCH_SCORE 55 | else: 56 | game_info['genre_match'] = False 57 | 58 | for game_tag in game_object.tags.all(): 59 | if game_tag in game['matching_tags']: 60 | match_score += TAG_MATCH_SCORE 61 | game_info['matched_tags'].append(game_tag) 62 | else: 63 | game_info['unmatched_tags'].append(game_tag) 64 | 65 | game_info['match_score'] = match_score 66 | all_games_info.append(game_info) 67 | 68 | ctx = { 69 | 'sorted_all_game_information': sorted( 70 | all_games_info, key=itemgetter('match_score'), reverse=True 71 | ), 72 | 'user_favorites': { 73 | 'favorite_tags_top_6': user_tags, 74 | 'favorite_genre': user_genre, 75 | 'favorite_developer': user_developer 76 | } 77 | } 78 | 79 | return render( 80 | request, 81 | template_name='recommend_games_manually_result.html', 82 | context=ctx 83 | ) 84 | 85 | messages.add_message(request, messages.ERROR, _('Form invalid')) 86 | return HttpResponseRedirect(reverse('game_recommendation:recommend-manually')) 87 | 88 | @staticmethod 89 | def _assign_matching_tags_to_game(user_tags, all_games_qs): 90 | """Returns all game objects with game tags which match user's favorites""" 91 | games_and_matching_tags = [] 92 | for game in all_games_qs: 93 | matching_tags = list(set(game.tags.all()).intersection(user_tags)) 94 | games_and_matching_tags.append({ 95 | 'game_object': game, 96 | 'matching_tags': matching_tags 97 | }) 98 | return games_and_matching_tags 99 | 100 | 101 | class RecommendByRatedGamesView(View): 102 | def get(self, request): 103 | return render( 104 | request, 105 | template_name='recommend_by_rated_games.html', 106 | context={'to_be_rated': Game.objects.filter(to_be_rated=True)} 107 | ) 108 | 109 | def post(self, request): 110 | games_rated = Game.objects.filter(to_be_rated=True) 111 | liked_games = [game for game in games_rated if request.POST["game_{}".format(game.id)] == 'like'] 112 | 113 | liked_tags = [] 114 | liked_genres = [] 115 | liked_developers = [] 116 | for game in liked_games: 117 | liked_tags = list(game.tags.all()) 118 | liked_genres.append(game.genre) 119 | liked_developers.append(game.developer) 120 | 121 | favorite_tags = self.find_best_of_category(liked_tags, 6) 122 | favorite_genre = self.find_best_of_category(liked_genres, 1) 123 | favorite_developer = self.find_best_of_category(liked_developers, 1) 124 | 125 | all_games_info = [] 126 | games_not_rated = list(set(Game.objects.all()) - set(games_rated)) 127 | for game_object in games_not_rated: 128 | game_info = { 129 | 'game': game_object, 130 | 'matched_tags': [], 131 | 'unmatched_tags': [] 132 | } 133 | match_score = 0 134 | 135 | game_tags = list(game_object.tags.all()) 136 | matching_tags = list(set(game_tags).intersection(favorite_tags)) 137 | match_score += TAG_MATCH_SCORE * len(matching_tags) 138 | game_info['matched_tags'] = matching_tags 139 | game_info['unmatched_tags'] = [tag for tag in game_tags if tag not in matching_tags] 140 | 141 | if game_object.genre in favorite_genre: 142 | game_info['genre_match'] = True 143 | match_score += GENRE_MATCH_SCORE 144 | else: 145 | game_info['genre_match'] = False 146 | 147 | if game_object.developer in favorite_developer: 148 | game_info['developer_match'] = True 149 | match_score += DEVELOPER_MATCH_SCORE 150 | else: 151 | game_info['developer_match'] = False 152 | 153 | game_info['match_score'] = match_score 154 | 155 | all_games_info.append(game_info) 156 | 157 | ctx = { 158 | 'sorted_all_game_information': sorted( 159 | all_games_info, key=itemgetter('match_score'), reverse=True 160 | ), 161 | 'user_favorites': { 162 | 'favorite_tags_top_6': favorite_tags, 163 | 'favorite_genre': favorite_genre, 164 | 'favorite_developer': favorite_developer 165 | } 166 | } 167 | 168 | return render( 169 | request, 170 | template_name='recommend_by_rated_games_result.html', 171 | context=ctx 172 | ) 173 | 174 | @staticmethod 175 | def find_best_of_category(category, number_of_items): 176 | result = [] 177 | counted_items = Counter(category).items() 178 | for element in sorted(counted_items, key=lambda x: x[1], reverse=True)[:number_of_items]: 179 | result.append(element[0]) 180 | return result 181 | -------------------------------------------------------------------------------- /genre/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/genre/__init__.py -------------------------------------------------------------------------------- /genre/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Genre 4 | 5 | admin.site.register(Genre) 6 | -------------------------------------------------------------------------------- /genre/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class GenreConfig(AppConfig): 5 | name = 'genre' 6 | -------------------------------------------------------------------------------- /genre/forms.py: -------------------------------------------------------------------------------- 1 | from django.forms import (CharField, Form) 2 | from django.utils.translation import ugettext_lazy as _ 3 | 4 | 5 | class AddGenreForm(Form): 6 | name = CharField(max_length=64, label=_('Name')) 7 | -------------------------------------------------------------------------------- /genre/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.2 on 2019-05-18 10:16 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='Genre', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=64)), 19 | ], 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /genre/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/genre/migrations/__init__.py -------------------------------------------------------------------------------- /genre/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Genre(models.Model): 5 | name = models.CharField(max_length=64) 6 | 7 | def __str__(self): 8 | return self.name 9 | -------------------------------------------------------------------------------- /genre/templates/games_by_genre.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load bootstrap4 i18n staticfiles %} 3 | {% block content %} 4 | 5 | 6 | 7 | 8 | 9 | 10 |

{% trans 'Games with genre: ' %}{{ selected_genre.name }}

11 | {% for game in all_games_with_genre %} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 28 | 31 | 32 | 33 | 34 | 37 | 38 | 39 | 40 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 55 | 56 | 57 |

{{ game.title }}

{% trans 'Genre' %}

22 | {% for genre in game.genre.all %} 23 |
24 | {{ genre.name }} 25 |
26 | {% endfor %} 27 |
29 | {% trans 'No image' %} 30 |

{% trans 'Developer' %}

35 | {{ game.developer }}
36 |

{% trans 'Tags' %}

41 | {% for tag in game.tags.all %} 42 | {{ tag.name }}
43 | {% endfor %} 44 |

{% trans 'Year' %}

{{ game.year }}
52 | {% trans 'Delete' %} 54 |
58 | {% endfor %} 59 | {% endblock %} 60 | -------------------------------------------------------------------------------- /genre/templates/genre_create.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load bootstrap4 i18n staticfiles %} 3 | {% block content %} 4 | 5 | 6 | 7 | 8 | 9 | 10 |

{% trans 'Create genre' %}

11 | 12 | {% include 'message_block.html' %} 13 | 14 | 21 | 22 |
15 |
16 | {% csrf_token %} 17 | {{ form }} 18 | 19 |
20 |
23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /genre/templates/genre_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load bootstrap4 i18n staticfiles %} 3 | {% block content %} 4 | 5 | 6 | 7 | 8 | 9 | 10 |

{% trans 'Genre' %}

11 | {% if user.is_authenticated %} 12 | 13 | 14 | 15 | {% trans 'Create genre' %} 17 | 18 | 19 | 20 | {% endif %} 21 |

22 | {% include 'message_block.html' %} 23 | {% for genre in object_list %} 24 | 25 | 26 | 27 | 44 | 45 | 46 |
28 |
30 | 35 | {% if user.is_staff %} 36 | 41 | {% endif %} 42 |
43 |
47 | {% endfor %} 48 | {% endblock %} 49 | -------------------------------------------------------------------------------- /genre/tests.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.test import TestCase 3 | from django.urls import reverse 4 | 5 | from genre.models import Genre 6 | from .forms import AddGenreForm 7 | 8 | 9 | class GenreFormTests(TestCase): 10 | 11 | def test_genre_form(self): 12 | # redirects to login screen 13 | response = self.client.get(reverse('genre:genre-create')) 14 | self.assertEqual('/users/login?next=/pl/genre/genre_create/', response['location']) 15 | self.assertEquals(response.status_code, 302) 16 | 17 | self.client.force_login(User.objects.get_or_create(username='user_1')[0]) 18 | response = self.client.get(reverse('genre:genre-create')) 19 | self.assertEquals(response.status_code, 200) 20 | 21 | form = AddGenreForm() 22 | self.assertFalse(form.is_valid()) 23 | 24 | data = {"name": ""} 25 | form = AddGenreForm(data=data) 26 | self.assertFalse(form.is_valid()) 27 | self.assertEqual(form.errors, {'name': ['To pole jest wymagane.']}) 28 | 29 | data = {"name": "genre_1"} 30 | form = AddGenreForm(data=data) 31 | self.assertTrue(form.is_valid()) 32 | 33 | 34 | class GenreListTests(TestCase): 35 | 36 | def setUp(self): 37 | Genre.objects.create(name='genre_1') 38 | Genre.objects.create(name='genre_2') 39 | 40 | def test_text_content(self): 41 | genre_object = Genre.objects.get(name='genre_1') 42 | expected_object_name = f'{genre_object.name}' 43 | self.assertEquals(expected_object_name, 'genre_1') 44 | 45 | def test_genre_list_view(self): 46 | response = self.client.get(reverse('genre:genre-list')) 47 | self.assertEqual(response.status_code, 200) 48 | self.assertContains(response, 'genre_1') 49 | self.assertEqual(len(response.context[0]['object_list']), 2) 50 | self.assertTemplateUsed(response, 'genre_list.html') 51 | -------------------------------------------------------------------------------- /genre/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = 'genre' 6 | 7 | urlpatterns = [ 8 | path('genre_create/', views.GenreCreateView.as_view(), 9 | name='genre-create'), 10 | path('genre_list/', views.GenreListView.as_view(), 11 | name='genre-list'), 12 | path('genre_delete/', views.GenreDeleteView.as_view(), 13 | name='genre-delete'), 14 | path('games_by_genre/', views.GamesByGenreView.as_view(), 15 | name='games-by-genre') 16 | ] 17 | -------------------------------------------------------------------------------- /genre/views.py: -------------------------------------------------------------------------------- 1 | from decouple import config 2 | from django.contrib import messages 3 | from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin 4 | from django.http import HttpResponseRedirect 5 | from django.shortcuts import render 6 | from django.urls import reverse 7 | from django.utils.translation import ugettext as _ 8 | from django.views import View 9 | from django.views.generic import ListView 10 | from game.models import Game 11 | 12 | from .forms import AddGenreForm 13 | from .models import Genre 14 | 15 | NEWS_API_KEY = config('NEWS_API_KEY', cast=str) 16 | 17 | 18 | class GenreListView(ListView): 19 | model = Genre 20 | template_name = 'genre_list.html' 21 | 22 | 23 | class GenreCreateView(LoginRequiredMixin, View): 24 | def get(self, request): 25 | return render( 26 | request, 27 | template_name='genre_create.html', 28 | context={'form': AddGenreForm().as_p()} 29 | ) 30 | 31 | def post(self, request): 32 | form = AddGenreForm(request.POST) 33 | if form.is_valid(): 34 | name = form.cleaned_data['name'] 35 | 36 | if Genre.objects.filter(name=name).exists(): 37 | messages.add_message(request, messages.WARNING, _('Genre with this name already exists')) 38 | return HttpResponseRedirect(reverse('genre:genre-create')) 39 | 40 | Genre.objects.create(name=name) 41 | messages.add_message(request, messages.INFO, _('Genre: {} created successfully').format(name)) 42 | return HttpResponseRedirect(reverse('genre:genre-list')) 43 | 44 | messages.add_message(request, messages.ERROR, _('Form invalid')) 45 | return HttpResponseRedirect(reverse('genre:genre-create')) 46 | 47 | 48 | class GenreDeleteView(PermissionRequiredMixin, View): 49 | permission_required = 'genre.delete_genre' 50 | raise_exception = True 51 | 52 | def get(self, request, genre_id): 53 | genre = Genre.objects.get(id=genre_id) 54 | genre.delete() 55 | return HttpResponseRedirect(reverse('genre:genre-list')) 56 | 57 | 58 | class GamesByGenreView(View): 59 | """Display all games with selected genre""" 60 | 61 | def get(self, request, genre_id): 62 | selected_genre = Genre.objects.get(id=genre_id) 63 | ctx = { 64 | 'all_games_with_genre': Game.objects.filter(genre=selected_genre), 65 | 'selected_genre': selected_genre 66 | } 67 | 68 | return render( 69 | request, 70 | template_name='games_by_genre.html', 71 | context=ctx 72 | ) 73 | -------------------------------------------------------------------------------- /locale/pl/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: PACKAGE VERSION\n" 8 | "Report-Msgid-Bugs-To: \n" 9 | "POT-Creation-Date: 2019-05-19 19:56+0200\n" 10 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 11 | "Last-Translator: FULL NAME \n" 12 | "Language-Team: LANGUAGE \n" 13 | "Language: \n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n" 18 | "%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n" 19 | "%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" 20 | 21 | #: developer/forms.py:6 genre/forms.py:6 tag/forms.py:6 22 | msgid "Name" 23 | msgstr "Nazwa" 24 | 25 | #: developer/templates/developer_create.html:7 26 | #: developer/templates/developer_list.html:15 27 | msgid "Create developer" 28 | msgstr "Utwórz developera" 29 | 30 | #: developer/templates/developer_create.html:24 31 | #: game/templates/game_create.html:17 genre/templates/genre_create.html:24 32 | #: tag/templates/tag_create.html:24 tag/templates/tag_list.html:15 33 | #: users/templates/add_user.html:17 34 | msgid "Create" 35 | msgstr "Utwórz" 36 | 37 | #: developer/templates/developer_list.html:8 38 | #: rest_api/templates/rest_api_main.html:16 templates/base.html:107 39 | msgid "Developers" 40 | msgstr "Developerzy" 41 | 42 | #: developer/templates/developer_list.html:41 43 | #: developer/templates/games_by_developer.html:52 44 | #: game/templates/game_details.html:62 game/templates/game_list.html:77 45 | #: genre/templates/games_by_genre.html:53 genre/templates/genre_list.html:40 46 | #: tag/templates/games_by_tag.html:50 tag/templates/tag_list.html:42 47 | #: users/templates/user_list.html:68 48 | msgid "Delete" 49 | msgstr "Usuń" 50 | 51 | #: developer/templates/games_by_developer.html:7 52 | msgid "Games made by: " 53 | msgstr "Gry stworzone przez: " 54 | 55 | #: developer/templates/games_by_developer.html:20 56 | #: game/templates/game_details.html:12 game/templates/game_list.html:37 57 | #: game_recommendation/templates/recommend_by_rated_games_result.html:71 58 | #: game_recommendation/templates/recommend_games_manually_result.html:65 59 | #: genre/templates/games_by_genre.html:20 genre/templates/genre_list.html:7 60 | #: tag/templates/games_by_tag.html:20 61 | msgid "Genre" 62 | msgstr "Gatunek" 63 | 64 | #: developer/templates/games_by_developer.html:29 65 | #: game/templates/game_details.html:19 game/templates/game_list.html:45 66 | #: game/templates/game_list.html:49 67 | #: game_recommendation/templates/recommend_by_rated_games.html:16 68 | #: game_recommendation/templates/recommend_by_rated_games_result.html:88 69 | #: genre/templates/games_by_genre.html:29 tag/templates/games_by_tag.html:27 70 | msgid "No image" 71 | msgstr "Brak zdjęcia" 72 | 73 | #: developer/templates/games_by_developer.html:33 74 | #: game/templates/game_details.html:22 game/templates/game_list.html:54 75 | #: game_recommendation/templates/recommend_by_rated_games_result.html:93 76 | #: game_recommendation/templates/recommend_games_manually_result.html:87 77 | #: genre/templates/games_by_genre.html:33 tag/templates/games_by_tag.html:31 78 | msgid "Developer" 79 | msgstr "Utwórz developera" 80 | 81 | #: developer/templates/games_by_developer.html:38 82 | #: game/templates/game_details.html:27 game/templates/game_list.html:60 83 | #: genre/templates/games_by_genre.html:39 84 | #: rest_api/templates/rest_api_main.html:20 tag/templates/games_by_tag.html:36 85 | #: tag/templates/tag_list.html:7 templates/base.html:111 86 | msgid "Tags" 87 | msgstr "Tagi" 88 | 89 | #: developer/templates/games_by_developer.html:46 90 | #: game/templates/game_details.html:35 game/templates/game_list.html:68 91 | #: game_recommendation/templates/recommend_by_rated_games_result.html:133 92 | #: game_recommendation/templates/recommend_games_manually_result.html:125 93 | #: genre/templates/games_by_genre.html:47 tag/templates/games_by_tag.html:44 94 | msgid "Year" 95 | msgstr "Rok" 96 | 97 | #: developer/views.py:37 98 | msgid "Developer with this name already exists" 99 | msgstr "Developer z taką nazwą już istnieje" 100 | 101 | #: developer/views.py:41 102 | msgid "Developer: {} created successfully" 103 | msgstr "Developer: {} utworzony poprawnie" 104 | 105 | #: developer/views.py:44 game/views.py:58 game_recommendation/views.py:85 106 | #: genre/views.py:44 tag/views.py:41 users/views.py:58 users/views.py:98 107 | msgid "Form invalid" 108 | msgstr "Niepoprawny formularz" 109 | 110 | #: developer/views.py:55 111 | msgid "Developer: {} has been deleted" 112 | msgstr "Developer: {} został utworzony" 113 | 114 | #: final_project_coderslab/settings.py:122 115 | msgid "English" 116 | msgstr "Angielski" 117 | 118 | #: final_project_coderslab/settings.py:123 119 | msgid "Polish" 120 | msgstr "Polski" 121 | 122 | #: game/forms.py:31 game_recommendation/forms.py:23 123 | msgid "Tags (Select 6)" 124 | msgstr "Tagi (Wybierz 6)" 125 | 126 | #: game/forms.py:36 127 | msgid "Should be rated?" 128 | msgstr "Czy ma być oceniana" 129 | 130 | #: game/templates/game_create.html:7 game/templates/game_list.html:14 131 | msgid "Add game" 132 | msgstr "Dodaj grę" 133 | 134 | #: game/templates/game_details.html:39 135 | msgid "Rating" 136 | msgstr "Ocena" 137 | 138 | #: game/templates/game_details.html:44 139 | msgid "No rating" 140 | msgstr "Brak oceny" 141 | 142 | #: game/templates/game_details.html:50 143 | msgid "Rate game" 144 | msgstr "Oceń grę" 145 | 146 | #: game/templates/game_list.html:7 rest_api/templates/rest_api_main.html:14 147 | #: templates/base.html:69 148 | msgid "Games" 149 | msgstr "Gry" 150 | 151 | #: game/templates/game_list.html:73 152 | msgid "Details" 153 | msgstr "Szczegóły" 154 | 155 | #: game/views.py:40 156 | msgid "Game with this name already exists" 157 | msgstr "Developer z taką nazwą już istnieje" 158 | 159 | #: game/views.py:55 160 | msgid "Game: {} created successfully" 161 | msgstr "Developer: {} utworzony poprawnie" 162 | 163 | #: game/views.py:88 164 | msgid "User {} can't rate games" 165 | msgstr "Użytkownik {} nie może oceniać gier" 166 | 167 | #: game_recommendation/templates/recommend_by_rated_games.html:8 168 | msgid "Rate games" 169 | msgstr "Oceń grę" 170 | 171 | #: game_recommendation/templates/recommend_by_rated_games.html:24 172 | msgid "Like" 173 | msgstr "Lubię" 174 | 175 | #: game_recommendation/templates/recommend_by_rated_games.html:28 176 | msgid "Dislike" 177 | msgstr "Nie lubię" 178 | 179 | #: game_recommendation/templates/recommend_by_rated_games_result.html:7 180 | #: game_recommendation/templates/recommend_games_manually_result.html:7 181 | msgid "According to your answers" 182 | msgstr "Zgodnie z Twoimi odpowiedziami" 183 | 184 | #: game_recommendation/templates/recommend_by_rated_games_result.html:10 185 | #: users/templates/user_list.html:53 186 | msgid "Favorite genre" 187 | msgstr "Ulubione gry" 188 | 189 | #: game_recommendation/templates/recommend_by_rated_games_result.html:11 190 | #: game_recommendation/templates/recommend_games_manually_result.html:11 191 | #: users/templates/user_list.html:62 192 | msgid "Favorite developer" 193 | msgstr "Utwórz developera" 194 | 195 | #: game_recommendation/templates/recommend_by_rated_games_result.html:12 196 | #: game_recommendation/templates/recommend_games_manually_result.html:12 197 | #: users/templates/user_list.html:45 198 | msgid "Favorite tags" 199 | msgstr "Ulubione tagi" 200 | 201 | #: game_recommendation/templates/recommend_by_rated_games_result.html:39 202 | #: game_recommendation/templates/recommend_games_manually_result.html:33 203 | msgid "Games you may like" 204 | msgstr "Gry stworzone przez: " 205 | 206 | #: game_recommendation/templates/recommend_by_rated_games_result.html:56 207 | msgid "Score:" 208 | msgstr "Wynik" 209 | 210 | #: game_recommendation/templates/recommend_by_rated_games_result.html:77 211 | #: game_recommendation/templates/recommend_by_rated_games_result.html:101 212 | #: game_recommendation/templates/recommend_games_manually_result.html:72 213 | #: game_recommendation/templates/recommend_games_manually_result.html:94 214 | msgid "Match" 215 | msgstr "Dopasowanie" 216 | 217 | #: game_recommendation/templates/recommend_by_rated_games_result.html:82 218 | #: game_recommendation/templates/recommend_by_rated_games_result.html:106 219 | #: game_recommendation/templates/recommend_games_manually_result.html:77 220 | #: game_recommendation/templates/recommend_games_manually_result.html:99 221 | msgid "No match" 222 | msgstr "Brak dopasowania" 223 | 224 | #: game_recommendation/templates/recommend_by_rated_games_result.html:113 225 | #: game_recommendation/templates/recommend_games_manually_result.html:107 226 | msgid "Matching tags" 227 | msgstr "Pasujące tagi" 228 | 229 | #: game_recommendation/templates/recommend_by_rated_games_result.html:123 230 | #: game_recommendation/templates/recommend_games_manually_result.html:116 231 | msgid "Other tags" 232 | msgstr "Pozostałe tagi" 233 | 234 | #: game_recommendation/templates/recommend_games_manually.html:7 235 | msgid "Choose your preferences" 236 | msgstr "Wybierz ulubione" 237 | 238 | #: game_recommendation/templates/recommend_games_manually.html:17 239 | msgid "Submit" 240 | msgstr "Zatwierdź" 241 | 242 | #: game_recommendation/templates/recommend_games_manually_result.html:10 243 | msgid "Favorite game" 244 | msgstr "Ulubiona gra" 245 | 246 | #: game_recommendation/templates/recommend_games_manually_result.html:50 247 | msgid "Score" 248 | msgstr "Wynik" 249 | 250 | #: genre/templates/games_by_genre.html:7 251 | msgid "Games with genre: " 252 | msgstr "Gry z gatunku: " 253 | 254 | #: genre/templates/genre_create.html:7 genre/templates/genre_list.html:14 255 | msgid "Create genre" 256 | msgstr "Utwórz" 257 | 258 | #: genre/views.py:37 259 | msgid "Genre with this name already exists" 260 | msgstr "Developer z taką nazwą już istnieje" 261 | 262 | #: genre/views.py:41 263 | msgid "Genre: {} created successfully" 264 | msgstr "Developer: {} utworzony poprawnie" 265 | 266 | #: news_api/templates/news_main.html:8 267 | msgid "Gaming an Tech news" 268 | msgstr "Wiadomości gamingowe i technologiczne" 269 | 270 | #: news_api/templates/news_main.html:46 271 | msgid "Read news" 272 | msgstr "Czytaj" 273 | 274 | #: rest_api/templates/rest_api_main.html:7 275 | msgid "Select API" 276 | msgstr "Rest API" 277 | 278 | #: rest_api/templates/rest_api_main.html:12 templates/base.html:85 279 | msgid "Users" 280 | msgstr "Użytkownicy" 281 | 282 | #: rest_api/templates/rest_api_main.html:18 templates/base.html:109 283 | msgid "Genres" 284 | msgstr "Gatunek" 285 | 286 | #: tag/templates/games_by_tag.html:7 287 | msgid "Games with tag: " 288 | msgstr "Gry z tagiem: " 289 | 290 | #: tag/templates/tag_create.html:7 291 | msgid "Create tag" 292 | msgstr "Utwórz" 293 | 294 | #: tag/views.py:34 295 | msgid "Tag with this name already exists" 296 | msgstr "Developer z taką nazwą już istnieje" 297 | 298 | #: tag/views.py:38 299 | msgid "Tag: {} created successfully" 300 | msgstr "Developer: {} utworzony poprawnie" 301 | 302 | #: templates/base.html:6 templates/base.html:155 303 | msgid "Gameweb" 304 | msgstr "Gameweb" 305 | 306 | #: templates/base.html:31 307 | msgid "News" 308 | msgstr "Wiadomości" 309 | 310 | #: templates/base.html:51 311 | msgid "Recommend Game" 312 | msgstr "Poleć grę" 313 | 314 | #: templates/base.html:56 315 | msgid "Recommend by tags" 316 | msgstr "Poleć na podstawie tagów" 317 | 318 | #: templates/base.html:59 319 | msgid "Recommend by rated games" 320 | msgstr "Poleć na podstawie ocenionych" 321 | 322 | #: templates/base.html:73 323 | msgid "Add games" 324 | msgstr "Dodaj grę" 325 | 326 | #: templates/base.html:75 327 | msgid "Show games" 328 | msgstr "Pokaż gry" 329 | 330 | #: templates/base.html:89 331 | msgid "Add users" 332 | msgstr "Dodaj użytkownika" 333 | 334 | #: templates/base.html:91 335 | msgid "Show users" 336 | msgstr "Pokaż użytkowników" 337 | 338 | #: templates/base.html:101 339 | msgid "Other" 340 | msgstr "Inne" 341 | 342 | #: templates/base.html:105 343 | msgid "Rest API" 344 | msgstr "Rest API" 345 | 346 | #: templates/base.html:113 347 | msgid "About" 348 | msgstr "O stronie" 349 | 350 | #: templates/base.html:126 351 | msgid "Logout" 352 | msgstr "Wyloguj" 353 | 354 | #: templates/base.html:135 355 | msgid "Register" 356 | msgstr "Zarejestruj" 357 | 358 | #: templates/base.html:139 users/templates/login.html:7 359 | #: users/templates/login.html:17 360 | msgid "Login" 361 | msgstr "Zaloguj" 362 | 363 | #: users/templates/add_user.html:7 users/templates/user_list.html:14 364 | msgid "Create user" 365 | msgstr "Utwórz" 366 | 367 | #: users/templates/user_list.html:7 368 | msgid "User list" 369 | msgstr "Lista użytkowników" 370 | 371 | #: users/templates/user_list.html:37 372 | msgid "Full name" 373 | msgstr "Pełna nazwa" 374 | 375 | #: users/templates/user_list.html:41 376 | msgid "Email" 377 | msgstr "Email" 378 | 379 | #: users/views.py:43 380 | msgid "User with this name already exists" 381 | msgstr "Developer z taką nazwą już istnieje" 382 | 383 | #: users/views.py:55 384 | msgid "User: {} created successfully" 385 | msgstr "Developer: {} utworzony poprawnie" 386 | 387 | #: users/views.py:95 388 | msgid "Wrong password" 389 | msgstr "Błędne hasło" 390 | -------------------------------------------------------------------------------- /main_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/main_app/__init__.py -------------------------------------------------------------------------------- /main_app/admin.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/main_app/admin.py -------------------------------------------------------------------------------- /main_app/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MainAppConfig(AppConfig): 5 | name = 'main_app' 6 | -------------------------------------------------------------------------------- /main_app/context_processor.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import os 3 | 4 | from django.conf import settings 5 | 6 | 7 | def gaming_quotes_processor(request): 8 | all_quotes = [] 9 | path = os.path.join(settings.MEDIA_ROOT, 'csv/gaming_quotes.csv') 10 | with open(path) as f: 11 | for row in csv.reader(f, delimiter=';'): 12 | all_quotes.append({'author': row[0], 'quote': row[1]}) 13 | return {'all_quotes': all_quotes} 14 | -------------------------------------------------------------------------------- /main_app/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/main_app/migrations/__init__.py -------------------------------------------------------------------------------- /main_app/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/main_app/models.py -------------------------------------------------------------------------------- /main_app/tasks.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import os 3 | from datetime import datetime 4 | 5 | from celery.decorators import periodic_task, task 6 | from celery.task.schedules import crontab 7 | from celery.utils.log import get_task_logger 8 | from django.conf import settings 9 | 10 | from game.models import Game 11 | 12 | logger = get_task_logger(__name__) 13 | 14 | 15 | def save_games_to_csv(filename): 16 | all_games = Game.objects.all() 17 | with open(os.path.join(settings.MEDIA_ROOT, 'csv', filename), 'w') as filepath: 18 | writer = csv.DictWriter( 19 | filepath, 20 | fieldnames=['time', 'title', 'developer', 'genre', 'year'], 21 | delimiter=';' 22 | ) 23 | writer.writeheader() 24 | for game in all_games: 25 | writer.writerow({ 26 | 'time': datetime.now(), 27 | 'title': game.title, 28 | 'developer': game.developer.name, 29 | 'genre': game.genre.name, 30 | 'year': game.year 31 | }) 32 | logger.info("Games downloaded to CSV successfully") 33 | 34 | 35 | @task(name="save_games_to_csv_task") 36 | def save_games_to_csv_task(): 37 | """Saves all games to CSV file in background""" 38 | save_games_to_csv('game_data.csv') 39 | 40 | 41 | @periodic_task( 42 | run_every=crontab(minute=0, hour=0), 43 | name="save_games_to_csv_backup", 44 | ignore_result=True 45 | ) 46 | def save_games_backup(): 47 | """Saves all games to CSV file everyday at midnight""" 48 | save_games_to_csv('game_data_backup.csv') 49 | -------------------------------------------------------------------------------- /main_app/templates/403.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load bootstrap4 i18n staticfiles %} 3 | {% block content %} 4 | 5 | 6 | 7 | 8 | 9 | 10 |

{% trans "Sorry. You can't do that" %}

11 | {% endblock %} -------------------------------------------------------------------------------- /main_app/templates/about.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load bootstrap4 i18n staticfiles %} 3 | {% block content %} 4 | 5 | 6 | 7 | 13 | 14 | 15 | 21 | 22 | 23 |
8 |

{% trans 'Loga made with:' %} 9 | DesignEvo 11 |

12 |
16 |

{% trans 'Banner from:' %} 17 | www.ytgraphics.com 19 |

20 |
24 | {% endblock %} -------------------------------------------------------------------------------- /main_app/templates/download_games_to_csv_result.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load bootstrap4 i18n staticfiles %} 3 | {% block content %} 4 | {% include 'message_block.html' %} 5 | {% endblock %} -------------------------------------------------------------------------------- /main_app/tests.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/main_app/tests.py -------------------------------------------------------------------------------- /main_app/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = 'main_app' 6 | 7 | urlpatterns = [ 8 | path('about/', views.AboutView.as_view(), 9 | name='about'), 10 | path('download_to_csv/', views.DownloadGamesToCSVView.as_view(), 11 | name='download-to-csv') 12 | ] 13 | -------------------------------------------------------------------------------- /main_app/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.shortcuts import render 3 | from django.utils.translation import ugettext as _ 4 | from django.views import View 5 | from django.views.generic import TemplateView 6 | 7 | from .tasks import save_games_to_csv_task 8 | 9 | 10 | class AboutView(TemplateView): 11 | template_name = 'about.html' 12 | 13 | 14 | class DownloadGamesToCSVView(View): 15 | 16 | def get(self, request): 17 | save_games_to_csv_task.delay() 18 | messages.add_message(request, messages.INFO, _( 19 | 'Games will be downloaded in background. Come back in few moments' 20 | )) 21 | return render( 22 | request, 23 | template_name='download_games_to_csv_result.html' 24 | ) 25 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "final_project_coderslab.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /media/csv/game_data.csv: -------------------------------------------------------------------------------- 1 | time;title;developer;genre;year 2 | -------------------------------------------------------------------------------- /media/csv/gaming_quotes.csv: -------------------------------------------------------------------------------- 1 | Paarthurnax, Elder Scrolls V: Skyrim;What is better? To be born good or to overcome your evil nature through great effort? 2 | Javik, Mass Effect 3;Stand in the ashes of a trillion dead souls, and asks the ghosts if honor matters. The silence is your answer. 3 | Khan, Metro 2033;Even in dark times, we cannot relinquish the things that make us human. -------------------------------------------------------------------------------- /news_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/news_api/__init__.py -------------------------------------------------------------------------------- /news_api/admin.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/news_api/admin.py -------------------------------------------------------------------------------- /news_api/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class NewsApiConfig(AppConfig): 5 | name = 'news_api' 6 | -------------------------------------------------------------------------------- /news_api/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/news_api/migrations/__init__.py -------------------------------------------------------------------------------- /news_api/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/news_api/models.py -------------------------------------------------------------------------------- /news_api/templates/news_main.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load bootstrap4 i18n staticfiles %} 3 | {% block content %} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 28 | 29 | 30 |

{% trans 'Gaming an Tech news' %}

{{ source_name }}

15 | Polygon 18 | IGN 21 | The Verge 24 | TechRadar 27 |
31 | {% for article in articles %} 32 | 33 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 44 | 48 | 49 |

{{ article.title }}

36 | 'No image 38 |
{{ article.description }}
45 | {% trans 'Read news' %} 47 |
50 | {% endfor %} 51 | {% endblock %} 52 | -------------------------------------------------------------------------------- /news_api/tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from django.test import TestCase 4 | from django.urls import reverse 5 | 6 | 7 | class NewsAPITests(TestCase, unittest.TestCase): 8 | 9 | def test_game_form(self): 10 | response = self.client.get(reverse('news_api:show-news', kwargs={'news_source': 'polygon'})) 11 | self.assertEquals(response.status_code, 200) 12 | self.assertTemplateUsed(response, 'news_main.html') 13 | -------------------------------------------------------------------------------- /news_api/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = 'news_api' 6 | 7 | urlpatterns = [ 8 | path('', views.TechNewsView.as_view(), 9 | name='mainpage'), 10 | path('show_news/', views.TechNewsView.as_view(), 11 | name='show-news'), 12 | ] 13 | -------------------------------------------------------------------------------- /news_api/views.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from decouple import config 3 | from django.shortcuts import render 4 | from django.views import View 5 | 6 | from constants import NEWS_SOURCE_DATA_ALL 7 | 8 | NEWS_API_KEY = config('NEWS_API_KEY', cast=str) 9 | 10 | 11 | class TechNewsView(View): 12 | """Display gaming and tech news using API""" 13 | 14 | def get(self, request, news_source='polygon'): 15 | image_url = None 16 | source_name = None 17 | selected_source = None 18 | for news_source_key, news_source_values in NEWS_SOURCE_DATA_ALL.items(): 19 | if news_source == news_source_key: 20 | selected_source = news_source_key 21 | image_url = news_source_values['image_url'] 22 | source_name = news_source_values['api_name'] 23 | url = ( 24 | 'https://newsapi.org/v2/top-headlines?sources={}&apiKey={}'.format( 25 | selected_source, NEWS_API_KEY) 26 | ) 27 | response = requests.get(url) 28 | ctx = { 29 | 'articles': response.json()['articles'], 30 | 'image_url': image_url, 31 | 'source_name': source_name 32 | } 33 | return render( 34 | request, 35 | template_name='news_main.html', 36 | context=ctx 37 | ) 38 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | amqp==2.4.2 2 | atomicwrites==1.3.0 3 | attrs==19.1.0 4 | backcall==0.1.0 5 | billiard==3.6.0.0 6 | boto3==1.9.154 7 | botocore==1.12.154 8 | celery==4.3.0 9 | certifi==2018.4.16 10 | chardet==3.0.4 11 | coverage==4.5.3 12 | decorator==4.4.0 13 | dj-database-url==0.5.0 14 | Django==2.1.11 15 | django-bootstrap4==0.0.8 16 | django-debug-toolbar==1.11 17 | django-filter==2.1.0 18 | django-heroku==0.3.1 19 | django-storages==1.7.1 20 | django-timezone-field==3.0 21 | djangorestframework==3.9.4 22 | docutils==0.14 23 | ez-setup==0.9 24 | flake8==3.6.0 25 | gunicorn==19.9.0 26 | idna==2.6 27 | ipdb==0.12 28 | ipython==7.5.0 29 | ipython-genutils==0.2.0 30 | jedi==0.13.3 31 | jmespath==0.9.4 32 | kombu==4.5.0 33 | Markdown==3.1 34 | mccabe==0.6.1 35 | more-itertools==7.0.0 36 | parso==0.4.0 37 | pexpect==4.7.0 38 | pickleshare==0.7.5 39 | Pillow==6.2.0 40 | pluggy==0.11.0 41 | prompt-toolkit==2.0.9 42 | psycopg2==2.7.5 43 | psycopg2-binary==2.8.2 44 | ptyprocess==0.6.0 45 | py==1.8.0 46 | pycodestyle==2.4.0 47 | pyflakes==2.0.0 48 | Pygments==2.4.0 49 | pytest==4.5.0 50 | python-crontab==2.3.6 51 | python-dateutil==2.8.0 52 | python-decouple==3.1 53 | pytz==2018.4 54 | redis==3.2.1 55 | requests==2.18.4 56 | s3transfer==0.2.0 57 | six==1.12.0 58 | sqlparse==0.3.0 59 | traitlets==4.3.2 60 | urllib3==1.22 61 | vine==1.3.0 62 | wcwidth==0.1.7 63 | whitenoise==4.1 64 | -------------------------------------------------------------------------------- /rest_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/rest_api/__init__.py -------------------------------------------------------------------------------- /rest_api/admin.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/rest_api/admin.py -------------------------------------------------------------------------------- /rest_api/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class RestApiConfig(AppConfig): 5 | name = 'rest_api' 6 | -------------------------------------------------------------------------------- /rest_api/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/rest_api/migrations/__init__.py -------------------------------------------------------------------------------- /rest_api/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/rest_api/models.py -------------------------------------------------------------------------------- /rest_api/serializers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | 3 | from rest_framework import serializers 4 | 5 | from developer.models import Developer 6 | from genre.models import Genre 7 | from tag.models import Tag 8 | from game.models import Game 9 | 10 | 11 | class UserSerializer(serializers.HyperlinkedModelSerializer): 12 | url = serializers.HyperlinkedIdentityField(view_name="rest_api:users-detail") 13 | 14 | class Meta: 15 | model = User 16 | fields = ('url', 'username', 'email') 17 | 18 | 19 | class DeveloperSerializer(serializers.HyperlinkedModelSerializer): 20 | url = serializers.HyperlinkedIdentityField(view_name="rest_api:developers-detail") 21 | 22 | class Meta: 23 | model = Developer 24 | fields = ('url', 'name') 25 | 26 | 27 | class GenreSerializer(serializers.HyperlinkedModelSerializer): 28 | url = serializers.HyperlinkedIdentityField(view_name="rest_api:genres-detail") 29 | 30 | class Meta: 31 | model = Genre 32 | fields = ('url', 'name') 33 | 34 | 35 | class TagSerializer(serializers.HyperlinkedModelSerializer): 36 | url = serializers.HyperlinkedIdentityField(view_name="rest_api:tags-detail") 37 | 38 | class Meta: 39 | model = Tag 40 | fields = ('url', 'name') 41 | 42 | 43 | class GameSerializer(serializers.HyperlinkedModelSerializer): 44 | url = serializers.HyperlinkedIdentityField(view_name="rest_api:games-detail") 45 | developer = serializers.HyperlinkedRelatedField( 46 | many=False, 47 | read_only=True, 48 | view_name='rest_api:developers-detail' 49 | ) 50 | genre = serializers.HyperlinkedRelatedField( 51 | many=False, 52 | read_only=True, 53 | view_name='rest_api:genres-detail' 54 | ) 55 | tags = serializers.HyperlinkedRelatedField( 56 | many=True, 57 | read_only=True, 58 | view_name='rest_api:tags-detail' 59 | ) 60 | 61 | class Meta: 62 | model = Game 63 | fields = ('url', 'title', 'year', 'developer', 'genre', 'tags', 'image', 'to_be_rated') 64 | -------------------------------------------------------------------------------- /rest_api/templates/rest_api_main.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load bootstrap4 i18n staticfiles %} 3 | 4 | {% block content %} 5 |
6 |
7 |

{% trans 'Select API' %}

8 |
9 | 23 |
24 | {% endblock %} -------------------------------------------------------------------------------- /rest_api/tests.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/rest_api/tests.py -------------------------------------------------------------------------------- /rest_api/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | from rest_framework import routers 3 | 4 | from . import views 5 | 6 | app_name = 'rest_api' 7 | 8 | router = routers.DefaultRouter() 9 | router.register(r'users', views.UserViewSet, 'users') 10 | router.register(r'games', views.GameViewSet, 'games') 11 | router.register(r'developers', views.DeveloperViewSet, 'developers') 12 | router.register(r'genres', views.GenreViewSet, 'genres') 13 | router.register(r'tags', views.TagViewSet, 'tags') 14 | 15 | urlpatterns = [ 16 | url(r'^', include(router.urls)), 17 | url(r'^api-main/', views.RestApiMain.as_view(), name='api-main'), 18 | url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), 19 | ] 20 | -------------------------------------------------------------------------------- /rest_api/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.views.generic import TemplateView 3 | from rest_framework import viewsets 4 | 5 | from developer.models import Developer 6 | from game.models import Game 7 | from genre.models import Genre 8 | from tag.models import Tag 9 | from .serializers import DeveloperSerializer, GameSerializer, GenreSerializer, TagSerializer, UserSerializer 10 | 11 | 12 | class RestApiMain(TemplateView): 13 | template_name = 'rest_api_main.html' 14 | 15 | 16 | class UserViewSet(viewsets.ModelViewSet): 17 | """ 18 | API endpoint that allows user objects to be viewed or edited. 19 | """ 20 | queryset = User.objects.all().order_by('-date_joined') 21 | serializer_class = UserSerializer 22 | 23 | 24 | class DeveloperViewSet(viewsets.ModelViewSet): 25 | """ 26 | API endpoint that allows developer objects to be viewed or edited. 27 | """ 28 | queryset = Developer.objects.all() 29 | serializer_class = DeveloperSerializer 30 | 31 | 32 | class GenreViewSet(viewsets.ModelViewSet): 33 | """ 34 | API endpoint that allows genre objects to be viewed or edited. 35 | """ 36 | queryset = Genre.objects.all() 37 | serializer_class = GenreSerializer 38 | 39 | 40 | class TagViewSet(viewsets.ModelViewSet): 41 | """ 42 | API endpoint that allows tag objects to be viewed or edited. 43 | """ 44 | queryset = Tag.objects.all() 45 | serializer_class = TagSerializer 46 | 47 | 48 | class GameViewSet(viewsets.ModelViewSet): 49 | """ 50 | API endpoint that allows game objects to be viewed or edited. 51 | """ 52 | queryset = Game.objects.all() 53 | serializer_class = GameSerializer 54 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.6.8 2 | -------------------------------------------------------------------------------- /settings.ini.example: -------------------------------------------------------------------------------- 1 | [settings] 2 | DEBUG = True 3 | WORK_ON_POSTGRE_SQL = False 4 | 5 | SECRET_KEY = 6 | 7 | # NEWS API 8 | NEWS_API_KEY = 9 | 10 | # DATABASE CONFIG 11 | DB_NAME = 12 | DB_USER = 13 | DB_PASSWORD = 14 | DB_HOST = 15 | DB_PORT = 16 | 17 | # REDIS CONFIG 18 | BROKER_URL = 19 | CELERY_RESULT_BACKEND = 20 | 21 | # AWS CONFIG 22 | AWS_ACCESS_KEY_ID = 23 | AWS_SECRET_ACCESS_KEY = 24 | AWS_STORAGE_BUCKET_NAME = 25 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = */docs/*,*/.tox/*,*/.venv/*,*/migrations/* 3 | max-line-length = 119 4 | -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin-top: 70px; 3 | font-family: 'Righteous', serif; 4 | background-color: whitesmoke; 5 | } 6 | 7 | .title { 8 | font-family: 'Faster One', serif; 9 | color: mintcream; 10 | padding: 20px; 11 | text-align: center; 12 | } 13 | 14 | div.textContent { 15 | display: none; 16 | } 17 | 18 | .header { 19 | background-color: #005ec3; 20 | height: 140px; 21 | } 22 | 23 | .content { 24 | margin-left: auto; 25 | margin-right: auto; 26 | margin-top: 10px; 27 | width: 80%; 28 | } 29 | 30 | a:hover { 31 | color: mintcream; 32 | text-decoration: none; 33 | } 34 | 35 | th, h4 { 36 | color: #2B2C5F; 37 | margin-left: 100px; 38 | margin-right: auto; 39 | } 40 | 41 | h1, h2, h3, h5, h6, p, a, label { 42 | color: #2B2C5F; 43 | text-align: center; 44 | } 45 | 46 | a:hover { 47 | color: darkblue; 48 | } 49 | 50 | .table { 51 | border-collapse: separate !important; 52 | background-color: #E4E5EE; 53 | border: 3px solid #005ec3; 54 | border-radius: 15px; 55 | } 56 | 57 | .table td { 58 | vertical-align: middle; 59 | } 60 | 61 | .navbar { 62 | background-color: #005ec3; 63 | height: 50px; 64 | } 65 | 66 | .navbar.navbar-expand-lg.navbar-dark li a { 67 | color: mintcream; 68 | font-size: 16px; 69 | } 70 | 71 | .navbar-nav > li > .dropdown-menu { 72 | background-color: #005ec3; 73 | } 74 | 75 | .navbar-nav > li > .dropdown-menu > a { 76 | border: transparent solid 2px; 77 | } 78 | 79 | .navbar-nav > li > .dropdown-menu > a:hover { 80 | background-color: #005ec3; 81 | border: white solid 2px; 82 | } 83 | 84 | .dropdown-toggle::after { 85 | display: none 86 | } 87 | 88 | .table > tbody > tr > td, 89 | .table > tbody > tr > th, 90 | .table > thead > tr > td, 91 | .table > thead > tr > th { 92 | border: none; 93 | } 94 | 95 | input[type="checkbox"] { 96 | width: 50px; 97 | height: 30px; 98 | cursor: pointer; 99 | } 100 | 101 | .checkboxmultiple { 102 | border-radius: 15px; 103 | padding: 20px 100px 20px 100px; 104 | list-style-type: none; 105 | margin: auto; 106 | } 107 | 108 | .checkboxmultiple li { 109 | color: black; 110 | font-weight: bold; 111 | } 112 | 113 | .btn-primary { 114 | width: 250px; 115 | margin-left: auto; 116 | margin-right: auto; 117 | } 118 | 119 | .btn-secondary { 120 | background-color: #005ec3; 121 | width: 250px; 122 | margin-left: auto; 123 | margin-right: auto; 124 | } 125 | 126 | .btn-secondary:hover { 127 | background-color: #420EE8; 128 | } 129 | 130 | .btn-success { 131 | align-self: center; 132 | align-items: center; 133 | align-content: center; 134 | width: 250px; 135 | margin-left: auto; 136 | margin-right: auto; 137 | } 138 | 139 | .btn-dark { 140 | background-color: #005ec3; 141 | } 142 | 143 | .btn-dark:hover { 144 | background-color: #420EE8; 145 | } 146 | 147 | .btn-group-justified > .btn-group:nth-of-type(odd) { 148 | width: .25%; 149 | } 150 | 151 | .btn-danger { 152 | width: 200px; 153 | border: none; 154 | } 155 | 156 | .btn-danger > a { 157 | color: mintcream; 158 | } 159 | 160 | input[type=text] { 161 | padding: 5px; 162 | border: 2px solid #ccc; 163 | -webkit-border-radius: 5px; 164 | border-radius: 5px; 165 | width: 100%; 166 | } 167 | 168 | input[type=password] { 169 | padding: 5px; 170 | border: 2px solid #ccc; 171 | -webkit-border-radius: 5px; 172 | border-radius: 5px; 173 | width: 100%; 174 | } 175 | 176 | input[type=email] { 177 | padding: 5px; 178 | border: 2px solid #ccc; 179 | -webkit-border-radius: 5px; 180 | border-radius: 5px; 181 | width: 100%; 182 | } 183 | 184 | select { 185 | padding: 5px; 186 | border: 2px solid #ccc; 187 | -webkit-border-radius: 5px; 188 | border-radius: 5px; 189 | width: 100%; 190 | } 191 | 192 | textarea { 193 | padding: 5px; 194 | border: 2px solid #ccc; 195 | -webkit-border-radius: 5px; 196 | border-radius: 5px; 197 | width: 100%; 198 | height: 50px; 199 | } 200 | 201 | input[type=number] { 202 | padding: 5px; 203 | border: 2px solid #ccc; 204 | -webkit-border-radius: 5px; 205 | border-radius: 5px; 206 | width: 100%; 207 | } 208 | 209 | input[type=file] { 210 | padding: 5px; 211 | border: 2px solid #ccc; 212 | -webkit-border-radius: 5px; 213 | border-radius: 5px; 214 | width: 100%; 215 | } 216 | 217 | input[type=number]:focus { 218 | border-color: #333; 219 | } 220 | 221 | input[type=text]:focus { 222 | border-color: #333; 223 | } 224 | 225 | input[type=checkbox].css-checkbox { 226 | position: absolute; 227 | z-index: -1000; 228 | left: -1000px; 229 | overflow: hidden; 230 | clip: rect(0 0 0 0); 231 | height: 1px; 232 | width: 1px; 233 | margin: -1px; 234 | padding: 0; 235 | border: 0; 236 | } 237 | 238 | .container { 239 | display: block; 240 | position: relative; 241 | padding-left: 35px; 242 | cursor: pointer; 243 | font-size: 18px; 244 | -webkit-user-select: none; 245 | -moz-user-select: none; 246 | -ms-user-select: none; 247 | user-select: none; 248 | } 249 | 250 | .radio_buttons_container { 251 | background-color: #6B6DD8; 252 | border: 3px solid #005ec3; 253 | border-radius: 5px; 254 | padding: 5px; 255 | margin-right: 150px; 256 | } 257 | 258 | /* Hide the browser's default radio button */ 259 | .container input { 260 | position: absolute; 261 | opacity: 0; 262 | cursor: pointer; 263 | } 264 | 265 | /* Create a custom radio button */ 266 | .checkmark { 267 | position: absolute; 268 | top: 0; 269 | left: 0; 270 | height: 25px; 271 | width: 25px; 272 | background-color: whitesmoke; 273 | border-radius: 50%; 274 | } 275 | 276 | /* On mouse-over, add a grey background color */ 277 | .container:hover input ~ .checkmark { 278 | background-color: #ccc; 279 | } 280 | 281 | /* When the radio button is checked, add a blue background */ 282 | .container input:checked ~ .checkmark { 283 | background-color: #2196F3; 284 | } 285 | 286 | /* Create the indicator (the dot/circle - hidden when not checked) */ 287 | .checkmark:after { 288 | content: ""; 289 | position: absolute; 290 | display: none; 291 | } 292 | 293 | /* Show the indicator (dot/circle) when checked */ 294 | .container input:checked ~ .checkmark:after { 295 | display: block; 296 | } 297 | 298 | /* Style the indicator (dot/circle) */ 299 | .container .checkmark:after { 300 | top: 9px; 301 | left: 9px; 302 | width: 8px; 303 | height: 8px; 304 | border-radius: 50%; 305 | background: white; 306 | } 307 | 308 | .news_button { 309 | width: 22%; 310 | } 311 | 312 | .header-title{ 313 | color: mintcream; 314 | font-size: 30px; 315 | margin-top: -10px; 316 | } 317 | 318 | #header-logo{ 319 | height: 140px; 320 | width: auto; 321 | float: left; 322 | } 323 | 324 | #quote-text{ 325 | font-size: 18px; 326 | color: mintcream; 327 | } 328 | 329 | #image-banner{ 330 | height: 180px; 331 | width: 100%; 332 | } 333 | 334 | #news-source-logo{ 335 | width: 150px; 336 | height: 150px; 337 | border-radius: 5px; 338 | margin-left: 70px; 339 | } 340 | 341 | .news-header-text{ 342 | margin-right: 300px; 343 | } 344 | -------------------------------------------------------------------------------- /static/img/IGN.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/static/img/IGN.jpg -------------------------------------------------------------------------------- /static/img/icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/static/img/icon.jpg -------------------------------------------------------------------------------- /static/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/static/img/icon.png -------------------------------------------------------------------------------- /static/img/ign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/static/img/ign.png -------------------------------------------------------------------------------- /static/img/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/static/img/logo.jpg -------------------------------------------------------------------------------- /static/img/new_banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/static/img/new_banner.jpg -------------------------------------------------------------------------------- /static/img/polygon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/static/img/polygon.png -------------------------------------------------------------------------------- /static/img/techradar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/static/img/techradar.png -------------------------------------------------------------------------------- /static/img/verge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/static/img/verge.png -------------------------------------------------------------------------------- /static/js/dropdown_hover.js: -------------------------------------------------------------------------------- 1 | $('.navbar .dropdown').hover(function() { 2 | $(this).find('.dropdown-menu').first().stop(true, true).delay(250).slideDown(); 3 | }, function() { 4 | $(this).find('.dropdown-menu').first().stop(true, true).delay(100).slideUp() 5 | }); -------------------------------------------------------------------------------- /static/js/main.js: -------------------------------------------------------------------------------- 1 | let limit = 7; 2 | $('input[name="tags"]').on('change', function (evt) { 3 | if ($('input[name="tags"]:checked').length >= limit) { 4 | this.checked = false; 5 | } 6 | }); 7 | 8 | -------------------------------------------------------------------------------- /static/js/slider.js: -------------------------------------------------------------------------------- 1 | let cnt = 0, 2 | texts = []; 3 | 4 | $(".textContent").each(function () { 5 | texts[cnt++] = $(this).html(); 6 | }); 7 | 8 | function slide() { 9 | if (cnt >= texts.length) cnt = 0; 10 | $('#textMessage') 11 | .html(texts[cnt++]) 12 | .fadeIn(3000).delay(3000) 13 | .fadeOut(3000, slide); 14 | } 15 | 16 | slide(); -------------------------------------------------------------------------------- /tag/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/tag/__init__.py -------------------------------------------------------------------------------- /tag/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Tag 4 | 5 | admin.site.register(Tag) 6 | -------------------------------------------------------------------------------- /tag/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TagConfig(AppConfig): 5 | name = 'tag' 6 | -------------------------------------------------------------------------------- /tag/forms.py: -------------------------------------------------------------------------------- 1 | from django.forms import (CharField, Form) 2 | from django.utils.translation import ugettext_lazy as _ 3 | 4 | 5 | class AddTagForm(Form): 6 | name = CharField(max_length=64, label=_('Name')) 7 | -------------------------------------------------------------------------------- /tag/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.2 on 2019-05-18 10:16 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='Tag', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=64)), 19 | ], 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /tag/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/tag/migrations/__init__.py -------------------------------------------------------------------------------- /tag/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Tag(models.Model): 5 | name = models.CharField(max_length=64) 6 | 7 | def __str__(self): 8 | return self.name 9 | -------------------------------------------------------------------------------- /tag/templates/games_by_tag.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load bootstrap4 i18n staticfiles %} 3 | {% block content %} 4 | 5 | 6 | 7 | 8 | 9 | 10 |

{% trans 'Games with tag: ' %}{{ selected_tag.name }}

11 | {% for game in all_games_with_tag %} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 26 | 29 | 30 | 31 | 32 | 34 | 35 | 36 | 37 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 51 | 52 | 53 |

{{ game.title }}

{% trans 'Genre' %}

22 | {% for genre in game.genre.all %} 23 |
{{ genre.name }}
24 | {% endfor %} 25 |
27 | {% trans 'No image' %} 28 |

{% trans 'Developer' %}

33 | {{ game.developer }}

{% trans 'Tags' %}

38 | {% for tag in game.tags.all %} 39 | {{ tag.name }}
40 | {% endfor %} 41 |

{% trans 'Year' %}

{{ game.year }}
49 | {% trans 'Delete' %}
54 | {% endfor %} 55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /tag/templates/tag_create.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load bootstrap4 i18n staticfiles %} 3 | {% block content %} 4 | 5 | 6 | 7 | 8 | 9 | 10 |

{% trans 'Create tag' %}

11 | 12 | {% include 'message_block.html' %} 13 | 14 | 21 | 22 |
15 |
16 | {% csrf_token %} 17 | {{ form }} 18 | 19 |
20 |
23 | {% endblock %} -------------------------------------------------------------------------------- /tag/templates/tag_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load bootstrap4 i18n staticfiles %} 3 | {% block content %} 4 | 5 | 6 | 7 | 8 | 9 | 10 |

{% trans 'Tags' %}

11 | {% if user.is_authenticatd %} 12 | 13 | 14 | 15 | {% trans 'Create' %} 17 | 18 | 19 | 20 | {% endif %} 21 |

22 | {% include 'message_block.html' %} 23 | {% for tag in object_list %} 24 | 25 | 26 | 27 | 43 | 44 | 45 |
28 |
30 | 35 | {% if user.is_staff %} 36 | 40 | {% endif %} 41 |
42 |
46 | {% endfor %} 47 | {% endblock %} 48 | -------------------------------------------------------------------------------- /tag/tests.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.test import TestCase 3 | from django.urls import reverse 4 | 5 | from tag.models import Tag 6 | from .forms import AddTagForm 7 | 8 | 9 | class TagFormTests(TestCase): 10 | 11 | def test_genre_form(self): 12 | # redirects to login screen 13 | response = self.client.get(reverse('tag:tag-create')) 14 | self.assertEqual('/users/login?next=/pl/tag/tag_create/', response['location']) 15 | self.assertEquals(response.status_code, 302) 16 | 17 | self.client.force_login(User.objects.get_or_create(username='user_1')[0]) 18 | response = self.client.get(reverse('tag:tag-create')) 19 | self.assertEquals(response.status_code, 200) 20 | 21 | form = AddTagForm() 22 | self.assertFalse(form.is_valid()) 23 | 24 | data = {"name": ""} 25 | form = AddTagForm(data=data) 26 | self.assertFalse(form.is_valid()) 27 | self.assertEqual(form.errors, {'name': ['To pole jest wymagane.']}) 28 | 29 | data = {"name": "tag_1"} 30 | form = AddTagForm(data=data) 31 | self.assertTrue(form.is_valid()) 32 | 33 | 34 | class TagListTests(TestCase): 35 | 36 | def setUp(self): 37 | Tag.objects.create(name='tag_1') 38 | Tag.objects.create(name='tag_2') 39 | 40 | def test_text_content(self): 41 | tag_object = Tag.objects.get(name='tag_1') 42 | expected_object_name = f'{tag_object.name}' 43 | self.assertEquals(expected_object_name, 'tag_1') 44 | 45 | def test_tag_list(self): 46 | response = self.client.get(reverse('tag:tag-list')) 47 | self.assertEqual(response.status_code, 200) 48 | self.assertContains(response, 'tag_1') 49 | self.assertEqual(len(response.context[0]['object_list']), 2) 50 | self.assertTemplateUsed(response, 'tag_list.html') 51 | -------------------------------------------------------------------------------- /tag/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = 'tag' 6 | 7 | urlpatterns = [ 8 | path('tag_list/', views.TagListView.as_view(), 9 | name='tag-list'), 10 | path('tag_create/', views.TagCreateView.as_view(), 11 | name='tag-create'), 12 | path('tag_delete/', views.TagDeleteView.as_view(), 13 | name='tag-delete'), 14 | path('games_by_tag/', views.GamesByTagView.as_view(), 15 | name='games-by-tag') 16 | ] 17 | -------------------------------------------------------------------------------- /tag/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin 3 | from django.http import HttpResponseRedirect 4 | from django.shortcuts import render 5 | from django.urls import reverse 6 | from django.utils.translation import ugettext as _ 7 | from django.views import View 8 | from django.views.generic import ListView 9 | 10 | from game.models import Game 11 | from .forms import AddTagForm 12 | from .models import Tag 13 | 14 | 15 | class TagListView(ListView): 16 | model = Tag 17 | template_name = 'tag_list.html' 18 | 19 | 20 | class TagCreateView(LoginRequiredMixin, View): 21 | def get(self, request): 22 | return render( 23 | request, 24 | template_name='tag_create.html', 25 | context={'form': AddTagForm().as_p()} 26 | ) 27 | 28 | def post(self, request): 29 | form = AddTagForm(request.POST) 30 | if form.is_valid(): 31 | name = form.cleaned_data['name'] 32 | 33 | if Tag.objects.filter(name=name).exists(): 34 | messages.add_message(request, messages.WARNING, _('Tag with this name already exists')) 35 | return HttpResponseRedirect(reverse('tag:tag-create')) 36 | 37 | Tag.objects.create(name=name) 38 | messages.add_message(request, messages.INFO, _('Tag: {} created successfully').format(name)) 39 | return HttpResponseRedirect(reverse('tag:tag-list')) 40 | 41 | messages.add_message(request, messages.ERROR, _('Form invalid')) 42 | return HttpResponseRedirect(reverse('tag:tag-create')) 43 | 44 | 45 | class TagDeleteView(PermissionRequiredMixin, View): 46 | permission_required = 'tag.delete_tag' 47 | raise_exception = True 48 | 49 | def get(self, request, tag_id): 50 | tag = Tag.objects.get(id=tag_id) 51 | tag.delete() 52 | 53 | return HttpResponseRedirect(reverse('tag:tag-list')) 54 | 55 | 56 | class GamesByTagView(View): 57 | def get(self, request, tag_id): 58 | """Display all games with selected tag""" 59 | selected_tag = Tag.objects.get(id=tag_id) 60 | 61 | ctx = { 62 | 'all_games_with_tag': Game.objects.filter(tags=selected_tag), 63 | 'selected_tag': selected_tag 64 | } 65 | 66 | return render( 67 | request, 68 | template_name='games_by_tag.html', 69 | context=ctx 70 | ) 71 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | {% load bootstrap4 i18n staticfiles %} 3 | 4 | 5 | 6 | {% trans 'Gameweb' %} 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | {% if user.is_staff or user.is_superuser %} 20 | {% include 'navbar_staff.html' %} 21 | {% elif user.is_authenticated %} 22 | {% include 'navbar_user_authenticated.html' %} 23 | {% else %} 24 | {% include 'navbar_user_unauthenticated.html' %} 25 | {% endif %} 26 |
27 |
28 |
29 | 30 |
31 |
32 |

{% trans 'Gameweb' %}

33 |
34 |
35 | {% for quote in all_quotes %} 36 |
37 |

{{ quote.quote }} - {{ quote.author }}

38 |
39 | {% endfor %} 40 |
41 |
42 |
43 | {% trans 'No image' %} 46 |
47 |
48 | {% block content %} 49 | {% endblock %} 50 |
51 | 52 | 53 | 56 | 59 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /templates/message_block.html: -------------------------------------------------------------------------------- 1 | {% if messages %} 2 | {% for message in messages %} 3 | 8 | {% endfor %} 9 | {% endif %} -------------------------------------------------------------------------------- /templates/navbar_staff.html: -------------------------------------------------------------------------------- 1 | {% load bootstrap4 i18n staticfiles %} 2 | -------------------------------------------------------------------------------- /templates/navbar_user_authenticated.html: -------------------------------------------------------------------------------- 1 | {% load bootstrap4 i18n staticfiles %} 2 | -------------------------------------------------------------------------------- /templates/navbar_user_unauthenticated.html: -------------------------------------------------------------------------------- 1 | {% load bootstrap4 i18n staticfiles %} 2 | -------------------------------------------------------------------------------- /users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/users/__init__.py -------------------------------------------------------------------------------- /users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import User 4 | 5 | admin.site.register(User) 6 | -------------------------------------------------------------------------------- /users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersConfig(AppConfig): 5 | name = 'users' 6 | -------------------------------------------------------------------------------- /users/forms.py: -------------------------------------------------------------------------------- 1 | from django.forms import (CharField, EmailField, Form, PasswordInput) 2 | 3 | 4 | class AddUserForm(Form): 5 | username = CharField(label='Username', strip=True) 6 | password = CharField(label='Password', widget=PasswordInput) 7 | first_name = CharField(label='First Name', strip=True, required=False) 8 | last_name = CharField(label='Surname', strip=True, required=False) 9 | email = EmailField(label='Email', required=False) 10 | 11 | def __init__(self, *args, **kwargs): 12 | super().__init__(*args, **kwargs) 13 | self.fields['username'].widget.attrs.update({'placeholder': 'Required'}) 14 | self.fields['password'].widget.attrs.update({'placeholder': 'Required'}) 15 | 16 | 17 | class LoginUserForm(Form): 18 | username = CharField(label='User', strip=True) 19 | password = CharField(label='Password', widget=PasswordInput) 20 | -------------------------------------------------------------------------------- /users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.2 on 2019-05-18 10:16 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('tag', '0001_initial'), 13 | ('genre', '0001_initial'), 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ('developer', '0001_initial'), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='User', 21 | fields=[ 22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('favorite_developer', models.ForeignKey(blank=True, null=True, on_delete=None, to='developer.Developer')), 24 | ('favorite_genre', models.ForeignKey(blank=True, null=True, on_delete=None, to='genre.Genre')), 25 | ('favorite_tags', models.ManyToManyField(blank=True, to='tag.Tag')), 26 | ('user', models.ForeignKey(on_delete=None, to=settings.AUTH_USER_MODEL)), 27 | ], 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /users/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawidbudzynski/gameweb_python_django/b95719f04b43d2cd0c804e5972a43648e05d9e39/users/migrations/__init__.py -------------------------------------------------------------------------------- /users/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User as DjangoUser 2 | from django.db import models 3 | 4 | 5 | class User(models.Model): 6 | user = models.ForeignKey(DjangoUser, on_delete=None) 7 | favorite_tags = models.ManyToManyField('tag.Tag', blank=True) 8 | favorite_genre = models.ForeignKey('genre.Genre', blank=True, null=True, on_delete=None) 9 | favorite_developer = models.ForeignKey('developer.Developer', blank=True, null=True, on_delete=None) 10 | 11 | def __str__(self): 12 | return self.user.username 13 | -------------------------------------------------------------------------------- /users/templates/user_create.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load bootstrap4 i18n staticfiles %} 3 | {% block content %} 4 | 5 | 6 | 7 | 8 | 9 | 10 |

{% trans 'Create user' %}

11 | 12 | 13 | 20 | 21 |
14 |
15 | {% csrf_token %} 16 | {{ form }} 17 | 18 |
19 |
22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /users/templates/user_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load bootstrap4 i18n staticfiles %} 3 | {% block content %} 4 | 5 | 6 | 7 | 8 | 9 | 10 |

{% trans 'User list' %}

11 | 12 | 13 | 14 | {% trans 'Create user' %} 15 | 16 | 17 | 18 |

19 | {% include 'message_block.html' %} 20 | {% for user in all_users %} 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 43 | 44 | 45 | 47 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 62 | 63 | 64 |

{{ user.user.username }}

{% trans 'Full name' %}

{{ user.user.first_name }} {{ user.user.last_name }}

{% trans 'Email' %}

{{ user.user.email }}

{% trans 'Favorite tags' %}

39 | {% for tag in user.favorite_tags.all %} 40 | {{ tag.name }} 41 | {% endfor %} 42 |

{% trans 'Favorite genre' %}

46 |
48 | {% for genre in user.favorite_genre.all %} 49 | {{ genre.name }} 50 | {% endfor %} 51 |

{% trans 'Favorite developer' %}

{{ user.favorite_developer }}

59 | {% trans 'Delete' %} 61 |
65 | {% endfor %} 66 | {% endblock %} 67 | 68 | 69 | -------------------------------------------------------------------------------- /users/templates/user_login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load bootstrap4 i18n staticfiles %} 3 | {% block content %} 4 | 5 | 6 | 7 | 8 | 9 | 10 |

{% trans 'Login' %}

11 | 12 | 13 | 20 | 21 |
14 |
15 | {% csrf_token %} 16 | {{ form }} 17 | 18 |
19 |
22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /users/tests.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User as DjangoUser 2 | from django.test import TestCase 3 | from django.urls import reverse 4 | 5 | from users.models import User 6 | from .forms import AddUserForm 7 | 8 | 9 | class UserFormTests(TestCase): 10 | 11 | def test_user_form(self): 12 | response = self.client.get(reverse('users:user-create')) 13 | self.assertEquals(response.status_code, 200) 14 | 15 | self.client.force_login(DjangoUser.objects.get_or_create(username='user_1', is_superuser=True)[0]) 16 | response = self.client.get(reverse('users:user-create')) 17 | self.assertEquals(response.status_code, 200) 18 | 19 | form = AddUserForm() 20 | self.assertFalse(form.is_valid()) 21 | 22 | data = {"name": "", 'password': ''} 23 | form = AddUserForm(data=data) 24 | self.assertFalse(form.is_valid()) 25 | self.assertEqual(form.errors, { 26 | 'username': ['To pole jest wymagane.'], 27 | 'password': ['To pole jest wymagane.'] 28 | }) 29 | 30 | data = {"username": "user_1", 'password': 'password_1'} 31 | form = AddUserForm(data=data) 32 | self.assertTrue(form.is_valid()) 33 | 34 | 35 | class UserViewsTests(TestCase): 36 | 37 | def setUp(self): 38 | django_user = DjangoUser.objects.create( 39 | username='user_1', 40 | password='password_1', 41 | first_name='first_name_1', 42 | last_name='last_name_1', 43 | email='email_1' 44 | ) 45 | User.objects.create(user=django_user) 46 | -------------------------------------------------------------------------------- /users/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = 'users' 6 | 7 | urlpatterns = [ 8 | path('user_list/', views.UserListView.as_view(), 9 | name='user_list'), 10 | path('user_create/', views.UserCreateView.as_view(), 11 | name='user-create'), 12 | path('user_delete/', views.UserDeleteView.as_view(), 13 | name='user-delete'), 14 | path('login', views.LoginView.as_view(), 15 | name='login'), 16 | path('logout', views.LogoutView.as_view(), 17 | name='logout'), 18 | ] 19 | -------------------------------------------------------------------------------- /users/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.contrib.auth import authenticate, login, logout 3 | from django.contrib.auth.models import User as DjangoUser 4 | from django.http import HttpResponseRedirect 5 | from django.shortcuts import render 6 | from django.urls import reverse 7 | from django.utils.translation import ugettext as _ 8 | from django.views import View 9 | 10 | from .forms import AddUserForm, LoginUserForm 11 | from .models import User 12 | 13 | 14 | class UserListView(View): 15 | def dispatch(self, request, *args, **kwargs): 16 | if not (request.user.is_staff or request.user.is_superuser): 17 | messages.add_message(request, messages.WARNING, _('Sorry, you can\'t do that')) 18 | return HttpResponseRedirect(reverse('news_api:mainpage')) 19 | return super().dispatch(request, *args, **kwargs) 20 | 21 | def get(self, request): 22 | return render( 23 | request, 24 | template_name='user_list.html', 25 | context={'all_users': User.objects.all().order_by('user__username')} 26 | ) 27 | 28 | 29 | class UserCreateView(View): 30 | 31 | def get(self, request): 32 | return render( 33 | request, 34 | template_name='user_create.html', 35 | context={'form': AddUserForm().as_p()} 36 | ) 37 | 38 | def post(self, request): 39 | form = AddUserForm(request.POST) 40 | if form.is_valid(): 41 | 42 | username = form.cleaned_data['username'] 43 | password = form.cleaned_data['password'] 44 | first_name = form.cleaned_data['first_name'] 45 | last_name = form.cleaned_data['last_name'] 46 | email = form.cleaned_data['email'] 47 | 48 | if DjangoUser.objects.filter(username=username).exists(): 49 | messages.add_message(request, messages.WARNING, _('User with this name already exists')) 50 | return HttpResponseRedirect(reverse('users:user-create')) 51 | 52 | django_user = DjangoUser.objects.create_user( 53 | username=username, 54 | password=password, 55 | first_name=first_name, 56 | last_name=last_name, 57 | email=email 58 | ) 59 | 60 | User.objects.create(user=django_user) 61 | messages.add_message(request, messages.INFO, _('User: {} created successfully').format(username)) 62 | return HttpResponseRedirect(reverse('users:user_list')) 63 | 64 | messages.add_message(request, messages.ERROR, _('Form invalid')) 65 | return HttpResponseRedirect(reverse('users:user-create')) 66 | 67 | 68 | class UserDeleteView(View): 69 | 70 | def dispatch(self, request, *args, **kwargs): 71 | if not (request.user.is_staff or request.user.is_superuser): 72 | messages.add_message(request, messages.WARNING, _('Sorry, you can\'t do that')) 73 | return HttpResponseRedirect(reverse('news_api:mainpage')) 74 | return super().dispatch(request, *args, **kwargs) 75 | 76 | def get(self, request, user_id): 77 | user = User.objects.get(id=user_id) 78 | user.delete() 79 | return HttpResponseRedirect(reverse('users:user-list')) 80 | 81 | 82 | class LoginView(View): 83 | def get(self, request): 84 | if request.session.get('loggedUser') is None: 85 | return render( 86 | request, 87 | template_name='user_login.html', 88 | context={'form': LoginUserForm().as_p()} 89 | ) 90 | del request.session['loggedUser'] 91 | return HttpResponseRedirect('/') 92 | 93 | def post(self, request): 94 | form = LoginUserForm(request.POST) 95 | if form.is_valid(): 96 | username = form.cleaned_data['username'] 97 | password = form.cleaned_data['password'] 98 | request.session['loggedUser'] = username 99 | user = authenticate(username=username, password=password) 100 | 101 | if user is not None: 102 | login(request, user) 103 | return HttpResponseRedirect('/') 104 | else: 105 | messages.add_message(request, messages.ERROR, _('Wrong password')) 106 | return HttpResponseRedirect(reverse('users:user-create')) 107 | 108 | messages.add_message(request, messages.ERROR, _('Form invalid')) 109 | return HttpResponseRedirect(reverse('users:user-create')) 110 | 111 | 112 | class LogoutView(View): 113 | def get(self, request): 114 | logout(request) 115 | return HttpResponseRedirect('/') 116 | -------------------------------------------------------------------------------- /utils/tests.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User as DjangoUser 2 | 3 | import random 4 | 5 | from developer.models import Developer 6 | from game.models import Game 7 | from genre.models import Genre 8 | from tag.models import Tag 9 | from users.models import User 10 | 11 | 12 | def populate_database(): 13 | django_user = DjangoUser.objects.create( 14 | username='user_1', 15 | password='password_1', 16 | first_name='first_name_1', 17 | last_name='last_name_1', 18 | email='email_1' 19 | ) 20 | User.objects.create(user=django_user) 21 | for i in range(1, 21): 22 | Tag.objects.create(name=f'tag_{i}') 23 | Genre.objects.create(name=f'genre_{i}') 24 | Developer.objects.create(name=f'developer_{i}') 25 | for i in range(1, 21): 26 | Game.objects.create( 27 | developer=Developer.objects.get(name='developer_{}'.format(random.randint(1, 20))), 28 | genre=Genre.objects.get(name='genre_{}'.format(random.randint(1, 20))), 29 | title=f'game_{i}', 30 | year=random.randint(2000, 2019), 31 | to_be_rated=random.choice([True, False]) 32 | ) 33 | for game in Game.objects.all(): 34 | for tag in random.choices(list(Tag.objects.all()), k=5): 35 | game.tags.add(tag) 36 | --------------------------------------------------------------------------------