Skip to content

Commit b3d6446

Browse files
committed
baseapp-pages: initial package
1 parent bde6ff3 commit b3d6446

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+2346
-2
lines changed

.github/workflows/github-actions.yml

+5-1
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,8 @@ jobs:
5555
baseapp-follows:
5656
uses: ./.github/workflows/project-workflow.yml
5757
with:
58-
project: baseapp-follows
58+
project: baseapp-follows
59+
baseapp-pages:
60+
uses: ./.github/workflows/project-workflow.yml
61+
with:
62+
project: baseapp-pages

.github/workflows/publish-actions.yml

+5
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,9 @@ jobs:
6565
with:
6666
project: baseapp-follows
6767
secrets: inherit
68+
baseapp-pages:
69+
uses: ./.github/workflows/publish-workflow.yml
70+
with:
71+
project: baseapp-pages
72+
secrets: inherit
6873

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ Functionality for url shortening
5252

5353
Reusable app to enable any model follow/unfollow any model.
5454

55+
## [baseapp-pages](baseapp-pages)
56+
57+
Reusable app to handle pages, URL's paths and metadata. It provides useful models and GraphQL Interfaces.
58+
5559
## How to develop
5660

5761
Each module of baseapp-backend has a demo project in `testproject/` directory, which can be run as a standalone Django app to test. Then in baseapp-backend directory:

baseapp-core/baseapp_core/tests/settings.py

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
MIDDLEWARE = [
5959
"django.middleware.security.SecurityMiddleware",
6060
"django.contrib.sessions.middleware.SessionMiddleware",
61+
"django.middleware.locale.LocaleMiddleware",
6162
"django.middleware.common.CommonMiddleware",
6263
"django.middleware.csrf.CsrfViewMiddleware",
6364
"django.contrib.auth.middleware.AuthenticationMiddleware",

baseapp-notifications/pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ line-length = 100
77
target_version = ['py38']
88

99
[tool.isort]
10-
profile = "black"
10+
profile = "black"

baseapp-pages/README.md

+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# BaseApp Pages
2+
3+
Reusable app to handle pages, URL's paths and metadata. It provides useful models and GraphQL Interfaces.
4+
5+
## Whats missing
6+
- [ ] Allow for custom settings.LANGUAGES per project
7+
- [ ] Make create migration work with TranslatedField
8+
9+
Currenly if you have a different set of languages in your projects it will create a new migration changing the fields. So if you have a migration check test it will fail because the `settings.LANGUAGES are different.
10+
11+
## How to install:
12+
13+
Add dependencies to your `requirements/base.txt` file:
14+
15+
```
16+
baseapp-pages
17+
```
18+
19+
And run provision or manually `pip install -r requirements/base.ext`
20+
21+
If you want to develop, [install using this other guide](#how-to-develop).
22+
23+
## How to use
24+
25+
Add `baseapp_pages` and `django_quill` to your project's `INSTALLED_APPS` and run `./manage.py migrate` as any other django model:
26+
27+
```python
28+
INSTALLED_APPS = [
29+
# ...
30+
'baseapp_pages',
31+
'django_quill',
32+
# ...
33+
]
34+
```
35+
36+
Add `django.middleware.locale.LocaleMiddleware` to the `MIDDLEWARE` list in your django settings file. [Check django's documentation for more information](https://docs.djangoproject.com/en/5.0/topics/i18n/translation/#how-django-discovers-language-preference).
37+
38+
Add `baseapp_pages.permissions.PagesPermissionsBackend` to the `AUTHENTICATION_BACKENDS` list in your django settings file.
39+
40+
Expose `PagesMutations` and `PagesQuery` in your GraphQL/graphene endpoint, like:
41+
42+
```python
43+
from baseapp_pages.graphql.mutations import PagesMutations
44+
from baseapp_pages.graphql.queries import PagesQuery
45+
46+
class Query(graphene.ObjectType, PagesQuery):
47+
pass
48+
49+
class Mutation(graphene.ObjectType, PagesMutations):
50+
pass
51+
52+
schema = graphene.Schema(query=Query, mutation=Mutation)
53+
```
54+
55+
This will expose `urlPath` and `page` query.
56+
57+
### `urlPath` query:
58+
59+
Example:
60+
61+
```graphql
62+
{
63+
urlPath(path: '/about') {
64+
path
65+
language
66+
target {
67+
metadata {
68+
metaTitle
69+
}
70+
71+
... on Page {
72+
title
73+
}
74+
}
75+
}
76+
}
77+
```
78+
79+
### PageInterface
80+
81+
`PageInterface` is a GraphQL interface that can be used to query for pages. It has the following fields:
82+
83+
- `urlPath` return the active `URLPath`
84+
- `urlPaths` return all `URLPath` for the object, including inactive ones and in other languages
85+
- `metadata` return the `Metadata` for the object
86+
87+
ObjectTypes that implements `PageInterface` is required to implement a resolve for `metadata` like this:
88+
89+
```python
90+
from django.utils.translation import get_language
91+
from baseapp_core.graphql import DjangoObjectType
92+
from baseapp_pages.graphql import PageInterface, MetadataObjectType
93+
94+
95+
class MyModelObjectType(DjangoObjectType):
96+
class Meta:
97+
model = MyModel
98+
interfaces = (relay.Node, PageInterface)
99+
100+
@classmethod
101+
def resolve_metadata(cls, instance, info, **kwargs):
102+
return MetadataObjectType(
103+
meta_title=instance.title,
104+
meta_description=instance.body[:160],
105+
meta_og_image=instance.image.url,
106+
meta_robots='noindex,nofollow'
107+
)
108+
```
109+
110+
If you want to support `Metadata` being manually set or overriden in the admin you can use the following code:
111+
112+
```python
113+
class MyModelObjectType(DjangoObjectType):
114+
# ...
115+
116+
@classmethod
117+
def resolve_metadata(cls, instance, info, **kwargs):
118+
target_content_type = ContentType.objects.get_for_model(instance)
119+
metadata = MetadataObjectType._model.objects.filter(
120+
target_content_type=target_content_type,
121+
target_object_id=self.id,
122+
language=get_language(),
123+
).first()
124+
if not metadata:
125+
return MetadataObjectType(
126+
meta_title=instance.title,
127+
# ...
128+
)
129+
return metadata
130+
```
131+
132+
<!-- ## How to to customize the Page model
133+
134+
In some cases you may need to extend Page model, and we can do it following the next steps:
135+
136+
Start by creating a barebones django app:
137+
138+
```
139+
mkdir my_project/pages
140+
touch my_project/pages/__init__.py
141+
touch my_project/pages/models.py
142+
```
143+
144+
Your `models.py` will look something like this:
145+
146+
```python
147+
from django.db import models
148+
149+
from baseapp_pages.models import AbstractPage
150+
151+
152+
class Page(AbstractPage):
153+
custom_field = models.CharField(null=True)
154+
```
155+
156+
Now make your to add your new app to your `INSTALLED_APPS` and run `makemigrations` and `migrate` like any normal django app.
157+
158+
Now in your `settings/base.py` make sure to tell baseapp-pages what is your custom model for Page:
159+
160+
```python
161+
BASEAPP_PAGES_PAGE_MODEL = 'pages.Page'
162+
``` -->
163+
164+
## How to develop
165+
166+
General development instructions can be found in [main README](..#how-to-develop).

baseapp-pages/baseapp_pages/__init__.py

Whitespace-only changes.

baseapp-pages/baseapp_pages/admin.py

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import swapper
2+
from django.contrib import admin
3+
from django.contrib.contenttypes.admin import GenericStackedInline, GenericTabularInline
4+
from translated_fields import TranslatedFieldAdmin
5+
6+
from baseapp_pages.models import Metadata, URLPath
7+
8+
Page = swapper.load_model("baseapp_pages", "Page")
9+
10+
11+
class URLPathAdminInline(GenericTabularInline):
12+
model = URLPath
13+
extra = 0
14+
ct_field = "target_content_type"
15+
ct_fk_field = "target_object_id"
16+
17+
18+
@admin.register(URLPath)
19+
class URLPathAdmin(admin.ModelAdmin):
20+
search_fields = ("path",)
21+
list_display = ("id", "path", "language", "is_active", "target", "created")
22+
list_filter = ("target_content_type", "language", "is_active")
23+
24+
25+
class MetadataAdminInline(GenericStackedInline):
26+
model = Metadata
27+
extra = 0
28+
ct_field = "target_content_type"
29+
ct_fk_field = "target_object_id"
30+
31+
32+
@admin.register(Page)
33+
class PageAdmin(TranslatedFieldAdmin, admin.ModelAdmin):
34+
search_fields = ("title", "body")
35+
raw_id_fields = ("user",)
36+
list_display = ("id", "title", "created", "modified")
37+
inlines = [URLPathAdminInline, MetadataAdminInline]
38+
39+
40+
@admin.register(Metadata)
41+
class MetadataAdmin(admin.ModelAdmin):
42+
search_fields = ("meta_title", "meta_description")
43+
list_display = ("target", "meta_title", "language", "created", "modified")
44+
list_filter = ("target_content_type", "language")

baseapp-pages/baseapp_pages/apps.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from django.apps import AppConfig
2+
3+
4+
class BaseAppPagesConfig(AppConfig):
5+
name = "baseapp_pages"
6+
verbose_name = "BaseApp Pages"
7+
default_auto_field = "django.db.models.AutoField"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from .mutations import PageCreate, PageEdit # noqa
2+
from .object_types import ( # noqa
3+
MetadataObjectType,
4+
PageInterface,
5+
PageObjectType,
6+
URLPathNode,
7+
)
8+
from .queries import PagesQueries # noqa
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import graphene
2+
import swapper
3+
from baseapp_core.graphql import (
4+
SerializerMutation,
5+
get_pk_from_relay_id,
6+
login_required,
7+
)
8+
from django.contrib.contenttypes.models import ContentType
9+
from django.db.models import Q
10+
from django.utils.translation import get_language
11+
from django.utils.translation import gettext_lazy as _
12+
from rest_framework import serializers
13+
14+
from ..models import URLPath
15+
from .object_types import PageObjectType
16+
17+
Page = swapper.load_model("baseapp_pages", "Page")
18+
19+
20+
class PageSerializer(serializers.ModelSerializer):
21+
user = serializers.HiddenField(default=serializers.CurrentUserDefault())
22+
url_path = serializers.CharField(required=False, allow_blank=True)
23+
title = serializers.CharField(required=False, allow_blank=True)
24+
body = serializers.CharField(required=False, allow_blank=True)
25+
26+
class Meta:
27+
model = Page
28+
fields = ("user", "title", "body", "url_path")
29+
30+
def validate_url_path(self, value):
31+
language = get_language()
32+
queryset = URLPath.objects.filter(
33+
Q(language=language) | Q(language__isnull=True), path=value
34+
)
35+
if self.instance:
36+
queryset = queryset.exclude(
37+
target_content_type=ContentType.objects.get_for_model(self.instance),
38+
target_object_id=self.instance.pk,
39+
)
40+
41+
if queryset.exists():
42+
raise serializers.ValidationError(_("URL Path already being used"))
43+
44+
return value
45+
46+
def save(self, **kwargs):
47+
url_path = self.validated_data.pop("url_path", None)
48+
instance = super().save(**kwargs)
49+
language = get_language()
50+
if url_path:
51+
URLPath.objects.create(
52+
target=instance, path=url_path, language=language, is_active=True
53+
)
54+
return instance
55+
56+
57+
class PageCreate(SerializerMutation):
58+
page = graphene.Field(PageObjectType._meta.connection.Edge)
59+
60+
class Meta:
61+
serializer_class = PageSerializer
62+
63+
@classmethod
64+
@login_required
65+
def mutate_and_get_payload(cls, root, info, **input):
66+
if not info.context.user.has_perm("baseapp_pages.add_page"):
67+
raise PermissionError(_("You don't have permission to create a page"))
68+
69+
return super().mutate_and_get_payload(root, info, **input)
70+
71+
@classmethod
72+
def perform_mutate(cls, serializer, info):
73+
obj = serializer.save()
74+
return cls(
75+
errors=None,
76+
page=PageObjectType._meta.connection.Edge(node=obj),
77+
)
78+
79+
80+
class PageEdit(SerializerMutation):
81+
page = graphene.Field(PageObjectType)
82+
83+
class Meta:
84+
serializer_class = PageSerializer
85+
86+
class Input:
87+
id = graphene.ID(required=True)
88+
89+
@classmethod
90+
def get_serializer_kwargs(cls, root, info, **input):
91+
pk = get_pk_from_relay_id(input.get("id"))
92+
instance = Page.objects.get(pk=pk)
93+
if not info.context.user.has_perm("baseapp_pages.change_page", instance):
94+
raise PermissionError(_("You don't have permission to edit this page"))
95+
return {
96+
"instance": instance,
97+
"data": input,
98+
"partial": True,
99+
"context": {"request": info.context},
100+
}
101+
102+
@classmethod
103+
def perform_mutate(cls, serializer, info):
104+
obj = serializer.save()
105+
return cls(
106+
errors=None,
107+
page=obj,
108+
)
109+
110+
@classmethod
111+
@login_required
112+
def mutate_and_get_payload(cls, root, info, **input):
113+
return super().mutate_and_get_payload(root, info, **input)
114+
115+
116+
class PagesMutations(object):
117+
page_create = PageCreate.Field()
118+
page_edit = PageEdit.Field()

0 commit comments

Comments
 (0)