RelatedPost: mapping['thread'] now manage relation between thread and parent of related object

This commit is contained in:
bkfox 2015-10-13 12:38:18 +02:00
parent cde58334bd
commit 0032e216b8
6 changed files with 200 additions and 111 deletions

View File

@ -82,7 +82,37 @@ class RelatedPostBase (models.base.ModelBase):
"""
Metaclass for RelatedPost children.
"""
def __new__ (cls, name, bases, attrs):
registry = {}
@classmethod
def register (cl, key, model):
"""
Register a model and return the key under which it is registered.
Raise a ValueError if another model is yet associated under this key.
"""
if key in cl.registry and cl.registry[key] is not model:
raise ValueError('A model has yet been registered with "{}"'
.format(key))
cl.registry[key] = model
return key
@classmethod
def check_thread_mapping (cl, relation, model, field):
"""
Add information related to the mapping 'thread' info.
"""
if not field:
return
parent_model = model._meta.get_field(field).rel.to
thread_model = cl.registry.get(parent_model)
if not thread_model:
raise ValueError('no registered RelatedPost for the model {}'
.format(model.__name__))
relation.thread_model = thread_model
def __new__ (cl, name, bases, attrs):
rel = attrs.get('Relation')
rel = (rel and rel.__dict__) or {}
@ -96,9 +126,17 @@ class RelatedPostBase (models.base.ModelBase):
if name is not 'RelatedPost':
_relation = RelatedPost.Relation()
_relation.__dict__.update(rel)
mapping = rel.get('mapping')
cl.check_thread_mapping(
_relation,
related_model,
mapping and mapping.get('thread')
)
attrs['_relation'] = _relation
return super().__new__(cls, name, bases, attrs)
model = super().__new__(cl, name, bases, attrs)
cl.register(related_model, model)
return model
class RelatedPost (Post, metaclass = RelatedPostBase):
@ -108,25 +146,80 @@ class RelatedPost (Post, metaclass = RelatedPostBase):
abstract = True
class Relation:
related_model = None
mapping = None # dict of related mapping values
bind_mapping = False # update fields of related data on save
"""
Relation descriptor used to generate and manage the related object.
* model: model of the related object
* mapping: values that are bound between the post and the related
object. When the post is saved, these fields are updated on it.
It is a dict of { post_attr: rel_attr }
If there is a post_attr "thread", the corresponding rel_attr is used
to update the post thread to the correct Post model.
"""
model = None
mapping = None # values to map { post_attr: rel_attr }
bind = False # update fields of related data on save
thread_model = None
def get_attribute (self, attr):
attr = self._relation.mappings.get(attr)
return self.related.__dict__[attr] if attr else None
def update_thread_mapping (self, save = True):
"""
Update the parent object designed by Relation.mapping.thread if the
type matches to the one related of the current instance's thread.
If there is no thread assigned to self, set it to the parent of the
related object.
"""
relation = self._relation
print(relation.__dict__)
thread_model = relation.thread_model
if not thread_model:
return
# self.related.parent -> self.thread
rel_parent = relation.mapping.get('thread')
if not self.thread:
rel_parent = getattr(self.related, rel_parent)
thread = thread_model.objects.filter(related = rel_parent)
if thread.count():
self.thread = thread[0]
if save:
self.save()
return
# self.thread -> self.related.parent
if thread_model is not self.thread_type.model_class():
return
setattr(self.related, rel_parent, self.thread.related)
if save:
self.save()
def update_mapping (self):
relation = self._relation
mapping = relation.mapping
if not mapping:
return
related = self.related
related.__dict__.update({
rel_attr: self.__dict__[attr]
for attr, rel_attr in mapping.items()
if attr is not 'thread' and attr in self.__dict__
})
self.update_thread_mapping(save = False)
related.save()
def save (self, *args, **kwargs):
if not self.title and self.related:
self.title = self.get_attribute('title')
if self._relation.bind_mapping:
self.related.__dict__.update({
rel_attr: self.__dict__[attr]
for attr, rel_attr in self.Relation.mapping.items()
})
self.related.save()
self.update_mapping()
super().save(*args, **kwargs)

View File

@ -57,8 +57,8 @@ class Route:
return ''
@classmethod
def as_url (cl, model, model_name, view, view_kwargs = None):
pattern = '^{}/{}'.format(model_name, cl.name)
def as_url (cl, name, model, view, view_kwargs = None):
pattern = '^{}/{}'.format(name, cl.name)
if cl.url_args:
url_args = '/'.join([
'(?P<{}>{}){}'.format(
@ -77,7 +77,7 @@ class Route:
kwargs.update(view_kwargs)
return url(pattern, view, kwargs = kwargs,
name = model_name + '_' + cl.name)
name = name + '_' + cl.name)
class DetailRoute (Route):

View File

@ -385,50 +385,3 @@ class Sections:
return context
class ViewSet:
"""
A ViewSet is a class helper that groups detail and list views that can be
used to generate views and routes given a model and a name used for the
routing.
"""
model = None
list_view = PostListView
list_routes = []
detail_view = PostDetailView
detail_sections = [
Sections.PostContent(),
Sections.PostImage(),
]
def get_list_name (self):
"""
Return a string with the name to use in the route for the lists
"""
return self.model._meta.verbose_name_plural.lower()
@classmethod
def get_detail_name (cl):
"""
Return a string with the name to use in the route for the details
"""
return cl.model._meta.verbose_name.lower()
def connect (self, website = None):
self.detail_view = self.detail_view.as_view(
model = self.model,
sections = self.detail_sections,
website = website,
)
self.list_view = self.list_view.as_view(
model = self.model,
website = website,
)
self.urls = [ route.as_url(self.model, self.get_list_name(),
self.list_view) for route in self.list_routes ] + \
[ routes.DetailRoute.as_url(self.model,
self.get_detail_name(), self.detail_view ) ]

View File

@ -1,4 +1,5 @@
import aircox_cms.routes as routes
import aircox_cms.views as views
class Website:
name = ''
@ -12,26 +13,67 @@ class Website:
'right', 'bottom',
'header', 'footer']
router = None
urls = []
registry = {}
@property
def urls (self):
return self.router.get_urlpatterns()
def __init__ (self, **kwargs):
self.registry = {}
self.urls = []
self.__dict__.update(kwargs)
if not self.router:
self.router = routes.Router()
def register_set (self, view_set):
def register_model (self, name, model):
"""
Register a ViewSet (or subclass) to the router,
and connect it to self.
Register a model and return the name under which it is registered.
Raise a ValueError if another model is yet associated under this name.
"""
view_set = view_set()
view_set.connect(website = self)
self.registry[view_set.get_detail_name()] = view_set
self.router.register_set(view_set)
if name in self.registry and self.registry[name] is not model:
raise ValueError('A model has yet been registered under "{}"'
.format(name))
self.registry[name] = model
return name
def register_detail (self, name, model, view = views.PostDetailView,
**view_kwargs):
"""
Register a model and the detail view
"""
name = self.register_model(name, model)
view = view.as_view(
website = self,
model = model,
**view_kwargs,
)
self.urls.append(routes.DetailRoute.as_url(name, model, view))
self.registry[name] = model
def register_list (self, name, model, view = views.PostListView,
routes = [], **view_kwargs):
"""
Register a model and the given list view using the given routes
"""
name = self.register_model(name, model)
view = view.as_view(
website = self,
model = model,
**view_kwargs
)
self.urls += [ route.as_url(name, model, view) for route in routes ]
self.registry[name] = model
def register (self, name, model, sections = None, routes = None,
list_kwargs = {}, detail_kwargs = {}):
if sections:
self.register_detail(
name, model,
sections = sections,
**detail_kwargs
)
if routes:
self.register_list(
name, model,
routes = routes,
**list_kwargs
)
def get_menu (self, position):
"""

View File

@ -17,6 +17,7 @@ class Episode (RelatedPost):
related_model = programs.Episode
bind_mapping = True
mapping = {
'thread': 'program',
'title': 'name',
'content': 'description',
}

View File

@ -4,42 +4,10 @@ from website.models import *
from website.views import *
from aircox_cms.models import Article
from aircox_cms.views import Menu, Section, Sections, ViewSet
from aircox_cms.views import Menu, Section, Sections
from aircox_cms.routes import *
from aircox_cms.website import Website
class ProgramSet (ViewSet):
model = Program
list_routes = [
AllRoute,
ThreadRoute,
SearchRoute,
DateRoute,
]
detail_sections = ViewSet.detail_sections + [
ScheduleSection(),
EpisodesSection(),
]
class EpisodeSet (ViewSet):
model = Episode
list_routes = [
AllRoute,
ThreadRoute,
SearchRoute,
DateRoute,
]
class ArticleSet (ViewSet):
model = Article
list_routes = [
AllRoute,
ThreadRoute,
SearchRoute,
DateRoute,
]
website = Website(
name = 'RadioCampus',
@ -70,8 +38,40 @@ website = Website(
],
)
website.register_set(ProgramSet)
website.register_set(EpisodeSet)
website.register_set(ArticleSet)
base_sections = [
Sections.PostContent(),
Sections.PostImage(),
]
base_routes = [
AllRoute,
ThreadRoute,
SearchRoute,
DateRoute,
]
website.register(
'article',
Article,
sections = base_sections,
routes = base_routes
)
website.register(
'program',
Program,
sections = base_sections + [
ScheduleSection(),
EpisodesSection(),
],
routes = base_routes,
)
website.register (
'episode',
Episode,
sections = base_sections,
)
urlpatterns = website.urls