diff --git a/core/fixtures/pages.json b/core/fixtures/pages.json new file mode 100644 index 00000000..d3e49ab6 --- /dev/null +++ b/core/fixtures/pages.json @@ -0,0 +1 @@ +[{"model": "core.page", "pk": 1, "fields": {"title": "TROLL", "full_name": "guy2", "content": "ZZZZZZZZZZZZZZZZZZZZZ", "name": "guy2", "parent": null, "revision": 1, "is_locked": false}}, {"model": "core.page", "pk": 2, "fields": {"title": "Bibou", "full_name": "guy/bibou", "content": "Bibou Troll", "name": "bibou", "parent": 4, "revision": 1, "is_locked": false}}, {"model": "core.page", "pk": 3, "fields": {"title": "Troll", "full_name": "guy/bibou/troll", "content": "blbbllblbl", "name": "troll", "parent": 2, "revision": 1, "is_locked": false}}, {"model": "core.page", "pk": 4, "fields": {"title": "TROLL", "full_name": "guy", "content": "", "name": "guy", "parent": null, "revision": 1, "is_locked": false}}, {"model": "core.page", "pk": 5, "fields": {"title": "Bibou", "full_name": "bibou", "content": "", "name": "bibou", "parent": null, "revision": 1, "is_locked": false}}, {"model": "core.page", "pk": 6, "fields": {"title": "Bibou-Guy", "full_name": "bibou/guy", "content": "Bwahahahahahaha", "name": "guy", "parent": 5, "revision": 1, "is_locked": false}}] \ No newline at end of file diff --git a/users.json b/core/fixtures/users.json similarity index 100% rename from users.json rename to core/fixtures/users.json diff --git a/core/migrations/0002_page_full_name.py b/core/migrations/0002_page_full_name.py new file mode 100644 index 00000000..8a9e680f --- /dev/null +++ b/core/migrations/0002_page_full_name.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='page', + name='full_name', + field=models.CharField(blank=True, verbose_name='page name', max_length=255), + ), + ] diff --git a/core/models.py b/core/models.py index 0d57ad13..7a1c3614 100644 --- a/core/models.py +++ b/core/models.py @@ -109,12 +109,25 @@ class User(AbstractBaseUser, PermissionsMixin): class Page(models.Model): + """ + The page class to build a Wiki + Each page may have a parent and it's URL is of the form my.site/page/// + It has an ID field, but don't use it, since it's only there for DB part, and because compound primary key is + awkward! + Prefere querying pages with Page.get_page_by_full_name() + + Be careful with the full_name attribute: this field may not be valid until you call save(). It's made for fast + query, but don't rely on it when playing with a Page object, use get_full_name() instead! + """ name = models.CharField(_('page name'), max_length=30, blank=False) title = models.CharField(_("page title"), max_length=255, blank=True) content = models.TextField(_("page content"), blank=True) revision = models.PositiveIntegerField(_("current revision"), default=1) is_locked = models.BooleanField(_("page mutex"), default=False) parent = models.ForeignKey('self', related_name="children", null=True, blank=True, on_delete=models.SET_NULL) + # Attention: this field may not be valid until you call save(). It's made for fast query, but don't rely on it when + # playing with a Page object, use get_full_name() instead! + full_name = models.CharField(_('page name'), max_length=255, blank=True) class Meta: unique_together = ('name', 'parent') @@ -125,38 +138,43 @@ class Page(models.Model): @staticmethod def get_page_by_full_name(name): - parent_name = '/'.join(name.split('/')[:-1]) - name = name.split('/')[-1] - if parent_name == "": - qs = Page.objects.filter(name=name, parent=None) - else: - qs = Page.objects.filter(name=name, parent__name=parent_name) - return qs.first() + """ + Quicker to get a page with that method rather than building the request every time + """ + return Page.objects.filter(full_name=name).first() def __init__(self, *args, **kwargs): super(Page, self).__init__(*args, **kwargs) def clean(self): """ - This function maintains coherence between full_name, name, and parent.full_name - Be careful modifying it, it could break the entire page table! - - This function is mandatory since Django does not support compound primary key, - otherwise, Page class would have had PRIMARY_KEY(name, parent) + Cleans up only the name for the moment, but this can be used to make any treatment before saving the object """ if '/' in self.name: self.name = self.name.split('/')[-1] + if Page.objects.exclude(pk=self.pk).filter(full_name=self.get_full_name()).exists(): + raise ValidationError( + _('Duplicate page'), + code='duplicate', + ) super(Page, self).clean() - print("name: "+self.name) def save(self, *args, **kwargs): self.full_clean() + # This reset the full_name just before saving to maintain a coherent field quicker for queries than the + # recursive method + self.full_name = self.get_full_name() super(Page, self).save(*args, **kwargs) def __str__(self): return self.get_full_name() def get_full_name(self): + """ + Computes the real full_name of the page based on its name and its parent's name + You can and must rely on this function when working on a page object that is not freshly fetched from the DB + (For example when treating a Page object coming from a form) + """ if self.parent is None: return self.name return '/'.join([self.parent.get_full_name(), self.name]) diff --git a/core/tests.py b/core/tests.py index bb99ea2e..d03917fa 100644 --- a/core/tests.py +++ b/core/tests.py @@ -148,3 +148,64 @@ class UserRegistrationTest(SimpleTestCase): response = c.post(reverse('core:login'), {'username': 'gcarlier', 'password': 'guy'}) self.assertTrue(response.status_code == 200) self.assertTrue('LOGIN_FAIL' in str(response.content)) + + def test_create_page_ok(self): + """ + Should create a page correctly + """ + c = Client() + response = c.post(reverse('core:page_edit', kwargs={'page_name': 'guy'}), {'parent': '', + 'name': 'guy', + 'title': 'Guy', + 'Content': 'Guyéuyuyé', + }) + self.assertTrue(response.status_code == 200) + self.assertTrue('PAGE_SAVED' in str(response.content)) + + def test_create_child_page_ok(self): + """ + Should create a page correctly + """ + c = Client() + c.post(reverse('core:page_edit', kwargs={'page_name': 'guy'}), {'parent': '', + 'name': 'guy', + 'title': 'Guy', + 'Content': 'Guyéuyuyé', + }) + response = c.post(reverse('core:page_edit', kwargs={'page_name': 'guy/bibou'}), {'parent': '1', + 'name': 'bibou', + 'title': 'Bibou', + 'Content': + 'Bibibibiblblblblblbouuuuuuuuu', + }) + self.assertTrue(response.status_code == 200) + self.assertTrue('PAGE_SAVED' in str(response.content)) + + def test_access_child_page_ok(self): + """ + Should display a page correctly + """ + c = Client() + c.post(reverse('core:page_edit', kwargs={'page_name': 'guy'}), {'parent': '', + 'name': 'guy', + 'title': 'Guy', + 'Content': 'Guyéuyuyé', + }) + c.post(reverse('core:page_edit', kwargs={'page_name': 'guy/bibou'}), {'parent': '1', + 'name': 'bibou', + 'title': 'Bibou', + 'Content': + 'Bibibibiblblblblblbouuuuuuuuu', + }) + response = c.get(reverse('core:page', kwargs={'page_name': 'guy/bibou'})) + self.assertTrue(response.status_code == 200) + self.assertTrue('PAGE_FOUND : Bibou' in str(response.content)) + + def test_access_page_not_found(self): + """ + Should not display a page correctly + """ + c = Client() + response = c.get(reverse('core:page', kwargs={'page_name': 'swagg'})) + self.assertTrue(response.status_code == 200) + self.assertTrue('PAGE_NOT_FOUND' in str(response.content)) diff --git a/core/views.py b/core/views.py index 649c4cc1..a50679bc 100644 --- a/core/views.py +++ b/core/views.py @@ -64,12 +64,15 @@ def login(request): def logout(request): """ - The logout view:w + The logout view """ auth_logout(request) return redirect('core:index') def user(request, user_id=None): + """ + Display a user's profile + """ context = {'title': 'View a user'} if user_id == None: context['user_list'] = User.objects.all @@ -78,6 +81,9 @@ def user(request, user_id=None): return render(request, "core/user.html", context) def user_edit(request, user_id=None): + """ + This view allows a user, or the allowed users to modify a profile + """ user_id = int(user_id) context = {'title': 'Edit a user'} if user_id is not None: @@ -88,6 +94,9 @@ def user_edit(request, user_id=None): return user(request, user_id) def page(request, page_name=None): + """ + This view displays a page or the link to create it if 404 + """ context = {'title': 'View a Page'} if page_name == None: context['page_list'] = Page.objects.all @@ -96,21 +105,30 @@ def page(request, page_name=None): if context['page'] is not None: context['view_page'] = True context['title'] = context['page'].title - context['test'] = "PAGE_FOUND" + context['tests'] = "PAGE_FOUND : "+context['page'].title else: context['title'] = "This page does not exist" context['new_page'] = page_name - context['test'] = "PAGE_NOT_FOUND" + context['tests'] = "PAGE_NOT_FOUND" return render(request, "core/page.html", context) def page_edit(request, page_name=None): + """ + page_edit view, able to create a page, save modifications, and display the page ModelForm + """ context = {'title': 'Edit a page', 'page_name': page_name} p = Page.get_page_by_full_name(page_name) + # New page if p == None: - # TODO: guess page name by splitting on '/' - # Same for the parent, try to guess - p = Page(name=page_name) + parent_name = '/'.join(page_name.split('/')[:-1]) + name = page_name.split('/')[-1] + if parent_name == "": + p = Page(name=name) + else: + parent = Page.get_page_by_full_name(parent_name) + p = Page(name=name, parent=parent) + # Saving page if request.method == 'POST': f = PageForm(request.POST, instance=p) if f.is_valid(): @@ -118,6 +136,7 @@ def page_edit(request, page_name=None): context['tests'] = "PAGE_SAVED" else: context['tests'] = "PAGE_NOT_SAVED" + # Default: display the edit form without change else: context['tests'] = "POST_NOT_RECEIVED" f = PageForm(instance=p)