-
10 |
- Published on 11 |
- 14 | 15 | 16 |
├── .DS_Store ├── .gitattributes ├── .gitignore ├── README.md ├── backend ├── .DS_Store ├── backend │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── blog │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_post_likes.py │ │ ├── 0003_remove_post_likes_post_likes.py │ │ ├── 0004_comment_likes.py │ │ └── __init__.py │ ├── models.py │ ├── mutations.py │ ├── queries.py │ ├── schema.py │ ├── tests.py │ ├── types.py │ └── views.py ├── manage.py └── requirements.txt ├── frontend ├── .gitignore ├── README.md ├── babel.config.js ├── jsconfig.json ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ ├── favicon.ico │ ├── index.html │ └── uploads │ │ ├── posts │ │ └── featured_images │ │ │ └── 2022 │ │ │ ├── 04 │ │ │ └── 27 │ │ │ │ ├── File_1.jpeg │ │ │ │ ├── File_2.jpeg │ │ │ │ └── File_3.jpeg │ │ │ └── 08 │ │ │ └── 17 │ │ │ └── logtail-livetail.png │ │ └── users │ │ └── avatars │ │ └── 2022 │ │ └── 04 │ │ └── 27 │ │ └── FB_IMG_1642559522547.jpeg ├── src │ ├── App.vue │ ├── apollo-config.js │ ├── assets │ │ └── logo.png │ ├── components │ │ ├── CommentSection.vue │ │ ├── CommentSingle.vue │ │ └── PostList.vue │ ├── index.css │ ├── main.js │ ├── mutations.js │ ├── queries.js │ ├── router │ │ └── index.js │ ├── stores │ │ └── user.js │ └── views │ │ ├── main │ │ ├── AllCategories.vue │ │ ├── AllTags.vue │ │ ├── Category.vue │ │ ├── Home.vue │ │ ├── Post.vue │ │ └── Tag.vue │ │ └── user │ │ ├── Account.vue │ │ ├── Profile.vue │ │ ├── SignIn.vue │ │ └── SignUp.vue ├── tailwind.config.js └── vue.config.js └── screenshots ├── Screen Shot 2022-02-13 at 7.13.52 PM.png ├── Screen Shot 2022-02-13 at 7.14.07 PM.png ├── Screen Shot 2022-02-13 at 7.14.20 PM.png ├── Screen Shot 2022-02-13 at 7.14.34 PM.png ├── Screen Shot 2022-02-13 at 7.14.43 PM.png ├── Screen Shot 2022-02-13 at 7.15.21 PM.png ├── Screen Shot 2022-02-13 at 7.15.33 PM.png ├── Screen Shot 2022-02-13 at 7.42.00 PM.png ├── Screen Shot 2022-02-16 at 10.20.18 AM.png └── Screen Shot 2022-02-16 at 10.20.36 AM.png /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedevspacehq/django-vue-starter-blog/69d57b41085230b90029602a27b5b052dd82cf12/.DS_Store -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .nox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # IPython 77 | profile_default/ 78 | ipython_config.py 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # Environments 90 | .env 91 | .venv 92 | env/ 93 | venv/ 94 | ENV/ 95 | env.bak/ 96 | venv.bak/ 97 | 98 | # Spyder project settings 99 | .spyderproject 100 | .spyproject 101 | 102 | # Rope project settings 103 | .ropeproject 104 | 105 | # mkdocs documentation 106 | /site 107 | 108 | # mypy 109 | .mypy_cache/ 110 | .dmypy.json 111 | dmypy.json 112 | 113 | # Pyre type checker 114 | .pyre/ 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Vue.js Starter Blog 2 | 3 | A simple blog application created using Django, Vue.js and GraphQL. 4 | 5 | Interest in learning how to create this demo app with Django and Vue? Take a look at this tutorial -> [How to Create a Modern App with Django and Vue](https://www.thedevspace.io/community/django-vue) 6 | 7 | ## Features 8 | 9 | - Including recent posts, category, tag, and post page 10 | - User registration and login. Built with JWT and Vuex (migrated to Pinia, which is the recommended package for stores). 11 | - Comment section. Only authenticated users can leave comment, and it won’t show up until approved by the admin. 12 | - User profile page. Guest user can see and edit all comments that belong to that user. 13 | - Like system. Guest user can like posts and comments. 14 | 15 | ## Coming Soon 16 | 17 | - Author verification. Guest user can verify to become authors, who can post new articles. 18 | 19 | ## Screenshots 20 | 21 | Home Page 22 | 23 |  24 | 25 | All Categories 26 | 27 |  28 | 29 | All Tags 30 | 31 |  32 | 33 | Sign In Page 34 | 35 |  36 | 37 | Sign Up Page 38 | 39 |  40 | 41 | Post Page 42 | 43 |  44 | 45 | Comment Section 46 | 47 |  48 | 49 | User Profile Page 50 | 51 |  52 | 53 | User Profile Page Comment Section 54 | 55 |  56 | 57 | Django Admin Panel 58 | 59 |  60 | 61 | ## Installation 62 | 63 | For the backend, first create a virtual environment. 64 | 65 | ```bash 66 | cd backend 67 | python3 -m venv env 68 | source env/bin/activate 69 | ``` 70 | 71 | Install required packages. 72 | 73 | ```bash 74 | pip install -r requirements.txt 75 | ``` 76 | 77 | Run migrations. 78 | 79 | ```bash 80 | python manage.py makemigrations 81 | python manage.py migrate 82 | ``` 83 | 84 | If you get this error: `ImportError: cannot import name 'force_text' from 'django.utils.encoding'`, you can replace `force_text` with `force_str` like [this article](https://exerror.com/importerror-cannot-import-name-force_text-from-django-utils-encoding/). This issue should be resolved in future versions of Django. 85 | 86 | Start dev server. 87 | 88 | ```bash 89 | python manage.py runserver 90 | ``` 91 | 92 | For the frontend, install packages. 93 | 94 | ```bash 95 | cd frontend 96 | npm install 97 | ``` 98 | 99 | If you are getting errors when installing packages, just run `npm install --force`. Some packages has been deprecated, but everything still work for now. I will try to update this project as soon as possible. 100 | 101 | Start frontend dev server. 102 | 103 | ```bash 104 | npm run serve 105 | ``` 106 | -------------------------------------------------------------------------------- /backend/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedevspacehq/django-vue-starter-blog/69d57b41085230b90029602a27b5b052dd82cf12/backend/.DS_Store -------------------------------------------------------------------------------- /backend/backend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedevspacehq/django-vue-starter-blog/69d57b41085230b90029602a27b5b052dd82cf12/backend/backend/__init__.py -------------------------------------------------------------------------------- /backend/backend/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for backend project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /backend/backend/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for backend project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.0.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | from pathlib import Path 15 | 16 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 17 | BASE_DIR = Path(__file__).resolve().parent.parent 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = 'django-insecure-vs50-o5-6%&=9^0%ynywu25i_ce!bs2%7u@#!0j*j5p5cs1)*y' 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = [] 30 | 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = [ 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 'blog', 42 | 'graphene_django', 43 | 'ckeditor', 44 | 'ckeditor_uploader', 45 | 'corsheaders', 46 | ] 47 | 48 | MIDDLEWARE = [ 49 | 'corsheaders.middleware.CorsMiddleware', 50 | 'django.middleware.security.SecurityMiddleware', 51 | 'django.contrib.sessions.middleware.SessionMiddleware', 52 | 'django.middleware.common.CommonMiddleware', 53 | 'django.middleware.csrf.CsrfViewMiddleware', 54 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 55 | 'django.contrib.messages.middleware.MessageMiddleware', 56 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 57 | ] 58 | 59 | ROOT_URLCONF = 'backend.urls' 60 | 61 | TEMPLATES = [ 62 | { 63 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 64 | 'DIRS': [], 65 | 'APP_DIRS': True, 66 | 'OPTIONS': { 67 | 'context_processors': [ 68 | 'django.template.context_processors.debug', 69 | 'django.template.context_processors.request', 70 | 'django.contrib.auth.context_processors.auth', 71 | 'django.contrib.messages.context_processors.messages', 72 | ], 73 | }, 74 | }, 75 | ] 76 | 77 | WSGI_APPLICATION = 'backend.wsgi.application' 78 | 79 | 80 | # Database 81 | # https://docs.djangoproject.com/en/4.0/ref/settings/#databases 82 | 83 | DATABASES = { 84 | 'default': { 85 | 'ENGINE': 'django.db.backends.sqlite3', 86 | 'NAME': BASE_DIR / 'db.sqlite3', 87 | } 88 | } 89 | 90 | 91 | # Password validation 92 | # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators 93 | 94 | AUTH_PASSWORD_VALIDATORS = [ 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 100 | }, 101 | { 102 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 103 | }, 104 | { 105 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 106 | }, 107 | ] 108 | 109 | 110 | # Internationalization 111 | # https://docs.djangoproject.com/en/4.0/topics/i18n/ 112 | 113 | LANGUAGE_CODE = 'en-us' 114 | 115 | TIME_ZONE = 'UTC' 116 | 117 | USE_I18N = True 118 | 119 | USE_TZ = True 120 | 121 | 122 | # Static files (CSS, JavaScript, Images) 123 | # https://docs.djangoproject.com/en/4.0/howto/static-files/ 124 | 125 | STATIC_URL = 'static/' 126 | 127 | # Default primary key field type 128 | # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field 129 | 130 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 131 | 132 | # CKEditor 133 | CKEDITOR_UPLOAD_PATH = "posts/uploads/%Y/%m/%d/" 134 | 135 | # Media Files 136 | MEDIA_ROOT = os.path.join(BASE_DIR, '../frontend/public/uploads') 137 | MEDIA_URL = '/media/' 138 | 139 | # Change Default User Model 140 | AUTH_USER_MODEL = 'blog.User' 141 | 142 | # Configure GraphQL 143 | GRAPHENE = { 144 | "SCHEMA": "blog.schema.schema", 145 | 'MIDDLEWARE': [ 146 | 'graphql_jwt.middleware.JSONWebTokenMiddleware', 147 | ], 148 | } 149 | 150 | # Cross origin resource sharing 151 | CORS_ORIGIN_ALLOW_ALL = False 152 | # Matches the port that Vue.js is using 153 | CORS_ORIGIN_WHITELIST = ("http://localhost:8080",) 154 | 155 | # JWT setting 156 | AUTHENTICATION_BACKENDS = [ 157 | 'graphql_jwt.backends.JSONWebTokenBackend', 158 | 'django.contrib.auth.backends.ModelBackend', 159 | ] -------------------------------------------------------------------------------- /backend/backend/urls.py: -------------------------------------------------------------------------------- 1 | """backend URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/4.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path 18 | from django.views.decorators.csrf import csrf_exempt 19 | from graphene_django.views import GraphQLView 20 | from graphene_file_upload.django import FileUploadGraphQLView 21 | from django.conf import settings 22 | from django.conf.urls.static import static 23 | 24 | urlpatterns = [ 25 | path('admin/', admin.site.urls), 26 | path("graphql", csrf_exempt(FileUploadGraphQLView.as_view(graphiql=True))), 27 | ] 28 | 29 | urlpatterns = urlpatterns + \ 30 | static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 31 | -------------------------------------------------------------------------------- /backend/backend/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for backend 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/4.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', 'backend.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /backend/blog/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedevspacehq/django-vue-starter-blog/69d57b41085230b90029602a27b5b052dd82cf12/backend/blog/__init__.py -------------------------------------------------------------------------------- /backend/blog/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import * 3 | 4 | # Register your models here. 5 | class UserAdmin(admin.ModelAdmin): 6 | list_display = ('username', 'first_name', 'last_name', 'email', 'date_joined') 7 | 8 | class CategoryAdmin(admin.ModelAdmin): 9 | prepopulated_fields = {'slug': ('name',)} 10 | 11 | 12 | class TagAdmin(admin.ModelAdmin): 13 | prepopulated_fields = {'slug': ('name',)} 14 | 15 | 16 | class PostAdmin(admin.ModelAdmin): 17 | prepopulated_fields = {'slug': ('title',)} 18 | list_display = ('title', 'is_published', 'is_featured', 'created_at') 19 | 20 | class CommentAdmin(admin.ModelAdmin): 21 | list_display = ('__str__', 'is_approved', 'created_at') 22 | 23 | 24 | admin.site.register(Site) 25 | admin.site.register(User, UserAdmin) 26 | admin.site.register(Category, CategoryAdmin) 27 | admin.site.register(Tag, TagAdmin) 28 | admin.site.register(Post, PostAdmin) 29 | admin.site.register(Comment, CommentAdmin) -------------------------------------------------------------------------------- /backend/blog/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BlogConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'blog' 7 | -------------------------------------------------------------------------------- /backend/blog/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.2 on 2022-02-13 20:09 2 | 3 | import ckeditor.fields 4 | from django.conf import settings 5 | import django.contrib.auth.models 6 | import django.contrib.auth.validators 7 | from django.db import migrations, models 8 | import django.db.models.deletion 9 | import django.utils.timezone 10 | 11 | 12 | class Migration(migrations.Migration): 13 | 14 | initial = True 15 | 16 | dependencies = [ 17 | ('auth', '0012_alter_user_first_name_max_length'), 18 | ] 19 | 20 | operations = [ 21 | migrations.CreateModel( 22 | name='User', 23 | fields=[ 24 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 25 | ('password', models.CharField(max_length=128, verbose_name='password')), 26 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 27 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 28 | ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), 29 | ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), 30 | ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), 31 | ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), 32 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 33 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 34 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 35 | ('avatar', models.ImageField(default='users/avatars/default.jpg', upload_to='users/avatars/%Y/%m/%d/')), 36 | ('bio', models.TextField(max_length=500, null=True)), 37 | ('location', models.CharField(max_length=30, null=True)), 38 | ('website', models.CharField(max_length=100, null=True)), 39 | ('joined_date', models.DateField(auto_now_add=True)), 40 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), 41 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), 42 | ], 43 | options={ 44 | 'verbose_name': 'user', 45 | 'verbose_name_plural': '2. Users', 46 | }, 47 | managers=[ 48 | ('objects', django.contrib.auth.models.UserManager()), 49 | ], 50 | ), 51 | migrations.CreateModel( 52 | name='Category', 53 | fields=[ 54 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 55 | ('name', models.CharField(max_length=200)), 56 | ('slug', models.SlugField()), 57 | ('description', models.TextField()), 58 | ], 59 | options={ 60 | 'verbose_name': 'category', 61 | 'verbose_name_plural': '3. Categories', 62 | }, 63 | ), 64 | migrations.CreateModel( 65 | name='Site', 66 | fields=[ 67 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 68 | ('name', models.CharField(max_length=200)), 69 | ('description', models.TextField()), 70 | ('logo', models.ImageField(upload_to='site/logo/')), 71 | ], 72 | options={ 73 | 'verbose_name': 'site', 74 | 'verbose_name_plural': '1. Site', 75 | }, 76 | ), 77 | migrations.CreateModel( 78 | name='Tag', 79 | fields=[ 80 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 81 | ('name', models.CharField(max_length=200)), 82 | ('slug', models.SlugField()), 83 | ('description', models.TextField()), 84 | ], 85 | options={ 86 | 'verbose_name': 'tag', 87 | 'verbose_name_plural': '4. Tags', 88 | }, 89 | ), 90 | migrations.CreateModel( 91 | name='Post', 92 | fields=[ 93 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 94 | ('title', models.CharField(max_length=200)), 95 | ('slug', models.SlugField()), 96 | ('content', ckeditor.fields.RichTextField()), 97 | ('featured_image', models.ImageField(upload_to='posts/featured_images/%Y/%m/%d/')), 98 | ('is_published', models.BooleanField(default=False)), 99 | ('is_featured', models.BooleanField(default=False)), 100 | ('created_at', models.DateField(auto_now_add=True)), 101 | ('modified_at', models.DateField(auto_now=True)), 102 | ('category', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='blog.category')), 103 | ('tag', models.ManyToManyField(to='blog.Tag')), 104 | ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), 105 | ], 106 | options={ 107 | 'verbose_name': 'post', 108 | 'verbose_name_plural': '5. Posts', 109 | }, 110 | ), 111 | migrations.CreateModel( 112 | name='Comment', 113 | fields=[ 114 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 115 | ('content', models.TextField(max_length=1000)), 116 | ('created_at', models.DateField(auto_now_add=True)), 117 | ('is_approved', models.BooleanField(default=False)), 118 | ('post', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='blog.post')), 119 | ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), 120 | ], 121 | options={ 122 | 'verbose_name': 'comment', 123 | 'verbose_name_plural': '6. Comments', 124 | }, 125 | ), 126 | ] 127 | -------------------------------------------------------------------------------- /backend/blog/migrations/0002_post_likes.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.2 on 2022-02-16 16:11 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('blog', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='post', 15 | name='likes', 16 | field=models.IntegerField(default=0), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/blog/migrations/0003_remove_post_likes_post_likes.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.2 on 2022-02-16 19:37 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('blog', '0002_post_likes'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='post', 16 | name='likes', 17 | ), 18 | migrations.AddField( 19 | model_name='post', 20 | name='likes', 21 | field=models.ManyToManyField(related_name='post_like', to=settings.AUTH_USER_MODEL), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /backend/blog/migrations/0004_comment_likes.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.2 on 2022-02-18 22:24 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('blog', '0003_remove_post_likes_post_likes'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='comment', 16 | name='likes', 17 | field=models.ManyToManyField(related_name='comment_like', to=settings.AUTH_USER_MODEL), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /backend/blog/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedevspacehq/django-vue-starter-blog/69d57b41085230b90029602a27b5b052dd82cf12/backend/blog/migrations/__init__.py -------------------------------------------------------------------------------- /backend/blog/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from ckeditor.fields import RichTextField 3 | from django.conf import settings 4 | from django.contrib.auth.models import AbstractUser 5 | 6 | # Create your models here. 7 | 8 | # General information for the entire website 9 | 10 | 11 | class Site(models.Model): 12 | name = models.CharField(max_length=200) 13 | description = models.TextField() 14 | logo = models.ImageField(upload_to='site/logo/') 15 | 16 | class Meta: 17 | verbose_name = 'site' 18 | verbose_name_plural = '1. Site' 19 | 20 | def __str__(self): 21 | return self.name 22 | 23 | 24 | # New user model 25 | class User(AbstractUser): 26 | avatar = models.ImageField( 27 | upload_to='users/avatars/%Y/%m/%d/', 28 | default='users/avatars/default.jpg' 29 | ) 30 | bio = models.TextField(max_length=500, null=True) 31 | location = models.CharField(max_length=30, null=True) 32 | website = models.CharField(max_length=100, null=True) 33 | joined_date = models.DateField(auto_now_add=True) 34 | 35 | class Meta: 36 | verbose_name = 'user' 37 | verbose_name_plural = '2. Users' 38 | 39 | def __str__(self): 40 | return self.username 41 | 42 | 43 | # Category model 44 | class Category(models.Model): 45 | name = models.CharField(max_length=200) 46 | slug = models.SlugField() 47 | description = models.TextField() 48 | 49 | class Meta: 50 | verbose_name = 'category' 51 | verbose_name_plural = '3. Categories' 52 | 53 | def __str__(self): 54 | return self.name 55 | 56 | 57 | # Tag model 58 | class Tag(models.Model): 59 | name = models.CharField(max_length=200) 60 | slug = models.SlugField() 61 | description = models.TextField() 62 | 63 | class Meta: 64 | verbose_name = 'tag' 65 | verbose_name_plural = '4. Tags' 66 | 67 | def __str__(self): 68 | return self.name 69 | 70 | 71 | # Post model 72 | class Post(models.Model): 73 | title = models.CharField(max_length=200) 74 | slug = models.SlugField() 75 | content = RichTextField() 76 | featured_image = models.ImageField( 77 | upload_to='posts/featured_images/%Y/%m/%d/') 78 | is_published = models.BooleanField(default=False) 79 | is_featured = models.BooleanField(default=False) 80 | created_at = models.DateField(auto_now_add=True) 81 | modified_at = models.DateField(auto_now=True) 82 | 83 | # Each post can receive likes from multiple users, and each user can like multiple posts 84 | likes = models.ManyToManyField(User, related_name='post_like') 85 | 86 | # Each post belong to one user and one category. 87 | # Each post has many tags, and each tag has many posts. 88 | category = models.ForeignKey( 89 | Category, on_delete=models.SET_NULL, null=True) 90 | tag = models.ManyToManyField(Tag) 91 | user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) 92 | 93 | class Meta: 94 | verbose_name = 'post' 95 | verbose_name_plural = '5. Posts' 96 | 97 | def __str__(self): 98 | return self.title 99 | 100 | def get_number_of_likes(self): 101 | return self.likes.count() 102 | 103 | 104 | # Comment model 105 | class Comment(models.Model): 106 | content = models.TextField(max_length=1000) 107 | created_at = models.DateField(auto_now_add=True) 108 | is_approved = models.BooleanField(default=False) 109 | 110 | # Each post can receive likes from multiple users, and each user can like multiple posts 111 | likes = models.ManyToManyField(User, related_name='comment_like') 112 | 113 | # Each comment belongs to one user and one post 114 | user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) 115 | post = models.ForeignKey(Post, on_delete=models.SET_NULL, null=True) 116 | 117 | class Meta: 118 | verbose_name = 'comment' 119 | verbose_name_plural = '6. Comments' 120 | 121 | def __str__(self): 122 | if len(self.content) > 50: 123 | comment = self.content[:50] + '...' 124 | else: 125 | comment = self.content 126 | return comment 127 | 128 | def get_number_of_likes(self): 129 | return self.likes.count() 130 | -------------------------------------------------------------------------------- /backend/blog/mutations.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | import graphql_jwt 3 | from graphene_file_upload.scalars import Upload 4 | from blog import models, types 5 | 6 | 7 | # Mutation sends data to the database 8 | 9 | # Customize the ObtainJSONWebToken behavior to include the user info 10 | class ObtainJSONWebToken(graphql_jwt.JSONWebTokenMutation): 11 | user = graphene.Field(types.UserType) 12 | 13 | @classmethod 14 | def resolve(cls, root, info, **kwargs): 15 | return cls(user=info.context.user) 16 | 17 | 18 | class CreateUser(graphene.Mutation): 19 | user = graphene.Field(types.UserType) 20 | 21 | class Arguments: 22 | username = graphene.String(required=True) 23 | password = graphene.String(required=True) 24 | email = graphene.String(required=True) 25 | 26 | def mutate(self, info, username, password, email): 27 | user = models.User( 28 | username=username, 29 | email=email, 30 | ) 31 | user.set_password(password) 32 | user.save() 33 | 34 | return CreateUser(user=user) 35 | 36 | 37 | class UpdateUserProfile(graphene.Mutation): 38 | user = graphene.Field(types.UserType) 39 | 40 | class Arguments: 41 | user_id = graphene.ID(required=True) 42 | first_name = graphene.String(required=False) 43 | last_name = graphene.String(required=False) 44 | avatar = Upload(required=False) 45 | bio = graphene.String(required=False) 46 | location = graphene.String(required=False) 47 | website = graphene.String(required=False) 48 | 49 | def mutate(self, info, user_id, first_name='', last_name='', avatar='', bio='', location='', website=''): 50 | user = models.User.objects.get(pk=user_id) 51 | 52 | user.first_name = first_name 53 | user.last_name = last_name 54 | user.avatar = avatar 55 | user.bio = bio 56 | user.location = location 57 | user.website = website 58 | 59 | user.save() 60 | 61 | return UpdateUserProfile(user=user) 62 | 63 | 64 | class CreateComment(graphene.Mutation): 65 | comment = graphene.Field(types.CommentType) 66 | 67 | class Arguments: 68 | content = graphene.String(required=True) 69 | user_id = graphene.ID(required=True) 70 | post_id = graphene.ID(required=True) 71 | 72 | def mutate(self, info, content, user_id, post_id): 73 | comment = models.Comment( 74 | content=content, 75 | user_id=user_id, 76 | post_id=post_id, 77 | ) 78 | comment.save() 79 | 80 | return CreateComment(comment=comment) 81 | 82 | 83 | class UpdatePostLike(graphene.Mutation): 84 | post = graphene.Field(types.PostType) 85 | 86 | class Arguments: 87 | post_id = graphene.ID(required=True) 88 | user_id = graphene.ID(required=True) 89 | 90 | def mutate(self, info, post_id, user_id): 91 | post = models.Post.objects.get(pk=post_id) 92 | 93 | if post.likes.filter(pk=user_id).exists(): 94 | post.likes.remove(user_id) 95 | else: 96 | post.likes.add(user_id) 97 | 98 | post.save() 99 | 100 | return UpdatePostLike(post=post) 101 | 102 | 103 | class UpdateCommentLike(graphene.Mutation): 104 | comment = graphene.Field(types.CommentType) 105 | 106 | class Arguments: 107 | comment_id = graphene.ID(required=True) 108 | user_id = graphene.ID(required=True) 109 | 110 | def mutate(self, info, comment_id, user_id): 111 | comment = models.Comment.objects.get(pk=comment_id) 112 | 113 | if comment.likes.filter(pk=user_id).exists(): 114 | comment.likes.remove(user_id) 115 | else: 116 | comment.likes.add(user_id) 117 | 118 | comment.save() 119 | 120 | return UpdateCommentLike(comment=comment) 121 | 122 | 123 | class Mutation(graphene.ObjectType): 124 | # Tokens 125 | token_auth = ObtainJSONWebToken.Field() 126 | verify_token = graphql_jwt.Verify.Field() 127 | refresh_token = graphql_jwt.Refresh.Field() 128 | 129 | create_user = CreateUser.Field() 130 | create_comment = CreateComment.Field() 131 | 132 | update_post_like = UpdatePostLike.Field() 133 | update_comment_like = UpdateCommentLike.Field() 134 | update_user_profile = UpdateUserProfile.Field() -------------------------------------------------------------------------------- /backend/blog/queries.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from blog import models 3 | from blog import types 4 | 5 | 6 | # The Query class 7 | class Query(graphene.ObjectType): 8 | site = graphene.Field(types.SiteType) 9 | all_posts = graphene.List(types.PostType) 10 | all_categories = graphene.List(types.CategoryType) 11 | all_tags = graphene.List(types.TagType) 12 | posts_by_category = graphene.List(types.PostType, category=graphene.String()) 13 | posts_by_tag = graphene.List(types.PostType, tag=graphene.String()) 14 | post_by_slug = graphene.Field(types.PostType, slug=graphene.String()) 15 | current_user = graphene.Field(types.UserType, username=graphene.String()) 16 | 17 | def resolve_site(root, info): 18 | return ( 19 | models.Site.objects.first() 20 | ) 21 | 22 | def resolve_all_posts(root, info): 23 | return ( 24 | models.Post.objects.all() 25 | ) 26 | 27 | def resolve_all_categories(root, info): 28 | return ( 29 | models.Category.objects.all() 30 | ) 31 | 32 | def resolve_all_tags(root, info): 33 | return ( 34 | models.Tag.objects.all() 35 | ) 36 | 37 | def resolve_posts_by_category(root, info, category): 38 | return ( 39 | models.Post.objects.filter(category__slug__iexact=category) 40 | ) 41 | 42 | def resolve_posts_by_tag(root, info, tag): 43 | return ( 44 | models.Post.objects.filter(tag__slug__iexact=tag) 45 | ) 46 | 47 | def resolve_post_by_slug(root, info, slug): 48 | return ( 49 | models.Post.objects.get(slug__iexact=slug) 50 | ) 51 | 52 | def resolve_current_user(self, info, username): 53 | return ( 54 | models.User.objects.get(username__iexact=username) 55 | ) -------------------------------------------------------------------------------- /backend/blog/schema.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from blog import queries, mutations 3 | 4 | 5 | schema = graphene.Schema(query=queries.Query, mutation=mutations.Mutation) 6 | -------------------------------------------------------------------------------- /backend/blog/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/blog/types.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene_django import DjangoObjectType 3 | from blog import models 4 | 5 | 6 | class SiteType(DjangoObjectType): 7 | class Meta: 8 | model = models.Site 9 | 10 | 11 | class UserType(DjangoObjectType): 12 | class Meta: 13 | model = models.User 14 | 15 | 16 | class CategoryType(DjangoObjectType): 17 | class Meta: 18 | model = models.Category 19 | 20 | 21 | class TagType(DjangoObjectType): 22 | class Meta: 23 | model = models.Tag 24 | 25 | 26 | class PostType(DjangoObjectType): 27 | class Meta: 28 | model = models.Post 29 | 30 | number_of_likes = graphene.String() 31 | 32 | def resolve_number_of_likes(self, info): 33 | return self.get_number_of_likes() 34 | 35 | 36 | class CommentType(DjangoObjectType): 37 | class Meta: 38 | model = models.Comment 39 | 40 | number_of_likes = graphene.String() 41 | 42 | def resolve_number_of_likes(self, info): 43 | return self.get_number_of_likes() 44 | -------------------------------------------------------------------------------- /backend/blog/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /backend/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | aniso8601==7.0.0 2 | asgiref==3.5.0 3 | autopep8==1.6.0 4 | Django==4.0.2 5 | django-ckeditor==6.2.0 6 | django-cors-headers==3.11.0 7 | django-graphql-jwt==0.3.4 8 | django-js-asset==1.2.2 9 | graphene==2.1.9 10 | graphene-django==2.15.0 11 | graphene-file-upload==1.3.0 12 | graphql-core==2.3.2 13 | graphql-relay==2.0.1 14 | Pillow==9.0.1 15 | promise==2.3 16 | pycodestyle==2.8.0 17 | PyJWT==2.3.0 18 | Rx==1.6.1 19 | singledispatch==3.7.0 20 | six==1.16.0 21 | sqlparse==0.4.2 22 | text-unidecode==1.3 23 | toml==0.10.2 24 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # frontend 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /frontend/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "baseUrl": "./", 6 | "moduleResolution": "node", 7 | "paths": { 8 | "@/*": [ 9 | "src/*" 10 | ] 11 | }, 12 | "lib": [ 13 | "esnext", 14 | "dom", 15 | "dom.iterable", 16 | "scripthost" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "@apollo/client": "^3.5.8", 12 | "@popperjs/core": "^2.11.2", 13 | "@tailwindcss/forms": "^0.4.0", 14 | "@vue/apollo-option": "^4.0.0-alpha.16", 15 | "apollo-upload-client": "^17.0.0", 16 | "core-js": "^3.8.3", 17 | "graphql": "^16.3.0", 18 | "graphql-tag": "^2.12.6", 19 | "jwt-decode": "^3.1.2", 20 | "pinia": "^2.0.13", 21 | "vue": "^3.2.13", 22 | "vue-router": "^4.0.0-0" 23 | }, 24 | "devDependencies": { 25 | "@babel/core": "^7.12.16", 26 | "@babel/eslint-parser": "^7.12.16", 27 | "@vue/cli-plugin-babel": "~5.0.0-rc.2", 28 | "@vue/cli-plugin-eslint": "~5.0.0-rc.2", 29 | "@vue/cli-plugin-router": "~4.5.0", 30 | "@vue/cli-service": "~5.0.0-rc.2", 31 | "autoprefixer": "^10.4.2", 32 | "eslint": "^7.32.0", 33 | "eslint-plugin-vue": "^8.0.3", 34 | "postcss": "^8.4.6", 35 | "tailwindcss": "^3.0.19", 36 | "vue-cli-plugin-apollo": "~0.22.2" 37 | }, 38 | "eslintConfig": { 39 | "root": true, 40 | "env": { 41 | "node": true 42 | }, 43 | "extends": [ 44 | "plugin:vue/vue3-essential", 45 | "eslint:recommended" 46 | ], 47 | "parserOptions": { 48 | "parser": "@babel/eslint-parser" 49 | }, 50 | "rules": {} 51 | }, 52 | "browserslist": [ 53 | "> 1%", 54 | "last 2 versions", 55 | "not dead", 56 | "not ie 11" 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedevspacehq/django-vue-starter-blog/69d57b41085230b90029602a27b5b052dd82cf12/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 |Comments:
4 | 5 | 6 |12 | {{ this.comment.user.username }} 13 |
14 |15 | - {{ formatDate(this.comment.createdAt) }} 16 |
17 |20 | {{ this.comment.content }} 21 |
22 | 23 | 24 |Likes:
26 |5 | A blog created with Django, Vue.js and TailwindCSS 6 |
7 | 8 |5 | A blog created with Django, Vue.js and TailwindCSS 6 |
7 | 8 |13 | {{ formatDate(this.postBySlug.createdAt) }} - By 14 | {{ this.postBySlug.user.username }} 15 |
16 |You have been signed in.
5 |Sorry, we cannot find your credentials.
6 |13 | {{ this.userInfo.firstName }} {{ this.userInfo.lastName }} 14 |
15 |@{{ this.userInfo.username }}
16 |Location: {{ this.userInfo.location }}
17 |18 | Website: {{ this.userInfo.website }} 19 |
20 |23 | {{ this.userInfo.bio }} 24 |
25 |213 | {{ comment.content }} 214 |
215 |