def test_article_admin_modelchoice_change_labels(db, admin_client): """ Admin change form should have language names in model choices fields. """ # Create new objects CategoryFactory(title="garlic", language="en") CategoryFactory(title="ail", language="fr") ArticleFactory(title="egg", language="en") obj_fr = ArticleFactory(title="baguette", language="fr") ArticleFactory(title="omelette", language="fr") # Build form and get its simple HTML representation to parse it f = ArticleAdminForm({}, instance=obj_fr) content = f.as_p() dom = html_pyquery(content) originals = dom.find("#id_original option") assert [item.text for item in originals] == [ f.fields["original"].empty_label, "egg [English]", ] relateds = dom.find("#id_related option") assert [item.text for item in relateds] == [ "omelette [Français]", ] categories = dom.find("#id_categories option") assert [item.text for item in categories] == [ "ail [Français]", ]
def test_article_creation(db): """ Factory should correctly create a new object without any errors. """ ping = CategoryFactory(slug="ping") pong = CategoryFactory(slug="pong") article = ArticleFactory( slug="foo", fill_categories=[ping, pong], ) assert article.slug == "foo" # Check related categories results = queryset_values(article.categories.all()) assert results == [ { "slug": "ping", "language": "en" }, { "slug": "pong", "language": "en" }, ] # Ensure no random categories are created when specifically required article = ArticleFactory(slug="bar", fill_categories=False) assert article.categories.count() == 0
def test_multilingual_article(db): """ Factory helper should create an original article with its required translations. """ ping = CategoryFactory(slug="ping") pong = CategoryFactory(slug="pong") # Create an article with a FR and DE translations. Also try to create # Deutsch translations twice, but "multilingual_article" is safe on unique # language. created = multilingual_article( slug="cheese", langs=["fr", "de", "de"], fill_categories=[ping, pong], contents={"fr": { "slug": "fromage", "fill_categories": [ping], }}, ) # Original slug is correct assert created["original"].slug == "cheese" # There is two related translations assert (len(created["translations"]) == 2) is True # Required translations have been create assert ("fr" in created["translations"]) is True assert ("de" in created["translations"]) is True # French translation have its own slug assert created["translations"]["fr"].slug == "fromage" # deutsch translation inherit from original slug assert created["translations"]["de"].slug == "cheese" # Check original categories original_categories = queryset_values(created["original"].categories.all()) assert original_categories == [ { "slug": "ping", "language": "en" }, { "slug": "pong", "language": "en" }, ] # Check french translation categories fr_categories = queryset_values( created["translations"]["fr"].categories.all()) assert fr_categories == [ { "slug": "ping", "language": "en" }, ]
def test_category_index_basic(db, client): """ Category index view should list every category. """ category1 = CategoryFactory(title="category1") category2 = CategoryFactory(title="category2") response = client.get("/") assert response.status_code == 200 dom = html_pyquery(response) items = dom.find(".category-list li") expected_titles = [ "category1", "category2", ] expected_urls = [ "/{category_pk}/".format(category_pk=category1.id), "/{category_pk}/".format(category_pk=category2.id), ] assert expected_titles == [item.find("a").text for item in items] assert expected_urls == [item.find("a").get("href") for item in items]
def test_category_admin_original_validation(db, admin_client): """ Changing language should not allow to trick constraint on original relation which must be in different language. """ # Create new object without a cover file obj_fr = CategoryFactory(language="fr", original=None) obj_en = CategoryFactory(language="en", original=None) # Build initial POST data ignore = ["id", "category", "articles"] data = build_post_data_from_object(Category, obj_fr, ignore=ignore) # 1) Edit to set original on 'obj_en', everything is ok data["original"] = obj_en f = CategoryAdminForm(data, instance=obj_fr) # No validation errors assert f.is_valid() is True assert compact_form_errors(f) == {} assert obj_fr.original.language != obj_fr.language obj_fr = f.save() # 2) Switch language to 'EN' should not allow to keep the original relation # on 'obj_en' in 'EN' language data["language"] = "en" f = CategoryAdminForm(data, instance=obj_fr) # Validation is raised on language field assert f.is_valid() is False assert compact_form_errors(f) == { "language": ["invalid"], "original": ["invalid"], }
def test_article_admin_original_choices(db, admin_client): """ Choices should be limited to some constraints: * 'original' field should not list items in same language and not the article itself; * 'related' field should not list items in different language and not the article itself; * 'categories' field should not list items in different language; """ # Create new object to check obj = ArticleFactory(language="en") # Create some objects in same language fillers_en = [ ArticleFactory(language="en"), ArticleFactory(language="en"), ] # Create some other objects in various other languages, these are the only # elligible articles for original field choices fillers_langs = [ ArticleFactory(language="fr"), ArticleFactory(language="fr"), ArticleFactory(language="de"), ] # Create some categories cat_en = CategoryFactory(language="en") CategoryFactory(language="fr") # Get the obj detail page url = get_admin_change_url(obj) response = admin_client.get(url) assert response.status_code == 200 dom = html_pyquery(response) # Get available 'original' choice ids from their values options = dom.find("#id_original option") option_ids = [ int(item.get("value")) for item in options if item.get("value") ] assert sorted(option_ids) == sorted([item.id for item in fillers_langs]) # Get available 'related' choice ids from their values options = dom.find("#id_related option") option_ids = [ int(item.get("value")) for item in options if item.get("value") ] assert sorted(option_ids) == sorted([item.id for item in fillers_en]) # Get available 'categories' choice ids from their values options = dom.find("#id_categories option") option_ids = [cat_en.id]
def test_article_creation(db): """ Factory should correctly create a new object without any errors. """ ping = CategoryFactory(slug="ping") pong = CategoryFactory(slug="pong") # Just a dummy article dummy = ArticleFactory(slug="dummy", language="fr") # Article with some relation article = ArticleFactory( slug="foo", fill_categories=[ping, pong], fill_related=[dummy], ) assert article.slug == "foo" # Check related categories results = queryset_values( article.categories.all() ) assert results == [ {"slug": "ping", "language": "en"}, {"slug": "pong", "language": "en"}, ] # Check related articles results = queryset_values( article.related.all() ) assert results == [ {"slug": "dummy", "language": "fr"}, ] # Check for reverse relation results = queryset_values( dummy.relations.all() ) assert results == [ {"slug": "foo", "language": "en"}, ] # Ensure 'related' is not symmetrical assert dummy.related.count() == 0 # Ensure no random relations are created when not specifically # required from "fill_****" methods article = ArticleFactory(slug="bar") assert article.authors.count() == 0 assert article.categories.count() == 0 assert article.related.count() == 0
def test_category_model_file_purge(db): """ Category 'cover' field file should follow correct behaviors: * When object is deleted, its files should be delete from filesystem too; * When changing file from an object, its previous files (if any) should be deleted; """ ping = CategoryFactory(cover=create_image_file(filename="machin.png")) pong = CategoryFactory(cover=create_image_file(filename="machin.png")) # Memorize some data to use after deletion ping_path = ping.cover.path pong_path = pong.cover.path # Delete object ping.delete() # File is deleted along its object assert os.path.exists(ping_path) is False # Paranoiac mode: other existing similar filename (as uploaded) are conserved # since Django rename file with a unique hash if filename alread exist, they # should not be mistaken assert os.path.exists(pong_path) is True # Change object file to a new one pong.cover = create_image_file(filename="new.png") pong.save() # During pre save signal, old file is removed from FS and new one is left # untouched assert os.path.exists(pong_path) is False assert os.path.exists(pong.cover.path) is True
def test_article_view_detail_content(db, admin_client): """ Detail view should contain all expected content and relations. Note we are requesting with admin mode to be able to see a draft article to check for the "draft" CSS class. Also, this does not care about textual content (title, lead, content, etc..). """ picsou = AuthorFactory(first_name="Picsou", last_name="McDuck") AuthorFactory(first_name="Flairsou", last_name="Cresus") cat_1 = CategoryFactory(title="cat_1") CategoryFactory(title="cat_2") CategoryFactory(title="cat_3") ArticleFactory(title="Foo") article_2 = ArticleFactory(title="Bar") article_3 = ArticleFactory( title="Ping", fill_categories=[cat_1], fill_related=[article_2], fill_authors=[picsou], status=STATUS_DRAFT, pinned=True, featured=True, private=True, ) # Get detail HTML page response = admin_client.get(article_3.get_absolute_url(), {'admin': 1}) assert response.status_code == 200 # Parse HTML response to get content and relations dom = html_pyquery(response) container = dom.find("#lotus-content .article-detail")[0] categories = [ item.text for item in dom.find("#lotus-content .categories li a") ] authors = [item.text for item in dom.find("#lotus-content .authors li")] relateds = [ item.text for item in dom.find("#lotus-content .relateds li a") ] cover = dom.find("#lotus-content .cover img")[0].get("src") large_img = dom.find("#lotus-content .image img")[0].get("src") classes = sorted( [v for v in container.get("class").split() if v != "article-detail"]) assert categories == ["cat_1"] assert authors == ["Picsou McDuck"] assert relateds == ["Bar"] assert classes == ["draft", "featured", "pinned", "private"] assert cover == article_3.cover.url assert large_img == article_3.image.url
def test_category_admin_article_relations_validation(db, admin_client): """ Category admin form should not allow to change language if object already have related articles in different language. """ build_fr = CategoryFactory.build() # Build initial POST data ignore = ["id", "category", "articles"] data = build_post_data_from_object(Category, build_fr, ignore=ignore) # 1) Set category without any article relation yet data["language"] = "fr" f = CategoryAdminForm(data) assert f.is_valid() is True assert compact_form_errors(f) == {} cat_fr = f.save() # Set an article relation ArticleFactory(language="fr", fill_categories=[cat_fr]) # 2) Change language to a different one from related articles data["language"] = "en" f = CategoryAdminForm(data, instance=cat_fr) assert f.is_valid() is False assert compact_form_errors(f) == { "language": ["invalid-language"], }
def test_category_admin_change_form(db, admin_client): """ Ensure the admin change form is working well (this should cover add form also) and ensure image upload is correct. """ # Create new object without a cover file obj = CategoryFactory(cover=None) # Build initial POST data ignore = ["id", "category", "articles"] data = build_post_data_from_object(Category, obj, ignore=ignore) file_data = { "cover": SimpleUploadedFile( "small.gif", DUMMY_GIF_BYTES, content_type="image/gif" ), } f = CategoryAdminForm(data, file_data, instance=obj) # No validation errors assert f.is_valid() is True assert f.errors.as_data() == {} updated_obj = f.save() # Check everything has been saved assert updated_obj.title == obj.title assert os.path.exists(obj.cover.path) is True
def test_category_managers(db): """ Category manager should be able to correctly filter on language. """ # Simple category on default language without translations CategoryFactory(slug="foobar") # Original category on different language than 'settings.LANGUAGE_CODE' and # with a translation for 'settings.LANGUAGE_CODE' lang. multilingual_category( slug="musique", language="fr", langs=["en"], contents={ "en": { "slug": "music", } }, ) # A category with a french translation inheriting original slug multilingual_category( slug="food", langs=["fr"], ) # A category with french translation with its own slug and deutsch # translation inheriting original slug multilingual_category( slug="recipe", langs=["fr", "de"], contents={ "fr": { "slug": "recette", } }, ) # Use default language as configured in settings assert queryset_values(Category.objects.get_for_lang()) == [ {"slug": "foobar", "language": "en"}, {"slug": "food", "language": "en"}, {"slug": "music", "language": "en"}, {"slug": "recipe", "language": "en"}, ] # For french language assert queryset_values(Category.objects.get_for_lang("fr")) == [ {"slug": "food", "language": "fr"}, {"slug": "musique", "language": "fr"}, {"slug": "recette", "language": "fr"}, ] # For deutsch language assert queryset_values(Category.objects.get_for_lang("de")) == [ {"slug": "recipe", "language": "de"}, ]
def test_category_index_pagination(settings, db, client): """ Category index view is paginated from setting limit, not every category is listed on the same page. """ # Twice the category pagination limit plus one entry so we can expect three # result pages category_total = (settings.BLOG_PAGINATION * 2) + 1 CategoryFactory.create_batch(category_total) assert category_total == Category.objects.all().count() # First result page response = client.get("/") assert response.status_code == 200 dom = html_pyquery(response) items = dom.find(".category-list li") assert settings.BLOG_PAGINATION == len(items) # Check pagination is correct pages = dom.find(".pagination a") assert 3 == len(pages) # Second result page response = client.get("/?page=2") assert response.status_code == 200 dom = html_pyquery(response) items = dom.find(".category-list li") assert settings.BLOG_PAGINATION == len(items) # Check current page is correct active = dom.find(".pagination a.active") assert 1 == len(active) assert "2" == active.text() # Third result page response = client.get("/?page=3") assert response.status_code == 200 dom = html_pyquery(response) items = dom.find(".category-list li") assert 1 == len(items)
def test_category_admin_detail(db, admin_client): """ Category model admin detail view should not raise error on GET request. """ obj = CategoryFactory() url = get_admin_change_url(obj) response = admin_client.get(url) assert response.status_code == 200
def test_category_admin_modelchoice_create_labels(db, admin_client): """ Admin create form should have language names in model choices fields. """ # Create new objects CategoryFactory(title="egg", language="en") CategoryFactory(title="baguette", language="fr") # Build form and get its simple HTML representation to parse it f = CategoryAdminForm() content = f.as_p() dom = html_pyquery(content) originals = dom.find("#id_original option") assert [item.text for item in originals] == [ f.fields["original"].empty_label, "baguette [Français]", "egg [English]", ]
def test_article_admin_category_create_validation(db, admin_client): """ Admin create form should not allow to set category with a different language. """ cat_en = CategoryFactory(language="en") cat_fr = CategoryFactory(language="fr") # Build object to create build_fr = ArticleFactory.build(language="fr") # Build initial POST data ignore = [ "id", "relations", "article", "authors", "related", "categories", ] data = build_post_data_from_object(Article, build_fr, ignore=ignore) # 1) Try to add category in different language, raise error data["categories"] = [cat_en.id] f = ArticleAdminForm(data) assert f.is_valid() is False assert compact_form_errors(f) == { "categories": ["invalid-categories"], } # 2) Correctly add category with same language, should work data["categories"] = [cat_fr.id] f = ArticleAdminForm(data) assert f.is_valid() is True assert compact_form_errors(f) == {} obj_fr_bis = f.save() assert obj_fr_bis.categories.all().count() == 1
def test_category_admin_original_choices(db, admin_client): """ Choices for 'original' should not list item in same language and not the category itself. """ # Create new object to check obj = CategoryFactory(language="en") # Create some objects in same language CategoryFactory(language="en") CategoryFactory(language="en") # Create some other objects in various other languages, these are the only # elligible categories for original field choices fillers_langs = [ CategoryFactory(language="fr"), CategoryFactory(language="fr"), CategoryFactory(language="de"), ] # Get the obj detail page url = get_admin_change_url(obj) response = admin_client.get(url) assert response.status_code == 200 dom = html_pyquery(response) # Get available choice ids from their values options = dom.find("#id_original option") option_ids = [int(item.get("value")) for item in options if item.get("value")] assert sorted(option_ids) == sorted([item.id for item in fillers_langs])
def test_category_detail_article_pagination(settings, db, client): """ Category index detail has a paginated list of article and so not every category articles are listed on the same page. """ category1 = CategoryFactory(title="category1") category_url = "/{category_pk}/".format(category_pk=category1.id) article_total = (settings.ARTICLE_PAGINATION * 2) + 1 ArticleFactory.create_batch(article_total, category=category1) assert article_total == Article.objects.all().count() # First result page response = client.get(category_url) assert response.status_code == 200 dom = html_pyquery(response) items = dom.find(".article-list li") assert settings.ARTICLE_PAGINATION == len(items) # Check pagination is correct pages = dom.find(".pagination a") assert 3 == len(pages) # Second result page response = client.get(category_url + "?page=2") assert response.status_code == 200 dom = html_pyquery(response) items = dom.find(".article-list li") assert settings.ARTICLE_PAGINATION == len(items) # Check current page is correct active = dom.find(".pagination a.active") assert 1 == len(active) assert "2" == active.text() # Third result page response = client.get(category_url + "?page=3") assert response.status_code == 200 dom = html_pyquery(response) items = dom.find(".article-list li") assert 1 == len(items)
def test_category_constraints(db): """ Category contraints should be respected. """ # Base original objects bar = CategoryFactory(slug="bar", ) CategoryFactory(slug="pong", ) # We can have an identical slug for a different language. # Note than original is just a marker to define an object as a translation # of "original" relation object. CategoryFactory( slug="bar", language="fr", original=bar, ) # But not an identical slug on the same language with transaction.atomic(): with pytest.raises(IntegrityError) as excinfo: CategoryFactory( slug="bar", language="en", ) assert str( excinfo.value) == ("UNIQUE constraint failed: " "lotus_category.slug, lotus_category.language") # And only an unique language for the same original object is allowed since # there can't be two translations for the same language. with transaction.atomic(): with pytest.raises(IntegrityError) as excinfo: CategoryFactory( slug="zap", language="fr", original=bar, ) assert str(excinfo.value) == ( "UNIQUE constraint failed: " "lotus_category.original_id, lotus_category.language") # Combination of constraints (slug+lang & original+lang) with transaction.atomic(): with pytest.raises(IntegrityError) as excinfo: CategoryFactory( slug="bar", language="fr", original=bar, ) # This is the original+language constraint which raise first assert str(excinfo.value) == ( "UNIQUE constraint failed: " "lotus_category.original_id, lotus_category.language")
def test_category_detail_no_article(db, client): """ Without any related article, category detail view should just contains its content and return the empty text for article list. """ category1 = CategoryFactory(title="category1") url = "/{category_pk}/".format(category_pk=category1.id) response = client.get(url) assert response.status_code == 200 dom = html_pyquery(response) category_title = dom.find(".category-detail h2") assert category_title.text() == category1.title content = dom.find(".article-list li")[0].text assert "No articles yet." == content
def create_categories(self): """ Create Category objects required length from factory. """ created = [] self.stdout.write( self.style.SUCCESS("* Creating {} categories".format(self.category_length)) ) for i in range(1, self.category_length+1): title = self.faker.unique.company() slug = slugify(title) obj = CategoryFactory( title=title, slug=slug, ) self.stdout.write(" - Category: {}".format(obj.title)) created.append(obj) return created
def test_category_get_by_lang(db): """ Demonstrate how we can get categories for original language and translations. """ created_foobar = CategoryFactory(slug="foobar") created_omelette = multilingual_category( slug="food", langs=["fr"], ) created_cheese = multilingual_category( slug="recipe", langs=["fr", "de"], contents={"fr": { "slug": "recette", }}, ) # Get full total all languages mixed and without any filtering assert queryset_values(Category.objects.all()) == [ { "slug": "foobar", "language": "en" }, { "slug": "food", "language": "en" }, { "slug": "food", "language": "fr" }, { "slug": "recette", "language": "fr" }, { "slug": "recipe", "language": "de" }, { "slug": "recipe", "language": "en" }, ] # Get available categories for a required language assert Category.objects.filter(language="en").count() == 3 assert Category.objects.filter(language="fr").count() == 2 assert Category.objects.filter(language="de").count() == 1 # Get only originals results = Category.objects.filter(original__isnull=True) assert queryset_values(results) == [ { "slug": "foobar", "language": "en" }, { "slug": "food", "language": "en" }, { "slug": "recipe", "language": "en" }, ] # Get only translations results = Category.objects.filter(original__isnull=False) assert queryset_values(results) == [ { "slug": "food", "language": "fr" }, { "slug": "recette", "language": "fr" }, { "slug": "recipe", "language": "de" }, ] # Get translations from original assert created_foobar.category_set.all().count() == 0 assert created_omelette["original"].category_set.all().count() == 1 assert created_cheese["original"].category_set.all().count() == 2
def test_article_admin_category_change_validation(db, admin_client): """ Admin change form should not allow to set category with a different language. """ cat_en = CategoryFactory(language="en") cat_fr = CategoryFactory(language="fr") obj_fr = ArticleFactory(language="fr") # Build initial POST data ignore = [ "id", "relations", "article", "authors", "related", "categories", ] data = build_post_data_from_object(Article, obj_fr, ignore=ignore) # 1) Try to add category in different language, raise error data["categories"] = [cat_en.id] f = ArticleAdminForm(data, instance=obj_fr) assert f.is_valid() is False assert compact_form_errors(f) == { "categories": ["invalid_choice"], } # 2) Try to add category in same language, should be ok data["categories"] = [cat_fr.id] f = ArticleAdminForm(data, instance=obj_fr) assert f.is_valid() is True assert compact_form_errors(f) == {} # Save it for next test part obj_fr = f.save() # 3) Try again to add category in different language, keep raising # error data["categories"] = [cat_fr.id, cat_en.id] f = ArticleAdminForm(data, instance=obj_fr) assert f.is_valid() is False assert compact_form_errors(f) == { "categories": ["invalid_choice"], } # 4) Restore working category object in same language data["categories"] = [cat_fr.id] f = ArticleAdminForm(data, instance=obj_fr) obj_fr = f.save() # 5) Try to change language with remaining category object in a different # language, raise an error data["language"] = "en" f = ArticleAdminForm(data, instance=obj_fr) assert f.is_valid() is False assert compact_form_errors(f) == { "categories": ["invalid-categories"], }
def test_category_creation(db): """ Factory should correctly create a new object without any errors. """ category = CategoryFactory(title="foo") assert category.title == "foo"
def test_category_get_articles(db): """ Demonstrate how to get category related articles. """ ping = CategoryFactory() pong = CategoryFactory() foo = multilingual_article( slug="foo", langs=["fr"], fill_categories=[ping, pong], contents={"fr": { "slug": "fou", "fill_categories": [ping, pong], }}, ) bar = multilingual_article( slug="bar", langs=["fr"], fill_categories=[ping], contents={"fr": { "slug": "barre", "fill_categories": [ping, pong], }}, ) moo = multilingual_article( slug="moo", fill_categories=[ping], ) yeah = multilingual_article( slug="yeah", langs=["fr"], fill_categories=[pong], contents={"fr": { "slug": "ouais", "fill_categories": [ping], }}, ) nope = multilingual_article(slug="nope") ping_articles = [(item.slug, item.language) for item in ping.get_articles()] pong_articles = [(item.slug, item.language) for item in pong.get_articles()] # Get unordered queryset then order by slug to avoid arbitrary order unordered_ping_articles = [ (item.slug, item.language) for item in ping.get_articles(ordered=False).order_by("slug") ] assert ping_articles == [ ("moo", "en"), ("bar", "en"), ("foo", "en"), ] assert pong_articles == [ ("yeah", "en"), ("foo", "en"), ] assert unordered_ping_articles == [ ("bar", "en"), ("foo", "en"), ("moo", "en"), ]